前言

扫码登录作为现代移动互联网时代的重要登录方式,以其便捷性和安全性受到广泛欢迎。通过二维码作为媒介,实现PC端和移动端之间的身份认证,既保证了用户体验的流畅性,又确保了登录过程的安全性。本文从二维码登录设计到安全认证,从登录流程到企业级方案,系统梳理扫码登录的完整解决方案。

一、扫码登录架构设计

1.1 扫码登录整体架构

1.2 扫码登录流程架构

二、二维码登录设计

2.1 二维码生成服务

2.1.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
/**
* 二维码数据结构
*/
public class QRCodeData {

/**
* 二维码唯一标识
*/
private String qrCodeId;

/**
* 二维码内容
*/
private String qrCodeContent;

/**
* 二维码状态
*/
private QRCodeStatus status;

/**
* 创建时间
*/
private Long createTime;

/**
* 过期时间
*/
private Long expireTime;

/**
* 用户ID(扫码后填充)
*/
private String userId;

/**
* 设备信息
*/
private String deviceInfo;

/**
* IP地址
*/
private String ipAddress;

/**
* 用户代理
*/
private String userAgent;

/**
* 二维码状态枚举
*/
public enum QRCodeStatus {
PENDING("待扫描"),
SCANNED("已扫描"),
CONFIRMED("已确认"),
EXPIRED("已过期"),
CANCELLED("已取消");

private final String description;

QRCodeStatus(String description) {
this.description = description;
}

public String getDescription() {
return description;
}
}

// getter和setter方法
public String getQrCodeId() { return qrCodeId; }
public void setQrCodeId(String qrCodeId) { this.qrCodeId = qrCodeId; }

public String getQrCodeContent() { return qrCodeContent; }
public void setQrCodeContent(String qrCodeContent) { this.qrCodeContent = qrCodeContent; }

public QRCodeStatus getStatus() { return status; }
public void setStatus(QRCodeStatus status) { this.status = status; }

public Long getCreateTime() { return createTime; }
public void setCreateTime(Long createTime) { this.createTime = createTime; }

public Long getExpireTime() { return expireTime; }
public void setExpireTime(Long expireTime) { this.expireTime = expireTime; }

public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }

public String getDeviceInfo() { return deviceInfo; }
public void setDeviceInfo(String deviceInfo) { this.deviceInfo = deviceInfo; }

public String getIpAddress() { return ipAddress; }
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }

public String getUserAgent() { return userAgent; }
public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
}

2.1.2 二维码生成服务

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
/**
* 二维码生成服务
*/
@Service
public class QRCodeGenerateService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private QRCodeRepository qrCodeRepository;

private final String QR_CODE_PREFIX = "qr_code:";
private final int QR_CODE_EXPIRE_TIME = 300; // 5分钟过期

/**
* 生成二维码
*/
public QRCodeResponse generateQRCode(HttpServletRequest request) {
// 1. 生成唯一ID
String qrCodeId = generateUniqueId();

// 2. 构建二维码内容
String qrCodeContent = buildQRCodeContent(qrCodeId);

// 3. 创建二维码数据
QRCodeData qrCodeData = createQRCodeData(qrCodeId, qrCodeContent, request);

// 4. 存储二维码数据
storeQRCodeData(qrCodeData);

// 5. 生成二维码图片
String qrCodeImage = generateQRCodeImage(qrCodeContent);

// 6. 返回响应
return buildQRCodeResponse(qrCodeId, qrCodeImage, qrCodeData);
}

/**
* 生成唯一ID
*/
private String generateUniqueId() {
return UUID.randomUUID().toString().replace("-", "");
}

/**
* 构建二维码内容
*/
private String buildQRCodeContent(String qrCodeId) {
QRCodeContent content = new QRCodeContent();
content.setQrCodeId(qrCodeId);
content.setTimestamp(System.currentTimeMillis());
content.setNonce(generateNonce());

// 使用JSON格式
return JSON.toJSONString(content);
}

/**
* 创建二维码数据
*/
private QRCodeData createQRCodeData(String qrCodeId, String qrCodeContent, HttpServletRequest request) {
QRCodeData qrCodeData = new QRCodeData();
qrCodeData.setQrCodeId(qrCodeId);
qrCodeData.setQrCodeContent(qrCodeContent);
qrCodeData.setStatus(QRCodeData.QRCodeStatus.PENDING);
qrCodeData.setCreateTime(System.currentTimeMillis());
qrCodeData.setExpireTime(System.currentTimeMillis() + QR_CODE_EXPIRE_TIME * 1000);
qrCodeData.setIpAddress(getClientIpAddress(request));
qrCodeData.setUserAgent(request.getHeader("User-Agent"));

return qrCodeData;
}

/**
* 存储二维码数据
*/
private void storeQRCodeData(QRCodeData qrCodeData) {
// 存储到Redis
String redisKey = QR_CODE_PREFIX + qrCodeData.getQrCodeId();
redisTemplate.opsForValue().set(redisKey, qrCodeData, Duration.ofSeconds(QR_CODE_EXPIRE_TIME));

// 存储到数据库
qrCodeRepository.save(qrCodeData);
}

/**
* 生成二维码图片
*/
private String generateQRCodeImage(String qrCodeContent) {
try {
// 使用ZXing库生成二维码
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(qrCodeContent, BarcodeFormat.QR_CODE, 200, 200);

// 转换为Base64图片
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream);
byte[] imageBytes = outputStream.toByteArray();

return "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes);

} catch (Exception e) {
throw new RuntimeException("生成二维码图片失败", e);
}
}

