在当今数字化时代,扫码登录已成为主流应用的标准功能之一。它不仅提供了便捷的用户体验,还增强了安全性。本文将从架构师的角度,深入分析如何设计和实现一个高效、安全的扫码登录系统。
OAuth 2.0设备授权流程(RFC 8628)
OpenID Connect
JWT (JSON Web Token)
OWASP安全指南
后端框架
缓存服务
消息队列
二维码生成
令牌管理
会话安全
身份验证
防止攻击
下面是一个基于Spring Boot和Spring Security实现的扫码登录核心代码:
JwtService.java
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtService {
private final String SECRET_KEY = "your-secret-key"; // 实际应用中应从配置中获取
private final long EXPIRATION_TIME = 86400000; // 24小时
public String generateToken(String userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
return Jwts.builder()
.setClaims(claims)
.setSubject(userId)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public String getUserIdFromToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
LoginStatus.java
public enum LoginStatus {
WAITING(0, "等待扫描"),
SCANNED(1, "已扫描"),
CONFIRMED(2, "已确认"),
CANCELLED(3, "已取消"),
EXPIRED(4, "已过期");
private int value;
private String description;
LoginStatus(int value, String description) {
this.value = value;
this.description = description;
}
public int getValue() {
return value;
}
public String getDescription() {
return description;
}
}
QRCodeGenerator.java
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import java.awt.image.BufferedImage;
public class QRCodeGenerator {
public BufferedImage generateQRCodeImage(String content, int width, int height) throws Exception {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, width, height);
return MatrixToImageWriter.toBufferedImage(bitMatrix);
}
}
QRCodeLoginController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/login")
public class QRCodeLoginController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private QRCodeGenerator qrCodeGenerator;
@Autowired
private UserService userService;
@Autowired
private JwtService jwtService;
/**
* 生成登录二维码
*/
@GetMapping("/qrcode")
public Map<String, Object> generateQRCode() {
Map<String, Object> result = new HashMap<>();
// 生成唯一标识
String uuid = UUID.randomUUID().toString();
// 初始化二维码状态
QRCodeStatus status = new QRCodeStatus();
status.setUuid(uuid);
status.setStatus(LoginStatus.WAITING);
status.setCreateTime(System.currentTimeMillis());
// 存储到Redis,有效期5分钟
redisTemplate.opsForValue().set("qrcode:" + uuid, status, 5, TimeUnit.MINUTES);
// 生成二维码图片
try {
String qrContent = "https://your-app.com/login?uuid=" + uuid;
BufferedImage qrImage = qrCodeGenerator.generateQRCodeImage(qrContent, 200, 200);
// 将图片转为Base64编码
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(qrImage, "png", baos);
String qrCodeBase64 = Base64.getEncoder().encodeToString(baos.toByteArray());
result.put("uuid", uuid);
result.put("qrCode", qrCodeBase64);
result.put("expireTime", 300); // 300秒过期
result.put("status", LoginStatus.WAITING.getValue());
} catch (Exception e) {
result.put("success", false);
result.put("message", "生成二维码失败");
}
return result;
}
/**
* 检查二维码状态(轮询)
*/
@GetMapping("/qrcode/status")
public Map<String, Object> checkQRCodeStatus(@RequestParam String uuid) {
Map<String, Object> result = new HashMap<>();
// 从Redis获取状态
QRCodeStatus status = (QRCodeStatus) redisTemplate.opsForValue().get("qrcode:" + uuid);
if (status == null) {
result.put("status", LoginStatus.EXPIRED.getValue());
result.put("message", "二维码已过期");
} else {
result.put("status", status.getStatus().getValue());
// 如果已确认登录,返回token
if (status.getStatus() == LoginStatus.CONFIRMED) {
String token = jwtService.generateToken(status.getUserId());
result.put("token", token);
result.put("userInfo", userService.getUserInfo(status.getUserId()));
// 登录成功后删除Redis中的状态
redisTemplate.delete("qrcode:" + uuid);
}
}
return result;
}
/**
* App扫描二维码
*/
@PostMapping("/qrcode/scan")
public Map<String, Object> scanQRCode(@RequestBody Map<String, String> request) {
Map<String, Object> result = new HashMap<>();
String uuid = request.get("uuid");
String userId = request.get("userId");
// 获取二维码状态
QRCodeStatus status = (QRCodeStatus) redisTemplate.opsForValue().get("qrcode:" + uuid);
if (status == null) {
result.put("success", false);
result.put("message", "二维码不存在或已过期");
} else if (status.getStatus() != LoginStatus.WAITING) {
result.put("success", false);
result.put("message", "二维码状态已更新,请刷新页面");
} else {
// 更新状态为已扫描
status.setStatus(LoginStatus.SCANNED);
status.setUserId(userId);
status.setScanTime(System.currentTimeMillis());
// 延长有效期
redisTemplate.opsForValue().set("qrcode:" + uuid, status, 5, TimeUnit.MINUTES);
result.put("success", true);
result.put("message", "扫描成功,请在手机上确认登录");
}
return result;
}
/**
* App确认登录
*/
@PostMapping("/qrcode/confirm")
public Map<String, Object> confirmLogin(@RequestBody Map<String, String> request) {
Map<String, Object> result = new HashMap<>();
String uuid = request.get("uuid");
String userId = request.get("userId");
// 获取二维码状态
QRCodeStatus status = (QRCodeStatus) redisTemplate.opsForValue().get("qrcode:" + uuid);
if (status == null) {
result.put("success", false);
result.put("message", "二维码不存在或已过期");
} else if (status.getStatus() != LoginStatus.SCANNED) {
result.put("success", false);
result.put("message", "二维码状态不正确,请重新扫描");
} else if (!userId.equals(status.getUserId())) {
result.put("success", false);
result.put("message", "用户身份不匹配");
} else {
// 验证用户身份
if (userService.validateUser(userId)) {
// 更新状态为已确认
status.setStatus(LoginStatus.CONFIRMED);
status.setConfirmTime(System.currentTimeMillis());
// 延长有效期
redisTemplate.opsForValue().set("qrcode:" + uuid, status, 1, TimeUnit.MINUTES);
result.put("success", true);
result.put("message", "登录确认成功");
} else {
result.put("success", false);
result.put("message", "用户验证失败");
}
}
return result;
}
}
QRCodeStatus.java
public class QRCodeStatus {
private String uuid;
private LoginStatus status;
private String userId;
private long createTime;
private long scanTime;
private long confirmTime;
// Getters and Setters
// ...
}
生产环境部署
性能优化
安全加固
在面试中回答这个问题时,可以按照以下思路组织答案:
架构设计:
技术选型:
安全考虑:
扩展性:
通过以上设计和实现,你可以构建一个安全、可靠、用户友好的扫码登录系统,满足现代应用的需求。这个方案不仅符合业界权威标准,还考虑了性能、安全和扩展性等关键因素。
通过以上内容,你可以全面了解Java实现自有应用App扫码登录功能的架构设计、技术选型、实现细节和安全考量,为面试和实际开发提供参考。