Springboot+redis完成app扫码登录操作(轮询or长轮询)
- 扫码登录流程
- 集群中使用长轮询完成状态感知
- demo完整代码地址:https://gitee.com/wyaoao/scan-code-login-demo
扫码登录实现大致流程
-
web端发起请求生成二维码 ,服务记录生成二维码状态(等待扫码)及其唯一标识ID。
-
web端渲染二维码、使用二维码标识ID发起轮询或者长轮询获取二维状态。
-
轮询 : 实现简单不存在考虑集群问题。
-
长轮询&长链接: 考虑到集群环境下实现会有繁琐的操作。
-
-
app端对二维码进行扫码,改变二维码状态(扫码中)
-
App在对扫码进行确认(已确认)或者取消操作(已取消)。
- 确认的时候进行web新token生成,在长轮询返回给web端
主要麻烦一点是使用 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完整代码地址
https://gitee.com/wyaoao/scan-code-login-demo