认证(3)-token

最后更新:2019-03-03

Token的意思是“令牌”,是服务端生成的一串字符串,作为客户端进行请求的一个标识

当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

简单Token的组成;uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,Token的前几位以哈希算法压缩成的一定长度的十六进制字符串。为防止Token泄露)。

1. 基于Token机制的身份认证

使用Token机制的身份验证方法,在服务器端不需要存储用户的登录记录。大概的流程:

  1. 客户端使用用户名和密码请求登录。
  2. 服务端收到请求,验证用户名和密码。
  3. 验证成功后,服务端会生成一个Token,然后把这个Token发送给客户端。
  4. 客户端收到Token后把它存储起来,可以放在cookie或者Local Storage(本地存储)里。
  5. 客户端每次向服务端发送请求的时候都需要带上服务端发给的Token。
  6. 服务端收到请求,然后去验证客户端请求里面带着Token,如果验证成功,就向客户端返回请求的数据。(如果这个 Token 在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌。)

2. Refresh Token

Token都会有一个有效期,但是这会带来一个新的问题:如果用户在正常操作的过程中,Token 过期失效了,要求用户重新登录,用户体验会很糟糕。

解决Token失效的一种方案是在服务器端保存 Token 状态,用户每次操作都会自动刷新(推迟) Token 的过期时间——Session 就是采用这种策略来保持用户登录状态的。

然而仍然存在这样一个问题,在前后端分离、单页 App 这些情况下,每秒种可能发起很多次请求,每次都去刷新过期时间会产生非常大的代价。

如果 Token 的过期时间被持久化到数据库或文件,代价就更大了。所以通常为了提升效率,减少消耗,会把 Token 的过期时保存在缓存或者内存中。

另一种方案是使用 Refresh Token,它可以避免频繁的读写操作。

这种方案中,服务端不需要刷新 Token 的过期时间,一旦 Token 过期,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。

服务端只需要在客户端请求更新 Token 的时候对 Refresh Token 的有效性进行一次检查,大大减少了更新有效期的操作,也就避免了频繁读写。 当然 Refresh Token 也是有有效期的,但是这个有效期就可以长一点了,比如,以天为单位的时间

  • Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。
  • Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。

还可以把这个机制设计得更复杂一些,比如,Refresh Token 每次使用的时候,都更新它的过期时间,直到与它的创建时间相比,已经超过了非常长的一段时间(比如三个月),这等于是在相当长一段时间内允许 Refresh Token 自动续期。

3. 无状态Token

如果我们把所有状态信息都附加在 Token 上,服务器就可以不保存。

但是服务端仍然需要认证 Token 有效。不过只要服务端能确认是自己签发的 Token,而且其信息未被改动过,那就可以认为 Token 有效——“签名”可以作此保证。

平时常说的签名都存在一方签发,另一方验证的情况,所以要使用非对称加密算法。但是在这里,签发和验证都是同一方,所以对称加密算法就能达到要求,而对称算法比非对称算法要快得多(可达数十倍差距)。

更进一步思考,对称加密算法除了加密,还带有还原加密内容的功能,而这一功能在对 Token 签名时并无必要——既然不需要解密,摘要(散列)算法就会更快。可以指定密码的散列算法,自然是 HMAC。

JWT 已经定义了详细的规范,而且有各种语言的若干实现。 http://edgar615.github.io/jwt.html

不过在使用无状态 Token 的时候在服务端会有一些变化,服务端虽然不保存有效的 Token 了,却需要保存未到期却已注销的 Token。 如果一个 Token 未到期就被用户主动注销,那么服务器需要保存这个被注销的 Token,以便下次收到使用这个仍在有效期内的 Token 时判其无效。

在前端可控的情况下(比如前端和服务端在同一个项目组内),可以协商:前端一但注销成功,就丢掉本地保存(比如保存在内存、LocalStorage 等)的 Token 和 Refresh Token。基于这样的约定,服务器就可以假设收到的 Token 一定是没注销的(因为注销之后前端就不会再使用了)。

如果前端不可控的情况,仍然可以进行上面的假设,但是这种情况下,需要尽量缩短 Token 的有效期,而且必须在用户主动注销的情况下让 Refresh Token 无效。这个操作存在一定的安全漏洞,因为用户会认为已经注销了,实际上在较短的一段时间内并没有注销。如果应用设计中,这点漏洞并不会造成什么损失,那采用这种策略就是可行的。

在使用无状态 Token 的时候,有两点需要注意:

  1. Refresh Token 有效时间较长,所以它应该在服务器端有状态,以增强安全性,确保用户注销时可控
  2. 应该考虑使用二次认证来增强敏感操作的安全性

4. 认证方式

4.1. 基于session的认证方式

缺点

  • Session需要保存在服务器端
  • Session通常是存在内存中,用户量增大会极大增大负载
  • 跨域不友好,请求会被拒绝
  • 容易招到CSRF攻击

题外话:CSRF攻击

4.2. 基于TOKEN的认证方式

5. Token的优势

  • 无状态、可扩展 在客户端存储的Tokens是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上。如果我们将已验证的用户的信息保存在Session中,则每次请求都需要用户向已验证的服务器发送验证信息(称为Session亲和性)。用户量大时,可能会造成 一些拥堵。但是不要着急。使用Token之后这些问题都迎刃而解,因为Token自己绑定了用户的验证信息。
  • 安全性 请求中发送Token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储Token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。Token是有时效的,一段时间之后用户需要重新验证。我们也不一定需要等到Token自动失效,Token有撤回的操作,通过Token revocataion可以使一个特定的Token或是一组有相同认证的Token无效。
  • 可扩展性 Tokens能够创建与其它程序共享权限的程序。使用Tokens时,可以提供可选的权限给第三方应用程序。当用户想让另一个应用程序访问它们的数据,我们可以通过建立自己的API,得出特殊权限的Tokens。
  • 减轻服务器压力 基于 Token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 Token 数据。用解析 Token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库

6. 参考资料

https://juejin.im/post/6844903556424826894

https://juejin.im/post/6844904034181070861

Edgar

Edgar
一个略懂Java的小菜比