认证与授权

背景

Authentication:身份认证,指的是对用户的身份进行验证,例如 tom 有 tom 的身份,jack 有 jack 的身份,应用程序 需要他们各自的 用户名 和 密码 来确认。
Authorization:权限,授权,这是在身份验证之后获得的权限,例如 tom 是 管理员,他就可以编辑文章,而 jack 只是 普通用户,只能阅读文章。
由于http协议是无状态的,每一次请求都无状态。当一个用户通过用户名和密码登录了之后,他的下一个请求不会携带任何状态, 应用程序无法知道他的身份,那就必须重新认证。因此我们希望用户登录成功之后的每一次http请求,都能够保存他的登录状态。
目前主流的用户认证方法有基于token和基于session两种方式。

基于 session 的用户认证

1.用户输入登陆信息
2.服务器验证登陆信息是否正确,并创建一个 session,并将其存储在数据库中
3.服务器为用户生成一个 session_id,将 session_id放在 Cookie 中返回给用户(的浏览器)
4.在后续的请求中,服务器会根据数据验证 session_id 的有效性。如果有效,则接受。
5.由于会话的信息存储在服务器,服务器可随时销毁 session,结束会话。

基于 token 的用户认证

目前最常用的是 JSON Web Token(JWT),常用以实现 单点登陆(sso)
1.用户输入登陆信息
2.服务器验证信息是否正确,并返回已签名的 token
3.token 存储在客户端,可以是 local storage 或 cookie 中
4.之后的 http 请求都将 token 添加到 请求头中(Authorization)
5.服务器解码 jwt,如果有效,则接受请求
6.一旦用户注销,令牌将在客户端被销毁,不需要与服务器进行交互。一个关键是,令牌是无状态的。
后端服务器不需要保存令牌或当前session的记录。

JWT 的使用

原理

一个 jwt 实际上就是一个字符串,由三部分组成:头部、载荷和签名。

头部(header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。

1
2
3
4
{
  "typ": "JWT",
  "alg": "HS256"
}
载荷(payload)
1
2
3
4
5
{
    "exp": 1571935278, 
    "iat": 1571848878, 
    "data": {"username": "tom"}
}

这里的数据可以自定义,但不可放敏感信息如密码等
JWT 标准定义了一些字段,下面是部分:

  • iss: 该JWT的签发者
  • sub: 该JWT所面向的用户
  • aud: 接收该JWT的一方
  • exp(expires): 什么时候过期,这里是一个Unix时间戳
  • iat(issued at): 在什么时候签发的

把头部和载荷分别进行 Base64 编码之后得到两个字符串,将这两个字符串用 . 连接在一起,形成新的字符串
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NzE5MzUyNzgsImlhdCI6MTU3MTg0ODg3OCwiZGF0YSI6eyJ1c2VybmFtZSI6InRvbSJ9fQ

签名(signature)

最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。加密后的内容也是一个字符串, 最后这个字符串就是签名,把这个签名拼接在刚才的字符串后面就能得到完整的jwt。header部分和payload部分如果被篡改,由于篡改者不知道 密钥是什么,也无法生成新的signature部分,服务端也就无法通过,在jwt中,消息体是透明的,使用签名可以保证消息不被篡改。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NzE5MzUyNzgsImlhdCI6MTU3MTg0ODg3OCwiZGF0YSI6eyJ1c2VybmFtZSI6InRvbSJ9fQ.B2ZuOG2YnOTieuKEcdAto3dVnrHMqKpHup2nw8khYw8

区别和优缺点

基于session和基于jwt的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而jwt是保存在客户端的。

jwt的优点:

  1. 可扩展性好 应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而jwt不需要。

  2. 无状态 jwt不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。 用户的认证状态引入这种附加影响,这破坏了这一原则。另外jwt的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。

jwt的缺点:

  1. 安全性
    由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。

  2. 性能
    jwt太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长, cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面, http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用jwt的http请求比使用session的开销大得多。

  3. 一次性
    无状态是jwt的特点,但也导致了这个问题,jwt是一次性的。想修改里面的内容,就必须签发一个新的jwt。

    (1)无法废弃
    通过上面jwt的验证机制可以看出来,一旦签发一个jwt,在到期之前就会始终有效,无法中途废弃。 例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个jwt,但是由于旧的jwt还没过期,拿着这个旧的jwt依旧可以登录, 那登录后服务端从jwt中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的jwt, 那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。

    (2)续签
    如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。 一样的道理,要改变jwt的有效时间,就要签发新的jwt。最简单的一种方式是每次请求刷新jwt,即每个http请求都返回一个新的jwt。 这个方法不仅暴力不优雅,而且每次请求都要做jwt的加密解密,会带来性能问题。另一种方法是在redis中单独为每个jwt设置过期时间,每次访问时刷新jwt的过期时间。

可以看出想要破解jwt一次性的特性,就需要在服务端存储jwt的状态。但是引入 redis 之后,就把无状态的jwt硬生生变成了有状态了, 违背了jwt的初衷。而且这个方案和session都差不多了。

总结

适合使用jwt的场景:
1.有效期短
2.只希望被使用一次
比如,用户注册后发一封邮件让其激活账户,通常邮件中需要有一个链接,这 个链接需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),不能被篡改以激活其他可能的账户,一次性的。这种场景就适合使用jwt。

而由于jwt具有一次性的特性。单点登录和会话管理非常不适合用jwt,如果在服务端部署额外的逻辑存储jwt的状态,那还不如使用session。 基于session有很多成熟的框架可以开箱即用,但是用jwt还要自己实现逻辑。