LI YAO'S BLOG

有趣的人生, 一半家长里短, 一半山川胡海

Gitee快捷登录配置

Gitee快捷登录配置

在《Google快捷登录配置》中,我们接入了 Google 登录。这一篇接入 Gitee(码云)登录。对于面向国内开发者的网站来说,Gitee 登录不需要科学上网,授权页加载快,是非常实用的第三方登录方式。

和 Google Identity Services 直接在前端返回 ID Token 不同,Gitee 使用的是标准的 OAuth2 授权码模式(Authorization Code),核心区别是:

1
2
Google:前端直接拿到 ID Token,后端只负责校验
Gitee:前端只能拿到授权码 code,必须由后端用 code + Client Secret 换取 Access Token,再调 Gitee API 获取用户信息

本文主要讲的是”使用 Gitee 账号登录网站”,不是调用 Gitee 的仓库、Issue 等开放 API。如果只是做快捷登录,拿到用户信息后就不需要长期保存 Access Token 和 Refresh Token。

一、整体登录流程

Gitee 快捷登录的核心流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
用户点击"使用 Gitee 登录"
↓
前端跳转到 Gitee 授权页 https://gitee.com/oauth/authorize
↓
用户在 Gitee 登录并同意授权
↓
Gitee 重定向回你配置的回调地址,URL 上带着 code 和 state
↓
前端回调页把 code 发给后端
↓
后端用 code + Client Secret 向 Gitee 换取 Access Token
↓
后端用 Access Token 调用 Gitee API 获取用户信息
↓
后端根据 Gitee 用户唯一 ID 查询或创建本地用户
↓
后端生成自己系统的登录态,比如 Session、JWT、Sa-Token
↓
用户登录成功

和 Google 登录一样,最重要的原则不变:

1
2
Gitee 只负责证明"这个 Gitee 用户是谁"
你自己的系统负责决定"这个用户在我系统里是谁、有什么权限、登录态怎么维护"

二、几个核心概念

1. Client ID 和 Client Secret

在 Gitee 创建第三方应用后,会得到一对凭证:

1
2
Client ID:应用的公开标识,可以出现在前端跳转链接里
Client Secret:应用密钥,只能保存在后端,绝不能出现在前端代码、前端环境变量或 Git 仓库中

授权码模式中 Client Secret 是必须的,因为换取 Access Token 这一步必须由后端完成。

2. 授权码 code

用户在 Gitee 同意授权后,Gitee 会重定向回你的回调地址,并在 URL 上附带一个临时授权码:

1
https://你的回调地址?code=xxxxxx&state=yyyyyy

code 的特点:

1
2
3
1. 一次性,用过即作废
2. 有效期很短,几分钟内必须使用
3. 单独拿到 code 没有用,还需要 Client Secret 才能换 token

3. Access Token

后端用 code 换到的访问令牌,用于调用 Gitee API。Gitee 的 Access Token 有效期是 86400 秒(一天),同时会返回 refresh_token 用于刷新。

做快捷登录时,换到 token 后马上调用一次用户信息接口即可,通常不需要把 Access Token 和 Refresh Token 落库。

4. state

state 是防 CSRF 攻击的随机字符串:

1
2
3
1. 前端跳转授权页之前生成随机 state,存入 sessionStorage
2. Gitee 回调时会原样带回 state
3. 前端(或后端)比对 state 是否一致,不一致直接拒绝

不带 state 的 OAuth 登录存在被伪造回调的风险,正式项目建议都带上。

5. Gitee 用户唯一 ID

Gitee 用户信息接口返回的 id 字段(数字)是用户唯一标识。

1
2
3
id:数字,永远不变,作为绑定标识
login:用户名,用户可以修改,不能作为唯一标识
email:邮箱,可能为空(取决于用户隐私设置),不能作为唯一标识

第三方账号绑定表应该保存 id。

三、在 Gitee 创建第三方应用

1. 进入第三方应用管理

登录 Gitee,点击右上角头像,进入:

1
设置 → 第三方应用

或者直接访问:

1
https://gitee.com/oauth/applications

点击”创建应用”。

2. 填写应用信息

1
2
3
4
应用名称:你的应用名,例如 Yao Admin,用户在授权页会看到
应用描述:可选
应用主页:例如 https://yao-admin.com
应用回调地址:授权完成后 Gitee 重定向的地址

