Sa-Token 接入第三方登录
Sa-Token 本身不直接支持 OAuth2 第三方登录(如微信、Google、Gitee),但可以通过扩展或集成 JustAuth 实现。本文档讲解两种主流方案。
1. 第三方登录核心原理
1.1 OAuth2 授权码流程
用户点击"微信登录"
↓
跳转到微信授权页面(携带 client_id、redirect_uri、state)
↓
用户同意授权
↓
微信回调 redirect_uri?code=xxxxx&state=xxxx
↓
服务端用 code 换 token(需配合 client_secret)
↓
用 token 获取用户信息(昵称、头像、唯一ID)
↓
在本地查找/创建用户,生成自己的 Token
1.2 Sa-Token 的角色定位
Sa-Token 负责本地 Token 管理,第三方登录只负责"用外部账号获取本地身份":
第三方平台(微信/GitHub) → 换取用户信息
↓
查询本地用户表(绑定关系)
↓
绑定/注册本地账号
↓
Sa-Token.login(userId) ← Sa-Token 在这里接入
2. 方案一:集成 JustAuth(推荐)
JustAuth 是最完整的第三方授权开源库,支持 70+ 平台。
2.1 引入依赖
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>1.6.2</version>
</dependency>
2.2 配置第三方应用信息
# application.yml
justauth:
enabled: true
clients:
gitee:
client-id: 你的Gitee Client ID
client-secret: 你的Gitee Client Secret
redirect-uri: http://localhost:8080/oauth/gitee/callback
github:
client-id: 你的GitHub Client ID
client-secret: 你的GitHub Client Secret
redirect-uri: http://localhost:8080/oauth/github/callback
2.3 Controller 实现
@RestController
@RequestMapping("/oauth")
public class OAuthController {
@Autowired
private AuthController authController; // JustAuth 内置控制器
// 跳转到第三方授权页面
@GetMapping("/login/gitee")
public void loginGitee(HttpServletResponse response) throws IOException {
response.sendRedirect(buildAuthUrl("gitee"));
}
@GetMapping("/login/github")
public void loginGithub(HttpServletResponse response) throws IOException {
response.sendRedirect(buildAuthUrl("github"));
}
// 授权回调:第三方同意后跳转回来
@GetMapping("/callback/{source}")
public Result<?> callback(@PathVariable String source,
@RequestParam String code,
@RequestParam String state) {
// 1. 用 code 换取第三方 token
AuthToken authToken = authController.auth(source, code, state);
// 2. 用 token 获取第三方用户信息
AuthUser authUser = authController.userInfo(source, authToken);
// 3. 绑定本地用户或注册新用户
User localUser = userService.bindOrCreate(authUser);
// 4. Sa-Token 登录
StpUtil.login(localUser.getId());
String token = StpUtil.getTokenValue();
return Result.ok().put("token", token);
}
}
2.4 本地用户绑定逻辑
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User bindOrCreate(AuthUser authUser) {
// 1. 查找是否已绑定过该第三方账号
String openId = authUser.getId(); // 第三方的唯一ID
User user = userMapper.findByOpenId(authUser.getSource(), openId);
if (user != null) {
// 已绑定:直接返回
return user;
}
// 2. 未绑定:检查是否已登录(用户已登录则绑定)
if (StpUtil.isLogin()) {
// 已登录:绑定第三方账号
User loginUser = userMapper.findById(StpUtil.getLoginIdAsLong());
loginUser.setOpenId(authUser.getSource(), openId);
userMapper.update(loginUser);
return loginUser;
}
// 3. 未登录也未绑定:自动注册(或引导完善信息)
User newUser = new User();
newUser.setNickname(authUser.getNickname());
newUser.setAvatar(authUser.getAvatar());
newUser.setOpenId(authUser.getSource(), openId);
userMapper.insert(newUser);
return newUser;
}
}
2.5 数据库设计
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
nickname VARCHAR(50),
avatar VARCHAR(255),
created_at DATETIME
);
-- 第三方绑定表
CREATE TABLE user_bind (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
source VARCHAR(20) NOT NULL, -- gitee, github, wechat...
openid VARCHAR(100) NOT NULL, -- 第三方唯一ID
UNIQUE KEY uk_source_openid (source, openid)
);
3. 方案二:手动实现(不走 JustAuth)
适合特定平台或自定义需求。
3.1 以 Gitee 为例:获取 AccessToken
@GetMapping("/oauth/gitee/callback")
public Result<?> giteeCallback(@RequestParam String code) {
// 1. 用 code 换 AccessToken
String tokenUrl = "https://gitee.com/oauth/token?" +
"grant_type=authorization_code&" +
"code=" + code + "&" +
"client_id=" + giteeClientId + "&" +
"redirect_uri=" + giteeRedirectUri + "&" +
"client_secret=" + giteeClientSecret;
HttpResponse tokenResponse = HttpUtil.createPost(tokenUrl).execute();
String body = tokenResponse.body();
// 解析 {"access_token":"xxx","token_type":"bearer",...}
// 2. 解析 token
JSONObject tokenJson = JSONUtil.parseObj(body);
String accessToken = tokenJson.getStr("access_token");
// 3. 用 token 获取用户信息
String userUrl = "https://gitee.com/api/v5/user?access_token=" + accessToken;
HttpResponse userResponse = HttpUtil.createGet(userUrl).execute();
JSONObject userJson = JSONUtil.parseObj(userResponse.body());
String openId = userJson.getStr("id");
String nickname = userJson.getStr("login");
String avatar = userJson.getStr("avatar_url");
// 4. 绑定/注册本地用户
User user = userService.bindOrCreate("gitee", openId, nickname, avatar);
// 5. Sa-Token 登录
StpUtil.login(user.getId());
return Result.ok().put("token", StpUtil.getTokenValue());
}
4. 多平台登录整合
4.1 统一登录入口
@RestController
@RequestMapping("/auth")
public class AuthController {
@GetMapping("/render/{source}")
public void renderAuth(@PathVariable String source, HttpServletResponse response) throws IOException {
String authUrl;
switch (source) {
case "gitee":
authUrl = "https://gitee.com/oauth/authorize?" +
"client_id=" + giteeClientId + "&" +
"redirect_uri=" + giteeRedirectUri + "&" +
"response_type=code";
break;
case "github":
authUrl = "https://github.com/login/oauth/authorize?" +
"client_id=" + githubClientId + "&" +
"redirect_uri=" + githubRedirectUri + "&" +
"scope=read:user";
break;
default:
throw new BizException("不支持的登录方式");
}
response.sendRedirect(authUrl);
}
}
4.2 前端调用
// Vue / React 中调用
// 跳转到第三方授权页
window.location.href = '/auth/render/gitee'
// 授权成功后,自动回调到 /oauth/callback/gitee
// 后端返回 token,前端存储
5. 注意事项
5.1 为什么要用 state 参数
防止 CSRF 攻击:
// 登录前:生成随机 state 存入 Session
String state = UUID.randomUUID().toString();
session.setAttribute("oauth_state", state);
response.sendRedirect(authUrl + "&state=" + state);
// 回调时:校验 state 是否匹配
String savedState = session.getAttribute("oauth_state").toString();
if (!savedState.equals(state)) {
throw new BizException("state 不匹配,可能是 CSRF 攻击");
}
5.2 第三方登录的安全建议
- HTTPS:授权回调必须走 HTTPS
- code 一次性:code 只能使用一次,换完 Token 后立即失效
- client_secret 保密:后端使用,绝不暴露在前端
- 绑定关系唯一:
source + openid唯一索引,防止重复绑定
5.3 Sa-Token 的局限
Sa-Token 本身不提供:
- 第三方登录的 SDK
- OAuth2 协议实现
- 第三方用户信息获取
这些需要配合 JustAuth 或自行实现。Sa-Token 只负责本地 Token 生成和验证。
5.4 与 Spring Security OAuth2 的对比
| 维度 | Sa-Token + JustAuth | Spring Security OAuth2 Client |
|---|---|---|
| 第三方登录 | 支持(通过 JustAuth) | 原生支持 |
| 学习成本 | 低(Sa-Token 简单) | 高(OAuth2 协议复杂) |
| Token 管理 | Sa-Token | Spring Security 内置 |
| 接入平台数量 | 70+(JustAuth) | 有限(需自定义) |
| 适合场景 | 国内项目(微信/QQ/钉钉) | 国际化(Google/GitHub) |