1. 微信登录扫码概述

微信扫码登录是一种基于OAuth2.0协议的第三方登录方式,用户通过扫描二维码完成身份验证和授权,无需输入用户名密码即可快速登录。本文将详细介绍微信扫码登录的配置、OAuth2.0授权流程、用户信息获取和登录状态管理的完整实现。

1.1 核心功能

  1. 扫码登录: 生成二维码供用户扫描登录
  2. OAuth2.0授权: 微信OAuth2.0授权流程实现
  3. 用户信息获取: 获取微信用户基本信息
  4. 登录状态管理: 登录状态跟踪和会话管理
  5. 安全控制: 防重放攻击和状态验证

1.2 技术架构

1
2
3
4
5
用户扫码 → 微信授权 → 回调处理 → 用户信息获取
↓ ↓ ↓ ↓
二维码 → 微信服务器 → 授权码 → 访问令牌
↓ ↓ ↓ ↓
登录成功 → 用户信息 → 本地存储 → 会话管理

2. 微信登录配置

2.1 微信登录配置类

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
/**
* 微信登录配置类
*/
@Configuration
public class WeChatLoginConfig {

@Value("${wechat.app-id}")
private String appId;

@Value("${wechat.app-secret}")
private String appSecret;

@Value("${wechat.redirect-uri}")
private String redirectUri;

@Value("${wechat.scope}")
private String scope;

@Value("${wechat.state-expire-time}")
private int stateExpireTime;

/**
* 微信登录配置属性
*/
@Bean
public WeChatLoginProperties weChatLoginProperties() {
return WeChatLoginProperties.builder()
.appId(appId)
.appSecret(appSecret)
.redirectUri(redirectUri)
.scope(scope)
.stateExpireTime(stateExpireTime)
.build();
}

/**
* 微信API客户端
*/
@Bean
public WeChatApiClient weChatApiClient() {
return new WeChatApiClient(weChatLoginProperties());
}

/**
* 微信登录服务
*/
@Bean
public WeChatLoginService weChatLoginService() {
return new WeChatLoginService(weChatLoginProperties(), weChatApiClient());
}
}

/**
* 微信登录配置属性
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WeChatLoginProperties {
private String appId;
private String appSecret;
private String redirectUri;
private String scope;
private int stateExpireTime;

// 微信API地址
private String authorizeUrl = "https://open.weixin.qq.com/connect/qrconnect";
private String accessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token";
private String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo";
private String refreshTokenUrl = "https://api.weixin.qq.com/sns/oauth2/refresh_token";
}

2.2 应用配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# application.yml
wechat:
app-id: your-app-id
app-secret: your-app-secret
redirect-uri: http://localhost:8080/auth/wechat/callback
scope: snsapi_login
state-expire-time: 600 # 10分钟

# 登录配置
login:
session:
timeout: 1800 # 30分钟
max-inactive-interval: 300 # 5分钟
token:
expire-time: 7200 # 2小时
refresh-expire-time: 604800 # 7天
security:
csrf-enabled: true
cors-enabled: true

3. 微信API客户端

3.1 微信API客户端

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
/**
* 微信API客户端
*/
@Component
public class WeChatApiClient {

private final WeChatLoginProperties properties;
private final RestTemplate restTemplate;

public WeChatApiClient(WeChatLoginProperties properties) {
this.properties = properties;
this.restTemplate = createRestTemplate();
}

/**
* 创建RestTemplate
*/
private RestTemplate createRestTemplate() {
RestTemplate template = new RestTemplate();

// 设置超时时间
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
template.setRequestFactory(factory);

return template;
}

/**
* 获取授权URL
* @param state 状态参数
* @return 授权URL
*/
public String getAuthorizeUrl(String state) {
try {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(properties.getAuthorizeUrl())
.queryParam("appid", properties.getAppId())
.queryParam("redirect_uri", URLEncoder.encode(properties.getRedirectUri(), "UTF-8"))
.queryParam("response_type", "code")
.queryParam("scope", properties.getScope())
.queryParam("state", state);

String url = builder.toUriString() + "#wechat_redirect";

log.debug("生成微信授权URL: state={}", state);

return url;

} catch (Exception e) {
log.error("生成微信授权URL失败: state={}", state, e);
throw new BusinessException("生成微信授权URL失败", e);
}
}

/**
* 通过授权码获取访问令牌
* @param code 授权码
* @return 访问令牌信息
*/
public WeChatAccessToken getAccessToken(String code) {
try {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(properties.getAccessTokenUrl())
.queryParam("appid", properties.getAppId())
.queryParam("secret", properties.getAppSecret())
.queryParam("code", code)
.queryParam("grant_type", "authorization_code");

String url = builder.toUriString();

ResponseEntity<WeChatAccessToken> response = restTemplate.getForEntity(url, WeChatAccessToken.class);

if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
WeChatAccessToken token = response.getBody();

if (token.getErrcode() != null && token.getErrcode() != 0) {
throw new BusinessException("获取访问令牌失败: " + token.getErrmsg());
}

log.debug("获取微信访问令牌成功: openid={}", token.getOpenid());

return token;
} else {
throw new BusinessException("获取访问令牌失败: HTTP状态码 " + response.getStatusCode());
}

} catch (Exception e) {
log.error("获取微信访问令牌失败: code={}", code, e);
throw new BusinessException("获取微信访问令牌失败", e);
}
}

