Github快捷登录配置
这是第三方登录系列的第三篇。前两篇分别接入了 Google 和 Gitee,这一篇接入 GitHub 登录。GitHub 登录在面向开发者的产品中几乎是标配,而且它和 Gitee 一样使用标准的 OAuth2 授权码模式,如果你已经看过《Gitee快捷登录配置》,这一篇会非常轻松——流程完全一样,只是换了接口地址和字段名。
1 2
| Google:前端直接拿到 ID Token,后端只负责校验 Gitee / GitHub:前端只能拿到授权码 code,由后端用 code + Client Secret 换取 Access Token,再调 API 获取用户信息
|
本文主要讲的是”使用 GitHub 账号登录网站”,不是调用 GitHub 的仓库、Issue 等 REST API。另外要注意,GitHub 在国内部分网络环境下访问不稳定,如果你的用户主要在国内,建议把 GitHub 登录作为辅助登录方式,主推 Gitee 或手机号登录。
一、整体登录流程
GitHub 快捷登录的核心流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 用户点击"使用 GitHub 登录" ↓ 前端跳转到 GitHub 授权页 https://github.com/login/oauth/authorize ↓ 用户在 GitHub 登录并同意授权 ↓ GitHub 重定向回你配置的回调地址,URL 上带着 code 和 state ↓ 前端回调页把 code 发给后端 ↓ 后端用 code + Client Secret 向 GitHub 换取 Access Token ↓ 后端用 Access Token 调用 GitHub API 获取用户信息 ↓ 后端根据 GitHub 用户唯一 ID 查询或创建本地用户 ↓ 后端生成自己系统的登录态,比如 Session、JWT、Sa-Token ↓ 用户登录成功
|
原则依然不变:
1 2
| GitHub 只负责证明"这个 GitHub 用户是谁" 你自己的系统负责决定"这个用户在我系统里是谁、有什么权限、登录态怎么维护"
|
二、几个核心概念
授权码模式的核心概念在《Gitee快捷登录配置》中已经详细讲过(Client ID、Client Secret、授权码 code、state 防 CSRF),这里只说 GitHub 特有的几点。
1. scope 权限范围
做快捷登录推荐申请:
1 2
| read:user 读取用户公开资料 user:email 读取用户邮箱(包括设为私密的邮箱)
|
多个 scope 用空格分隔。如果一个 scope 都不传,也能拿到用户的公开信息,但拿不到私密邮箱。
2. GitHub 用户唯一 ID
GitHub 用户信息接口返回的字段中:
1 2 3
| id:数字,永远不变,作为绑定标识 login:用户名,用户可以修改,不能作为唯一标识 email:邮箱,大概率是 null(见下文),不能作为唯一标识
|
3. email 大概率拿不到
这是 GitHub 登录最容易踩的坑:很多用户在 GitHub 设置中勾选了 “Keep my email addresses private”,此时 /user 接口返回的 email 字段是 null。
正确做法是申请 user:email 权限,再单独调用 /user/emails 接口,从邮箱列表中取 primary = true 且 verified = true 的那一条。
4. Access Token 的有效期
GitHub OAuth App 换到的 Access Token 是长期有效的,没有 refresh_token,只有用户主动撤销授权或 token 一年未使用时才会失效。
顺带澄清一个容易混淆的点:可过期 token + refresh_token 机制是 GitHub Apps 才有的功能(User-to-server token expiration),OAuth App 的设置里没有这个选项。
不过做快捷登录时不用关心这个差异:换到用户信息、完成绑定之后,这个 token 就可以丢弃了。
三、在 GitHub 创建 OAuth App
1. 进入开发者设置
登录 GitHub,点击右上角头像,进入:
1
| Settings → Developer settings → OAuth Apps → New OAuth App
|
或者直接访问:
1
| https://github.com/settings/developers
|
注意选择的是 OAuth Apps,不是 GitHub Apps。GitHub Apps 功能更强大但配置复杂,做网站快捷登录用 OAuth App 就够了。
2. 填写应用信息
1 2 3 4
| Application name:应用名称,例如 Yao Admin,用户在授权页会看到 Homepage URL:应用主页,例如 https://yao-admin.com Application description:可选 Authorization callback URL:授权回调地址
|
回调地址开发环境示例:
1
| http://localhost:5173/oauth/github/callback
|
生产环境示例:
1
| https://yao-admin.com/oauth/github/callback
|
注意事项:
1 2 3
| 1. 一个 OAuth App 只能填一个回调地址,开发和生产建议分别创建两个 OAuth App 2. GitHub 的回调匹配规则是前缀匹配,但协议、域名、端口必须完全一致 3. 这一点和 Gitee 不同:Gitee 一个应用可以配多行回调地址
|
3. 获取 Client ID 和 Client Secret
创建完成后进入应用详情页:
1 2
| Client ID:页面上直接显示 Client Secret:点击 Generate a new client secret 生成
|
Client Secret 只在生成时完整显示一次,立刻保存到安全的地方。和 Gitee 一样,它只能保存在后端。
四、前端接入:跳转授权与回调页
前端逻辑和 Gitee 完全一致:登录按钮负责”跳过去”,回调页负责”接回来”。
1. 配置前端环境变量
.env.development:
1 2
| VITE_GITHUB_CLIENT_ID=你的GitHubClientID VITE_GITHUB_REDIRECT_URI=http://localhost:5173/oauth/github/callback
|
.env.production:
1 2
| VITE_GITHUB_CLIENT_ID=生产环境GitHubClientID VITE_GITHUB_REDIRECT_URI=https://yao-admin.com/oauth/github/callback
|
注意开发和生产是两个不同的 OAuth App,Client ID 不一样。
2. 登录按钮:拼接授权地址并跳转
GitHub 授权地址格式:
1
| https://github.com/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=read:user user:email&state=STATE
|
React 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function buildState(): string { const state = crypto.randomUUID(); sessionStorage.setItem('github_oauth_state', state); return state; }
export default function GithubLoginButton() { function handleClick() { const params = new URLSearchParams({ client_id: import.meta.env.VITE_GITHUB_CLIENT_ID, redirect_uri: import.meta.env.VITE_GITHUB_REDIRECT_URI, scope: 'read:user user:email', state: buildState() });
window.location.href = `https://github.com/login/oauth/authorize?${params.toString()}`; }
return ( <button onClick={handleClick}> 使用 GitHub 登录 </button> ); }
|
3. 回调页:取出 code 并发给后端
回调页和 Gitee 版本只有两处不同:sessionStorage 的 key 和后端接口地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import { useEffect, useRef } from 'react';
export default function GithubCallback() { const executed = useRef(false);
useEffect(() => { if (executed.current) { return; } executed.current = true;
const params = new URLSearchParams(window.location.search); const code = params.get('code'); const state = params.get('state'); const savedState = sessionStorage.getItem('github_oauth_state');
sessionStorage.removeItem('github_oauth_state');
if (!code) { console.error('未获取到授权码'); window.location.href = '/login'; return; }
if (!state || state !== savedState) { console.error('state 校验失败,可能存在 CSRF 风险'); window.location.href = '/login'; return; }
handleGithubLogin(code); }, []);
async function handleGithubLogin(code: string) { const res = await fetch('/api/auth/github/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) });
if (!res.ok) { throw new Error('GitHub 登录失败'); }
const data = await res.json(); localStorage.setItem('token', data.token); window.location.href = '/'; }
return <div>正在登录,请稍候...</div>; }
|
五、后端实现:换取 Access Token 并获取用户信息
后端流程和 Gitee 一样,但有三个 GitHub 特有的细节:
1 2 3 4
| 1. 换 token 接口默认返回 application/x-www-form-urlencoded 格式, 必须显式设置请求头 Accept: application/json 才会返回 JSON 2. 调用 GitHub API 必须携带 User-Agent 请求头,否则直接返回 403 3. email 为 null 时需要再调一次 /user/emails 接口
|
1. 配置 application.yml
1 2 3 4 5
| github: oauth: client-id: ${GITHUB_CLIENT_ID} client-secret: ${GITHUB_CLIENT_SECRET} redirect-uri: ${GITHUB_REDIRECT_URI}
|
2. Controller 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import jakarta.validation.Valid; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/api/auth/github") public class GithubAuthController {
private final GithubAuthService githubAuthService;
public GithubAuthController(GithubAuthService githubAuthService) { this.githubAuthService = githubAuthService; }
@PostMapping("/login") public LoginResponse login(@Valid @RequestBody GithubLoginRequest request) { return githubAuthService.login(request.getCode()); } }
|
GithubLoginRequest 和 Gitee 版本一样,只有一个 code 字段,不再重复。
3. Token 响应对象
设置了 Accept: application/json 后,GitHub 换 token 接口返回:
1 2 3 4 5
| { "access_token": "gho_xxxxxxxxxxxxxxxx", "token_type": "bearer", "scope": "read:user,user:email" }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true) public class GithubTokenResponse {
@JsonProperty("access_token") private String accessToken;
@JsonProperty("token_type") private String tokenType;
private String scope;
public String getAccessToken() { return accessToken; }
public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
public String getTokenType() { return tokenType; }
public void setTokenType(String tokenType) { this.tokenType = tokenType; }
public String getScope() { return scope; }
public void setScope(String scope) { this.scope = scope; } }
|
4. 用户信息响应对象
GitHub /user 接口返回的主要字段:
1 2 3 4 5 6 7 8
| { "id": 9876543, "login": "handsomeliyao", "name": "LI YAO", "avatar_url": "https://avatars.githubusercontent.com/u/9876543", "email": null, "html_url": "https://github.com/handsomeliyao" }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true) public class GithubUserInfo {
private Long id;
private String login;
private String name;
@JsonProperty("avatar_url") private String avatarUrl;
private String email;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getAvatarUrl() { return avatarUrl; }
public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; } }
|
5. 邮箱列表响应对象
/user/emails 接口返回数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [ { "email": "yao@example.com", "primary": true, "verified": true, "visibility": "private" }, { "email": "9876543+handsomeliyao@users.noreply.github.com", "primary": false, "verified": true, "visibility": null } ]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true) public class GithubEmail {
private String email; private Boolean primary; private Boolean verified;
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Boolean getPrimary() { return primary; }
public void setPrimary(Boolean primary) { this.primary = primary; }
public Boolean getVerified() { return verified; }
public void setVerified(Boolean verified) { this.verified = verified; } }
|
6. 调用 GitHub 接口的 Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate;
import java.util.Collections; import java.util.List;
@Service public class GithubApiService {
private static final String TOKEN_URL = "https://github.com/login/oauth/access_token"; private static final String USER_URL = "https://api.github.com/user"; private static final String EMAILS_URL = "https://api.github.com/user/emails";
private final String clientId; private final String clientSecret; private final String redirectUri; private final RestTemplate restTemplate;
public GithubApiService(@Value("${github.oauth.client-id}") String clientId, @Value("${github.oauth.client-secret}") String clientSecret, @Value("${github.oauth.redirect-uri}") String redirectUri) { this.clientId = clientId; this.clientSecret = clientSecret; this.redirectUri = redirectUri; this.restTemplate = new RestTemplate(); }
public String getAccessToken(String code) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.add("client_id", clientId); params.add("client_secret", clientSecret); params.add("code", code); params.add("redirect_uri", redirectUri);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
GithubTokenResponse response = restTemplate.postForObject(TOKEN_URL, request, GithubTokenResponse.class);
if (response == null || response.getAccessToken() == null) { throw new RuntimeException("获取GitHub AccessToken失败"); }
return response.getAccessToken(); }
public GithubUserInfo getUserInfo(String accessToken) { HttpEntity<Void> request = new HttpEntity<>(buildApiHeaders(accessToken));
GithubUserInfo userInfo = restTemplate.exchange( USER_URL, HttpMethod.GET, request, GithubUserInfo.class ).getBody();
if (userInfo == null || userInfo.getId() == null) { throw new RuntimeException("获取GitHub用户信息失败"); }
if (userInfo.getEmail() == null) { userInfo.setEmail(getPrimaryEmail(accessToken)); }
return userInfo; }
private String getPrimaryEmail(String accessToken) { HttpEntity<Void> request = new HttpEntity<>(buildApiHeaders(accessToken));
List<GithubEmail> emails = restTemplate.exchange( EMAILS_URL, HttpMethod.GET, request, new ParameterizedTypeReference<List<GithubEmail>>() {} ).getBody();
if (emails == null) { return null; }
return emails.stream() .filter(e -> Boolean.TRUE.equals(e.getPrimary()) && Boolean.TRUE.equals(e.getVerified())) .map(GithubEmail::getEmail) .findFirst() .orElse(null); }
private HttpHeaders buildApiHeaders(String accessToken) { HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); headers.set("User-Agent", "yao-admin"); headers.set("X-GitHub-Api-Version", "2022-11-28"); return headers; } }
|
六、数据库表设计
继续复用系列前两篇中的两张表:
1 2
| sys_user:用户表 sys_oauth_account:第三方账号绑定表
|
GitHub 登录时的绑定数据:
1 2
| provider = github provider_user_id = GitHub 用户信息中的 id(转成字符串保存)
|
到这里这张表已经同时支撑了 google、gitee、github 三种登录方式,没有改过一行结构。
七、登录业务逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| import cn.dev33.satoken.stp.StpUtil; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
@Service public class GithubAuthService {
private static final String PROVIDER_GITHUB = "github";
private final GithubApiService githubApiService; private final UserRepository userRepository; private final OAuthAccountRepository oAuthAccountRepository;
public GithubAuthService(GithubApiService githubApiService, UserRepository userRepository, OAuthAccountRepository oAuthAccountRepository) { this.githubApiService = githubApiService; this.userRepository = userRepository; this.oAuthAccountRepository = oAuthAccountRepository; }
@Transactional(rollbackFor = Exception.class) public LoginResponse login(String code) { String accessToken = githubApiService.getAccessToken(code);
GithubUserInfo githubUserInfo = githubApiService.getUserInfo(accessToken);
OAuthAccount oauthAccount = oAuthAccountRepository.findByProviderAndProviderUserId( PROVIDER_GITHUB, String.valueOf(githubUserInfo.getId()) );
User user;
if (oauthAccount != null) { user = userRepository.findById(oauthAccount.getUserId()); if (user == null) { throw new RuntimeException("绑定的本地用户不存在"); } } else { user = createUser(githubUserInfo);
OAuthAccount newOAuthAccount = new OAuthAccount(); newOAuthAccount.setUserId(user.getId()); newOAuthAccount.setProvider(PROVIDER_GITHUB); newOAuthAccount.setProviderUserId(String.valueOf(githubUserInfo.getId())); newOAuthAccount.setEmail(githubUserInfo.getEmail()); newOAuthAccount.setNickname(githubUserInfo.getName()); newOAuthAccount.setAvatar(githubUserInfo.getAvatarUrl()); oAuthAccountRepository.save(newOAuthAccount); }
if (user.getStatus() == 0) { throw new RuntimeException("当前用户已被禁用"); }
StpUtil.login(user.getId()); String token = StpUtil.getTokenValue();
return new LoginResponse( token, new LoginResponse.UserInfo( user.getId(), user.getNickname(), user.getEmail(), user.getAvatar() ) ); }
private User createUser(GithubUserInfo githubUserInfo) { User newUser = new User(); newUser.setEmail(githubUserInfo.getEmail()); newUser.setNickname(githubUserInfo.getName() != null ? githubUserInfo.getName() : githubUserInfo.getLogin()); newUser.setAvatar(githubUserInfo.getAvatarUrl()); newUser.setStatus(1); userRepository.save(newUser); return newUser; } }
|
对比一下会发现,GithubAuthService 和 GiteeAuthService 几乎逐行相同。如果你的系统要接入更多平台,可以进一步抽象出一个 OAuthLoginTemplate,把”查绑定 → 建用户 → 生成登录态”的公共流程提取出来,每个平台只实现”换 token + 拿用户信息”两个方法。
八、前后端联调
1. 检查回调地址
确认 GitHub OAuth App(开发环境那个)配置的回调地址是:
1
| http://localhost:5173/oauth/github/callback
|
前端 VITE_GITHUB_REDIRECT_URI 和后端 GITHUB_REDIRECT_URI 保持同一个值。
2. 测试登录流程
1 2 3 4 5
| 1. 打开登录页,点击"使用 GitHub 登录" 2. 浏览器跳转到 github.com 授权页,登录并点击 Authorize 3. 浏览器回到 /oauth/github/callback?code=xxx&state=yyy 4. 回调页把 code 发给后端,后端返回本系统 token 5. 前端保存 token,跳转首页
|
3. 国内网络问题
后端服务器如果部署在国内,调用 github.com 和 api.github.com 可能超时。排查时先在服务器上直接测试连通性:
1
| curl -I https://api.github.com
|
如果不通,可选方案:
1 2 3
| 1. 后端服务器使用海外节点(例如香港、新加坡、东京的云服务器) 2. 给后端的 RestTemplate 配置代理 3. 评估是否真的需要 GitHub 登录,国内用户主推 Gitee 登录
|
九、生产环境部署注意事项
1. 生产环境单独创建 OAuth App
GitHub 一个 OAuth App 只能配一个回调地址,所以:
1 2
| Dev OAuth App → http://localhost:5173/oauth/github/callback Prod OAuth App → https://yao-admin.com/oauth/github/callback
|
两套 Client ID 和 Client Secret 分别配置到对应环境。
2. 必须使用 HTTPS
生产回调地址必须是 HTTPS。
3. Client Secret 保护
1 2 3
| 1. 只通过环境变量注入,不进代码仓库 2. GitHub 支持同时存在两个 Client Secret,轮换时先生成新的、再删除旧的,可以平滑过渡 3. 怀疑泄露时立即在 OAuth App 设置中 Revoke
|
4. Nginx 配置
和前两篇一致:/ 指向前端静态文件并配置 try_files $uri $uri/ /index.html;,/api/ 反向代理到后端。/oauth/github/callback 是前端路由,不需要额外配置。
十、常见问题
1. 换 token 拿到的响应解析失败
最常见的原因:没有设置 Accept: application/json 请求头。
GitHub 这个接口默认返回的是:
1
| access_token=gho_xxx&scope=read%3Auser&token_type=bearer
|
这是 urlencoded 字符串,不是 JSON。加上 Accept 头之后才返回 JSON。
2. 调用 API 返回 403
检查是否携带了 User-Agent 请求头。GitHub API 强制要求所有请求带 User-Agent,值随便填(建议填你的应用名),不带就是 403。
3. 换 token 返回 bad_verification_code
1 2 3 4 5
| 1. code 已经被使用过(code 是一次性的) 2. code 已过期(有效期 10 分钟) 3. React 18 开发模式 useEffect 执行两次,第二次用的是已消费的 code 4. Client ID 和 Client Secret 与生成 code 的 OAuth App 不匹配, 常见于开发环境误用了生产的凭证
|
4. email 是 null
用户开启了邮箱隐私保护,这在 GitHub 用户中非常普遍。按本文第五节的方式申请 user:email scope 并调用 /user/emails 接口兜底。如果两边都拿不到(用户没有验证过邮箱),就允许 email 为空,登录后引导用户补填。
5. 回调地址不匹配报错 redirect_uri_mismatch
GitHub 的回调校验规则:
1 2 3 4 5
| 1. 协议、域名、端口必须和 OAuth App 配置完全一致 2. 路径允许是配置路径的子路径(前缀匹配) 3. localhost 和 127.0.0.1 视为不同域名 4. 例外:回环地址(127.0.0.1 / localhost)不要求端口一致, 本地调试换端口不用改 OAuth App 配置,官方推荐用 127.0.0.1 而不是 localhost
|
6. 授权页显示的应用信息能改吗
可以。OAuth App 设置中可以上传应用 Logo、修改应用名称和主页。授权页展示的信息越完整,用户的信任度越高。
7. 用户撤销授权后会怎样
用户可以在 GitHub 的 Settings → Applications 中随时撤销授权。撤销后:
1 2 3
| 1. 已发放的 Access Token 立即失效 2. 不影响你系统中已生成的登录态和绑定关系 3. 用户下次点击 GitHub 登录时,会重新走一遍授权页
|
十一、最终落地清单
GitHub 平台需要完成:
1 2 3
| 1. Settings → Developer settings → OAuth Apps → New OAuth App 2. 填写应用名称、主页、回调地址(开发和生产分别创建) 3. 生成并保存 Client Secret
|
前端需要完成:
1 2 3 4 5
| 1. 配置 VITE_GITHUB_CLIENT_ID 和 VITE_GITHUB_REDIRECT_URI 2. 登录按钮拼接授权地址并跳转,scope 用 read:user user:email,带上随机 state 3. 回调页校验 state、取出 code,防止 useEffect 重复执行 4. 调用后端 /api/auth/github/login 5. 保存后端返回的本系统 token
|
后端需要完成:
1 2 3 4 5 6 7 8 9 10
| 1. 配置 client-id、client-secret、redirect-uri 2. POST https://github.com/login/oauth/access_token 换取 access_token, 记得加 Accept: application/json 3. GET https://api.github.com/user 获取用户信息, 记得加 User-Agent 和 Authorization: Bearer 4. email 为 null 时调用 GET https://api.github.com/user/emails 兜底 5. 根据 id 查询 sys_oauth_account 绑定关系 6. 查询或创建本地用户 7. 生成自己系统的登录态 8. 返回 token 和用户信息
|
十二、总结
GitHub 快捷登录和 Gitee 是同一套授权码模式,核心三步不变:
1 2 3
| 1. 前端跳转 https://github.com/login/oauth/authorize 拿授权码 code 2. 后端用 code + Client Secret 换 Access Token 3. 后端调用用户信息接口,根据 id 完成本地用户绑定和登录
|
GitHub 特有的几个坑:
1 2 3 4 5
| 1. 换 token 必须加 Accept: application/json,否则返回的不是 JSON 2. 调 API 必须加 User-Agent,否则 403 3. email 大概率是 null,要用 /user/emails 接口兜底 4. 一个 OAuth App 只能配一个回调地址,开发生产要分开建 5. 国内服务器访问 GitHub API 可能不稳定
|
至此,Google、Gitee、GitHub 三种快捷登录全部接入完成。回头看会发现:sys_oauth_account 绑定表 + “先查绑定、再建用户、最后生成自己系统登录态”的流程是通用骨架,任何新平台都只是往这个骨架上挂一个新的 provider 而已。