权限认证

为什么使用jwt

传统的用户认证流程如下:

  1. 用户向服务器发送用户名和密码
  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
  3. 服务器向用户返回一个 session_id,写入用户的 Cookie
  4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
  5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于扩展性不好,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session

在基于 Token 进行身份验证的的应用程序中,服务器通过 PayloadHeader和一个密钥(secret)创建令牌(Token)并将 Token 发送给客户端,客户端将 Token 保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP HeaderAuthorization字段中Authorization: Bearer Token。流程如下:

  1. 用户向服务器发送用户名和密码用于登陆系统。
  2. 身份验证服务响应并返回了签名的 JWT,上面包含了用户是谁的内容。
  3. 用户以后每次向后端发请求都在Header中带上 JWT。
  4. 服务端检查 JWT 并从中获取用户相关信息。

JWT的基本原理

JWT 实际上是一个字符串,中间用点.分割成三部分,三部分分别如下:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

Header 部分是一个 JSON 对象,描述的是JWT的元数据,表示的是 JSON 对象的签名和加密。其主要参数有以下这些,除了alg算法参数外,其他选项都是可选的。

  • alg 算法 (必选项)
  • typ 类型 (如果是 JWT 那么就带有一个值 JWT,如果存在的话)
  • kid 密钥 ID
  • cty 内容类型
  • jku JWK 指定 URL
  • jwk JSON 网络值
  • x5u X.509 URL
  • x5c X.509 证书链
  • x5t X.509 证书 SHA-1 指纹
  • x5t#S256 X.509 证书 SHA-256 指纹
  • crit 临界值

通常常用格式如下:

{
    "alg": "HS256",
    "typ": "JWT"
}

对其进行Base64URL编码后就得到JWT的头部(Header)

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

Payload

Payload 部分同样是一个JSON对象,用来存放实际需要传递的数据,JWT规定了7个官方字段供使用:

  • sub:该JWT所面向的对象的值(唯一),可以用来鉴别用户
  • iat(issued at):在什么时候签发的token
  • exp(expires):token什么时候过期
  • nbf(not before):token在此时间之前不能被接收处理,表示什么时间开始生效
  • jti:JWT ID为web token提供唯一标识
  • iss:该JWT的签发者
  • aud:受众,用来确认令牌的可能接收者的字符串数值。

后面三个参数通常是在更复杂的情况下(例如包含多个发行者时)才被使用。

除了官方字段,还可以在这个部分定义私有字段:

{
    "sub":"112345567",
    "name":"SilverBullet Ming",
    "admin":true
}

同样将将上面的JSON对象进行base64URL编码后得到的字符串就是JWT的载荷。如:

yJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWx
ob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4OD
ExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ

Signature

Signature 部分是对前两部分的签名,防止数据被修改。

首先需要指定一个密钥,这个密钥只有服务器知道,不泄露给用户。然后在 Header 中使用指定的签名算法(默认是 HMAC SHA 256)。

将头部和载荷编码后的字符串用.连接在一起后(头部在前),将拼接完的字符串用 H256 算法进行加密,同时提供了一个密钥(secret)就得到了签名。

按照下面的公式产生签名:

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

加密后的签名:

wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4

将得到的三部分字符串拼接在一起后就得到了JWT

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4ODExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ.
wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4

JWT有哪些特点?

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  2. JWT 不加密的情况下,不能将秘密数据写入 JWT。
  3. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

JWT认证的优势

相比于Session认证方面来说,使用token进行身份认证有下面三个优势:

1. 无状态

token 自身包含了身份验证所需要的所有信息,使得服务器不需要存储Session信息,这增加了系统的可用性和伸缩性,减轻了服务器的压力。但是,由于 token 无状态,也导致了它最大的缺点:当后端在 token 有有效期内废弃了一个 token 或者更改它的权限的话,不会立即生效,一般需要等到有效期过后才可以。另外,当用户 logout 的话,token 也仍然有效。除非在后端增加额外的逻辑处理。