/**
* 刷新访问令牌
* @param refreshToken 刷新令牌
* @return 新的访问令牌信息
*/
public WeChatAccessToken refreshAccessToken(String refreshToken) {
try {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(properties.getRefreshTokenUrl())
.queryParam("appid", properties.getAppId())
.queryParam("grant_type", "refresh_token")
.queryParam("refresh_token", refreshToken);

String url = builder.toUriString();

ResponseEntity<WeChatAccessToken> response = restTemplate.getForEntity(url, WeChatAccessToken.class);

if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
WeChatAccessToken token = response.getBody();

if (token.getErrcode() != null && token.getErrcode() != 0) {
throw new BusinessException("刷新访问令牌失败: " + token.getErrmsg());
}

log.debug("刷新微信访问令牌成功: openid={}", token.getOpenid());

return token;
} else {
throw new BusinessException("刷新访问令牌失败: HTTP状态码 " + response.getStatusCode());
}

} catch (Exception e) {
log.error("刷新微信访问令牌失败: refreshToken={}", refreshToken, e);
throw new BusinessException("刷新微信访问令牌失败", e);
}
}

/**
* 获取用户信息
* @param accessToken 访问令牌
* @param openid 用户openid
* @return 用户信息
*/
public WeChatUserInfo getUserInfo(String accessToken, String openid) {
try {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(properties.getUserInfoUrl())
.queryParam("access_token", accessToken)
.queryParam("openid", openid);

String url = builder.toUriString();

ResponseEntity<WeChatUserInfo> response = restTemplate.getForEntity(url, WeChatUserInfo.class);

if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
WeChatUserInfo userInfo = response.getBody();

if (userInfo.getErrcode() != null && userInfo.getErrcode() != 0) {
throw new BusinessException("获取用户信息失败: " + userInfo.getErrmsg());
}

log.debug("获取微信用户信息成功: openid={}, nickname={}",
userInfo.getOpenid(), userInfo.getNickname());

return userInfo;
} else {
throw new BusinessException("获取用户信息失败: HTTP状态码 " + response.getStatusCode());
}

} catch (Exception e) {
log.error("获取微信用户信息失败: accessToken={}, openid={}", accessToken, openid, e);
throw new BusinessException("获取微信用户信息失败", e);
}
}
}

/**
* 微信访问令牌
*/
@Data
public class WeChatAccessToken {
private String accessToken;
private Integer expiresIn;
private String refreshToken;
private String openid;
private String scope;
private String unionid;

// 错误信息
private Integer errcode;
private String errmsg;
}

/**
* 微信用户信息
*/
@Data
public class WeChatUserInfo {
private String openid;
private String nickname;
private Integer sex;
private String province;
private String city;
private String country;
private String headimgurl;
private String unionid;
private String language;

// 错误信息
private Integer errcode;
private String errmsg;
}

4. 微信登录服务

