GitHub 登录本质上是 OAuth2 授权码模式
- 把用户重定向到 GitHub 授权页面
- GitHub 授权后会跳转回你的回调接口,带上一个
code,你拿这个code换access_token,再用access_token换用户信息,最后在你系统里"登录或注册",签发自己的 JWT 返回给前端
流程图
flowchart TD
A["用户点击 GitHub 登录按钮"]
B["前端 location.href = 后端 /auth/loginByGithub"]
C["后端 redirect 到 GitHub 授权页"]
D["用户在 GitHub 点击授权"]
E["GitHub redirect 到 /auth/callback/github?code=xxx"]
F["后端 code 换 token,登录成功"]
M["token存在redis30s,返回随机数code用于换取token"]
G["后端 redirect 到前端页面?code=xxx"]
H["前端页面加载"]
I["从 URL 获取 code"]
N["从服务器获取token等信息"]
J["保存 token 到 localStorage"]
K["跳转到首页"]
L["后续 API 请求携带 token"]
A --> B --> C --> D --> E --> F --> M --> G --> H --> I --> N --> J --> K --> L
0. 先去 GitHub 申请 OAuth App
GitHub → Settings → Developer settings → OAuth Apps → New OAuth App,拿到 Client ID、Client Secret,并配置 Authorization callback URL(比如 http://your-domain.com/api/auth/callback/github)。
1. 配置文件
oauth:
github:
client-id: 你的client_id
client-secret: 你的client_secret
redirect-uri: http://your-domain.com/api/auth/callback/github
frontend-callback-url: http://your-frontend-domain.com/callback
authorize-url: https://github.com/login/oauth/authorize
token-url: https://github.com/login/oauth/access_token
user-info-url: https://api.github.com/user
@Data
@Component
@ConfigurationProperties(prefix = "oauth.github")
public class GithubOAuthProperties {
private String clientId;
private String clientSecret;
private String redirectUri;
private String authorizeUrl;
private String tokenUrl;
private String userInfoUrl;
private String frontendCallbackUrl;
}
2. 第三方账号绑定表
不要把 github_id 直接塞进 sys_user 表,因为以后可能还要支持微信/QQ登录,用一张独立表比较好扩展:
CREATE TABLE `sys_user_oauth` (
`id` bigint NOT NULL,
`user_id` bigint NOT NULL COMMENT '关联本地用户ID',
`platform` varchar(20) NOT NULL COMMENT '第三方平台,如github/wechat',
`open_id` varchar(100) NOT NULL COMMENT '第三方平台唯一标识',
`nickname` varchar(50) DEFAULT NULL COMMENT '第三方昵称',
`avatar` varchar(255) DEFAULT NULL COMMENT '第三方头像',
`create_at` datetime DEFAULT NULL,
`update_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_platform_openid` (`platform`,`open_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='第三方账号绑定表';
3. controller
@Slf4j
@RestController
@RequestMapping("/auth")
@Tag(name = "认证相关接口")
public class AuthController {
@Autowired
private GithubOAuthProperties githubOAuthProperties;
@Autowired
private SysUserOauthService sysUserOauthService;
@Autowired
private RedisService redisService;
@GetMapping("/loginByGithub")
@Operation(summary = "通过Github登录", description = "通过Github登录,跳转到Github授权页面")
public void loginByGithub(HttpServletResponse response) throws IOException {
String state = UUID.randomUUID().toString(); // 防CSRF,实际项目应存入redis校验
redisService.set("oauth:github:state:" + state, "1", 5, TimeUnit.MINUTES);
String url = githubOAuthProperties.getAuthorizeUrl()
+ "?client_id=" + githubOAuthProperties.getClientId()
+ "&redirect_uri=" + URLEncoder.encode(githubOAuthProperties.getRedirectUri(), StandardCharsets.UTF_8)
+ "&scope=read:user user:email"
+ "&state=" + state;
response.sendRedirect(url);
}
@GetMapping("/callback/github")
@Operation(summary = "Github回调", description = "Github回调,保存登录信息,第一次登录则自动创建新用户,跳转回前端页面,参数带上放在redis中的值(tokenCode),后续用于获取token")
public void callback(@RequestParam String code, @RequestParam(required = false) String state, HttpServletResponse response) throws IOException {
if (state == null || !redisService.hasKey("oauth:github:state:" + state)) {
log.info("Invalid state parameter {}", state);
throw new BusinessException("Invalid state parameter");
}
LoginVo loginVo = sysUserOauthService.loginByGithub(code);
// 重定向到前端页面,把token存储在redis,暴露code用于替换token 30s提高安全性,code当URL参数带过去
String tokenCode = UUID.randomUUID().toString();
redisService.set("oauth:token:code" + tokenCode, loginVo, 30, TimeUnit.SECONDS);
String redirectUrl = githubOAuthProperties.getFrontendCallbackUrl() + "?code=" + tokenCode;
response.sendRedirect(redirectUrl);
}
@GetMapping("/getToken")
@Operation(summary = "获取Token")
public Result getToken(@RequestParam String code) {
if (!redisService.hasKey("oauth:token:code" + code)) {
throw new BusinessException(ResultFailEnum.PARAMETER_ERROR);
}
LoginVo loginVo = (LoginVo) redisService.get("oauth:token:code" + code);
return Result.ok(loginVo);
}
}
redisService参考:https://dev.to/yyt0901/common-dependency-configuration-for-spring-boot-354-java-1721-1li7 (CTRL + F to search
redisServicein website)
@Data
public class LoginVo {
private String token;
private Long userId;
private String nickname;
private String avatar;
}
4. SysUserOauthServiceImpl
@Autowired
private GithubOAuthProperties githubOAuthProperties;
@Autowired
private RestTemplate restTemplate;
@Autowired
private SysUserService sysUserService;
@Autowired
private SysUserRoleService sysUserRoleService;
private static final String PLATFORM_GITHUB = "github";
@Override
public LoginVo loginByGithub(String code) {
// ---------- 1. 用code换access_token ----------
String accessToken = getGithubAccessToken(code);
// ---------- 2. 用access_token换用户信息 ----------
GithubUserInfo githubUser = getGithubUserInfo(accessToken);
// ---------- 3. 查是否已绑定过 ----------
SysUserOauth oauth = this.getOne(new LambdaQueryWrapper<SysUserOauth>()
.eq(SysUserOauth::getPlatform, PLATFORM_GITHUB)
.eq(SysUserOauth::getOpenId, githubUser.getId()));
Date curDate = new Date();
SysUser sysUser;
if (oauth != null) {
// ---------- 3a. 已绑定,直接登录 ----------
sysUser = sysUserService.getById(oauth.getUserId());
if (sysUser == null || sysUser.getStatus() == 0) {
throw new BusinessException("账号不存在或已被禁用");
}
// 顺便更新一下第三方昵称/头像
oauth.setNickname(githubUser.getName());
oauth.setAvatar(githubUser.getAvatarUrl());
oauth.setUpdateAt(curDate);
this.updateById(oauth);
} else {
// ---------- 3b. 未绑定,自动注册新用户 ----------
sysUser = new SysUser();
sysUser.setId(IdTools.generateId());
sysUser.setUsername("github_" + githubUser.getLogin());
sysUser.setNickname(githubUser.getName() != null ? githubUser.getName() : githubUser.getLogin());
sysUser.setAvatar(githubUser.getAvatarUrl());
sysUser.setPassword(""); // 三方登录用户无本地密码,禁止走密码登录
sysUser.setStatus(1);
sysUser.setCreateAt(curDate);
sysUser.setUpdateAt(curDate);
sysUserService.save(sysUser);
// 分配默认角色
SysUserRole userRole = new SysUserRole();
userRole.setId(IdTools.generateId());
userRole.setUserId(sysUser.getId());
userRole.setRoleId(RoleEnum.USER.getId());
userRole.setCreateAt(curDate);
sysUserRoleService.save(userRole);
// 保存绑定关系
SysUserOauth newOauth = new SysUserOauth();
newOauth.setId(IdTools.generateId());
newOauth.setUserId(sysUser.getId());
newOauth.setPlatform(PLATFORM_GITHUB);
newOauth.setOpenId(String.valueOf(githubUser.getId()));
newOauth.setNickname(githubUser.getName());
newOauth.setAvatar(githubUser.getAvatarUrl());
newOauth.setCreateAt(curDate);
newOauth.setUpdateAt(curDate);
this.save(newOauth);
}
sysUser.setLastLoginTime(curDate);
sysUserService.updateById(sysUser);
// ---------- 4. Sa-Token 登录,自动生成并管理token ----------
StpUtil.login(sysUser.getId());
String token = StpUtil.getTokenValue();
LoginVo vo = new LoginVo();
vo.setToken(token);
vo.setUserId(sysUser.getId());
vo.setNickname(sysUser.getNickname());
vo.setAvatar(sysUser.getAvatar());
return vo;
}
/**
* 用code换access_token
*/
private String getGithubAccessToken(String code) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
Map<String, String> body = new HashMap<>();
body.put("client_id", githubOAuthProperties.getClientId());
body.put("client_secret", githubOAuthProperties.getClientSecret());
body.put("code", code);
body.put("redirect_uri", githubOAuthProperties.getRedirectUri());
HttpEntity<Map<String, String>> request = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(
githubOAuthProperties.getTokenUrl(), request, Map.class);
Map<String, Object> result = response.getBody();
if (result == null || result.get("access_token") == null) {
log.error("GitHub换取access_token失败: {}", result);
throw new BusinessException("GitHub授权失败");
}
return (String) result.get("access_token");
}
/**
* 用access_token查询GitHub用户信息
*/
private GithubUserInfo getGithubUserInfo(String accessToken) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "token " + accessToken);
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
HttpEntity<Void> request = new HttpEntity<>(headers);
ResponseEntity<GithubUserInfo> response = restTemplate.exchange(
githubOAuthProperties.getUserInfoUrl(), HttpMethod.GET, request, GithubUserInfo.class);
GithubUserInfo userInfo = response.getBody();
if (userInfo == null || userInfo.getId() == null) {
throw new BusinessException("获取GitHub用户信息失败");
}
return userInfo;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class GithubUserInfo {
private Long id;
private String login; // GitHub用户名
private String name; // 昵称,可能为空
@JsonProperty("avatar_url")
private String avatarUrl;
private String email; // 可能为空(用户设置了隐私)
}

Top comments (0)