/**
* 获取客户端IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}

String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}

return request.getRemoteAddr();
}

/**
* 生成随机数
*/
private String generateNonce() {
return String.valueOf(System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000));
}
}

2.2 二维码状态管理

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
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
/**
* 二维码状态轮询服务
*/
@Service
public class QRCodeStatusPollingService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private QRCodeRepository qrCodeRepository;

private final String QR_CODE_PREFIX = "qr_code:";

/**
* 轮询二维码状态
*/
public QRCodeStatusResponse pollQRCodeStatus(String qrCodeId) {
// 1. 从Redis获取二维码数据
QRCodeData qrCodeData = getQRCodeDataFromRedis(qrCodeId);

if (qrCodeData == null) {
// 从数据库获取
qrCodeData = qrCodeRepository.findByQrCodeId(qrCodeId);
}

if (qrCodeData == null) {
return QRCodeStatusResponse.notFound();
}

// 2. 检查是否过期
if (isExpired(qrCodeData)) {
qrCodeData.setStatus(QRCodeData.QRCodeStatus.EXPIRED);
updateQRCodeStatus(qrCodeData);
return QRCodeStatusResponse.expired();
}

// 3. 构建响应
QRCodeStatusResponse response = new QRCodeStatusResponse();
response.setQrCodeId(qrCodeId);
response.setStatus(qrCodeData.getStatus());
response.setMessage(getStatusMessage(qrCodeData.getStatus()));

// 4. 如果已确认,返回登录信息
if (qrCodeData.getStatus() == QRCodeData.QRCodeStatus.CONFIRMED) {
response.setLoginSuccess(true);
response.setUserId(qrCodeData.getUserId());
response.setToken(generateLoginToken(qrCodeData.getUserId()));
}

return response;
}

/**
* 从Redis获取二维码数据
*/
private QRCodeData getQRCodeDataFromRedis(String qrCodeId) {
String redisKey = QR_CODE_PREFIX + qrCodeId;
return (QRCodeData) redisTemplate.opsForValue().get(redisKey);
}

/**
* 检查是否过期
*/
private boolean isExpired(QRCodeData qrCodeData) {
return System.currentTimeMillis() > qrCodeData.getExpireTime();
}

/**
* 更新二维码状态
*/
private void updateQRCodeStatus(QRCodeData qrCodeData) {
// 更新Redis
String redisKey = QR_CODE_PREFIX + qrCodeData.getQrCodeId();
redisTemplate.opsForValue().set(redisKey, qrCodeData, Duration.ofSeconds(300));

// 更新数据库
qrCodeRepository.save(qrCodeData);
}

/**
* 获取状态消息
*/
private String getStatusMessage(QRCodeData.QRCodeStatus status) {
switch (status) {
case PENDING:
return "等待扫描";
case SCANNED:
return "已扫描,等待确认";
case CONFIRMED:
return "登录成功";
case EXPIRED:
return "二维码已过期";
case CANCELLED:
return "登录已取消";
default:
return "未知状态";
}
}

/**
* 生成登录令牌
*/
private String generateLoginToken(String userId) {
// 实现JWT令牌生成逻辑
return JwtUtil.generateToken(userId);
}
}

2.2.2 状态更新服务

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
/**
* 二维码状态更新服务
*/
@Service
public class QRCodeStatusUpdateService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private QRCodeRepository qrCodeRepository;

@Autowired
private WebSocketService webSocketService;

private final String QR_CODE_PREFIX = "qr_code:";

/**
* 更新二维码状态
*/
public void updateQRCodeStatus(String qrCodeId, QRCodeData.QRCodeStatus status, String userId) {
// 1. 获取二维码数据
QRCodeData qrCodeData = getQRCodeData(qrCodeId);

if (qrCodeData == null) {
throw new IllegalArgumentException("二维码不存在");
}

// 2. 检查状态转换是否合法
if (!isValidStatusTransition(qrCodeData.getStatus(), status)) {
throw new IllegalArgumentException("无效的状态转换");
}

// 3. 更新状态
qrCodeData.setStatus(status);
if (userId != null) {
qrCodeData.setUserId(userId);
}

// 4. 保存更新
saveQRCodeData(qrCodeData);

// 5. 通知PC端
notifyPCClient(qrCodeId, status);
}

/**
* 扫码确认
*/
public void confirmScan(String qrCodeId, String userId, String deviceInfo) {
// 1. 更新状态为已扫描
updateQRCodeStatus(qrCodeId, QRCodeData.QRCodeStatus.SCANNED, userId);

// 2. 更新设备信息
QRCodeData qrCodeData = getQRCodeData(qrCodeId);
qrCodeData.setDeviceInfo(deviceInfo);
saveQRCodeData(qrCodeData);

// 3. 通知PC端
notifyPCClient(qrCodeId, QRCodeData.QRCodeStatus.SCANNED);
}

/**
* 登录确认
*/
public void confirmLogin(String qrCodeId, String userId) {
// 1. 更新状态为已确认
updateQRCodeStatus(qrCodeId, QRCodeData.QRCodeStatus.CONFIRMED, userId);

// 2. 创建登录会话
createLoginSession(qrCodeId, userId);

// 3. 通知PC端
notifyPCClient(qrCodeId, QRCodeData.QRCodeStatus.CONFIRMED);
}

/**
* 取消登录
*/
public void cancelLogin(String qrCodeId) {
updateQRCodeStatus(qrCodeId, QRCodeData.QRCodeStatus.CANCELLED, null);
notifyPCClient(qrCodeId, QRCodeData.QRCodeStatus.CANCELLED);
}

