扫码登陆实战方案
扫码登陆实战方案
需求
- 用户 使用手机扫描 浏览器登录页的二维码
- 用户手机上显示 用户许可页面,同时浏览器显示扫码成功,处于等待确认状态
- 用户在手机上确认登陆,浏览器显示确认成功并跳转登陆后页面
分析
- 浏览器请求服务器 并生成二维码,显然二维码内含有一些信息能被对应的手机客户端识别
- 二维码内的信息不能被其他软件识别,否则的话,浏览器会被其他人的客户端识别并发起登陆请求
- 二维码被扫描后不能再次被识别,有可能被其他账号扫描并登陆
- 浏览器二维码被展示出来后,需要实时监听到二维码的消费情况。
- 浏览器轮询显然是个不错的选择,但是我们可以使用**
Server Sent Event
**会是更好的选择
- 浏览器轮询显然是个不错的选择,但是我们可以使用**
- 二维码在一定时间内没有被消费,应当过期处理
- 断网从连不能影响消息的消费
代码实战
浏览器 获取二维码等待响应
@GetMapping("/qr-code/{client_id}")
public ResponseEntity<SseEmitter> QRLogin(@PathVariable("client_id") String clientId) {
String authcode = UUID.randomUUID().toString();
SseEmitter sseEmitter = new SseEmitter(90 * 1000L); //90s 内没有完成 SSE会超时
String id = clientId + '@' + authcode;
BlockingQueue<String> queue = redisson.getBlockingQueue("SSE_QUE:" + id); // 生成通道
localCachedMap.put(id, true);
threadPoolTaskExecutor.submit(() -> {
Map<String, String> body = new HashMap<>();
body.put("clientId", clientId);
body.put("authCode", authcode);
body.put("redirectUrl", "immigrant.com/login/qr");
try {
sseEmitter.send(body, MediaType.APPLICATION_JSON);
while (true) {
String s = queue.take();
log.info(s);
if ("END".equals(s)) {
localCachedMap.put(id, false); //完成后会关闭通道, 二维码泄露也不会发送消息到服务端
sseEmitter.complete();
} else {
sseEmitter.send(s);
}
}
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
});
// SSE 链接销毁时,要设置通道关闭
sseEmitter.onCompletion(() -> {
queue.clear();
log.info("sse {} completed", id);
});
return ResponseEntity.ok(sseEmitter);
客户端扫描二维码并发送请求
@PostMapping("/qr-code/{client_id}")
public ResponseEntity<SseEmitter> QRLogin(@PathVariable("client_id") String clientId,@RequestBody ScanRequest request) {
BlockingQueue<String> queue;
if (Boolean.TRUE.equals(localCachedMap.get(clientId))) {
queue = redisson.getBlockingQueue("SSE_QUE:" + clientId);
} else {
return new ResponseEntity<>(HttpStatus.GONE);
}
try {
queue.offer(objectMapper.writeValueAsString(scanRequest));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return ResponseEntity.noContent().build();
}