闽公网安备 35020302035485号
4.完成登录:授权通过后,后端更新Redis中二维码对应的登录状态为已登录,前端监听到登录状态变化后,完成登录流程,跳转至相应页面。


<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- ZXing for QR Code generation -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
配置spring:
redis:
host: localhost
port: 6379
database: 0
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
session:
store-type: redis
生成二维码/**
* 堆代码 duidaima.com
* 二维码生成工具类
*/
public class QRCodeUtil {
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
private static final String FORMAT = "png";
/**
* 生成二维码字节数组
* @param content 二维码内容
* @return 二维码图片字节数组
*/
public static byte[] generateQRCode(String content) throws WriterException, IOException {
Map<EncodeHintType, Object> hints = new HashMap<>();
// 设置字符编码
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
// 设置容错级别
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
// 设置边距
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = new MultiFormatWriter().encode(
content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, hints);
BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, FORMAT, outputStream);
return outputStream.toByteArray();
}
/**
* 生成二维码Base64字符串
*/
public static String generateQRCodeBase64(String content) throws WriterException, IOException {
byte[] bytes = generateQRCode(content);
return "data:image/png;base64," + java.util.Base64.getEncoder().encodeToString(bytes);
}
}
定义常量和实体类/**
* 常量类
*/
public class Constants {
// Redis 中二维码状态的前缀
public static final String QR_CODE_PREFIX = "qr:code:";
// 二维码过期时间(秒)
public static final long QR_CODE_EXPIRE = 5 * 60;
}
/**
* 二维码状态枚举
*/
public enum QRCodeStatus {
WAITING("waiting", "等待扫描"),
SCANNED("scanned", "已扫描"),
CONFIRMED("confirmed", "已确认"),
CANCELLED("cancelled", "已取消"),
EXPIRED("expired", "已过期"),
ERROR("error", "错误");
private String code;
private String message;
QRCodeStatus(String code, String message) {
this.code = code;
this.message = message;
}
// getter 方法
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}
/**
* WebSocket消息实体
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WebSocketMessage {
private String uuid;
private QRCodeStatus status;
private String message;
private Object data;
}
/**
* 用户信息实体
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
private Long userId;
private String username;
private String nickname;
private String avatar;
}
配置WebSocket/**
* WebSocket配置
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用简单消息代理,前缀为/topic的消息会被代理转发到订阅了相应主题的客户端
config.enableSimpleBroker("/topic");
// 客户端发送消息的前缀
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册STOMP端点,客户端通过此端点连接到WebSocket服务器
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
二维码服务实现/**
* 二维码服务
*/
@Service
public class QRCodeService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 创建新的二维码
* @return 二维码UUID
*/
public String createQRCode() {
String uuid = java.util.UUID.randomUUID().toString();
String redisKey = Constants.QR_CODE_PREFIX + uuid;
// 存储二维码状态到Redis
redisTemplate.opsForHash().put(redisKey, "status", QRCodeStatus.WAITING.getCode());
redisTemplate.expire(redisKey, Constants.QR_CODE_EXPIRE, TimeUnit.SECONDS);
return uuid;
}
/**
* 更新二维码状态
* @param uuid 二维码UUID
* @param status 新状态
*/
public void updateQRCodeStatus(String uuid, QRCodeStatus status) {
String redisKey = Constants.QR_CODE_PREFIX + uuid;
redisTemplate.opsForHash().put(redisKey, "status", status.getCode());
redisTemplate.expire(redisKey, getRemainingTime(uuid), TimeUnit.SECONDS);
}
/**
* 更新二维码状态并关联用户信息
* @param uuid 二维码UUID
* @param status 新状态
* @param userInfo 用户信息
*/
public void updateQRCodeStatusWithUser(String uuid, QRCodeStatus status, UserInfo userInfo) {
String redisKey = Constants.QR_CODE_PREFIX + uuid;
// 使用Hash结构存储状态和用户信息
redisTemplate.opsForHash().put(redisKey, "status", status.getCode());
redisTemplate.opsForHash().put(redisKey, "userInfo", userInfo);
redisTemplate.expire(redisKey, getRemainingTime(uuid), TimeUnit.SECONDS);
}
/**
* 获取二维码状态
* @param uuid 二维码UUID
* @return 状态
*/
public QRCodeStatus getQRCodeStatus(String uuid) {
String redisKey = Constants.QR_CODE_PREFIX + uuid;
Object status = redisTemplate.opsForHash().get(redisKey, "status");
if (status == null) {
return QRCodeStatus.EXPIRED;
}
for (QRCodeStatus qrCodeStatus : QRCodeStatus.values()) {
if (qrCodeStatus.getCode().equals(status.toString())) {
return qrCodeStatus;
}
}
return QRCodeStatus.ERROR;
}
/**
* 获取二维码关联的用户信息
* @param uuid 二维码UUID
* @return 用户信息
*/
public UserInfo getUserInfo(String uuid) {
String redisKey = Constants.QR_CODE_PREFIX + uuid;
return (UserInfo) redisTemplate.opsForHash().get(redisKey, "userInfo");
}
/**
* 获取Redis键的剩余时间
* @param uuid 二维码UUID
* @return 剩余时间(秒)
*/
private Long getRemainingTime(String uuid) {
String redisKey = Constants.QR_CODE_PREFIX + uuid;
return redisTemplate.getExpire(redisKey, TimeUnit.SECONDS);
}
/**
* 使二维码过期
* @param uuid 二维码UUID
*/
public void expireQRCode(String uuid) {
String redisKey = Constants.QR_CODE_PREFIX + uuid;
redisTemplate.delete(redisKey);
}
}
后端控制器实现/**
* 登录控制器
*/
@RestController
@RequestMapping("/api/login")
public class LoginController {
@Autowired
private QRCodeService qrCodeService;
@Autowired
private SimpMessagingTemplate messagingTemplate;
/**
* 生成二维码
*/
@GetMapping("/qrCode")
public Map<String, Object> generateQRCode() throws Exception {
String uuid = qrCodeService.createQRCode();
String qrCodeBase64 = QRCodeUtil.generateQRCodeBase64(uuid);
Map<String, Object> result = new HashMap<>();
result.put("uuid", uuid);
result.put("qrCode", qrCodeBase64);
result.put("expireTime", Constants.QR_CODE_EXPIRE);
return result;
}
/**
* 处理扫码请求
*/
@PostMapping("/scan")
public Map<String, Object> scanQRCode(@RequestBody Map<String, String> request) {
String uuid = request.get("uuid");
Long userId = Long.parseLong(request.get("userId"));
Map<String, Object> result = new HashMap<>();
// 检查二维码是否存在且有效
QRCodeStatus status = qrCodeService.getQRCodeStatus(uuid);
if (status != QRCodeStatus.WAITING) {
result.put("success", false);
result.put("message", "二维码无效或已过期");
return result;
}
// 获取用户信息(这里应该从数据库查询,简化示例)
UserInfo userInfo = new UserInfo(userId, "一安", "一安未来", "https://picsum.photos/200/200");
// 更新二维码状态为已扫描
qrCodeService.updateQRCodeStatusWithUser(uuid, QRCodeStatus.SCANNED, userInfo);
// 通过WebSocket通知前端二维码已被扫描
WebSocketMessage message = new WebSocketMessage(
uuid,
QRCodeStatus.SCANNED,
"二维码已被扫描,请确认登录",
userInfo
);
messagingTemplate.convertAndSend("/topic/qr/" + uuid, message);
result.put("success", true);
result.put("message", "扫码成功,请在PC端确认登录");
return result;
}
/**
* 处理授权请求
*/
@PostMapping("/authorize")
public Map<String, Object> authorize(@RequestBody Map<String, String> request) {
String uuid = request.get("uuid");
Boolean confirm = Boolean.parseBoolean(request.get("confirm"));
Map<String, Object> result = new HashMap<>();
// 检查二维码是否存在且已扫描
QRCodeStatus status = qrCodeService.getQRCodeStatus(uuid);
if (status != QRCodeStatus.SCANNED) {
result.put("success", false);
result.put("message", "二维码状态无效");
return result;
}
if (confirm) {
// 获取用户信息
UserInfo userInfo = qrCodeService.getUserInfo(uuid);
// 更新二维码状态为已确认
qrCodeService.updateQRCodeStatusWithUser(uuid, QRCodeStatus.CONFIRMED, userInfo);
// 通过WebSocket通知前端登录成功
WebSocketMessage message = new WebSocketMessage(
uuid,
QRCodeStatus.CONFIRMED,
"登录成功",
userInfo
);
messagingTemplate.convertAndSend("/topic/qr/" + uuid, message);
result.put("success", true);
result.put("message", "授权成功");
result.put("userInfo", userInfo);
} else {
// 更新二维码状态为已取消
qrCodeService.updateQRCodeStatus(uuid, QRCodeStatus.ERROR);
// 通过WebSocket通知前端登录已取消
WebSocketMessage message = new WebSocketMessage(
uuid,
QRCodeStatus.ERROR,
"用户取消登录",
null
);
messagingTemplate.convertAndSend("/topic/qr/" + uuid, message);
result.put("success", false);
result.put("message", "用户取消登录");
}
return result;
}
/**
* 检查二维码状态(轮询方式)
*/
@GetMapping("/checkStatus")
public Map<String, Object> checkStatus(@RequestParam String uuid) {
Map<String, Object> result = new HashMap<>();
QRCodeStatus status = qrCodeService.getQRCodeStatus(uuid);
result.put("status", status.getCode());
result.put("message", status.getMessage());
if (status == QRCodeStatus.SCANNED) {
// 获取用户信息
UserInfo userInfo = qrCodeService.getUserInfo(uuid);
result.put("userInfo", userInfo);
}
return result;
}
}
WebSocket消息处理器/**
* WebSocket消息处理器
*/
@Controller
public class WebSocketController {
/**
* 处理客户端订阅二维码状态的消息
*/
@MessageMapping("/subscribeQr")
@SendTo("/topic/qr/{uuid}")
public WebSocketMessage subscribeQr(@PathVariable String uuid) {
// 这里可以根据需要返回初始状态
return new WebSocketMessage(
uuid,
QRCodeStatus.WAITING,
"等待扫描",
null
);
}
}
三.总结6.前端建立WebSocket连接,准备接收状态更新(轮询方式不需要)
7.用户在移动端选择要登录的账号并确认
7.移动端显示登录成功界面