/**
* 获取二维码数据
*/
private QRCodeData getQRCodeData(String qrCodeId) {
// 先从Redis获取
String redisKey = QR_CODE_PREFIX + qrCodeId;
QRCodeData qrCodeData = (QRCodeData) redisTemplate.opsForValue().get(redisKey);

if (qrCodeData == null) {
// 从数据库获取
qrCodeData = qrCodeRepository.findByQrCodeId(qrCodeId);
}

return qrCodeData;
}

/**
* 检查状态转换是否合法
*/
private boolean isValidStatusTransition(QRCodeData.QRCodeStatus from, QRCodeData.QRCodeStatus to) {
switch (from) {
case PENDING:
return to == QRCodeData.QRCodeStatus.SCANNED ||
to == QRCodeData.QRCodeStatus.EXPIRED ||
to == QRCodeData.QRCodeStatus.CANCELLED;
case SCANNED:
return to == QRCodeData.QRCodeStatus.CONFIRMED ||
to == QRCodeData.QRCodeStatus.EXPIRED ||
to == QRCodeData.QRCodeStatus.CANCELLED;
case CONFIRMED:
case EXPIRED:
case CANCELLED:
return false; // 终态,不能转换
default:
return false;
}
}

/**
* 保存二维码数据
*/
private void saveQRCodeData(QRCodeData qrCodeData) {
// 保存到Redis
String redisKey = QR_CODE_PREFIX + qrCodeData.getQrCodeId();
redisTemplate.opsForValue().set(redisKey, qrCodeData, Duration.ofSeconds(300));

// 保存到数据库
qrCodeRepository.save(qrCodeData);
}

/**
* 通知PC端
*/
private void notifyPCClient(String qrCodeId, QRCodeData.QRCodeStatus status) {
QRCodeStatusNotification notification = new QRCodeStatusNotification();
notification.setQrCodeId(qrCodeId);
notification.setStatus(status);
notification.setTimestamp(System.currentTimeMillis());

webSocketService.sendMessage(qrCodeId, notification);
}

/**
* 创建登录会话
*/
private void createLoginSession(String qrCodeId, String userId) {
// 实现登录会话创建逻辑
LoginSession session = new LoginSession();
session.setQrCodeId(qrCodeId);
session.setUserId(userId);
session.setLoginTime(System.currentTimeMillis());
session.setExpireTime(System.currentTimeMillis() + 24 * 60 * 60 * 1000); // 24小时

// 保存会话
loginSessionRepository.save(session);
}
}

三、移动端扫码认证

3.1 扫码功能实现

3.1.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
/**
* 二维码扫描服务
*/
@Service
public class QRCodeScanService {

@Autowired
private QRCodeRepository qrCodeRepository;

@Autowired
private QRCodeStatusUpdateService statusUpdateService;

@Autowired
private UserService userService;

/**
* 扫描二维码
*/
public QRCodeScanResponse scanQRCode(String qrCodeContent, String userId, String deviceInfo) {
try {
// 1. 解析二维码内容
QRCodeContent content = parseQRCodeContent(qrCodeContent);

// 2. 验证二维码有效性
QRCodeData qrCodeData = validateQRCode(content.getQrCodeId());

if (qrCodeData == null) {
return QRCodeScanResponse.invalid("二维码无效");
}

// 3. 检查二维码状态
if (qrCodeData.getStatus() != QRCodeData.QRCodeStatus.PENDING) {
return QRCodeScanResponse.invalid("二维码已被使用");
}

// 4. 检查是否过期
if (isExpired(qrCodeData)) {
return QRCodeScanResponse.expired("二维码已过期");
}

// 5. 验证用户身份
User user = userService.getUserById(userId);
if (user == null) {
return QRCodeScanResponse.invalid("用户不存在");
}

// 6. 确认扫码
statusUpdateService.confirmScan(content.getQrCodeId(), userId, deviceInfo);

// 7. 返回响应
return QRCodeScanResponse.success(content.getQrCodeId(), user);

} catch (Exception e) {
log.error("扫描二维码失败", e);
return QRCodeScanResponse.error("扫描失败: " + e.getMessage());
}
}

/**
* 确认登录
*/
public QRCodeLoginResponse confirmLogin(String qrCodeId, String userId) {
try {
// 1. 验证二维码
QRCodeData qrCodeData = qrCodeRepository.findByQrCodeId(qrCodeId);
if (qrCodeData == null) {
return QRCodeLoginResponse.invalid("二维码不存在");
}

// 2. 检查状态
if (qrCodeData.getStatus() != QRCodeData.QRCodeStatus.SCANNED) {
return QRCodeLoginResponse.invalid("二维码状态不正确");
}

// 3. 验证用户
if (!userId.equals(qrCodeData.getUserId())) {
return QRCodeLoginResponse.invalid("用户不匹配");
}

// 4. 确认登录
statusUpdateService.confirmLogin(qrCodeId, userId);

// 5. 返回响应
return QRCodeLoginResponse.success();

} catch (Exception e) {
log.error("确认登录失败", e);
return QRCodeLoginResponse.error("登录失败: " + e.getMessage());
}
}

/**
* 取消登录
*/
public QRCodeCancelResponse cancelLogin(String qrCodeId, String userId) {
try {
// 1. 验证二维码
QRCodeData qrCodeData = qrCodeRepository.findByQrCodeId(qrCodeId);
if (qrCodeData == null) {
return QRCodeCancelResponse.invalid("二维码不存在");
}

// 2. 验证用户
if (!userId.equals(qrCodeData.getUserId())) {
return QRCodeCancelResponse.invalid("用户不匹配");
}

// 3. 取消登录
statusUpdateService.cancelLogin(qrCodeId);

// 4. 返回响应
return QRCodeCancelResponse.success();

} catch (Exception e) {
log.error("取消登录失败", e);
return QRCodeCancelResponse.error("取消失败: " + e.getMessage());
}
}

/**
* 解析二维码内容
*/
private QRCodeContent parseQRCodeContent(String qrCodeContent) {
try {
return JSON.parseObject(qrCodeContent, QRCodeContent.class);
} catch (Exception e) {
throw new IllegalArgumentException("二维码内容格式错误");
}
}

/**
* 验证二维码有效性
*/
private QRCodeData validateQRCode(String qrCodeId) {
return qrCodeRepository.findByQrCodeId(qrCodeId);
}

/**
* 检查是否过期
*/
private boolean isExpired(QRCodeData qrCodeData) {
return System.currentTimeMillis() > qrCodeData.getExpireTime();
}
}

