除了传统的登录方式以外,近几年越来越多的 Web 系统使用已经完成用户认证的手机端应用程序(App)扫描二维码来完成登录认证。比如手机微信扫码登录PC端微信、用京东 App扫码登录网页版京东。本章讨论此种登录方式的实现原理(只讨论原理,没有给出任何代码实现)。

33.1 手机App登录过程

在正式分析手机扫码登录原理之前,我们先讨论一下手机 App 的登录过程。

通常手机 App 可以登录使用用户、密码或手机及单次密码(验证码)登录应用系统,成功登录之后服务器返回签名的 JWT(即 JWS)给 App,之后 App可以凭借 JWS 访问服务器。这是标准的 JWS 登录认证过程。为了增强手机登录的安全性,通常在提交用户认证信息时,还可以将与当前手机相关的唯一设备信息一起提交给服务器。服务器在完成认证后记录该设备信息,并且在后续的每次访问中除了验证 JWS 以外,附加验证用户的设备信息。

为了增强认证强度,对安全级别要求高的 App 软件可以要求用户使用人脸识别或银行四要素登录来增强(或代替)用户名密码的登录方式。

33.2 京东扫码登录分析

我们以京东网页版扫码登录为研究对象,简要分析一下典型的扫码登录过程。

33.2.1 扫码登录入口

当准备登录京东网页版时,它跳转到 https://passport.jd.com/new/login.aspx 并默认展示扫码登录方式:
image.png
页面中的二维码是服务器动态生成的,它的链接地址为
https://qr.m.jd.com/show?appid=133&size=147&t=1634987384798
在这个链接中,参数 t 是用毫秒表示的请求二维码时的时间。

这个二维码编码的是如下形式的网址
https://qr.m.jd.com/p?k=AAEAIJ5WZcR0llry5x2MXfboUDVIRCEx-udUQOUkWz8Ua39z&appid=133
同时服务器在 Cookie 中放置了一个 QRCodeKey 变量

  1. Cookie: QRCodeKey=AAEAIG1-85QTOjkIozLiqXTo4B8i7ngOVDCsdzQQaPSXxQtJ;

分析可以认为二维码中的 k 值与 Cookie 中的 QRCodeKey 在服务器端是一一对应的,手机App用 k 值与服务器联系,网页用 QRCodeKey 与服务器联系。

33.2.2 等待用户扫码

在生成二维码后,网页每隔离3秒调用如下形式的链接
https://qr.m.jd.com/check?callback=jQuery2190258&appid=133&token=n78z8691tccuwbwk3wk8fo7r5du41l8p&_=1634976541532
其中

  • callback=jQuery2190258 是用来以 JSONP 的方式实现跨域访问
  • token 应该是和 Cookie 中的 QRCodeKey 一起在让服务器能够唯一确定当前PC 终端的页面
  • _ 是用毫秒表示的当前的时间

在等待用户扫码期间,服务器返回如下的内容

jQuery7221361({
   "code" : 201,
   "msg" : "二维码未扫描,请扫描二维码"
})

33.2.3 扫码后等待确认

用户收手机 App 扫描屏幕上的二维码,将自己的登录信息和从二维码中识别的信息发送给服务器,然后提示用户是否执行登录。下面是京东手机 App 显示的内容:
IMG_4405.jpg
服务器收到手机 App 的信息之后,在网页每次调用 check 时返回如下的状态信息:

jQuery8912881({
   "code" : 202,
   "msg" : "请手机客户端确认登录"
})

收到这个状态信息后,网页扫码部分变成如下内容
image.png

33.2.4 手机取消登录

如果用户在手机上点击“取消登录”,则服务器对之后的 check 调用返回如下状态

jQuery7948274({
   "code" : 205,
   "msg" : "二维码已取消授权"
})

网页程序收到此状态后不再持续调用 check 查询状态,并且把页面改成如下内容
image.png

33.2.5 手机确认登录

如果用户在手机上点击的时“确认登录电脑端”,则服务器对之后的 check 调用返回如下状态

jQuery6503652({
   "code" : 200,
   "ticket" : "AAEAMKBdYAy2x2iftxNV8eu6A4vK8-hPxG2RmpHBIXsMnYVKG4A9Fc48ig7NcL1GB9LXww"
})

网页端程序收到该状态后,调用如下的链接?
https://passport.jd.com/uc/qrCodeTicketValidation?t=AAEAMKBdYAy2x2iftxNV8eu6A4vK8-hPxG2RmpHBIXsMnYVKG4A9Fc48ig7NcL1GB9LXww&ReturnUr=登录后跳转的地址执行正式的登录操作。该操作可能有如下的返回结果:

  • 登录成功
  • 通知网页程序二维码已经失效
  • 登录失败(可能时多种原因)
  • 本次扫码存在风险,请改用密码登录

    33.2.6 等待超时状态

    在等候用户扫描二维码或等候用户确认是否登录的时候,网页每 3 秒执行一次查询请求。如果超过3分钟后用户还没有执行操作,则服务器返回下面的状态信息
    jQuery7537802({
     "code" : 203,
     "msg" : "二维码过期,请重新扫描"
    })
    
    网页收到此状态信息后显示如下的内容,不再继续执行查询请求
    image.png

    33.2.7 扫描失效二维码

    如果手机 App 扫描的是已经失效(超时或已经被扫描过)的二维码,收到服务器返回的失效状态后,京东App 显示如下的内容