回调地址是最关键的一项,开发环境示例:

1
http://localhost:5173/oauth/gitee/callback

生产环境示例:

1
https://yao-admin.com/oauth/gitee/callback

注意事项:

1
2
3
1. 回调地址必须和后续请求中的 redirect_uri 完全一致,包括协议、域名、端口、路径
2. Gitee 一个应用支持添加多个回调地址(创建页面点 + 号),开发和生产地址可以同时填上
3. 回调地址指向前端页面(推荐)或后端接口都可以,本文使用前端回调页模式

3. 勾选权限范围

做快捷登录只需要勾选:

1
user_info:访问用户的个人信息

如果需要读取用户邮箱(用户设置了隐私时 user_info 拿不到邮箱),再勾选:

1
emails:访问用户的邮箱地址

不要勾选 projects、pull_requests 等仓库权限,权限越少,用户授权时的心理负担越小。

4. 获取 Client ID 和 Client Secret

创建完成后,应用详情页会显示:

1
2
Client ID
Client Secret

Client Secret 只保存在后端配置或环境变量中。

四、前端接入:跳转授权与回调页

前端要做两件事:登录按钮负责”跳过去”,回调页负责”接回来”。

1. 配置前端环境变量

以 Vite 项目为例,.env.development:

1
2
VITE_GITEE_CLIENT_ID=你的GiteeClientID
VITE_GITEE_REDIRECT_URI=http://localhost:5173/oauth/gitee/callback

.env.production:

1
2
VITE_GITEE_CLIENT_ID=你的GiteeClientID
VITE_GITEE_REDIRECT_URI=https://yao-admin.com/oauth/gitee/callback

2. 登录按钮:拼接授权地址并跳转

Gitee 授权地址格式:

1
https://gitee.com/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&scope=user_info&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
25
function buildState(): string {
const state = crypto.randomUUID();
sessionStorage.setItem('gitee_oauth_state', state);
return state;
}

export default function GiteeLoginButton() {
function handleClick() {
const params = new URLSearchParams({
client_id: import.meta.env.VITE_GITEE_CLIENT_ID,
redirect_uri: import.meta.env.VITE_GITEE_REDIRECT_URI,
response_type: 'code',
scope: 'user_info',
state: buildState()
});

window.location.href = `https://gitee.com/oauth/authorize?${params.toString()}`;
}

return (
<button onClick={handleClick}>
使用 Gitee 登录
</button>
);
}

Vue3 写法基本一致,把 handleClick 绑定到按钮的 @click 即可。

3. 回调页:取出 code 并发给后端

用户授权后,浏览器会回到:

1
http://localhost:5173/oauth/gitee/callback?code=xxxxxx&state=yyyyyy

回调页组件(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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { useEffect } from 'react';

export default function GiteeCallback() {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const savedState = sessionStorage.getItem('gitee_oauth_state');

sessionStorage.removeItem('gitee_oauth_state');

if (!code) {
console.error('未获取到授权码');
window.location.href = '/login';
return;
}

if (!state || state !== savedState) {
console.error('state 校验失败,可能存在 CSRF 风险');
window.location.href = '/login';
return;
}

handleGiteeLogin(code);
}, []);

async function handleGiteeLogin(code: string) {
const res = await fetch('/api/auth/gitee/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ code })
});

if (!res.ok) {
throw new Error('Gitee 登录失败');
}

const data = await res.json();
localStorage.setItem('token', data.token);
window.location.href = '/';
}

return <div>正在登录,请稍候...</div>;
}

注意:React 18 开发模式下 useEffect 会执行两次,而 code 是一次性的,第二次请求必然失败。可以用 useRef 加一个执行标记,或者在生产构建下验证完整流程。

五、后端实现:换取 Access Token 并获取用户信息

后端要做的事情:

1
2
3
4
5
6
1. 接收前端传来的 code
2. 用 code + Client Secret 向 Gitee 换取 Access Token
3. 用 Access Token 调用 Gitee 用户信息接口
4. 查询或创建本地用户
5. 生成本系统登录 token
6. 返回给前端

下面以 Spring Boot 为例,不需要任何第三方 SDK,用 RestTemplate 就够了。

1. 配置 application.yml

1
2
3
4
5
gitee:
oauth:
client-id: ${GITEE_CLIENT_ID}
client-secret: ${GITEE_CLIENT_SECRET}
redirect-uri: ${GITEE_REDIRECT_URI}

