Springboot+redis完成app扫码登录操作(轮训or长轮询)

140 阅读2分钟

Springboot+redis完成app扫码登录操作(轮训or长轮询)

扫码登录实现大致流程

  1. web端发起请求生成二维码 ,服务记录生成二维码状态(等待扫码)及其唯一标识ID。

  2. web端渲染二维码、使用二维码标识ID发起轮训或者长轮询获取二维状态。

    • 轮询 : 实现简单不存在考虑集群问题。

    • 长轮询&长链接: 考虑到集群环境下实现会有繁琐的操作。

  3. app端对二维码进行扫码,改变二维码状态(扫码中)

  4. App在对扫码进行确认(已确认)或者取消操作(已取消)。

    • 确认的时候进行web新token生成,在长轮询返回给web端

image-20231214153256730.png 主要麻烦一点是使用 Spring DeferredResult 在集群环境下怎样去找到,正在查询二维码状态的那个长链接。

LongPollFactory

public class LongPollFactory {

    private static final Map<String, DeferredResult<Result<QrCodeInfo>>> ONLINE_SESSION_CLIENT_MAP = new ConcurrentHashMap<>();

    public static void put(String qrcodeId, DeferredResult<Result<QrCodeInfo>> session) {
        ONLINE_SESSION_CLIENT_MAP.put(qrcodeId, session);
    }

    public static DeferredResult<Result<QrCodeInfo>> get(String qrcodeId) {
        return ONLINE_SESSION_CLIENT_MAP.get(qrcodeId);
    }

    public static void remove(String qrcodeId) {
        ONLINE_SESSION_CLIENT_MAP.remove(qrcodeId);
    }

}

Controller的示例:

    @GetMapping("/checkQrStateV2/{qrcodeId}")
    public DeferredResult<Result<QrCodeInfo>> checkQrStateV2(@PathVariable("qrcodeId") String qrcodeId) {
        //初始化DeferredResult 超时时间  和超时后返回什么内容
        final DeferredResult<Result<QrCodeInfo>> 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发布订阅完成消息通知。

消息监听:

@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<Result<QrCodeInfo>> deferredResult = LongPollFactory.get(qrcodeId);
            if (Objects.isNull(deferredResult)) {
                return;
            }
            deferredResult.setResult(Result.ok(qrcodeLoginService.checkQrState(qrcodeId)));
        });
    }

}

demo完整代码地址

gitee.com/wyaoao/scan…