3.1.2 移动端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
/**
* 移动端扫码登录控制器
*/
@RestController
@RequestMapping("/api/mobile/qr-login")
public class MobileQRLoginController {

@Autowired
private QRCodeScanService qrCodeScanService;

@Autowired
private UserService userService;

/**
* 扫描二维码
*/
@PostMapping("/scan")
public ResponseEntity<QRCodeScanResponse> scanQRCode(
@RequestBody QRCodeScanRequest request,
HttpServletRequest httpRequest) {

try {
// 获取用户ID(从JWT令牌中解析)
String userId = getCurrentUserId(httpRequest);

// 获取设备信息
String deviceInfo = getDeviceInfo(httpRequest);

// 扫描二维码
QRCodeScanResponse response = qrCodeScanService.scanQRCode(
request.getQrCodeContent(), userId, deviceInfo);

return ResponseEntity.ok(response);

} catch (Exception e) {
log.error("扫描二维码失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(QRCodeScanResponse.error("扫描失败"));
}
}

/**
* 确认登录
*/
@PostMapping("/confirm")
public ResponseEntity<QRCodeLoginResponse> confirmLogin(
@RequestBody QRCodeLoginRequest request,
HttpServletRequest httpRequest) {

try {
// 获取用户ID
String userId = getCurrentUserId(httpRequest);

// 确认登录
QRCodeLoginResponse response = qrCodeScanService.confirmLogin(
request.getQrCodeId(), userId);

return ResponseEntity.ok(response);

} catch (Exception e) {
log.error("确认登录失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(QRCodeLoginResponse.error("登录失败"));
}
}

/**
* 取消登录
*/
@PostMapping("/cancel")
public ResponseEntity<QRCodeCancelResponse> cancelLogin(
@RequestBody QRCodeCancelRequest request,
HttpServletRequest httpRequest) {

try {
// 获取用户ID
String userId = getCurrentUserId(httpRequest);

// 取消登录
QRCodeCancelResponse response = qrCodeScanService.cancelLogin(
request.getQrCodeId(), userId);

return ResponseEntity.ok(response);

} catch (Exception e) {
log.error("取消登录失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(QRCodeCancelResponse.error("取消失败"));
}
}

/**
* 获取当前用户ID
*/
private String getCurrentUserId(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
return JwtUtil.getUserIdFromToken(token);
}
throw new UnauthorizedException("未授权访问");
}

/**
* 获取设备信息
*/
private String getDeviceInfo(HttpServletRequest request) {
StringBuilder deviceInfo = new StringBuilder();
deviceInfo.append("User-Agent: ").append(request.getHeader("User-Agent"));
deviceInfo.append(", IP: ").append(getClientIpAddress(request));
return deviceInfo.toString();
}

/**
* 获取客户端IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}

3.2 用户身份验证

3.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
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
/**
* 用户身份验证服务
*/
@Service
public class UserAuthenticationService {

@Autowired
private UserRepository userRepository;

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private JwtUtil jwtUtil;

/**
* 验证用户身份
*/
public AuthenticationResult authenticateUser(String username, String password) {
try {
// 1. 查找用户
User user = userRepository.findByUsername(username);
if (user == null) {
return AuthenticationResult.failure("用户不存在");
}

// 2. 验证密码
if (!passwordEncoder.matches(password, user.getPassword())) {
return AuthenticationResult.failure("密码错误");
}

// 3. 检查用户状态
if (user.getStatus() != UserStatus.ACTIVE) {
return AuthenticationResult.failure("用户已被禁用");
}

// 4. 生成JWT令牌
String token = jwtUtil.generateToken(user.getId());

// 5. 更新最后登录时间
user.setLastLoginTime(System.currentTimeMillis());
userRepository.save(user);

// 6. 返回成功结果
return AuthenticationResult.success(user, token);

} catch (Exception e) {
log.error("用户身份验证失败", e);
return AuthenticationResult.failure("验证失败");
}
}

/**
* 验证JWT令牌
*/
public AuthenticationResult validateToken(String token) {
try {
// 1. 验证令牌有效性
if (!jwtUtil.validateToken(token)) {
return AuthenticationResult.failure("令牌无效");
}

// 2. 获取用户ID
String userId = jwtUtil.getUserIdFromToken(token);

// 3. 查找用户
User user = userRepository.findById(userId).orElse(null);
if (user == null) {
return AuthenticationResult.failure("用户不存在");
}

// 4. 检查用户状态
if (user.getStatus() != UserStatus.ACTIVE) {
return AuthenticationResult.failure("用户已被禁用");
}

// 5. 返回成功结果
return AuthenticationResult.success(user, token);

} catch (Exception e) {
log.error("令牌验证失败", e);
return AuthenticationResult.failure("令牌验证失败");
}
}

/**
* 刷新令牌
*/
public AuthenticationResult refreshToken(String refreshToken) {
try {
// 1. 验证刷新令牌
if (!jwtUtil.validateRefreshToken(refreshToken)) {
return AuthenticationResult.failure("刷新令牌无效");
}

// 2. 获取用户ID
String userId = jwtUtil.getUserIdFromRefreshToken(refreshToken);

// 3. 查找用户
User user = userRepository.findById(userId).orElse(null);
if (user == null) {
return AuthenticationResult.failure("用户不存在");
}

// 4. 生成新令牌
String newToken = jwtUtil.generateToken(user.getId());
String newRefreshToken = jwtUtil.generateRefreshToken(user.getId());

// 5. 返回成功结果
return AuthenticationResult.success(user, newToken, newRefreshToken);

} catch (Exception e) {
log.error("刷新令牌失败", e);
return AuthenticationResult.failure("刷新令牌失败");
}
}
}

3.2.2 JWT工具类

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
/**
* JWT工具类
*/
@Component
public class JwtUtil {

@Value("${jwt.secret}")
private String secret;

@Value("${jwt.expiration}")
private Long expiration;

@Value("${jwt.refresh-expiration}")
private Long refreshExpiration;

/**
* 生成JWT令牌
*/
public String generateToken(String userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("type", "access");
return createToken(claims, userId, expiration);
}

/**
* 生成刷新令牌
*/
public String generateRefreshToken(String userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("type", "refresh");
return createToken(claims, userId, refreshExpiration);
}

/**
* 创建令牌
*/
private String createToken(Map<String, Object> claims, String subject, Long expiration) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}

/**
* 验证令牌
*/
public Boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}

/**
* 验证刷新令牌
*/
public Boolean validateRefreshToken(String token) {
try {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return "refresh".equals(claims.get("type"));
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}

/**
* 从令牌中获取用户ID
*/
public String getUserIdFromToken(String token) {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claims.get("userId", String.class);
}

/**
* 从刷新令牌中获取用户ID
*/
public String getUserIdFromRefreshToken(String token) {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claims.get("userId", String.class);
}

/**
* 获取令牌过期时间
*/
public Date getExpirationDateFromToken(String token) {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claims.getExpiration();
}

/**
* 检查令牌是否过期
*/
public Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
}

四、安全认证机制

4.1 安全防护措施

4.1.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
/**
* 防重放攻击服务
*/
@Service
public class AntiReplayAttackService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private final String NONCE_PREFIX = "nonce:";
private final int NONCE_EXPIRE_TIME = 300; // 5分钟

/**
* 验证随机数
*/
public boolean validateNonce(String nonce) {
String redisKey = NONCE_PREFIX + nonce;

// 检查随机数是否已使用
if (redisTemplate.hasKey(redisKey)) {
return false; // 随机数已使用,可能是重放攻击
}

// 存储随机数
redisTemplate.opsForValue().set(redisKey, "used", Duration.ofSeconds(NONCE_EXPIRE_TIME));

return true;
}

/**
* 验证时间戳
*/
public boolean validateTimestamp(Long timestamp) {
long currentTime = System.currentTimeMillis();
long timeDiff = Math.abs(currentTime - timestamp);

// 允许5分钟的时间差
return timeDiff <= 5 * 60 * 1000;
}

/**
* 验证签名
*/
public boolean validateSignature(String data, String signature, String secret) {
try {
String expectedSignature = calculateSignature(data, secret);
return signature.equals(expectedSignature);
} catch (Exception e) {
log.error("签名验证失败", e);
return false;
}
}

/**
* 计算签名
*/
private String calculateSignature(String data, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
mac.init(secretKeySpec);

byte[] signature = mac.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(signature);
} catch (Exception e) {
throw new RuntimeException("计算签名失败", e);
}
}
}