4.1 微信登录服务

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
/**
* 微信登录服务
*/
@Service
public class WeChatLoginService {

private final WeChatLoginProperties properties;
private final WeChatApiClient apiClient;
private final RedisTemplate<String, Object> redisTemplate;
private final UserService userService;

public WeChatLoginService(WeChatLoginProperties properties,
WeChatApiClient apiClient,
RedisTemplate<String, Object> redisTemplate,
UserService userService) {
this.properties = properties;
this.apiClient = apiClient;
this.redisTemplate = redisTemplate;
this.userService = userService;
}

/**
* 生成登录二维码
* @return 二维码信息
*/
public QRCodeInfo generateQRCode() {
try {
// 1. 生成状态参数
String state = generateState();

// 2. 生成授权URL
String authorizeUrl = apiClient.getAuthorizeUrl(state);

// 3. 生成二维码
QRCodeInfo qrCodeInfo = QRCodeInfo.builder()
.state(state)
.qrCodeUrl(authorizeUrl)
.expireTime(LocalDateTime.now().plusSeconds(properties.getStateExpireTime()))
.status("PENDING")
.build();

// 4. 保存状态到Redis
String stateKey = "wechat:login:state:" + state;
redisTemplate.opsForValue().set(stateKey, qrCodeInfo,
Duration.ofSeconds(properties.getStateExpireTime()));

log.info("生成微信登录二维码: state={}", state);

return qrCodeInfo;

} catch (Exception e) {
log.error("生成微信登录二维码失败", e);
throw new BusinessException("生成微信登录二维码失败", e);
}
}

/**
* 处理微信授权回调
* @param code 授权码
* @param state 状态参数
* @return 登录结果
*/
public LoginResult handleCallback(String code, String state) {
try {
// 1. 验证状态参数
if (!validateState(state)) {
return LoginResult.error("无效的状态参数");
}

// 2. 获取访问令牌
WeChatAccessToken accessToken = apiClient.getAccessToken(code);

// 3. 获取用户信息
WeChatUserInfo userInfo = apiClient.getUserInfo(accessToken.getAccessToken(), accessToken.getOpenid());

// 4. 处理用户登录
return processUserLogin(userInfo, accessToken);

} catch (Exception e) {
log.error("处理微信授权回调失败: code={}, state={}", code, state, e);
return LoginResult.error("登录失败: " + e.getMessage());
}
}

/**
* 处理用户登录
* @param userInfo 微信用户信息
* @param accessToken 访问令牌
* @return 登录结果
*/
private LoginResult processUserLogin(WeChatUserInfo userInfo, WeChatAccessToken accessToken) {
try {
// 1. 查找或创建用户
User user = findOrCreateUser(userInfo);

// 2. 更新用户信息
updateUserInfo(user, userInfo);

// 3. 保存微信授权信息
saveWeChatAuth(user, accessToken);

// 4. 生成登录令牌
String loginToken = generateLoginToken(user);

// 5. 更新二维码状态
updateQRCodeStatus(userInfo.getOpenid(), "SUCCESS");

log.info("微信用户登录成功: userId={}, openid={}", user.getId(), userInfo.getOpenid());

return LoginResult.success(user, loginToken);

} catch (Exception e) {
log.error("处理用户登录失败: openid={}", userInfo.getOpenid(), e);
return LoginResult.error("登录失败: " + e.getMessage());
}
}

/**
* 查找或创建用户
* @param userInfo 微信用户信息
* @return 用户信息
*/
private User findOrCreateUser(WeChatUserInfo userInfo) {
try {
// 1. 根据openid查找用户
User user = userService.findByWeChatOpenid(userInfo.getOpenid());

if (user == null) {
// 2. 创建新用户
user = User.builder()
.username("wx_" + userInfo.getOpenid())
.nickname(userInfo.getNickname())
.avatar(userInfo.getHeadimgurl())
.gender(userInfo.getSex())
.province(userInfo.getProvince())
.city(userInfo.getCity())
.country(userInfo.getCountry())
.weChatOpenid(userInfo.getOpenid())
.weChatUnionid(userInfo.getUnionid())
.status("ACTIVE")
.createTime(LocalDateTime.now())
.build();

user = userService.createUser(user);

log.info("创建新用户: userId={}, openid={}", user.getId(), userInfo.getOpenid());
}

return user;

} catch (Exception e) {
log.error("查找或创建用户失败: openid={}", userInfo.getOpenid(), e);
throw new BusinessException("查找或创建用户失败", e);
}
}

/**
* 更新用户信息
* @param user 用户信息
* @param userInfo 微信用户信息
*/
private void updateUserInfo(User user, WeChatUserInfo userInfo) {
try {
boolean needUpdate = false;

// 更新昵称
if (!Objects.equals(user.getNickname(), userInfo.getNickname())) {
user.setNickname(userInfo.getNickname());
needUpdate = true;
}

// 更新头像
if (!Objects.equals(user.getAvatar(), userInfo.getHeadimgurl())) {
user.setAvatar(userInfo.getHeadimgurl());
needUpdate = true;
}

// 更新地区信息
if (!Objects.equals(user.getProvince(), userInfo.getProvince()) ||
!Objects.equals(user.getCity(), userInfo.getCity()) ||
!Objects.equals(user.getCountry(), userInfo.getCountry())) {
user.setProvince(userInfo.getProvince());
user.setCity(userInfo.getCity());
user.setCountry(userInfo.getCountry());
needUpdate = true;
}

if (needUpdate) {
user.setUpdateTime(LocalDateTime.now());
userService.updateUser(user);

log.debug("更新用户信息: userId={}", user.getId());
}

} catch (Exception e) {
log.error("更新用户信息失败: userId={}", user.getId(), e);
}
}

/**
* 保存微信授权信息
* @param user 用户信息
* @param accessToken 访问令牌
*/
private void saveWeChatAuth(User user, WeChatAccessToken accessToken) {
try {
WeChatAuth auth = WeChatAuth.builder()
.userId(user.getId())
.openid(accessToken.getOpenid())
.unionid(accessToken.getUnionid())
.accessToken(accessToken.getAccessToken())
.refreshToken(accessToken.getRefreshToken())
.expiresIn(accessToken.getExpiresIn())
.scope(accessToken.getScope())
.createTime(LocalDateTime.now())
.build();

userService.saveWeChatAuth(auth);

} catch (Exception e) {
log.error("保存微信授权信息失败: userId={}", user.getId(), e);
}
}

/**
* 生成状态参数
* @return 状态参数
*/
private String generateState() {
return UUID.randomUUID().toString().replace("-", "");
}

/**
* 验证状态参数
* @param state 状态参数
* @return 是否有效
*/
private boolean validateState(String state) {
try {
String stateKey = "wechat:login:state:" + state;
QRCodeInfo qrCodeInfo = (QRCodeInfo) redisTemplate.opsForValue().get(stateKey);

if (qrCodeInfo == null) {
log.warn("状态参数不存在或已过期: state={}", state);
return false;
}

if (qrCodeInfo.getExpireTime().isBefore(LocalDateTime.now())) {
log.warn("状态参数已过期: state={}", state);
redisTemplate.delete(stateKey);
return false;
}

return true;

} catch (Exception e) {
log.error("验证状态参数失败: state={}", state, e);
return false;
}
}

/**
* 生成登录令牌
* @param user 用户信息
* @return 登录令牌
*/
private String generateLoginToken(User user) {
try {
String token = JWT.create()
.withSubject(user.getId().toString())
.withClaim("username", user.getUsername())
.withClaim("openid", user.getWeChatOpenid())
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis() + 7200000)) // 2小时
.sign(Algorithm.HMAC256("your-secret-key"));

// 保存令牌到Redis
String tokenKey = "login:token:" + token;
redisTemplate.opsForValue().set(tokenKey, user.getId(), Duration.ofHours(2));

return token;

} catch (Exception e) {
log.error("生成登录令牌失败: userId={}", user.getId(), e);
throw new BusinessException("生成登录令牌失败", e);
}
}

