# scan-code-login-demo **Repository Path**: wyaoao/scan-code-login-demo ## Basic Information - **Project Name**: scan-code-login-demo - **Description**: app scan login demo - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 0 - **Created**: 2023-12-14 - **Last Updated**: 2025-06-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Springboot+redis完成app扫码登录操作(轮训or长轮询) - **扫码登录流程** - **集群中使用长轮询完成状态感知** #### 扫码登录实现大致流程 1. web端发起请求生成二维码 ,服务记录生成二维码状态(等待扫码)及其唯一标识ID。 2. web端渲染二维码、使用二维码标识ID发起轮训或者长轮询获取二维状态。 - 轮询 : 实现简单不存在考虑集群问题。 - 长轮询&长链接: 考虑到集群环境下实现会有繁琐的操作。 3. app端对二维码进行扫码,改变二维码状态(扫码中) 4. App在对扫码进行确认(已确认)或者取消操作(已取消)。 - 确认的时候进行web新token生成,在长轮询返回给web端 ![输入图片说明](image-20231214153256730.png) 主要麻烦一点是使用 `Spring DeferredResult` 在集群环境下怎样去找到,正在查询二维码状态的那个长链接。 `LongPollFactory` ```java public class LongPollFactory { private static final Map>> ONLINE_SESSION_CLIENT_MAP = new ConcurrentHashMap<>(); public static void put(String qrcodeId, DeferredResult> session) { ONLINE_SESSION_CLIENT_MAP.put(qrcodeId, session); } public static DeferredResult> get(String qrcodeId) { return ONLINE_SESSION_CLIENT_MAP.get(qrcodeId); } public static void remove(String qrcodeId) { ONLINE_SESSION_CLIENT_MAP.remove(qrcodeId); } } ``` **Controller的示例:** ```java @GetMapping("/checkQrStateV2/{qrcodeId}") public DeferredResult> checkQrStateV2(@PathVariable("qrcodeId") String qrcodeId) { //初始化DeferredResult 超时时间 和超时后返回什么内容 final DeferredResult> deferredResult = new DeferredResult<>( 20000L, () -> Result.ok(qrcodeLoginService.checkQrState(qrcodeId)) ); //判断二维码不存在立即返回 final boolean exists = qrcodeLoginService.getQrCodeInfoBucket(qrcodeId).isExists(); if (!exists) { deferredResult.setResult(Result.ok(new QrCodeInfo(qrcodeId, QrCodeStatusEnum.INVALID))); } //超时 & 完成 时的操作 deferredResult.onTimeout(() -> { log.info("QrcodeLoginController#checkQrStateV2 调用超时 qrcodeId:{}", qrcodeId); LongPollFactory.remove(qrcodeId); }); deferredResult.onCompletion(() -> { log.info("QrcodeLoginController#checkQrStateV2 调用完成 qrcodeId:{}", qrcodeId); LongPollFactory.remove(qrcodeId); }); //放入长轮询的工程,在状态变化是通知使用 LongPollFactory.put(qrcodeId, deferredResult); return deferredResult; } ``` 这里项目正好使用了Redis,这里就使用redis发布订阅完成消息通知。 **消息监听:** ```java @Slf4j @Component public class QrCodeStateChangeListener implements ApplicationRunner { private final RedissonClient redissonClient; private final QrcodeLoginService qrcodeLoginService; public QrCodeStateChangeListener(RedissonClient redissonClient, QrcodeLoginService qrcodeLoginService) { this.redissonClient = redissonClient; this.qrcodeLoginService = qrcodeLoginService; } @Override public void run(ApplicationArguments args) { final String topic = RedisGenerateKeyUtils.buildKey(TopicConstant.QRCODE_STATE_CHANGE_TOPIC); final RTopic rTopic = redissonClient.getTopic(topic, JsonJacksonCodec.INSTANCE); rTopic.addListener(String.class, (channel, qrcodeId) -> { log.info("topic: {} 收到消息 {}.", channel, qrcodeId); //能够在工程 final DeferredResult> deferredResult = LongPollFactory.get(qrcodeId); if (Objects.isNull(deferredResult)) { return; } deferredResult.setResult(Result.ok(qrcodeLoginService.checkQrState(qrcodeId))); }); } } ```