IMG_4408.jpg
需要说明的是,用户点击“重新扫描”后,App 只会重现进入扫码功能,不会通过服务器指挥网页做任何变化。用户需要自己手动在浏览上点击刷新二维码。这么处理是为了安全。

33.2.8 扫码补充分析

通过对京东扫码登录进行分析,我们可以获得如下额外信息:

  • 生成二维码时间以网页发出查询请求的浏览器端时间为准。
  • 每3秒一次的查询均向服务器传递浏览器端时间,这样有利于提高服务器计算效率(估计服务器直接计算,无须转换成时间)。
  • 京东之前应该是把等候扫码超时和等候用户选择超时分成了两个不同状态,后来统一为一个状态之后没有把前端代码全部改完。

    33.3 扫码登录流程设计

    分析完京东网页版扫码登录之后就很容易完成这个过程的设计了:
  1. 用户打开网站的登录页面的时候,向服务器发送获取登录二维码的请求,并且告知服务器浏览器段的当前时间

  2. 服务器收到请求后,用某种算法随机生成唯一编码(如uuid),将这个编码作为 Key 值存入 Redis 服务器,同时记录浏览器传递的申请时间和该 Key 值的过期时间,把状态标记为“待扫描”。

  3. 将这个 Key 值和本公司的验证信息组合在一起,通过二维码生成接口生成一个二维码的图片,然后将二维码图片和Key 值一起返回给浏览器。

  4. 浏览器拿到二维码和 Key 后,每隔 3 秒向浏览器发送一次查询请求获得最新状态。在请求中携带 Kev 值和浏览器端时间。

  5. 手机 App 扫描二维码后解码获得编码,把编码发送给服务器。

  6. 服务器从编码中计算得出 Key 值,然后在 Redis 中用 Key 值查询,如果该 Ke 值已经过期,则告知手机 App需要重新扫描。否则提示用户选择是否执行登录,并且把 Key 值对应的状态标记为“已扫描待确认”。浏览器端再次查询状态时可以得到这个等待确认的状态,并且重置等候的起始时间。

  7. 如果用户选择取消登录,则 App 告知服务器不再执行登录,服务器把当前的 Key 值标记为“用户取消”。浏览器端再次查询状态时可以得到这个取消的状态,并且服务器在响应完该查询后将 Key 值从 Redis 中删除。
    这里有两个要点需要说明:一是不应该在 Redis 中保存用户取消登录的 Key 值,否则数量会非常巨大。二是因为不保存取消过的 Key 值,要求每次生成的 Key 必须是唯一的。

  8. 如果用户选择执行登录,则之前的编码和 App 当前已经登录的用户凭证(如 JWS)一起发给服务器。服务器从编码中解码得出 Key值,把当前的 Key 值标记为“确认登录”并且把 Key 与手机 App 提供的用户信息关联。浏览器端再次查询状态时可以得到这个确认的状态并且可以获得一个专门为此次登录生成的一个 Token (或 Ticket )。此时并不删除 Redisk 中的 Key 值。

  9. 浏览器端获得用户确认登录的状态和 Token 后,携带 Token 执行一个专门的请求。服务器根据收到的 Token解码或查询得出对应的用户信息及 Key 值,用事先保存的时间判断 Key 值是否依然有效,判断对应的用户是否可以正常登录,以及执行系统自己定义的风险控制检验。如果此三类检验都没有问题,则正式完成用户浏览器端网页程序的登录。

  10. 若服务器在响应浏览器查询状态的请求时计算出等候用户扫描或者等候用户确认超时(超过3分钟),则直接返回超时状态,并且删除当前的 Key值。

  11. 若服务器从手机 App 传递的编码中无法计算出 Key 值,或者计算出的 Key 值不存在于 Redis 中,则通知手机 App 该二维码无效,需要重新扫描。

当网页版的应用程序完成用户登录后,如果非“前后端”严格分开的应用,可以根据实际需要选择各种传统的方式记录登录状态。对于是本教程讨论的前后端分开的应用程序,应该使用 JWS 标识登录状态。

33.4 重要提醒

这里有3个非常重要的提醒:

  • 上面的流程设计中忽略了对各种验证失败时的处理,实际的开发中要对所有的状态进行处理。

  • 在浏览器执行登录请求的时候,绝对不能做出从数据库中查询出密码并且用密码进行登录的行为。数据库中不仅不允许保存明文密码,即便是用算法加密后的用户密码也是禁止通过网络传输的。在这方面,有个在网络上流传很广的流程图提到了从数据库中查询密码,这个犯了严重错误。

  • 不管是手机 App 还是浏览器的网页程序,他们和服务器之间的所有通信过程必须通过 HTTPS 完成。


33.5 参考资料

徒手撸一个扫码登录示例工程
Spring Boot高性能实现二维码扫码登录(中)——Redis版

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。