/**
* 更新二维码状态
* @param openid 用户openid
* @param status 状态
*/
private void updateQRCodeStatus(String openid, String status) {
try {
// 这里可以实现二维码状态更新逻辑
// 例如:通过WebSocket通知前端更新状态

log.debug("更新二维码状态: openid={}, status={}", openid, status);

} catch (Exception e) {
log.error("更新二维码状态失败: openid={}", openid, e);
}
}
}

/**
* 二维码信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class QRCodeInfo {
private String state;
private String qrCodeUrl;
private LocalDateTime expireTime;
private String status;
}

/**
* 登录结果
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginResult {
private boolean success;
private User user;
private String token;
private String message;

public static LoginResult success(User user, String token) {
return LoginResult.builder()
.success(true)
.user(user)
.token(token)
.build();
}

public static LoginResult error(String message) {
return LoginResult.builder()
.success(false)
.message(message)
.build();
}
}

/**
* 微信授权信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "wechat_auth")
public class WeChatAuth {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private String openid;
private String unionid;
private String accessToken;
private String refreshToken;
private Integer expiresIn;
private String scope;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

5. 微信登录控制器

5.1 微信登录控制器

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/**
* 微信登录控制器
*/
@RestController
@RequestMapping("/auth/wechat")
public class WeChatLoginController {

@Autowired
private WeChatLoginService weChatLoginService;

@Autowired
private UserService userService;

/**
* 生成登录二维码
*/
@GetMapping("/qrcode")
public ResponseEntity<Map<String, Object>> generateQRCode() {
try {
QRCodeInfo qrCodeInfo = weChatLoginService.generateQRCode();

Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("state", qrCodeInfo.getState());
result.put("qrCodeUrl", qrCodeInfo.getQrCodeUrl());
result.put("expireTime", qrCodeInfo.getExpireTime());

return ResponseEntity.ok(result);

} catch (Exception e) {
log.error("生成登录二维码失败", e);

Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "生成二维码失败: " + e.getMessage());

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}

/**
* 微信授权回调
*/
@GetMapping("/callback")
public ResponseEntity<Map<String, Object>> callback(
@RequestParam String code,
@RequestParam String state) {

try {
LoginResult loginResult = weChatLoginService.handleCallback(code, state);

Map<String, Object> result = new HashMap<>();
result.put("success", loginResult.isSuccess());

if (loginResult.isSuccess()) {
result.put("user", loginResult.getUser());
result.put("token", loginResult.getToken());
result.put("message", "登录成功");
} else {
result.put("message", loginResult.getMessage());
}

return ResponseEntity.ok(result);

} catch (Exception e) {
log.error("处理微信授权回调失败: code={}, state={}", code, state, e);

Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "登录失败: " + e.getMessage());

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}

/**
* 检查登录状态
*/
@GetMapping("/status/{state}")
public ResponseEntity<Map<String, Object>> checkLoginStatus(@PathVariable String state) {
try {
// 这里可以实现登录状态检查逻辑
// 例如:检查Redis中的状态信息

Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("status", "PENDING");
result.put("message", "等待扫码");

return ResponseEntity.ok(result);

} catch (Exception e) {
log.error("检查登录状态失败: state={}", state, e);

Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "检查状态失败: " + e.getMessage());

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}

/**
* 获取用户信息
*/
@GetMapping("/user/info")
public ResponseEntity<Map<String, Object>> getUserInfo(HttpServletRequest request) {
try {
String token = extractToken(request);
if (token == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
Collections.singletonMap("message", "未授权"));
}

User user = userService.getUserByToken(token);
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
Collections.singletonMap("message", "用户不存在"));
}

Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("user", user);

