第262集扫码登录详解架构实战:二维码登录设计、安全认证与企业级扫码登录方案 | 字数总计: 7.3k | 阅读时长: 36分钟 | 阅读量:
前言 扫码登录作为现代移动互联网时代的重要登录方式,以其便捷性和安全性受到广泛欢迎。通过二维码作为媒介,实现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; private String userId; private String deviceInfo; 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; } } 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 ; public QRCodeResponse generateQRCode (HttpServletRequest request) { String qrCodeId = generateUniqueId(); String qrCodeContent = buildQRCodeContent(qrCodeId); QRCodeData qrCodeData = createQRCodeData(qrCodeId, qrCodeContent, request); storeQRCodeData(qrCodeData); String qrCodeImage = generateQRCodeImage(qrCodeContent); return buildQRCodeResponse(qrCodeId, qrCodeImage, qrCodeData); } 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()); 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) { 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 { QRCodeWriter qrCodeWriter = new QRCodeWriter (); BitMatrix bitMatrix = qrCodeWriter.encode(qrCodeContent, BarcodeFormat.QR_CODE, 200 , 200 ); 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); } } 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) { QRCodeData qrCodeData = getQRCodeDataFromRedis(qrCodeId); if (qrCodeData == null ) { qrCodeData = qrCodeRepository.findByQrCodeId(qrCodeId); } if (qrCodeData == null ) { return QRCodeStatusResponse.notFound(); } if (isExpired(qrCodeData)) { qrCodeData.setStatus(QRCodeData.QRCodeStatus.EXPIRED); updateQRCodeStatus(qrCodeData); return QRCodeStatusResponse.expired(); } QRCodeStatusResponse response = new QRCodeStatusResponse (); response.setQrCodeId(qrCodeId); response.setStatus(qrCodeData.getStatus()); response.setMessage(getStatusMessage(qrCodeData.getStatus())); if (qrCodeData.getStatus() == QRCodeData.QRCodeStatus.CONFIRMED) { response.setLoginSuccess(true ); response.setUserId(qrCodeData.getUserId()); response.setToken(generateLoginToken(qrCodeData.getUserId())); } return response; } 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) { 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) { 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) { QRCodeData qrCodeData = getQRCodeData(qrCodeId); if (qrCodeData == null ) { throw new IllegalArgumentException ("二维码不存在" ); } if (!isValidStatusTransition(qrCodeData.getStatus(), status)) { throw new IllegalArgumentException ("无效的状态转换" ); } qrCodeData.setStatus(status); if (userId != null ) { qrCodeData.setUserId(userId); } saveQRCodeData(qrCodeData); notifyPCClient(qrCodeId, status); } public void confirmScan (String qrCodeId, String userId, String deviceInfo) { updateQRCodeStatus(qrCodeId, QRCodeData.QRCodeStatus.SCANNED, userId); QRCodeData qrCodeData = getQRCodeData(qrCodeId); qrCodeData.setDeviceInfo(deviceInfo); saveQRCodeData(qrCodeData); notifyPCClient(qrCodeId, QRCodeData.QRCodeStatus.SCANNED); } public void confirmLogin (String qrCodeId, String userId) { updateQRCodeStatus(qrCodeId, QRCodeData.QRCodeStatus.CONFIRMED, userId); createLoginSession(qrCodeId, userId); 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) { 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) { String redisKey = QR_CODE_PREFIX + qrCodeData.getQrCodeId(); redisTemplate.opsForValue().set(redisKey, qrCodeData, Duration.ofSeconds(300 )); qrCodeRepository.save(qrCodeData); } 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 ); 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 { QRCodeContent content = parseQRCodeContent(qrCodeContent); QRCodeData qrCodeData = validateQRCode(content.getQrCodeId()); if (qrCodeData == null ) { return QRCodeScanResponse.invalid("二维码无效" ); } if (qrCodeData.getStatus() != QRCodeData.QRCodeStatus.PENDING) { return QRCodeScanResponse.invalid("二维码已被使用" ); } if (isExpired(qrCodeData)) { return QRCodeScanResponse.expired("二维码已过期" ); } User user = userService.getUserById(userId); if (user == null ) { return QRCodeScanResponse.invalid("用户不存在" ); } statusUpdateService.confirmScan(content.getQrCodeId(), userId, deviceInfo); 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 { QRCodeData qrCodeData = qrCodeRepository.findByQrCodeId(qrCodeId); if (qrCodeData == null ) { return QRCodeLoginResponse.invalid("二维码不存在" ); } if (qrCodeData.getStatus() != QRCodeData.QRCodeStatus.SCANNED) { return QRCodeLoginResponse.invalid("二维码状态不正确" ); } if (!userId.equals(qrCodeData.getUserId())) { return QRCodeLoginResponse.invalid("用户不匹配" ); } statusUpdateService.confirmLogin(qrCodeId, userId); return QRCodeLoginResponse.success(); } catch (Exception e) { log.error("确认登录失败" , e); return QRCodeLoginResponse.error("登录失败: " + e.getMessage()); } } public QRCodeCancelResponse cancelLogin (String qrCodeId, String userId) { try { QRCodeData qrCodeData = qrCodeRepository.findByQrCodeId(qrCodeId); if (qrCodeData == null ) { return QRCodeCancelResponse.invalid("二维码不存在" ); } if (!userId.equals(qrCodeData.getUserId())) { return QRCodeCancelResponse.invalid("用户不匹配" ); } statusUpdateService.cancelLogin(qrCodeId); 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 { 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 { 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 { 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("取消失败" )); } } 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(); } 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 { User user = userRepository.findByUsername(username); if (user == null ) { return AuthenticationResult.failure("用户不存在" ); } if (!passwordEncoder.matches(password, user.getPassword())) { return AuthenticationResult.failure("密码错误" ); } if (user.getStatus() != UserStatus.ACTIVE) { return AuthenticationResult.failure("用户已被禁用" ); } String token = jwtUtil.generateToken(user.getId()); user.setLastLoginTime(System.currentTimeMillis()); userRepository.save(user); return AuthenticationResult.success(user, token); } catch (Exception e) { log.error("用户身份验证失败" , e); return AuthenticationResult.failure("验证失败" ); } } public AuthenticationResult validateToken (String token) { try { if (!jwtUtil.validateToken(token)) { return AuthenticationResult.failure("令牌无效" ); } String userId = jwtUtil.getUserIdFromToken(token); User user = userRepository.findById(userId).orElse(null ); if (user == null ) { return AuthenticationResult.failure("用户不存在" ); } if (user.getStatus() != UserStatus.ACTIVE) { return AuthenticationResult.failure("用户已被禁用" ); } return AuthenticationResult.success(user, token); } catch (Exception e) { log.error("令牌验证失败" , e); return AuthenticationResult.failure("令牌验证失败" ); } } public AuthenticationResult refreshToken (String refreshToken) { try { if (!jwtUtil.validateRefreshToken(refreshToken)) { return AuthenticationResult.failure("刷新令牌无效" ); } String userId = jwtUtil.getUserIdFromRefreshToken(refreshToken); User user = userRepository.findById(userId).orElse(null ); if (user == null ) { return AuthenticationResult.failure("用户不存在" ); } String newToken = jwtUtil.generateToken(user.getId()); String newRefreshToken = jwtUtil.generateRefreshToken(user.getId()); 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 @Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; @Value("${jwt.refresh-expiration}") private Long refreshExpiration; 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 ; } } public String getUserIdFromToken (String token) { Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); return claims.get("userId" , String.class); } 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 ; 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); 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 { 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 { if (!isValidQRCodeId(qrCodeData.getQrCodeId())) { return false ; } if (!isValidTimestamp(qrCodeData.getCreateTime())) { return false ; } if (!isValidIpAddress(qrCodeData.getIpAddress())) { return false ; } return true ; } catch (Exception e) { log.error("验证二维码完整性失败" , e); return false ; } } 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); return timeDiff <= 60 * 60 * 1000 ; } 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" ; } 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 { SecurityEvent event = new SecurityEvent (); event.setEventType(eventType); event.setQrCodeId(qrCodeId); event.setUserId(userId); event.setDetails(details); event.setTimestamp(System.currentTimeMillis()); event.setIpAddress(getCurrentIpAddress()); securityEventRepository.save(event); recordSecurityMetrics(eventType); 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) { long fiveMinutesAgo = System.currentTimeMillis() - 5 * 60 * 1000 ; long count = securityEventRepository.countByEventTypeAndTimestampAfter( event.getEventType(), fiveMinutesAgo); if (count > 10 ) { triggerSecurityAlert("异常频率告警" , event); } } private void checkSuspiciousBehavior (SecurityEvent event) { 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()); } private String getCurrentIpAddress () { 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); 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; } 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 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; } 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 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 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) { QRCodeData data = localCache.getIfPresent(qrCodeId); if (data != null ) { return data; } String redisKey = "qr_code:" + qrCodeId; data = (QRCodeData) redisTemplate.opsForValue().get(redisKey); if (data != null ) { localCache.put(qrCodeId, data); return data; } 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) 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 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);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 ); CREATE INDEX idx_qr_code_cover ON qr_code_data(qr_code_id, status, create_time, expire_time);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 关键要点
架构设计 :采用分层架构,支持PC端和移动端的协同工作
安全机制 :通过防重放攻击、防伪造攻击等机制保证安全性
状态管理 :通过Redis和数据库实现二维码状态的实时管理
监控告警 :建立完善的监控体系,及时发现和处理问题
性能优化 :通过缓存、数据库优化等手段提高系统性能
6.2 最佳实践
二维码设计 :使用唯一ID和时间戳,确保二维码的唯一性和时效性
安全防护 :实施多重安全防护措施,防止各种攻击
状态同步 :通过WebSocket实现实时状态同步
监控告警 :建立完善的监控告警体系
性能优化 :通过缓存和数据库优化提高系统性能
通过以上措施,可以构建一个安全、高效、可扩展的扫码登录系统,为用户提供优质的登录体验。