4.1.2 防伪造攻击

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
/**
* 防伪造攻击服务
*/
@Service
public class AntiForgeryAttackService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private final String QR_CODE_SIGNATURE_PREFIX = "qr_signature:";

/**
* 生成二维码签名
*/
public String generateQRCodeSignature(QRCodeData qrCodeData) {
try {
// 构建签名数据
StringBuilder data = new StringBuilder();
data.append(qrCodeData.getQrCodeId());
data.append(qrCodeData.getCreateTime());
data.append(qrCodeData.getIpAddress());
data.append(qrCodeData.getUserAgent());

// 计算签名
String signature = calculateHMAC(data.toString(), getSecretKey());

// 存储签名
String redisKey = QR_CODE_SIGNATURE_PREFIX + qrCodeData.getQrCodeId();
redisTemplate.opsForValue().set(redisKey, signature, Duration.ofSeconds(300));

return signature;

} catch (Exception e) {
throw new RuntimeException("生成二维码签名失败", e);
}
}

/**
* 验证二维码签名
*/
public boolean validateQRCodeSignature(String qrCodeId, String signature) {
try {
// 从Redis获取存储的签名
String redisKey = QR_CODE_SIGNATURE_PREFIX + qrCodeId;
String storedSignature = (String) redisTemplate.opsForValue().get(redisKey);

if (storedSignature == null) {
return false; // 签名不存在或已过期
}

return signature.equals(storedSignature);

} catch (Exception e) {
log.error("验证二维码签名失败", e);
return false;
}
}