生产环境通过环境变量注入:

1
2
3
export GITEE_CLIENT_ID="你的ClientID"
export GITEE_CLIENT_SECRET="你的ClientSecret"
export GITEE_REDIRECT_URI="https://yao-admin.com/oauth/gitee/callback"

注意:换 token 时传的 redirect_uri 必须和前端跳转授权时用的完全一致,所以后端也要配置同一个值。

2. 请求对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import jakarta.validation.constraints.NotBlank;

public class GiteeLoginRequest {

@NotBlank(message = "code不能为空")
private String code;

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}
}

3. 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/gitee")
public class GiteeAuthController {

private final GiteeAuthService giteeAuthService;

public GiteeAuthController(GiteeAuthService giteeAuthService) {
this.giteeAuthService = giteeAuthService;
}

@PostMapping("/login")
public LoginResponse login(@Valid @RequestBody GiteeLoginRequest request) {
return giteeAuthService.login(request.getCode());
}
}

LoginResponse 和《Google快捷登录配置》中的完全一样,这里不再重复。

4. Token 响应对象

Gitee 换取 token 接口返回的 JSON:

1
2
3
4
5
6
7
8
{
"access_token": "xxxxxx",
"token_type": "bearer",
"expires_in": 86400,
"refresh_token": "xxxxxx",
"scope": "user_info",
"created_at": 1718160000
}

对应的 Java 对象:

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
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown = true)
public class GiteeTokenResponse {

@JsonProperty("access_token")
private String accessToken;

@JsonProperty("token_type")
private String tokenType;

@JsonProperty("expires_in")
private Long expiresIn;

@JsonProperty("refresh_token")
private String refreshToken;

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 Long getExpiresIn() {
return expiresIn;
}

public void setExpiresIn(Long expiresIn) {
this.expiresIn = expiresIn;
}

public String getRefreshToken() {
return refreshToken;
}

public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

public String getScope() {
return scope;
}

public void setScope(String scope) {
this.scope = scope;
}
}

5. 用户信息响应对象

Gitee 用户信息接口返回的主要字段:

1
2
3
4
5
6
7
8
{
"id": 1234567,
"login": "yao-coder",
"name": "yao",
"avatar_url": "https://foruda.gitee.com/avatar/xxx.png",
"email": "yao@example.com",
"html_url": "https://gitee.com/yao-coder"
}

对应的 Java 对象:

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 GiteeUserInfo {

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;
}
}

6. 调用 Gitee 接口的 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
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
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;