2. 有效避免了CSRF攻击

CSRF(Cross Site Request Forgery) 被称为跨站请求伪造,属于网络攻击领域范围。相比于 SQL 脚本注入、XSS 等等安全攻击方式,CSRF 知名度较低。

CSRF 是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

例如:

小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了10000元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。

<a src=http://www.mybank.com/Transfer?bankId=11&money=10000>科学理财,年盈利率过万</>

进行 Session 认证的时候,我们一般使用 Cookie 来存储 SessionId,当我们登陆后后端生成一个SessionId 放在 Cookie 中返回给客户端,服务端通过 Redis 或者其他存储工具记录保存着这个Sessionid,客户端登录以后每次请求都会带上这个 SessionId,服务端通过这个 SessionId 来标示你这个人。如果别人通过 cookie 拿到了 SessionId 后就可以代替你的身份访问系统了。

Session 认证中 Cookie 中的 SessionId 是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。

但是,我们使用 token 的话就不会存在这个问题,在我们登录成功获得 token 之后,一般会选择存放在 local storage 中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 token,这样就不会出现 CSRF 漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 token 的,所以这个请求将是非法的。

需要注意的是不论是 Cookie 还是 token 都无法避免跨站脚本攻击(Cross Site Scripting)XSS。

XSS:用户在浏览网站、使用即时通讯软件、甚至在阅读电子邮件时,通常会点击其中的链接。攻击者通过在链接中插入恶意代码,就能够盗取用户信息。攻击者通常会用十六进制(或其他编码方式)将链接编码,以免用户怀疑它的合法性。网站在接收到包含恶意代码的请求之后会产成一个包含恶意代码的页面,而这个页面看起来就像是那个网站应当生成的合法页面一样。许多流行的留言本和论坛程序允许用户发表包含HTML和javascript的帖子。假设用户甲发表了一篇包含恶意脚本的帖子,那么用户乙在浏览这篇帖子时,恶意脚本就会执行,盗取用户乙的session信息。

可以选择将 token 存储在标记为 httpOnly 的cookie 中。但是,这样又导致了必须自己提供CSRF保护。

具体采用哪种方式需要是具体情况而定。大部分情况下存放在 local storage 下都是最好的选择,某些情况下可能需要存放在标记为 httpOnly 的cookie 中会更好。

3. 适合移动端应用

使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 SessionId),所以不适合移动端。

但是,使用 token 进行身份认证就不会存在这种问题,因为只要 token 可以被客户端存储就能够使用,而且 token 还可以跨语言使用。

4. 单点登录友好

使用 Session 进行身份认证的话,实现单点登陆,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 token 进行认证的话, token 被保存在客户端,不会存在这些问题。

如何解决注销登录等场景下 token 还有效的问题

类似的场景还有:

  • 退出登录;
  • 修改密码;
  • 服务端修改了某个用户具有的权限或者角色;
  • 用户的帐户被删除/暂停;
  • 用户由管理员注销

这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 token 认证的方式就不好解决了。解决方案有以下几种:

  • 将 token 存入内存数据库:将 token 存入 DB 中,redis 内存数据库在这里是是不错的选择。如果需要让某个 token 失效就直接从 redis 中删除这个 token 即可。但是,这样会导致每次使用 token 发送请求都要先从 DB 中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则。
  • 黑名单机制:和上面的方式类似,使用内存数据库比如 redis 维护一个黑名单,如果想让某个 token 失效的话就直接将这个 token 加入到黑名单即可。然后,每次使用 token 进行请求的话都会先判断这个 token 是否存在于黑名单中。
  • 修改密钥(Secret):为每个用户都创建一个专属密钥,如果我们想让某个 token 失效,我们直接修改对应用户的密钥即可。但是,这样相比于前两种引入内存数据库带来了危害更大,比如:
  1. 如果服务是分布式的,则每次发出新的 token 时都必须在多台机器同步密钥。为此,你需要将必须将机密存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。
  2. 如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。
  • 保持令牌的有效期限短并经常轮换:很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。

