ai-engineering-init 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/skill-forced-eval.js +46 -62
- package/.claude/settings.json +10 -1
- package/.claude/skills/api-development/SKILL.md +179 -130
- package/.claude/skills/architecture-design/SKILL.md +102 -212
- package/.claude/skills/backend-annotations/SKILL.md +166 -220
- package/.claude/skills/bug-detective/SKILL.md +225 -186
- package/.claude/skills/code-patterns/SKILL.md +127 -244
- package/.claude/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.claude/skills/crud-development/SKILL.md +226 -307
- package/.claude/skills/data-permission/SKILL.md +131 -202
- package/.claude/skills/database-ops/SKILL.md +158 -355
- package/.claude/skills/error-handler/SKILL.md +224 -285
- package/.claude/skills/file-oss-management/SKILL.md +174 -169
- package/.claude/skills/git-workflow/SKILL.md +123 -341
- package/.claude/skills/json-serialization/SKILL.md +121 -137
- package/.claude/skills/performance-doctor/SKILL.md +83 -89
- package/.claude/skills/redis-cache/SKILL.md +134 -185
- package/.claude/skills/scheduled-jobs/SKILL.md +187 -224
- package/.claude/skills/security-guard/SKILL.md +168 -276
- package/.claude/skills/sms-mail/SKILL.md +266 -228
- package/.claude/skills/social-login/SKILL.md +257 -195
- package/.claude/skills/tenant-management/SKILL.md +172 -188
- package/.claude/skills/utils-toolkit/SKILL.md +214 -222
- package/.claude/skills/websocket-sse/SKILL.md +251 -172
- package/.claude/skills/workflow-engine/SKILL.md +178 -250
- package/.codex/skills/api-development/SKILL.md +179 -130
- package/.codex/skills/architecture-design/SKILL.md +102 -212
- package/.codex/skills/backend-annotations/SKILL.md +166 -220
- package/.codex/skills/bug-detective/SKILL.md +225 -186
- package/.codex/skills/code-patterns/SKILL.md +127 -244
- package/.codex/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.codex/skills/crud-development/SKILL.md +226 -307
- package/.codex/skills/data-permission/SKILL.md +131 -202
- package/.codex/skills/database-ops/SKILL.md +158 -355
- package/.codex/skills/error-handler/SKILL.md +224 -285
- package/.codex/skills/file-oss-management/SKILL.md +174 -169
- package/.codex/skills/git-workflow/SKILL.md +123 -341
- package/.codex/skills/json-serialization/SKILL.md +121 -137
- package/.codex/skills/performance-doctor/SKILL.md +83 -89
- package/.codex/skills/redis-cache/SKILL.md +134 -185
- package/.codex/skills/scheduled-jobs/SKILL.md +187 -224
- package/.codex/skills/security-guard/SKILL.md +168 -276
- package/.codex/skills/sms-mail/SKILL.md +266 -228
- package/.codex/skills/social-login/SKILL.md +257 -195
- package/.codex/skills/tenant-management/SKILL.md +172 -188
- package/.codex/skills/utils-toolkit/SKILL.md +214 -222
- package/.codex/skills/websocket-sse/SKILL.md +251 -172
- package/.codex/skills/workflow-engine/SKILL.md +178 -250
- package/.cursor/hooks/cursor-skill-eval.js +66 -6
- package/.cursor/skills/api-development/SKILL.md +179 -130
- package/.cursor/skills/architecture-design/SKILL.md +102 -212
- package/.cursor/skills/backend-annotations/SKILL.md +166 -220
- package/.cursor/skills/bug-detective/SKILL.md +225 -186
- package/.cursor/skills/code-patterns/SKILL.md +127 -244
- package/.cursor/skills/collaborating-with-codex/SKILL.md +96 -113
- package/.cursor/skills/crud-development/SKILL.md +226 -307
- package/.cursor/skills/data-permission/SKILL.md +131 -202
- package/.cursor/skills/database-ops/SKILL.md +158 -355
- package/.cursor/skills/error-handler/SKILL.md +224 -285
- package/.cursor/skills/file-oss-management/SKILL.md +174 -169
- package/.cursor/skills/git-workflow/SKILL.md +123 -341
- package/.cursor/skills/json-serialization/SKILL.md +121 -137
- package/.cursor/skills/performance-doctor/SKILL.md +83 -89
- package/.cursor/skills/redis-cache/SKILL.md +134 -185
- package/.cursor/skills/scheduled-jobs/SKILL.md +187 -224
- package/.cursor/skills/security-guard/SKILL.md +168 -276
- package/.cursor/skills/sms-mail/SKILL.md +266 -228
- package/.cursor/skills/social-login/SKILL.md +257 -195
- package/.cursor/skills/tenant-management/SKILL.md +172 -188
- package/.cursor/skills/utils-toolkit/SKILL.md +214 -222
- package/.cursor/skills/websocket-sse/SKILL.md +251 -172
- package/.cursor/skills/workflow-engine/SKILL.md +178 -250
- package/AGENTS.md +49 -540
- package/CLAUDE.md +73 -119
- package/README.md +37 -6
- package/bin/index.js +5 -1
- package/package.json +1 -1
- package/src/skills/api-development/SKILL.md +179 -130
- package/src/skills/architecture-design/SKILL.md +102 -212
- package/src/skills/backend-annotations/SKILL.md +166 -220
- package/src/skills/bug-detective/SKILL.md +225 -186
- package/src/skills/code-patterns/SKILL.md +127 -244
- package/src/skills/collaborating-with-codex/SKILL.md +96 -113
- package/src/skills/crud-development/SKILL.md +226 -307
- package/src/skills/data-permission/SKILL.md +131 -202
- package/src/skills/database-ops/SKILL.md +158 -355
- package/src/skills/error-handler/SKILL.md +224 -285
- package/src/skills/file-oss-management/SKILL.md +174 -169
- package/src/skills/git-workflow/SKILL.md +123 -341
- package/src/skills/json-serialization/SKILL.md +121 -137
- package/src/skills/performance-doctor/SKILL.md +83 -89
- package/src/skills/redis-cache/SKILL.md +134 -185
- package/src/skills/scheduled-jobs/SKILL.md +187 -224
- package/src/skills/security-guard/SKILL.md +168 -276
- package/src/skills/sms-mail/SKILL.md +266 -228
- package/src/skills/social-login/SKILL.md +257 -195
- package/src/skills/tenant-management/SKILL.md +172 -188
- package/src/skills/utils-toolkit/SKILL.md +214 -222
- package/src/skills/websocket-sse/SKILL.md +251 -172
- package/src/skills/workflow-engine/SKILL.md +178 -250
- package/.claude/skills/skill-creator/LICENSE.txt +0 -202
- package/.claude/skills/skill-creator/SKILL.md +0 -479
- package/.claude/skills/skill-creator/agents/analyzer.md +0 -274
- package/.claude/skills/skill-creator/agents/comparator.md +0 -202
- package/.claude/skills/skill-creator/agents/grader.md +0 -223
- package/.claude/skills/skill-creator/assets/eval_review.html +0 -146
- package/.claude/skills/skill-creator/eval-viewer/generate_review.py +0 -471
- package/.claude/skills/skill-creator/eval-viewer/viewer.html +0 -1325
- package/.claude/skills/skill-creator/references/schemas.md +0 -430
- package/.claude/skills/skill-creator/scripts/__init__.py +0 -0
- package/.claude/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
- package/.claude/skills/skill-creator/scripts/generate_report.py +0 -326
- package/.claude/skills/skill-creator/scripts/improve_description.py +0 -248
- package/.claude/skills/skill-creator/scripts/package_skill.py +0 -136
- package/.claude/skills/skill-creator/scripts/quick_validate.py +0 -103
- package/.claude/skills/skill-creator/scripts/run_eval.py +0 -310
- package/.claude/skills/skill-creator/scripts/run_loop.py +0 -332
- package/.claude/skills/skill-creator/scripts/utils.py +0 -47
|
@@ -1,266 +1,328 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: social-login
|
|
3
3
|
description: |
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
通用 OAuth2 第三方登录开发指南。涵盖授权码流程、接口设计、账号绑定机制、多平台接入。
|
|
6
5
|
触发场景:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
触发词:第三方登录、微信登录、QQ登录、OAuth、OAuth2、JustAuth、社交登录、扫码登录、AuthRequest、SocialUtils、授权登录、GitHub登录、钉钉登录
|
|
6
|
+
- 接入微信/QQ/GitHub 等第三方登录
|
|
7
|
+
- 实现 OAuth2 授权码流程
|
|
8
|
+
- 实现社交账号与系统账号绑定/解绑
|
|
9
|
+
- 获取第三方用户信息
|
|
10
|
+
触发词:第三方登录、OAuth、OAuth2、社交登录、微信登录、QQ登录、GitHub登录、扫码登录、授权码、授权登录
|
|
11
|
+
注意:如果项目有专属技能,优先使用专属版本。
|
|
14
12
|
---
|
|
15
13
|
|
|
16
|
-
#
|
|
14
|
+
# OAuth2 第三方登录开发指南
|
|
17
15
|
|
|
18
|
-
>
|
|
19
|
-
> **特性**:Sa-Token 认证集成、Redis 状态缓存(防 CSRF)、多租户支持、账号绑定机制
|
|
16
|
+
> 通用模板。如果项目有专属技能,优先使用。
|
|
20
17
|
|
|
21
|
-
##
|
|
18
|
+
## 设计原则
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
| 支付宝 | `alipay_wallet` | QQ | `qq` |
|
|
28
|
-
| 微信开放平台 | `wechat_open` | 微信公众号 | `wechat_mp` |
|
|
29
|
-
| 企业微信 | `wechat_enterprise` | 抖音 | `douyin` |
|
|
30
|
-
| 华为 | `huawei` | 微软 | `microsoft` |
|
|
31
|
-
| MaxKey | `maxkey` | TopIAM | `topiam` |
|
|
32
|
-
| GitLab | `gitlab` | Gitea | `gitea` |
|
|
33
|
-
|
|
34
|
-
完整平台列表及特殊配置详见 `references/provider-configs.md`。
|
|
20
|
+
1. **标准协议**:遵循 OAuth 2.0 授权码模式(Authorization Code Flow),这是最安全的 OAuth 流程。
|
|
21
|
+
2. **状态防护**:使用 `state` 参数防止 CSRF 攻击,每次授权请求生成唯一 state 并校验。
|
|
22
|
+
3. **绑定机制**:第三方账号与系统账号通过绑定表关联,支持一个系统账号绑定多个第三方平台。
|
|
23
|
+
4. **信息最小化**:只获取必要的第三方用户信息(OpenID、昵称、头像),不过度获取。
|
|
35
24
|
|
|
36
25
|
---
|
|
37
26
|
|
|
38
|
-
##
|
|
27
|
+
## OAuth 2.0 授权码流程
|
|
39
28
|
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
redirect-uri: ${justauth.address}/social-callback?source=gitee
|
|
52
|
-
dingtalk:
|
|
53
|
-
client-id: ${DINGTALK_APP_KEY:}
|
|
54
|
-
client-secret: ${DINGTALK_APP_SECRET:}
|
|
55
|
-
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
|
|
29
|
+
```
|
|
30
|
+
用户 -> 前端 -> 后端(生成授权URL) -> 第三方平台(授权页)
|
|
31
|
+
|
|
|
32
|
+
用户授权 |
|
|
33
|
+
v
|
|
34
|
+
第三方平台 -> 前端回调页(携带 code + state)-> 后端
|
|
35
|
+
|
|
|
36
|
+
后端用 code 换 access_token |
|
|
37
|
+
后端用 access_token 获取用户信息 |
|
|
38
|
+
v
|
|
39
|
+
查绑定关系 -> 登录/绑定
|
|
56
40
|
```
|
|
57
41
|
|
|
58
|
-
|
|
42
|
+
### 步骤详解
|
|
59
43
|
|
|
60
|
-
|
|
44
|
+
| 步骤 | 描述 | 关键参数 |
|
|
45
|
+
|------|------|---------|
|
|
46
|
+
| 1. 构建授权 URL | 拼接第三方授权地址 | client_id, redirect_uri, state, scope |
|
|
47
|
+
| 2. 用户授权 | 用户在第三方平台确认授权 | - |
|
|
48
|
+
| 3. 回调获取 code | 第三方重定向回应用 | code, state |
|
|
49
|
+
| 4. code 换 token | 后端调用第三方 Token 接口 | code, client_id, client_secret |
|
|
50
|
+
| 5. 获取用户信息 | 后端调用第三方用户信息接口 | access_token |
|
|
51
|
+
| 6. 登录/绑定 | 根据 OpenID 查找绑定关系 | openId, source |
|
|
61
52
|
|
|
62
|
-
|
|
53
|
+
---
|
|
63
54
|
|
|
64
|
-
|
|
55
|
+
## 实现模式
|
|
65
56
|
|
|
66
|
-
|
|
57
|
+
### 一、抽象接口设计
|
|
67
58
|
|
|
68
59
|
```java
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
AuthRequest authRequest = SocialUtils.getAuthRequest("github", socialProperties);
|
|
76
|
-
|
|
77
|
-
// 生成授权 URL
|
|
78
|
-
String authorizeUrl = authRequest.authorize(state);
|
|
60
|
+
// 第三方认证请求接口
|
|
61
|
+
public interface SocialAuthProvider {
|
|
62
|
+
String getSource(); // 平台标识
|
|
63
|
+
String buildAuthorizeUrl(String state); // 构建授权URL
|
|
64
|
+
SocialUser authenticate(String code, String state); // 回调认证
|
|
65
|
+
}
|
|
79
66
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
String
|
|
87
|
-
String
|
|
88
|
-
String
|
|
67
|
+
// 第三方用户信息
|
|
68
|
+
@Data
|
|
69
|
+
public class SocialUser {
|
|
70
|
+
private String openId; // 平台唯一标识
|
|
71
|
+
private String source; // 来源平台(github, wechat_open 等)
|
|
72
|
+
private String nickname; // 昵称
|
|
73
|
+
private String avatar; // 头像
|
|
74
|
+
private String email; // 邮箱(可能为空)
|
|
75
|
+
private String accessToken; // 第三方 Token
|
|
76
|
+
private Map<String, Object> rawInfo; // 原始数据
|
|
89
77
|
}
|
|
90
78
|
```
|
|
91
79
|
|
|
92
|
-
###
|
|
80
|
+
### 二、GitHub 实现示例
|
|
93
81
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
| `nickname` | 昵称 | `avatar` | 头像 |
|
|
98
|
-
| `email` | 邮箱 | `source` | 来源平台 |
|
|
99
|
-
| `token` | Token 信息 | `rawUserInfo` | 原始数据(Map) |
|
|
82
|
+
```java
|
|
83
|
+
@Component
|
|
84
|
+
public class GitHubAuthProvider implements SocialAuthProvider {
|
|
100
85
|
|
|
101
|
-
|
|
86
|
+
@Value("${social.github.client-id}")
|
|
87
|
+
private String clientId;
|
|
102
88
|
|
|
103
|
-
|
|
89
|
+
@Value("${social.github.client-secret}")
|
|
90
|
+
private String clientSecret;
|
|
104
91
|
|
|
105
|
-
|
|
92
|
+
@Value("${social.github.redirect-uri}")
|
|
93
|
+
private String redirectUri;
|
|
106
94
|
|
|
107
|
-
|
|
95
|
+
@Override
|
|
96
|
+
public String getSource() { return "github"; }
|
|
108
97
|
|
|
109
|
-
|
|
98
|
+
@Override
|
|
99
|
+
public String buildAuthorizeUrl(String state) {
|
|
100
|
+
return "https://github.com/login/oauth/authorize"
|
|
101
|
+
+ "?client_id=" + clientId
|
|
102
|
+
+ "&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8)
|
|
103
|
+
+ "&state=" + state
|
|
104
|
+
+ "&scope=user:email";
|
|
105
|
+
}
|
|
110
106
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
@Override
|
|
108
|
+
public SocialUser authenticate(String code, String state) {
|
|
109
|
+
// 1. code 换 access_token
|
|
110
|
+
String tokenUrl = "https://github.com/login/oauth/access_token";
|
|
111
|
+
Map<String, String> body = Map.of(
|
|
112
|
+
"client_id", clientId,
|
|
113
|
+
"client_secret", clientSecret,
|
|
114
|
+
"code", code
|
|
115
|
+
);
|
|
116
|
+
String accessToken = httpPost(tokenUrl, body); // 解析响应获取 token
|
|
117
|
+
|
|
118
|
+
// 2. 获取用户信息
|
|
119
|
+
String userInfo = httpGet("https://api.github.com/user",
|
|
120
|
+
Map.of("Authorization", "Bearer " + accessToken));
|
|
121
|
+
|
|
122
|
+
// 3. 构建 SocialUser
|
|
123
|
+
SocialUser user = new SocialUser();
|
|
124
|
+
user.setOpenId(parseField(userInfo, "id"));
|
|
125
|
+
user.setSource("github");
|
|
126
|
+
user.setNickname(parseField(userInfo, "login"));
|
|
127
|
+
user.setAvatar(parseField(userInfo, "avatar_url"));
|
|
128
|
+
user.setEmail(parseField(userInfo, "email"));
|
|
129
|
+
return user;
|
|
130
|
+
}
|
|
117
131
|
}
|
|
118
132
|
```
|
|
119
133
|
|
|
120
|
-
###
|
|
134
|
+
### 三、Controller 层
|
|
121
135
|
|
|
122
136
|
```java
|
|
123
|
-
@
|
|
124
|
-
@
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
@RestController
|
|
138
|
+
@RequestMapping("/auth/social")
|
|
139
|
+
public class SocialLoginController {
|
|
140
|
+
|
|
141
|
+
@Autowired
|
|
142
|
+
private Map<String, SocialAuthProvider> providers; // Spring 自动注入所有实现
|
|
143
|
+
|
|
144
|
+
@Autowired
|
|
145
|
+
private StringRedisTemplate redisTemplate;
|
|
146
|
+
|
|
147
|
+
@Autowired
|
|
148
|
+
private SocialBindService bindService;
|
|
149
|
+
|
|
150
|
+
// 1. 获取授权 URL
|
|
151
|
+
@GetMapping("/authorize/{source}")
|
|
152
|
+
public Result<String> authorize(@PathVariable String source) {
|
|
153
|
+
SocialAuthProvider provider = getProvider(source);
|
|
154
|
+
String state = UUID.randomUUID().toString().replace("-", "");
|
|
155
|
+
// state 存入 Redis,3分钟有效
|
|
156
|
+
redisTemplate.opsForValue().set("social:state:" + state, source, 3, TimeUnit.MINUTES);
|
|
157
|
+
return Result.ok(provider.buildAuthorizeUrl(state));
|
|
158
|
+
}
|
|
127
159
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
loginBody.getSource(), loginBody.getSocialCode(),
|
|
136
|
-
loginBody.getSocialState(), socialProperties);
|
|
137
|
-
if (!response.ok()) {
|
|
138
|
-
throw new ServiceException(response.getMsg());
|
|
160
|
+
// 2. 回调登录
|
|
161
|
+
@PostMapping("/callback")
|
|
162
|
+
public Result<?> callback(@RequestBody SocialCallbackDTO dto) {
|
|
163
|
+
// 校验 state
|
|
164
|
+
String cachedSource = redisTemplate.opsForValue().get("social:state:" + dto.getState());
|
|
165
|
+
if (cachedSource == null) {
|
|
166
|
+
throw new [你的异常类]("授权已过期,请重新操作");
|
|
139
167
|
}
|
|
168
|
+
redisTemplate.delete("social:state:" + dto.getState());
|
|
169
|
+
|
|
170
|
+
// 获取第三方用户信息
|
|
171
|
+
SocialAuthProvider provider = getProvider(dto.getSource());
|
|
172
|
+
SocialUser socialUser = provider.authenticate(dto.getCode(), dto.getState());
|
|
140
173
|
|
|
141
|
-
//
|
|
142
|
-
String authId =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
174
|
+
// 查找绑定关系
|
|
175
|
+
String authId = socialUser.getSource() + ":" + socialUser.getOpenId();
|
|
176
|
+
SocialBind bind = bindService.findByAuthId(authId);
|
|
177
|
+
|
|
178
|
+
if (bind == null) {
|
|
179
|
+
// 未绑定 -> 返回第三方信息,引导绑定或注册
|
|
180
|
+
return Result.fail("NEED_BINDIND", "请绑定系统账号", socialUser);
|
|
146
181
|
}
|
|
147
182
|
|
|
148
|
-
//
|
|
149
|
-
LoginUser loginUser =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.setTimeout(client.getTimeout())
|
|
153
|
-
.setActiveTimeout(client.getActiveTimeout()));
|
|
154
|
-
|
|
155
|
-
LoginVo loginVo = new LoginVo();
|
|
156
|
-
loginVo.setAccessToken(StpUtil.getTokenValue());
|
|
157
|
-
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
|
158
|
-
return loginVo;
|
|
183
|
+
// 已绑定 -> 执行登录
|
|
184
|
+
LoginUser loginUser = loadUserById(bind.getUserId());
|
|
185
|
+
String token = [你的认证工具类].login(loginUser);
|
|
186
|
+
return Result.ok(Map.of("token", token));
|
|
159
187
|
}
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
188
|
|
|
163
|
-
|
|
189
|
+
// 3. 绑定(已登录用户绑定第三方账号)
|
|
190
|
+
@PostMapping("/bind")
|
|
191
|
+
public Result<?> bind(@RequestBody SocialCallbackDTO dto) {
|
|
192
|
+
[你的认证工具类].checkLogin();
|
|
193
|
+
Long currentUserId = [你的认证工具类].getCurrentUserId();
|
|
164
194
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@PostMapping("/social/callback")
|
|
168
|
-
public R<Void> socialCallback(@RequestBody SocialLoginBody loginBody) {
|
|
169
|
-
StpUtil.checkLogin();
|
|
170
|
-
AuthResponse<AuthUser> response = SocialUtils.loginAuth(...);
|
|
171
|
-
if (!response.ok()) return R.fail(response.getMsg());
|
|
172
|
-
loginService.socialRegister(response.getData());
|
|
173
|
-
return R.ok();
|
|
174
|
-
}
|
|
195
|
+
SocialAuthProvider provider = getProvider(dto.getSource());
|
|
196
|
+
SocialUser socialUser = provider.authenticate(dto.getCode(), dto.getState());
|
|
175
197
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
```
|
|
198
|
+
String authId = socialUser.getSource() + ":" + socialUser.getOpenId();
|
|
199
|
+
bindService.bindOrUpdate(currentUserId, authId, socialUser);
|
|
200
|
+
return Result.ok("绑定成功");
|
|
201
|
+
}
|
|
181
202
|
|
|
182
|
-
|
|
203
|
+
// 4. 解绑
|
|
204
|
+
@DeleteMapping("/unbind/{bindId}")
|
|
205
|
+
public Result<?> unbind(@PathVariable Long bindId) {
|
|
206
|
+
[你的认证工具类].checkLogin();
|
|
207
|
+
bindService.unbind(bindId, [你的认证工具类].getCurrentUserId());
|
|
208
|
+
return Result.ok("已解除绑定");
|
|
209
|
+
}
|
|
183
210
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
211
|
+
private SocialAuthProvider getProvider(String source) {
|
|
212
|
+
// providers Map 的 key 是 Bean 名称,需要匹配 source
|
|
213
|
+
return providers.values().stream()
|
|
214
|
+
.filter(p -> p.getSource().equals(source))
|
|
215
|
+
.findFirst()
|
|
216
|
+
.orElseThrow(() -> new [你的异常类]("不支持的登录平台: " + source));
|
|
217
|
+
}
|
|
189
218
|
}
|
|
190
219
|
```
|
|
191
220
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
221
|
+
### 四、绑定表设计
|
|
222
|
+
|
|
223
|
+
```sql
|
|
224
|
+
CREATE TABLE sys_social_bind (
|
|
225
|
+
id BIGINT NOT NULL COMMENT '主键',
|
|
226
|
+
user_id BIGINT NOT NULL COMMENT '系统用户ID',
|
|
227
|
+
auth_id VARCHAR(128) NOT NULL COMMENT '唯一标识 (source:openId)',
|
|
228
|
+
source VARCHAR(32) NOT NULL COMMENT '来源平台',
|
|
229
|
+
open_id VARCHAR(128) NOT NULL COMMENT '平台用户ID',
|
|
230
|
+
nickname VARCHAR(64) COMMENT '昵称',
|
|
231
|
+
avatar VARCHAR(512) COMMENT '头像',
|
|
232
|
+
email VARCHAR(128) COMMENT '邮箱',
|
|
233
|
+
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
234
|
+
PRIMARY KEY (id),
|
|
235
|
+
UNIQUE KEY uk_auth_id (auth_id),
|
|
236
|
+
KEY idx_user_id (user_id)
|
|
237
|
+
);
|
|
238
|
+
```
|
|
195
239
|
|
|
196
|
-
|
|
197
|
-
// 跳转授权
|
|
198
|
-
const { data } = await request.get(`/auth/binding/${source}`);
|
|
199
|
-
window.location.href = data;
|
|
240
|
+
### 五、配置
|
|
200
241
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
242
|
+
```yaml
|
|
243
|
+
social:
|
|
244
|
+
github:
|
|
245
|
+
client-id: ${GITHUB_CLIENT_ID:}
|
|
246
|
+
client-secret: ${GITHUB_CLIENT_SECRET:}
|
|
247
|
+
redirect-uri: https://your-domain.com/social-callback?source=github
|
|
248
|
+
wechat:
|
|
249
|
+
app-id: ${WECHAT_APP_ID:}
|
|
250
|
+
app-secret: ${WECHAT_APP_SECRET:}
|
|
251
|
+
redirect-uri: https://your-domain.com/social-callback?source=wechat_open
|
|
209
252
|
```
|
|
210
253
|
|
|
211
254
|
---
|
|
212
255
|
|
|
213
|
-
##
|
|
256
|
+
## 选型建议
|
|
214
257
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
258
|
+
| 方案 | 优点 | 缺点 | 适用场景 |
|
|
259
|
+
|------|------|------|---------|
|
|
260
|
+
| 自研(如上) | 完全可控、无依赖 | 每个平台需手动对接 | 接入 1-3 个平台 |
|
|
261
|
+
| JustAuth | 开箱即用、20+ 平台 | 引入第三方依赖 | 多平台快速接入 |
|
|
262
|
+
| Spring Security OAuth2 Client | Spring 生态原生 | 配置复杂 | 企业级、标准 OAuth2 |
|
|
218
263
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
264
|
+
### 常见平台接入
|
|
265
|
+
|
|
266
|
+
| 平台 | 标识 | 特殊要求 |
|
|
267
|
+
|------|------|---------|
|
|
268
|
+
| GitHub | `github` | 无 |
|
|
269
|
+
| 微信开放平台 | `wechat_open` | 需企业开发者认证 |
|
|
270
|
+
| 微信公众号 | `wechat_mp` | 需服务号 |
|
|
271
|
+
| QQ | `qq` | 需备案域名 |
|
|
272
|
+
| 钉钉 | `dingtalk` | 需创建 H5 微应用 |
|
|
273
|
+
| 企业微信 | `wechat_enterprise` | 需 agentId |
|
|
274
|
+
| 支付宝 | `alipay` | 需应用公钥/私钥 |
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 常见错误
|
|
222
279
|
|
|
223
280
|
```java
|
|
224
|
-
//
|
|
225
|
-
|
|
281
|
+
// 1. 不校验 state 参数(CSRF 攻击风险)
|
|
282
|
+
SocialUser user = provider.authenticate(code, state);
|
|
283
|
+
// 应先从 Redis 校验 state 是否有效
|
|
226
284
|
|
|
227
|
-
//
|
|
228
|
-
|
|
285
|
+
// 2. 回调地址与第三方平台配置不一致
|
|
286
|
+
// 应确保 redirect_uri 与第三方平台配置完全一致(包括协议、域名、路径、参数)
|
|
229
287
|
|
|
230
|
-
//
|
|
231
|
-
|
|
288
|
+
// 3. 不检查认证响应
|
|
289
|
+
SocialUser user = provider.authenticate(code, state);
|
|
290
|
+
user.getOpenId(); // 认证可能失败,user 为 null 或字段缺失
|
|
291
|
+
// 应先检查认证结果
|
|
232
292
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
```
|
|
293
|
+
// 4. source 标识拼写错误
|
|
294
|
+
getProvider("wechat"); // 不存在
|
|
295
|
+
getProvider("wechat_open"); // 正确
|
|
237
296
|
|
|
238
|
-
|
|
297
|
+
// 5. 绑定关系不检查冲突
|
|
298
|
+
// 同一个第三方账号被多个系统账号绑定
|
|
299
|
+
// authId 应设为唯一索引
|
|
239
300
|
|
|
240
|
-
|
|
301
|
+
// 6. 未处理 Token 过期
|
|
302
|
+
// 第三方 access_token 有有效期,需要用 refresh_token 刷新
|
|
303
|
+
// 或每次登录重新获取
|
|
241
304
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
public AuthCustomRequest(AuthConfig config, AuthStateCache stateCache) {
|
|
245
|
-
super(config, AuthCustomSource.CUSTOM, stateCache);
|
|
246
|
-
}
|
|
247
|
-
@Override
|
|
248
|
-
protected AuthToken getAccessToken(AuthCallback authCallback) { ... }
|
|
249
|
-
@Override
|
|
250
|
-
protected AuthUser getUserInfo(AuthToken authToken) { ... }
|
|
251
|
-
}
|
|
252
|
-
// 然后在 SocialUtils.getAuthRequest() 中添加对应 case
|
|
305
|
+
// 7. 前端直接传 client_secret
|
|
306
|
+
// client_secret 只能在后端使用,绝不能暴露给前端
|
|
253
307
|
```
|
|
254
308
|
|
|
255
|
-
|
|
309
|
+
### 前端集成参考
|
|
256
310
|
|
|
257
|
-
|
|
311
|
+
```javascript
|
|
312
|
+
// 跳转授权
|
|
313
|
+
const { data: authorizeUrl } = await request.get(`/auth/social/authorize/${source}`);
|
|
314
|
+
window.location.href = authorizeUrl;
|
|
258
315
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
316
|
+
// 回调页面处理
|
|
317
|
+
const { source, code, state } = getQueryParams();
|
|
318
|
+
const { data } = await request.post('/auth/social/callback', {
|
|
319
|
+
source, code, state
|
|
320
|
+
});
|
|
321
|
+
if (data.token) {
|
|
322
|
+
setToken(data.token);
|
|
323
|
+
router.push('/');
|
|
324
|
+
} else {
|
|
325
|
+
// 引导绑定或注册
|
|
326
|
+
router.push({ path: '/bindAccount', query: { source, code, state } });
|
|
327
|
+
}
|
|
328
|
+
```
|