return ResponseEntity.ok(result);

} catch (Exception e) {
log.error("获取用户信息失败", e);

Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "获取用户信息失败: " + e.getMessage());

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}

/**
* 刷新访问令牌
*/
@PostMapping("/refresh-token")
public ResponseEntity<Map<String, Object>> refreshToken(HttpServletRequest request) {
try {
String token = extractToken(request);
if (token == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
Collections.singletonMap("message", "未授权"));
}

User user = userService.getUserByToken(token);
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
Collections.singletonMap("message", "用户不存在"));
}

// 获取微信授权信息
WeChatAuth auth = userService.getWeChatAuth(user.getId());
if (auth == null) {
return ResponseEntity.badRequest().body(
Collections.singletonMap("message", "未绑定微信"));
}

// 刷新访问令牌
WeChatAccessToken newToken = weChatLoginService.refreshAccessToken(auth.getRefreshToken());

// 更新授权信息
auth.setAccessToken(newToken.getAccessToken());
auth.setRefreshToken(newToken.getRefreshToken());
auth.setExpiresIn(newToken.getExpiresIn());
auth.setUpdateTime(LocalDateTime.now());

userService.updateWeChatAuth(auth);

Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "令牌刷新成功");

return ResponseEntity.ok(result);

} catch (Exception e) {
log.error("刷新访问令牌失败", e);

Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "刷新令牌失败: " + e.getMessage());

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}

/**
* 解绑微信
*/
@PostMapping("/unbind")
public ResponseEntity<Map<String, Object>> unbindWeChat(HttpServletRequest request) {
try {
String token = extractToken(request);
if (token == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
Collections.singletonMap("message", "未授权"));
}

User user = userService.getUserByToken(token);
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
Collections.singletonMap("message", "用户不存在"));
}

// 删除微信授权信息
userService.deleteWeChatAuth(user.getId());

// 清除用户微信信息
user.setWeChatOpenid(null);
user.setWeChatUnionid(null);
user.setUpdateTime(LocalDateTime.now());

userService.updateUser(user);

Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "解绑成功");

return ResponseEntity.ok(result);

} catch (Exception e) {
log.error("解绑微信失败", e);

Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "解绑失败: " + e.getMessage());

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}

/**
* 从请求中提取令牌
* @param request HTTP请求
* @return 令牌
*/
private String extractToken(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
}

6. 前端实现