对于修改密码后 token 还有效问题的解决还是比较容易的,说一种比较好的方式:使用用户的密码的哈希值对 token 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。

token 的续签问题

token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?

在 Session 认证中一般的做法:假如 session 的有效期30分钟,如果 30 分钟内用户有访问,就把 session 有效期被延长30分钟。

  1. 类似于 Session 认证中的做法:这种方案满足于大部分场景。假设服务端给的 token 有效期设置为30分钟,服务端每次进行校验时,如果发现 token 的有效期马上快过期了,服务端就重新生成 token 给客户端。客户端每次请求都检查新旧token,如果不一致,则更新本地的 token。这种做法的问题是仅仅在快过期的时候请求才会更新 token ,对客户端不是很友好。
  2. 每次请求都返回新 token:这种方案的的思路很简单,但是,很明显,开销会比较大。
  3. token 有效期设置到半夜 :这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。
  4. 用户登录返回两个 token:第一个是 acessToken ,它的过期时间是 token 本身的过期时间比如半个小时,另外一个是 refreshToken 它的过期时间更长一点比如为1天。客户端登录后,将 accessTokenrefreshToken 保存在本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。该方案的不足是:
  • 需要客户端来配合;
  • 用户注销的时候需要同时保证两个 token 都无效;
  • 重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当 accessToken 快过期的时候,提前去通过 refreshToken 获取新的 accessToken)。

什么是单点登录(单点登录的机制)

单点登录(SSO,Single Sign On)就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录。

单系统登录机制

http无状态协议

web应用采用 browser/server 架构,http 作为通信协议。http 是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联。这也同时意味着,任何用户都能通过浏览器访问服务器资源,如果想保护服务器的某些资源,必须限制浏览器请求;要限制浏览器请求,必须鉴别浏览器请求,响应合法请求,忽略非法请求;要鉴别浏览器请求,必须清楚浏览器请求状态。既然http协议无状态,那就需要服务器和浏览器共同维护一个状态,这就是会话机制。

会话机制

浏览器第一次请求服务器,服务器创建一个会话,并将会话的 id 作为响应的一部分发送给浏览器,浏览器存储会话 id,并在后续第二次和第三次请求中带上会话 id,服务器取得请求中的会话 id 就知道是不是同一个用户了(登录的用户在 session 里面是可以查询到已经存储的 isLogin 属性设置为 true),这个过程用下图说明,后续请求与第一次请求产生了关联

会话机制

服务器在内存中保存会话对象,浏览器保存会话 id 有两种方式:

  • 请求参数
  • 通过cookie实现

将会话 id 作为每一个请求的参数,服务器接收请求自然能解析参数获得会话 id,并借此判断是否来自同一会话,很明显,这种方式不靠谱。

另外的方法是让浏览器自己来维护这个会话 id,每次发送 http 请求时浏览器自动发送会话 id,cookie 机制正好用来做这件事。cookie 是浏览器用来存储少量数据的一种机制,数据以key/value形式存储,浏览器发送 http 请求时自动附带 cookie 信息

tomcat 会话机制也实现 了cookie(就是当客户端访问 tomcat 的时候,tomcat 也可以给客户端发送一个 cookie),访问 tomcat 服务器时,浏览器中可以看到一个名为JSESSIONID的 cookie,这就是 tomcat 会话机制维护的会话 id,使用了 cookie 的请求响应过程如下图:

tomcat实现会话机制原理

登录状态

有了会话机制,登录状态就好明白了,假设浏览器第一次请求服务器需要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,正确的话说明当前持有这个会话的用户是合法用户,应该将这个会话标记为“已授权”或者“已登录”等等之类的状态,既然是会话的状态,自然要保存在会话对象中,tomcat 在会话对象中设置登录状态如下:

Tomcat登录状态

每次请求受保护资源时都会检查会话对象中的登录状态,只有 isLogin=true 的会话才能访问,登录机制因此而实现。

多系统下一个应用可能有很多系统组成,单系统登录功能主要是用 Session 保存用户信息来实现的,但是,多系统即可能有多个 Tomcat,而 Session 是依赖当前系统的Tomcat,所以系统 A 的 Session 和系统 B 的 Session 是不共享的。

这时候就需要使用单点登录。

单点登录机制和实现方法

相比于单系统登录,sso 需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌(tokens)实现,sso 认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理,用下图说明:

单点登录原理

浏览器首先访问系统1,系统1检测到用户没有登录。于是带上系统1的地址转发到 sso 认证中心,也发现没有登录,于是带上系统1的地址来到登录页面进行登录,登录完毕之后,sso 认证中心就开始创建全局会话

单点登录过程

如何创建全局会话的?
答:sso认证中心校验用户信息,创建用户与 sso 认证中心之间的会话,称为全局会话),创建授权令牌 tokens,接着有了 tokens 令牌之后就可以跳转到系统1的地址了,这个时候系统1向 sso 认证中心校验令牌有效,sso 认证中心把系统1的地址注册到 sso 认证中心上,然后返回给系统1,令牌有效,于是浏览器就可以访问系统1了。
再接着如果浏览器要访问系统2的话,在系统2上一验证,发现没有登录,这个时候就带着系统2的地址跳去 sso 认证中心,sso认证中心上验证已经登录,于是把令牌 tokens 发送给系统2,这个时候系统2再次带着系统2的地址和令牌来到 sso 认证中心验证时,令牌肯定是有效的,然后 sso 中心把系统2的地址注册到本地上,接着告诉系统2,令牌有效,于是浏览器就可以和系统2进行局部会话了

注销登录过程:

单点登录注销登录过程

单点登录注销登录流程说明

如何使用Session进行身份验证?

很多时候都是通过 SessionID 来实现特定的用户,SessionID 一般会选择存放在 Redis 中。举个例子:用户成功登陆系统,然后返回给客户端具有 SessionID 的 Cookie,当用户向后端发起请求的时候会把 SessionID 带上,这样后端就知道你的身份状态了。关于这种认证方式更详细的过程如下:

  1. 用户向服务器发送用户名和密码用于登陆系统。
  2. 服务器验证通过后,服务器为用户创建一个 Session,并将 Session信息存储 起来。
  3. 服务器向用户返回一个 SessionID,写入用户的 Cookie。
  4. 当用户保持登录状态时,Cookie 将与每个后续请求一起被发送出去。
  5. 服务器可以将存储在 Cookie 上的 Session ID 与存储在内存中或者数据库中的 Session 信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。

使用 Session 的时候需要注意下面几个点:

  • 依赖Session的关键业务一定要确保客户端开启了Cookie。
  • 注意Session的过期时间

如果没有Cookie的话Session还能用吗?

一般是通过 Cookie 来保存 SessionID ,假如使用了 Cookie 保存 SessionID的方案的话, 如果客户端禁用了Cookie,那么Seesion就无法正常工作。

但是,并不是没有 Cookie 之后就不能用 Session 了,比如可以将 SessionID 放在请求的 url 里面https://javaguide.cn/?session_id=xxx 。这种方案的话可行,但是安全性和用户体验感降低。当然,为了安全也可以对 SessionID 进行一次加密之后再传入后端。

什么是OAuth 2.0?

OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。而 OAuth 2.0 是对 OAuth 1.0 的完全重新设计,OAuth 2.0 更快,更容易实现,OAuth 1.0 已经被废弃。

实际上它就是一种授权机制,它的最终目的是为第三方应用颁发一个有时效性的令牌 token,使得第三方应用能够通过该令牌获取相关的资源。

OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了第三方登录的时候一般就是使用的 OAuth 2.0 协议。

另外,现在OAuth 2.0也常见于支付场景(微信支付、支付宝支付)和开发平台(微信开放平台、阿里开放平台等等)。

和 SSO 区别

OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。SSO 解决的是一个公司的多个相关的自系统的之间的登陆问题比如京东旗下相关子系统京东金融、京东超市、京东家电等等。

二维码扫码登录的原理

二维码登录的本质

二维码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情!

  1. 告诉系统我是谁
  2. 向系统证明我是谁

比如账号密码登录,账号就是告诉系统我是谁, 密码就是向系统证明我是谁; 比如手机验证码登录,手机号就是告诉系统我是谁,验证码就是向系统证明我是谁;

那么扫码登录是怎么做到这两件事情的呢?

手机端应用扫PC端二维码,手机端确认后,账号就在PC端登录成功了!这里,PC端登录的账号肯定与手机端是同一个账号。不可能手机端登录的是账号A,而扫码登录以后,PC端登录的是账号B。

所以,第一件事情,告诉系统我是谁,是比较清楚的!通过扫描二维码,把手机端的账号信息传递到PC端,至于是怎么传的,后面再说。

第二件事情,向系统证明我是谁。扫码登录过程中,用户并没有去输入密码,也没有输入验证码,或者其他什么码。那是怎么证明的呢?

这里可能会想,是不是扫码过程中,把密码传到了PC端呢?

但这是不可能的。因为那样太不安全的,客户端也根本不会去存储密码。仔细想一下,其实手机端APP它是已经登录过的,就是说手机端是已经通过登录认证。所说只要扫码确认是这个手机且是这个账号操作的,其实就能间接证明我谁。

什么是二维码

在认识二维码之前先看一下一维码:

一维码

所谓一维码,也就是条形码,超市里的条形码–这个相信大家都非常熟悉,条形码实际上就是一串数字,它上面存储了商品的序列号。

二维码其实与条形码类似,只不过它存储的不一定是数字,还可以是任何的字符串,可以认为,它就是字符串的另外一种表现形式,

在搜索引擎中搜索二维码,可以找到很多在线生成二维码的工具网站,这些网站可以提供字符串与二维码之间相互转换的功能

系统认证机制

认识了二维码,了解一下移动互联网下的系统认证机制。

前面说过,为了安全,手机端它是不会存储你的登录密码的。但是在日常使用过程中,应该会注意到,只有在应用下载下来后,第一次登录的时候,才需要进行一个账号密码的登录, 那之后即使这个应用进程被杀掉,或者手机重启,都是不需要再次输入账号密码的,它可以自动登录。

其实这背后就是一套基于token的认证机制,来看一下这套机制是怎么运行的:

  1. 账号密码登录时,客户端会将设备信息一起传递给服务端

  2. 如果账号密码校验通过,服务端会把账号与设备进行一个绑定,存在一个数据结构中,这个数据结构中包含了账号ID,设备ID,设备类型等等:

    const token = {
      acountid:'账号ID',
      deviceid:'登录的设备ID',
      deviceType:'设备类型,如 iso,android,pc......',
    }
    
  3. 服务端会生成一个token,用它来映射数据结构,这个token其实就是一串有着特殊意义的字符串,它的意义就在于,通过它可以找到对应的账号与设备信息

  4. 客户端得到这个 token 后,需要进行一个本地保存,每次访问系统 API 都携带上 token 与设备信息

  5. 服务端可以通过 token 找到与它绑定的账号与设备信息,然后把绑定的设备信息与客户端每次传来的设备信息进行比较, 如果相同,那么校验通过,返回 API 接口响应数据, 如果不同,那就是校验不通过拒绝访问

从前面这个流程可以看到,客户端不会也没必要保存密码,相反,它是保存了token。可能有些同学会想,这个token这么重要,万一被别人知道了怎么办。实际上,知道了也没有影响, 因为设备信息是唯一的,只要你的设备信息别人不知道, 别人拿其他设备来访问,验证也是不通过的。