@Service
public class GiteeApiService {

private static final String TOKEN_URL = "https://gitee.com/oauth/token";
private static final String USER_URL = "https://gitee.com/api/v5/user";

private final String clientId;
private final String clientSecret;
private final String redirectUri;
private final RestTemplate restTemplate;

public GiteeApiService(@Value("${gitee.oauth.client-id}") String clientId,
@Value("${gitee.oauth.client-secret}") String clientSecret,
@Value("${gitee.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);

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("code", code);
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
params.add("redirect_uri", redirectUri);

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

GiteeTokenResponse response = restTemplate.postForObject(TOKEN_URL, request, GiteeTokenResponse.class);

if (response == null || response.getAccessToken() == null) {
throw new RuntimeException("获取Gitee AccessToken失败");
}

return response.getAccessToken();
}

public GiteeUserInfo getUserInfo(String accessToken) {
String url = USER_URL + "?access_token=" + accessToken;

GiteeUserInfo userInfo = restTemplate.getForObject(url, GiteeUserInfo.class);

if (userInfo == null || userInfo.getId() == null) {
throw new RuntimeException("获取Gitee用户信息失败");
}

return userInfo;
}
}

常见错误处理建议:code 无效或过期时,Gitee 会返回 401 或错误 JSON,RestTemplate 默认会抛出 HttpClientErrorException,实际项目中建议捕获后转换成业务异常,给前端一个明确的”授权已过期,请重新登录”提示。

六、数据库表设计

直接复用《Google快捷登录配置》中的两张表:

1
2
sys_user:用户表
sys_oauth_account:第三方账号绑定表

Gitee 登录时的绑定数据:

1
2
provider = gitee
provider_user_id = Gitee 用户信息中的 id(转成字符串保存)

这正是当初把 provider 设计成字段而不是写死的好处,接入第二、第三个登录方式时,表结构完全不用动。

七、登录业务逻辑

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 GiteeAuthService {

private static final String PROVIDER_GITEE = "gitee";

private final GiteeApiService giteeApiService;
private final UserRepository userRepository;
private final OAuthAccountRepository oAuthAccountRepository;

public GiteeAuthService(GiteeApiService giteeApiService,
UserRepository userRepository,
OAuthAccountRepository oAuthAccountRepository) {
this.giteeApiService = giteeApiService;
this.userRepository = userRepository;
this.oAuthAccountRepository = oAuthAccountRepository;
}

@Transactional(rollbackFor = Exception.class)
public LoginResponse login(String code) {
// 1. code 换 access_token
String accessToken = giteeApiService.getAccessToken(code);

// 2. access_token 换用户信息
GiteeUserInfo giteeUserInfo = giteeApiService.getUserInfo(accessToken);

// 3. 查询绑定关系
OAuthAccount oauthAccount = oAuthAccountRepository.findByProviderAndProviderUserId(
PROVIDER_GITEE,
String.valueOf(giteeUserInfo.getId())
);

User user;

if (oauthAccount != null) {
user = userRepository.findById(oauthAccount.getUserId());
if (user == null) {
throw new RuntimeException("绑定的本地用户不存在");
}
} else {
user = createUser(giteeUserInfo);

OAuthAccount newOAuthAccount = new OAuthAccount();
newOAuthAccount.setUserId(user.getId());
newOAuthAccount.setProvider(PROVIDER_GITEE);
newOAuthAccount.setProviderUserId(String.valueOf(giteeUserInfo.getId()));
newOAuthAccount.setEmail(giteeUserInfo.getEmail());
newOAuthAccount.setNickname(giteeUserInfo.getName());
newOAuthAccount.setAvatar(giteeUserInfo.getAvatarUrl());
oAuthAccountRepository.save(newOAuthAccount);
}

if (user.getStatus() == 0) {
throw new RuntimeException("当前用户已被禁用");
}

// 4. 生成本系统登录态
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(GiteeUserInfo giteeUserInfo) {
User newUser = new User();
newUser.setEmail(giteeUserInfo.getEmail());
newUser.setNickname(giteeUserInfo.getName() != null ? giteeUserInfo.getName() : giteeUserInfo.getLogin());
newUser.setAvatar(giteeUserInfo.getAvatarUrl());
newUser.setStatus(1);
userRepository.save(newUser);
return newUser;
}
}

和 Google 登录相比,唯一的区别在前两步:Google 是校验前端传来的 ID Token,Gitee 是用 code 换 token 再查用户信息。从第三步开始,绑定逻辑完全一致。

注意一点:Gitee 用户的邮箱可能为空(用户设置了隐私保护),所以 createUser 里不要假设 email 一定有值,用户表的 email 字段也要允许 NULL。

八、前后端联调

1. 本地启动

前端 5173 端口,后端 8080 端口,Vite 代理配置和 Google 那篇一样:

1
2
3
4
5
6
7
8
9
10
11
12
// vite.config.ts
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
});

2. 检查回调地址

确认 Gitee 应用配置中的回调地址包含:

1
http://localhost:5173/oauth/gitee/callback

并且前端环境变量 VITE_GITEE_REDIRECT_URI 和后端 GITEE_REDIRECT_URI 都是这个值。

3. 测试登录流程

1
2
3
4
5
1. 打开登录页,点击"使用 Gitee 登录"
2. 浏览器跳转到 gitee.com 授权页,登录并点击"同意授权"
3. 浏览器回到 /oauth/gitee/callback?code=xxx&state=yyy
4. 回调页把 code 发给后端,后端返回本系统 token
5. 前端保存 token,跳转首页

如果第 4 步失败,优先检查后端日志中 Gitee 返回的错误信息,大概率是 redirect_uri 不一致或 code 已被使用。

九、生产环境部署注意事项

1. 回调地址加上生产域名

Gitee 应用配置中补充:

1
https://yao-admin.com/oauth/gitee/callback

2. 必须使用 HTTPS

生产环境的回调地址必须是 HTTPS,否则授权过程中的 code 有被窃听的风险。

3. Client Secret 保护

1
2
3
1. 只通过环境变量注入,不进代码仓库
2. 怀疑泄露时,在 Gitee 应用设置中重置 Client Secret
3. 重置后记得同步更新服务器环境变量

