理解JWT

July 29, 2018

什么是JWT

官方解释如下:

JWT全称JSON Web Token,是一种开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在通信双方以JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用密钥(使用HMAC算法)或者使用RSA或ECDSA的公钥/私钥对进行签名。

通俗点讲,JWT就是一种认证规范、标准。

JWT的组成

JWT就是一个token,其结构是一个字符串,由三部分组成,以点号 . 分隔,通常像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

三个部分分别为:

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

Header原数据是一个JSON对象,有algtyp两个字段,alg表示生成JWT使用的散列算法,如HMAC SHA256RSA等,默认是HMAC SHA256(简称HS256);typ表示JWT的类型,其值一般就是JWT。

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

然后将该JSON对象进行Base64URL编码成字符串。

Payload

Payload部分也是一个JSON对象,用来存放需要传递的数据,官方提供但非必需的字段有以下七个

  • iss (Issuer):签发人
  • sub (Subject):主题
  • aud (Audience):受众
  • exp (Expiration Time):过期时间
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

可以发现官方声明的字段都只有三个字符,这是因为JWT意味着紧凑。

我们也可以定义一些自己的私有字段:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

然后将该JSON对象进行Base64URL编码成字符串。

注意:不要将敏感信息放在HeaderPayload中,因为Base64URL编码后的字符串可以被解码,任何人都可以获取到其中包含的信息。

Signature

要生成一个签名,我们必须先有以下三部分信息:

  • Base64URL编码后的Header
  • Base64URL编码后的Payload
  • 密钥secret

然后将编码后的Header和编码后的Payload. 拼接成一个字符串,最后将这个字符串与密钥使用 Header 中定义的加密算法进行加密,生成的字符串就是签名,算法如下:

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

最后将HeaderPayloadSignature三部分以 . 拼接,获得的字符串就是JWT,也就是通常所说的token。

注意:上面Header和Payload使用的编码都是Base64URL,而不是Base64。因为JWT有时会以参数的形式放在URL中,如http://bazingafeng.com/?token=xxx ,而Base64编码后的字符串有三个字符+/=,这三个字符在URL中有特殊含义,可能会导致一些问题。而Base64URL编码后的字符串将这三个字符分别使用-_空格进行了替换,使得token可以作为URL的参数。

JWT工作原理

JWT是如何验证客户端传过来的token的?

服务端收到客户端传过来的token之后,会对token以 . 进行分隔,获得HeaderPayloadSignature三部分,再解码Header获得其中的签名算法alg,然后使用HeaderPayloadsecret生成新的签名,最后比较新的签名和原始签名是否一致,若不一致则表示token无效。若用户篡改了token的Payload信息,则生成的新的签名和token中包含的原始签名肯定不一致,也就无法通过认证。

可以看到,认证过程最终比较的是签名,由于生成签名使用的算法是不可逆的,且用户不知道secret,所以无法篡改签名。但如果用户知道了服务端存储的secret,就可以任意更改token了,这就相当于用户自己给自己签名了,因此一定要注意不能泄露secret

JWT如何使用

用户登录成功后,服务端会返回一个token,用户可以将这个token存储在Cookie或localStorage。每当用户与服务端通信,访问需要授权的资源时,都要传递这个token,传递方式一般是以Authorization字段放在HTTP请求头中,并带上Bearer标注:

Authorization: Bearer <token> 

当然也可以直接在URL中以参数的形式传递。

http://bazingafeng.com/?token=xxx

注意:不建议将token以Cookie的形式传递给服务端,因为这会存在跨域问题,也可能会有CSRF攻击的风险,而放在请求头中就不会有这样的问题。

JWT vs. Session

Session

一般session认证过程:用户登录成功后,服务端会生成sessionID并存储,同时在客户端以cookie的形式存储,然后客户端每次请求都会带上这个cookie,服务端再去通过session做校验和认证。

这种方式使服务端必须把sessionID存储在内存或数据库中,可不管存在哪里都有缺点。若存储在内存中,对于分布式应用则需要多台服务器之间同步session;若存储在数据库中,则每次请求都要去查一次数据库。

  • 优点
    • 可以主动清除session信息
  • 缺点
    • 占用更多内存

JWT

相比session,JWT是无状态的,token存储在客户端,服务端只保存密钥secret,不存储任何session信息。那么,服务端是无法清除token让用户退出登录的,只能等待token过期。

  • 优点
    • 服务端不用存储session信息,节省内存
    • 解决跨域问题,防CSRF攻击(token通过请求头传递)
  • 缺点
    • 服务端无法主动清除token,只能等待token过期
    • 无法保证实效性,若token中存储了用户角色信息,而服务后台修改了该用户的角色,在该token过期之前,用户的角色不会变更。

针对服务端无法主动清除token的问题,查阅了一些文章,解决办法是把token存到Redis或其他数据库,当需要时再去清除或更新token。个人觉得使用JWT,服务端不应该存储token,保证其无状态特性。如果服务端存储了token,那和session又有什么区别?

JWT使用场景

JWT适用于具有时效性的一次性授权token的设计。如:

  • 邮箱验证。
  • restful api的鉴权。用户一旦登录,其每个后续请求都要包含token,来访问需要授权的路由、服务和资源。

是否适用于会话管理?

网上有人认为不适合会话管理,认为使用传统的session + cookie方案更好。也有人认为可以做会话管理,认为不适合会话管理的问题都可以解决。就我个人观点来看,我倾向于不适合做会话管理,认为服务端不应该存储token。

使用JWT注意事项

  • 不要在Payload中存储敏感信息
  • 不要泄露secret
  • 尽量使用HTTPS 因为token一旦被其他人获取,则他们可以冒充我们向服务器发起任意请求了。比如在HTTP请求中,我们发送token给服务端时,该token可能会被人抓包获取。针对这个问题,也没有什么解决办法,一般建议使用HTTPS而非HTTP。虽然这样不能保证token一定不会被人获取,但可以更加安全。

(完)