可以说,客户端登录的目的,就是获得属于自己的token。

那么在扫码登录过程中,PC端是怎么获得属于自己的token呢?不可能手机端直接把自己的token给PC端用!token只能属于某个客户端私有,其他人或者是其他客户端是用不了的。

在分析这个问题之前,有必要先梳理一下,扫描二维码登录的一般步骤是什么样的。这可以帮助我们梳理清楚整个过程。

扫描二维码的一般步骤

  1. 扫码前,手机端应用是已登录状态,PC端显示一个二维码,等待扫描
  2. 手机端打开应用,扫描PC端的二维码,扫描后,会提示”已扫描,请在手机端点击确认”
  3. 用户在手机端点击确认,确认后PC端登录就成功了

可以看到,二维码在中间有三个状态, 待扫描,已扫描待确认,已确认。那么可以想象:

  1. 二维码的背后它一定存在一个唯一性的ID,当二维码生成时,这个ID也一起生成,并且绑定了PC端的设备信息
  2. 手机去扫描这个二维码
  3. 二维码切换为已扫描待确认状态, 此时就会将账号信息与这个ID绑定
  4. 当手机端确认登录时,它就会生成PC端用于登录的token,并返回给PC端

好了,到这里,基本思路就已经清晰了,接下来把整个过程再具体化一下:

二维码准备

按二维码不同状态来看, 首先是等待扫描状态,用户打开PC端,切换到二维码登录界面时。

  1. PC端向服务端发起请求,告诉服务端,我要生成用户登录的二维码,并且把PC端设备信息也传递给服务端
  2. 服务端收到请求后,它生成二维码ID,并将二维码ID与PC端设备信息进行绑定
  3. 然后把二维码ID返回给PC端
  4. PC端收到二维码ID后,生成二维码(二维码中肯定包含了ID)
  5. 为了及时知道二维码的状态,PC端在展现二维码后,不断的轮询服务端,比如每隔一秒就轮询一次,请求服务端告诉当前二维码的状态及相关信息

二维码已经准好了,接下来就是扫描状态。

扫描状态切换

  1. 用户用手机去扫描PC端的二维码,通过二维码内容取到其中的二维码ID
  2. 再调用服务端API将移动端的身份信息与二维码ID一起发送给服务端
  3. 服务端接收到后,它可以将身份信息与二维码ID进行绑定,生成临时token。然后返回给手机端
  4. 因为PC端一直在轮询二维码状态,所以这时候二维码状态发生了改变,它就可以在界面上把二维码状态更新为已扫描

那么为什么需要返回给手机端一个临时token呢?临时token与token一样,它也是一种身份凭证,不同的地方在于它只能用一次,用过就失效。

在第三步骤中返回临时token,为的就是手机端在下一步操作时,可以用它作为凭证。以此确保扫码,登录两步操作是同一部手机端发出的。

状态确认

  1. 手机端在接收到临时 token 后会弹出确认登录界面,用户点击确认时,手机端携带临时 token 用来调用服务端的接口,告诉服务端,我已经确认
  2. 服务端收到确认后,根据二维码ID绑定的设备信息与账号信息,生成用户 PC 端登录的 token
  3. 这时候PC端的轮询接口,它就可以得知二维码的状态已经变成了”已确认”。并且从服务端可以获取到用户登录的 token
  4. 到这里,登录就成功了,后端 PC 端就可以用token去访问服务端的资源了

参考内容

主要参考以来两篇博客以及相关博客推荐,因找的博客比较多,没注意记录,最后好多忘了在哪2333,如果有侵权,请及时联系我,非常抱歉。

https://github.com/Snailclimb/JavaGuide

https://github.com/CyC2018/CS-Notes

TikTok二面:“聊聊二维码扫码登录的原理”