/**
* 验证二维码完整性
*/
public boolean validateQRCodeIntegrity(QRCodeData qrCodeData) {
try {
// 验证二维码ID格式
if (!isValidQRCodeId(qrCodeData.getQrCodeId())) {
return false;
}

// 验证时间戳
if (!isValidTimestamp(qrCodeData.getCreateTime())) {
return false;
}

// 验证IP地址
if (!isValidIpAddress(qrCodeData.getIpAddress())) {
return false;
}

return true;

} catch (Exception e) {
log.error("验证二维码完整性失败", e);
return false;
}
}

/**
* 验证二维码ID格式
*/
private boolean isValidQRCodeId(String qrCodeId) {
return qrCodeId != null &&
qrCodeId.length() == 32 &&
qrCodeId.matches("[a-f0-9]+");
}

/**
* 验证时间戳
*/
private boolean isValidTimestamp(Long timestamp) {
long currentTime = System.currentTimeMillis();
long timeDiff = Math.abs(currentTime - timestamp);

// 允许1小时的时间差
return timeDiff <= 60 * 60 * 1000;
}

/**
* 验证IP地址
*/
private boolean isValidIpAddress(String ipAddress) {
if (ipAddress == null || ipAddress.isEmpty()) {
return false;
}

try {
InetAddress.getByName(ipAddress);
return true;
} catch (UnknownHostException e) {
return false;
}
}

/**
* 获取密钥
*/
private String getSecretKey() {
return "your-secret-key"; // 应该从配置文件或密钥管理系统获取
}

/**
* 计算HMAC
*/
private String calculateHMAC(String data, String key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "HmacSHA256");
mac.init(secretKeySpec);

byte[] signature = mac.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(signature);
} catch (Exception e) {
throw new RuntimeException("计算HMAC失败", e);
}
}
}

4.2 安全监控

4.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
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
/**
* 安全事件监控服务
*/
@Service
public class SecurityEventMonitoringService {

@Autowired
private MeterRegistry meterRegistry;

@Autowired
private SecurityEventRepository securityEventRepository;

/**
* 记录安全事件
*/
public void recordSecurityEvent(SecurityEventType eventType, String qrCodeId, String userId, String details) {
try {
// 1. 创建安全事件
SecurityEvent event = new SecurityEvent();
event.setEventType(eventType);
event.setQrCodeId(qrCodeId);
event.setUserId(userId);
event.setDetails(details);
event.setTimestamp(System.currentTimeMillis());
event.setIpAddress(getCurrentIpAddress());

// 2. 存储事件
securityEventRepository.save(event);

// 3. 记录指标
recordSecurityMetrics(eventType);

// 4. 检查告警条件
checkAlertConditions(event);

} catch (Exception e) {
log.error("记录安全事件失败", e);
}
}

/**
* 记录安全指标
*/
private void recordSecurityMetrics(SecurityEventType eventType) {
Counter.builder("security.event.count")
.description("安全事件计数")
.tag("event_type", eventType.name())
.register(meterRegistry)
.increment();
}

/**
* 检查告警条件
*/
private void checkAlertConditions(SecurityEvent event) {
// 检查异常频率
checkAbnormalFrequency(event);

// 检查可疑行为
checkSuspiciousBehavior(event);

// 检查攻击模式
checkAttackPattern(event);
}

/**
* 检查异常频率
*/
private void checkAbnormalFrequency(SecurityEvent event) {
// 统计最近5分钟内的安全事件
long fiveMinutesAgo = System.currentTimeMillis() - 5 * 60 * 1000;
long count = securityEventRepository.countByEventTypeAndTimestampAfter(
event.getEventType(), fiveMinutesAgo);

// 如果超过阈值,触发告警
if (count > 10) {
triggerSecurityAlert("异常频率告警", event);
}
}

/**
* 检查可疑行为
*/
private void checkSuspiciousBehavior(SecurityEvent event) {
// 检查同一IP的异常行为
long oneHourAgo = System.currentTimeMillis() - 60 * 60 * 1000;
long ipCount = securityEventRepository.countByIpAddressAndTimestampAfter(
event.getIpAddress(), oneHourAgo);

if (ipCount > 50) {
triggerSecurityAlert("可疑IP告警", event);
}
}

/**
* 检查攻击模式
*/
private void checkAttackPattern(SecurityEvent event) {
// 检查重放攻击模式
if (event.getEventType() == SecurityEventType.REPLAY_ATTACK) {
triggerSecurityAlert("重放攻击检测", event);
}

// 检查伪造攻击模式
if (event.getEventType() == SecurityEventType.FORGERY_ATTACK) {
triggerSecurityAlert("伪造攻击检测", event);
}
}

/**
* 触发安全告警
*/
private void triggerSecurityAlert(String alertType, SecurityEvent event) {
SecurityAlert alert = new SecurityAlert();
alert.setAlertType(alertType);
alert.setEvent(event);
alert.setTimestamp(System.currentTimeMillis());
alert.setSeverity(SecuritySeverity.HIGH);

// 发送告警通知
sendSecurityAlert(alert);
}

/**
* 发送安全告警
*/
private void sendSecurityAlert(SecurityAlert alert) {
// 实现告警发送逻辑
log.warn("安全告警: {}", alert.getAlertType());
}

/**
* 获取当前IP地址
*/
private String getCurrentIpAddress() {
// 实现获取当前IP地址的逻辑
return "127.0.0.1";
}
}

