扫码登录流程
- 流程
- 问题
- 参考
流程
PC 端
用户在浏览器打开登录页面。
浏览器向服务器请求二维码信息。
服务器生成一个 qrcodeId 存储到 redis,并设置过期时间,将 qrcodeId 或者带有 qrcodeId 参数的 url(微信是 https://open.weixin.qq.com/connect/confirm?uuid=001Cwqch0e8IGa1U)返回给浏览器。
redis key 是 qrcodeId,value 如下:
{"state":"waiting"}浏览器根据 url 渲染二维码。
长轮询查询二维码状态。
手机端
确保手机应用是已登录状态,扫描二维码,从二维码中解析出 url,带着 mobile token 访问 url。
服务器查询 redis 判断 qrcodeId 是否已过期,状态是否为 waiting,若不符合条件则返回无效的二维码错误。若符合条件则生成一个与 qrcodeId 关联的临时 token,同时保存用户信息到 qrcodeId 的 key 中,二维码状态改为已扫描,更新 redis 的过期时间,最后返回临时 token。
更新 key qrcodeId 的 value 如下:
{"state":"scanned",“tmpToken”:"xxx","userId":"1","nickname":"xx",// 昵称"avatar":"xx"// 头像}手机端显示确认登录页面。
PC 端
长轮询查询二维码状态请求此时返回内容,包括状态 scanned 和用户信息。
浏览器显示状态为已扫描,同时可显示扫描人的昵称、头像。
手机端
用户点击允许登录,浏览器携带 qrcodeId、临时 token 发起确认登录请求。
服务器查询 redis 判断 qrcodeId 是否已过期,状态是否为 scanned,tmpToken 是否与参数传入的一致,若不符合条件则返回非法操作错误。若符合条件则生成一个与当前 mobile token 关联的 pc token,二维码状态改为已确认,更新 redis 的过期时间。
更新 key qrcodeId 的 value 如下:
{"state":"confirmed",“tmpToken”:"xxx","userId":"1","nickname":"xx",// 昵称"avatar":"xx",// 头像"pcToken":"xx"}
PC 端
- 长轮询查询二维码状态请求此时返回内容,包括状态 confirmed 和 pcToken。
- 浏览器使用 pcToken 完成登录流程。
问题
为什么需要生成临时 token?
为了保证扫码和点击允许登录是同一个设备。
保证二维码只会被扫描一次。多次扫码会发现临时 token 已存在,然后报错。二维码状态查询接口如何感知状态变更并返回给浏览器?
由于使用的是长轮询,所以必须使用异步请求,否则很容易导致 servlet 容器线程耗尽。spring 异步请求可以使用 DeferredResult 实现。
使用一个全局的 Map,key 是 qrcodeId,value 是 DeferredResult。
调用二维码状态查询接口时,先执行
map.set(qrcodeId, DeferredResult),然后查询 redis 判断当前状态与传入的状态是否相同,如果不相同则直接map.remove(qrcodeId).setResult(x),相同则等待。当用户点击允许登录时,在更新完 redis 后,调用
map.remove(qrcodeId).setResult(x)将最新状态返回浏览器,实现了两个线程间的交互。分布式环境中二维码状态查询请求和允许登录请求可能打到不同的服务器,如何解决?
方法一:在网关层根据 qrcodeId 进行负载均衡,保证同一个 qrcodeId 的请求到达同一台服务器。
方法二:与单机类似,每个服务器维护自己的全局 Map,处理允许登录请求时,向 MQ 发送一条广播消息,所有服务器都消费,检查 qrcodeId 是否在自己的全局 Map 里,如果是就
map.remove(qrcodeId).setResult(x)将最新状态返回浏览器。
参考
- 手机扫一下二维码,电脑端是怎么自动登录的?五分钟带你搞懂扫码登录的核心逻辑,面试官想听的不只是“传token”java面试必看!
- 二维码扫描登录是什么原理?
- 让你设计一个扫码登录怎么实现?