6.1 前端二维码显示和状态检查

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信扫码登录</title>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
</head>
<body>
<div id="login-container">
<h2>微信扫码登录</h2>
<div id="qrcode-container">
<div id="qrcode"></div>
<div id="status">正在生成二维码...</div>
</div>
<div id="user-info" style="display: none;">
<h3>登录成功</h3>
<div id="user-details"></div>
</div>
</div>

<script>
class WeChatLogin {
constructor() {
this.state = null;
this.statusCheckInterval = null;
this.init();
}

/**
* 初始化
*/
async init() {
try {
await this.generateQRCode();
this.startStatusCheck();
} catch (error) {
console.error('初始化失败:', error);
this.showError('初始化失败: ' + error.message);
}
}

/**
* 生成二维码
*/
async generateQRCode() {
try {
const response = await fetch('/auth/wechat/qrcode');
const data = await response.json();

if (data.success) {
this.state = data.state;

// 生成二维码
const qrCodeContainer = document.getElementById('qrcode');
qrCodeContainer.innerHTML = '';

QRCode.toCanvas(qrCodeContainer, data.qrCodeUrl, {
width: 200,
height: 200,
color: {
dark: '#000000',
light: '#FFFFFF'
}
}, (error) => {
if (error) {
console.error('生成二维码失败:', error);
this.showError('生成二维码失败');
} else {
this.updateStatus('请使用微信扫描二维码');
}
});
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('生成二维码失败:', error);
this.showError('生成二维码失败: ' + error.message);
}
}

/**
* 开始状态检查
*/
startStatusCheck() {
this.statusCheckInterval = setInterval(async () => {
await this.checkLoginStatus();
}, 2000); // 每2秒检查一次
}

/**
* 检查登录状态
*/
async checkLoginStatus() {
try {
const response = await fetch(`/auth/wechat/status/${this.state}`);
const data = await response.json();

if (data.success) {
if (data.status === 'SUCCESS') {
this.handleLoginSuccess(data.user);
} else if (data.status === 'EXPIRED') {
this.handleQRCodeExpired();
} else {
this.updateStatus('等待扫码...');
}
}
} catch (error) {
console.error('检查登录状态失败:', error);
}
}

/**
* 处理登录成功
*/
handleLoginSuccess(user) {
// 停止状态检查
if (this.statusCheckInterval) {
clearInterval(this.statusCheckInterval);
}

// 隐藏二维码
document.getElementById('qrcode-container').style.display = 'none';

// 显示用户信息
const userInfoContainer = document.getElementById('user-info');
const userDetails = document.getElementById('user-details');

userDetails.innerHTML = `
<p><strong>用户名:</strong> ${user.username}</p>
<p><strong>昵称:</strong> ${user.nickname || '未设置'}</p>
<p><strong>头像:</strong> <img src="${user.avatar || '/default-avatar.png'}" width="50" height="50"></p>
`;

userInfoContainer.style.display = 'block';

// 保存登录信息
localStorage.setItem('loginToken', user.token);
localStorage.setItem('userInfo', JSON.stringify(user));

// 跳转到主页
setTimeout(() => {
window.location.href = '/dashboard';
}, 2000);
}

/**
* 处理二维码过期
*/
handleQRCodeExpired() {
// 停止状态检查
if (this.statusCheckInterval) {
clearInterval(this.statusCheckInterval);
}

this.showError('二维码已过期,请刷新页面重新生成');
}

/**
* 更新状态
*/
updateStatus(message) {
document.getElementById('status').textContent = message;
}

/**
* 显示错误
*/
showError(message) {
document.getElementById('status').textContent = message;
document.getElementById('status').style.color = 'red';
}
}

// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
new WeChatLogin();
});
</script>
</body>
</html>

7. 总结

通过微信登录扫码的实现,我们成功构建了一个完整的第三方登录系统。关键特性包括:

7.1 核心优势

  1. 扫码登录: 生成二维码供用户扫描登录
  2. OAuth2.0授权: 微信OAuth2.0授权流程实现
  3. 用户信息获取: 获取微信用户基本信息
  4. 登录状态管理: 登录状态跟踪和会话管理
  5. 安全控制: 防重放攻击和状态验证

7.2 最佳实践

  1. 安全设计: 状态参数验证和防重放攻击
  2. 用户体验: 实时状态检查和友好提示
  3. 错误处理: 完善的异常处理和错误提示
  4. 性能优化: 令牌缓存和状态管理
  5. 扩展性: 支持多种第三方登录方式

这套微信登录扫码方案不仅能够提供便捷的登录体验,还包含了完善的安全控制和状态管理机制,是现代Web应用的重要基础设施。