4. Nginx 配置

前后端同域名部署时,Nginx 配置和《Google快捷登录配置》中完全一致,/ 指向前端静态文件,/api/ 反向代理到后端,回调地址 /oauth/gitee/callback 走前端路由即可,不需要额外配置。

注意:如果前端是单页应用,Nginx 必须配置 try_files $uri $uri/ /index.html;,否则用户刷新 /oauth/gitee/callback 这种前端路由地址会 404。

十、常见问题

1. 回调时报 redirect_uri_mismatch

授权请求里的 redirect_uri 和 Gitee 应用配置的回调地址不一致。检查:

1
2
3
4
1. 协议:http 和 https 不能混
2. 域名:localhost 和 127.0.0.1 是两个不同地址
3. 端口:5173 和 3000 不能混
4. 路径:必须完全一致,包括结尾是否有斜杠

2. 换取 token 时返回 invalid_grant

常见原因:

1
2
3
4
1. code 已经被使用过(code 是一次性的)
2. code 已过期,用户停留在回调页太久
3. 换 token 时传的 redirect_uri 和授权时的不一致
4. React 18 开发模式 useEffect 执行两次,第二次用的是已消费的 code

3. 拿到的用户信息里 email 是 null

用户在 Gitee 设置中隐藏了邮箱。处理方式:

1
2
方式一:允许 email 为空,用户登录后在个人中心自行补填
方式二:申请 emails 权限,调用 https://gitee.com/api/v5/emails 获取

对快捷登录来说,方式一通常就够用了。

4. 能不能把换 token 放在前端做

不能。

换 token 必须携带 Client Secret,放在前端等于把密钥公开。授权码模式的设计初衷就是让 code 经过前端、让 Secret 永远留在后端。

5. 需要保存 Gitee 的 Access Token 吗

如果只做登录,不需要。

换到用户信息、完成绑定之后,这个 token 的使命就结束了。只有当你后续还要代表用户调用 Gitee API(例如读取仓库列表)时,才需要考虑保存 access_token 和 refresh_token,并处理一天过期后的刷新逻辑。

6. 用户取消授权会发生什么

Gitee 会重定向回你的回调地址,但 URL 上没有 code,而是带着 error 参数。回调页要处理这种情况,给出”已取消登录”的提示并跳回登录页,而不是直接报错。

十一、最终落地清单

Gitee 平台需要完成:

1
2
3
4
1. 设置 → 第三方应用 → 创建应用
2. 填写应用名称、主页、回调地址
3. 勾选 user_info 权限
4. 复制 Client ID 和 Client Secret

前端需要完成:

1
2
3
4
5
1. 配置 VITE_GITEE_CLIENT_ID 和 VITE_GITEE_REDIRECT_URI
2. 登录按钮拼接授权地址并跳转,带上随机 state
3. 回调页校验 state、取出 code
4. 调用后端 /api/auth/gitee/login
5. 保存后端返回的本系统 token

后端需要完成:

1
2
3
4
5
6
7
1. 配置 client-id、client-secret、redirect-uri
2. 接收 code,POST https://gitee.com/oauth/token 换取 access_token
3. GET https://gitee.com/api/v5/user 获取用户信息
4. 根据 id 查询 sys_oauth_account 绑定关系
5. 查询或创建本地用户
6. 生成自己系统的登录态
7. 返回 token 和用户信息

十二、总结

Gitee 快捷登录是最标准的 OAuth2 授权码模式,核心就是三步:

1
2
3
1. 前端跳转 https://gitee.com/oauth/authorize 拿授权码 code
2. 后端用 code + Client Secret 换 Access Token
3. 后端调用用户信息接口,根据 id 完成本地用户绑定和登录

实际开发时,最容易出错的是:

1
2
3
4
5
1. redirect_uri 三处(Gitee 应用配置、前端跳转、后端换 token)不一致
2. code 被重复使用,尤其是 React 开发模式下 useEffect 执行两次
3. 把 Client Secret 放到了前端
4. 错误地用 login 或 email 作为唯一标识,应该用数字 id
5. 没有处理 email 为空的情况

理解了授权码模式之后,接入 GitHub、微博、QQ 等平台的登录都是同一套思路,区别只是接口地址和字段名。下一篇《Github快捷登录配置》会验证这一点。

赞赏一杯咖啡