4.2.2 安全审计

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
/**
* 安全审计服务
*/
@Service
public class SecurityAuditService {

@Autowired
private SecurityEventRepository securityEventRepository;

@Autowired
private QRCodeRepository qrCodeRepository;

/**
* 生成安全审计报告
*/
public SecurityAuditReport generateAuditReport(String startTime, String endTime) {
try {
long start = parseTime(startTime);
long end = parseTime(endTime);

SecurityAuditReport report = new SecurityAuditReport();
report.setStartTime(start);
report.setEndTime(end);
report.setGenerateTime(System.currentTimeMillis());

// 统计安全事件
Map<SecurityEventType, Long> eventStats = getEventStatistics(start, end);
report.setEventStatistics(eventStats);

// 统计异常IP
List<String> suspiciousIPs = getSuspiciousIPs(start, end);
report.setSuspiciousIPs(suspiciousIPs);

// 统计异常用户
List<String> suspiciousUsers = getSuspiciousUsers(start, end);
report.setSuspiciousUsers(suspiciousUsers);

// 统计二维码使用情况
QRCodeUsageStats usageStats = getQRCodeUsageStats(start, end);
report.setUsageStats(usageStats);

return report;

} catch (Exception e) {
log.error("生成安全审计报告失败", e);
throw new RuntimeException("生成安全审计报告失败", e);
}
}

/**
* 获取事件统计
*/
private Map<SecurityEventType, Long> getEventStatistics(long start, long end) {
Map<SecurityEventType, Long> stats = new HashMap<>();

for (SecurityEventType eventType : SecurityEventType.values()) {
long count = securityEventRepository.countByEventTypeAndTimestampBetween(eventType, start, end);
stats.put(eventType, count);
}

return stats;
}

/**
* 获取可疑IP
*/
private List<String> getSuspiciousIPs(long start, long end) {
return securityEventRepository.findSuspiciousIPs(start, end);
}

/**
* 获取可疑用户
*/
private List<String> getSuspiciousUsers(long start, long end) {
return securityEventRepository.findSuspiciousUsers(start, end);
}

/**
* 获取二维码使用统计
*/
private QRCodeUsageStats getQRCodeUsageStats(long start, long end) {
QRCodeUsageStats stats = new QRCodeUsageStats();

// 总生成数
long totalGenerated = qrCodeRepository.countByCreateTimeBetween(start, end);
stats.setTotalGenerated(totalGenerated);

// 成功登录数
long successfulLogins = qrCodeRepository.countByStatusAndCreateTimeBetween(
QRCodeData.QRCodeStatus.CONFIRMED, start, end);
stats.setSuccessfulLogins(successfulLogins);

// 过期数
long expired = qrCodeRepository.countByStatusAndCreateTimeBetween(
QRCodeData.QRCodeStatus.EXPIRED, start, end);
stats.setExpired(expired);

// 取消数
long cancelled = qrCodeRepository.countByStatusAndCreateTimeBetween(
QRCodeData.QRCodeStatus.CANCELLED, start, end);
stats.setCancelled(cancelled);

return stats;
}

/**
* 解析时间
*/
private long parseTime(String timeStr) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(timeStr).getTime();
} catch (Exception e) {
throw new IllegalArgumentException("时间格式错误", e);
}
}
}

五、企业级扫码登录方案

5.1 高可用架构

5.1.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
# nginx.conf
upstream qr_login_backend {
server 192.168.1.10:8080 weight=1;
server 192.168.1.11:8080 weight=1;
server 192.168.1.12:8080 weight=1;
}

