ikki@github.io:~$

扫码登陆实战方案

扫码登陆实战方案

需求

  • 用户 使用手机扫描 浏览器登录页的二维码
  • 用户手机上显示 用户许可页面,同时浏览器显示扫码成功,处于等待确认状态
  • 用户在手机上确认登陆,浏览器显示确认成功并跳转登陆后页面

分析

  1. 浏览器请求服务器 并生成二维码,显然二维码内含有一些信息能被对应的手机客户端识别
    • 二维码内的信息不能被其他软件识别,否则的话,浏览器会被其他人的客户端识别并发起登陆请求
    • 二维码被扫描后不能再次被识别,有可能被其他账号扫描并登陆
  2. 浏览器二维码被展示出来后,需要实时监听到二维码的消费情况。
    • 浏览器轮询显然是个不错的选择,但是我们可以使用**Server Sent Event **会是更好的选择
  3. 二维码在一定时间内没有被消费,应当过期处理
    • 断网从连不能影响消息的消费

代码实战

浏览器 获取二维码等待响应

    @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();
    }