server {
listen 80;
server_name qr-login.example.com;

location / {
proxy_pass http://qr_login_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 超时设置
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;

# 缓冲设置
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}

# WebSocket支持
location /ws {
proxy_pass http://qr_login_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

5.1.2 集群部署

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
# docker-compose.yml
version: '3.8'
services:
qr-login-app:
image: qr-login:latest
deploy:
replicas: 3
resources:
limits:
memory: 1G
cpus: '0.5'
reservations:
memory: 512M
cpus: '0.25'
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
environment:
- SPRING_PROFILES_ACTIVE=prod
- REDIS_HOST=redis
- MYSQL_HOST=mysql
depends_on:
- redis
- mysql
networks:
- qr-login-network

redis:
image: redis:6.2-alpine
deploy:
replicas: 1
resources:
limits:
memory: 512M
cpus: '0.25'
volumes:
- redis_data:/data
networks:
- qr-login-network

mysql:
image: mysql:8.0
deploy:
replicas: 1
resources:
limits:
memory: 1G
cpus: '0.5'
environment:
- MYSQL_ROOT_PASSWORD=root123
- MYSQL_DATABASE=qr_login
volumes:
- mysql_data:/var/lib/mysql
- ./sql:/docker-entrypoint-initdb.d
networks:
- qr-login-network

volumes:
redis_data:
mysql_data:

networks:
qr-login-network:
driver: overlay

5.2 监控告警

5.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
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 扫码登录监控指标
*/
@Component
public class QRLoginMetrics {

private final MeterRegistry meterRegistry;

public QRLoginMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}

/**
* 记录二维码生成
*/
public void recordQRCodeGenerated() {
Counter.builder("qr_code.generated")
.description("二维码生成次数")
.register(meterRegistry)
.increment();
}

/**
* 记录二维码扫描
*/
public void recordQRCodeScanned() {
Counter.builder("qr_code.scanned")
.description("二维码扫描次数")
.register(meterRegistry)
.increment();
}

/**
* 记录登录成功
*/
public void recordLoginSuccess() {
Counter.builder("qr_login.success")
.description("扫码登录成功次数")
.register(meterRegistry)
.increment();
}

/**
* 记录登录失败
*/
public void recordLoginFailure() {
Counter.builder("qr_login.failure")
.description("扫码登录失败次数")
.register(meterRegistry)
.increment();
}

/**
* 记录二维码过期
*/
public void recordQRCodeExpired() {
Counter.builder("qr_code.expired")
.description("二维码过期次数")
.register(meterRegistry)
.increment();
}

/**
* 记录安全事件
*/
public void recordSecurityEvent(String eventType) {
Counter.builder("security.event")
.description("安全事件次数")
.tag("event_type", eventType)
.register(meterRegistry)
.increment();
}

/**
* 记录响应时间
*/
public void recordResponseTime(String operation, long duration) {
Timer.builder("qr_login.response_time")
.description("响应时间")
.tag("operation", operation)
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
}
}

5.2.2 告警规则

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
# prometheus-rules.yml
groups:
- name: qr_login_alerts
rules:
- alert: HighLoginFailureRate
expr: rate(qr_login_failure[5m]) / rate(qr_login_success[5m] + qr_login_failure[5m]) > 0.1
for: 2m
labels:
severity: warning
annotations:
summary: "扫码登录失败率过高"
description: "扫码登录失败率超过10%,当前值: {{ $value }}"

- alert: HighQRCodeExpiredRate
expr: rate(qr_code_expired[5m]) / rate(qr_code_generated[5m]) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "二维码过期率过高"
description: "二维码过期率超过50%,当前值: {{ $value }}"

- alert: SecurityEventDetected
expr: increase(security_event[1m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "检测到安全事件"
description: "检测到安全事件: {{ $labels.event_type }}"

- alert: HighResponseTime
expr: qr_login_response_time{quantile="0.95"} > 2000
for: 2m
labels:
severity: warning
annotations:
summary: "响应时间过长"
description: "扫码登录响应时间P95超过2秒,当前值: {{ $value }}ms"

5.3 性能优化

5.3.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
/**
* 缓存优化服务
*/
@Service
public class CacheOptimizationService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private CaffeineCache localCache;

/**
* 多级缓存获取
*/
public QRCodeData getQRCodeData(String qrCodeId) {
// 1. 本地缓存
QRCodeData data = localCache.getIfPresent(qrCodeId);
if (data != null) {
return data;
}

// 2. Redis缓存
String redisKey = "qr_code:" + qrCodeId;
data = (QRCodeData) redisTemplate.opsForValue().get(redisKey);
if (data != null) {
localCache.put(qrCodeId, data);
return data;
}

// 3. 数据库
data = qrCodeRepository.findByQrCodeId(qrCodeId);
if (data != null) {
// 写入缓存
redisTemplate.opsForValue().set(redisKey, data, Duration.ofMinutes(5));
localCache.put(qrCodeId, data);
}

return data;
}

/**
* 缓存预热
*/
@PostConstruct
public void warmupCache() {
// 预热热点数据
List<QRCodeData> hotData = qrCodeRepository.findHotQRCodes();
hotData.parallelStream().forEach(data -> {
String redisKey = "qr_code:" + data.getQrCodeId();
redisTemplate.opsForValue().set(redisKey, data, Duration.ofMinutes(5));
localCache.put(data.getQrCodeId(), data);
});
}

/**
* 缓存清理
*/
@Scheduled(fixedRate = 300000) // 5分钟
public void cleanupCache() {
// 清理本地缓存
localCache.cleanUp();

// 清理过期缓存
cleanupExpiredCache();
}

/**
* 清理过期缓存
*/
private void cleanupExpiredCache() {
// 实现过期缓存清理逻辑
}
}

5.3.2 数据库优化

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
-- 数据库优化配置
-- 1. 索引优化
CREATE INDEX idx_qr_code_id ON qr_code_data(qr_code_id);
CREATE INDEX idx_qr_code_status ON qr_code_data(status);
CREATE INDEX idx_qr_code_create_time ON qr_code_data(create_time);
CREATE INDEX idx_qr_code_expire_time ON qr_code_data(expire_time);
CREATE INDEX idx_qr_code_user_id ON qr_code_data(user_id);

-- 2. 分区表
ALTER TABLE qr_code_data PARTITION BY RANGE (create_time) (
PARTITION p202301 VALUES LESS THAN (UNIX_TIMESTAMP('2023-02-01')),
PARTITION p202302 VALUES LESS THAN (UNIX_TIMESTAMP('2023-03-01')),
PARTITION p202303 VALUES LESS THAN (UNIX_TIMESTAMP('2023-04-01')),
PARTITION p_future VALUES LESS THAN MAXVALUE
);

-- 3. 查询优化
-- 使用覆盖索引
CREATE INDEX idx_qr_code_cover ON qr_code_data(qr_code_id, status, create_time, expire_time);

-- 4. 连接池优化
-- application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000

六、总结

扫码登录作为现代移动互联网时代的重要登录方式,通过合理的架构设计和安全机制,能够为用户提供便捷、安全的登录体验。本文从二维码登录设计到安全认证,从登录流程到企业级方案,系统梳理了扫码登录的完整解决方案。

6.1 关键要点

  1. 架构设计:采用分层架构,支持PC端和移动端的协同工作
  2. 安全机制:通过防重放攻击、防伪造攻击等机制保证安全性
  3. 状态管理:通过Redis和数据库实现二维码状态的实时管理
  4. 监控告警:建立完善的监控体系,及时发现和处理问题
  5. 性能优化:通过缓存、数据库优化等手段提高系统性能

6.2 最佳实践

  1. 二维码设计:使用唯一ID和时间戳,确保二维码的唯一性和时效性
  2. 安全防护:实施多重安全防护措施,防止各种攻击
  3. 状态同步:通过WebSocket实现实时状态同步
  4. 监控告警:建立完善的监控告警体系
  5. 性能优化:通过缓存和数据库优化提高系统性能

通过以上措施,可以构建一个安全、高效、可扩展的扫码登录系统,为用户提供优质的登录体验。