让你认清JWT的真面目

1,830 阅读6分钟

何为json web token?

json web token 简称为jwt,是区别于传统token的一种形式。

它是由三元素通过加密和算法形成的字符串。 三元素分别是Header头部,Payload负载和Signature签名。

步骤是Header部分用Base64加密,Payload部分用Base64加密,然后对Header的base64UrlEncode加密, Payload的base64UrlEncode加密,结合secret使用 Header中typ表示的算法进行加密,然后三部分通过.拼接起来

比如jwt:xxxxx.yyyyy.zzzzz
JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

Header

Header 通常由两部分组成:

  • typ(Type):令牌类型,也就是 JWT。
  • alg(Algorithm) :签名算法,比如 HS256。

示例:

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

JSON 形式的 Header 被转换成 Base64 编码,成为 JWT 的第一部分。

Payload

Payload 也是 JSON 格式数据,其中包含了 Claims(声明,包含 JWT 的相关信息)。

Claims 分为三种类型:

  • Registered Claims(注册声明) :预定义的一些声明,建议使用,但不是强制性的。
  • Public Claims(公有声明) :JWT 签发方可以自定义的声明,但是为了避免冲突,应该在 IANA JSON Web Token Registry[5] 中定义它们。
  • Private Claims(私有声明) :JWT 签发方因为项目需要而自定义的声明,更符合实际项目场景使用。

下面是一些常见的注册声明:

  • iss(issuer):JWT 签发方。
  • iat(issued at time):JWT 签发时间。
  • sub(subject):JWT 主题。
  • aud(audience):JWT 接收方。
  • exp(expiration time):JWT 的过期时间。
  • nbf(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
  • jti(JWT ID):JWT 唯一标识。

示例:

{
  "uid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
  "sub": "1234567890",
  "name": "John Doe",
  "exp": 15323232,
  "iat": 1516239022,
  "scope": ["admin", "user"]
}

Payload 部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!

JSON 形式的 Payload 被转换成 Base64 编码,成为 JWT 的第二部分。

Signature

Signature 部分是对前两部分的签名,作用是防止 Token(主要是 payload) 被篡改。

这个签名的生成需要用到:

  • Header + Payload。
  • 存放在服务端的密钥。 以后用于解密客户端传来的jwt(一定不要泄露出去)
  • 签名算法。

签名的计算公式如下:

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

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,成为 JWT 的第三部分。

JWT 与传统token区别?

1 传统Token因为存储到redis缓存或者其它数据库中需要查redis缓存或者其它数据库等去验证token是否有效,而JWT不用查redis缓存或者其它库等,直接在服务端进行校验(本文后面有介绍)

  • 传统token: 每次请求传递来的token,都会去请求另外的服务(去服务器或者redis缓存上查看是否有该token)进行token的检查

  • JWT: 因为用户的信息及加密信息,和过期时间,都在JWT里,只要在服务端进行校验就行(不用另外去请求check token服务进行验证),并且校验也是JWT自己实现的。

    服务端拿到 JWT 后,会解析出其中包含的 Header、Payload 以及 Signature 。服务端会根据 Header、Payload、密钥(服务端存储的secret)再次生成一个 Signature。拿新生成的 Signature 和 Token 中的 Signature 作对比,如果一样就说明 Header 和 Payload 没有被修改,完成了token检查。。 当然也可以服务端直接使用secret密钥进行解密,解签证解出第一部分和第二部分,然后比对第二部分的信息和客户端传过来的信息是否一致。如果一致验证成功,否则验证失败。

2 JWT相对于传统token更安全

  • JWT 通常存放到 localstorage 中, 传统token放到cookie, 避免CSRF攻击
  • JWT 使用安全系数高的加密算法。
  • JWT生成的token是无状态的,服务端不存储,即一旦生成在有效期之前一直可用,无法销毁
  • 如果需要刷新token有效期或者提前失效需要借助缓存、数据库或者redis来自己实现对应逻辑
  • 由于json的通用性,所以JWT是可以进行跨语言支持的

如何加强 JWT 的安全性?

  1. 使用安全系数高的加密算法。
  2. 使用成熟的开源库。
  3. JWT 存放在 localStorage中而不是 Cookie 中,避免 CSRF 风险。
  4. 一定不要将隐私信息存放在 Payload 当中
  5. 密钥一定保管好,一定不要泄露出去JWT 安全的核心在于签名,签名安全的核心在密钥
  6. Payload 要加入 exp (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不易过长。

如何基于 JWT 进行身份验证?

在基于 JWT 进行身份验证的的应用程序中,服务器通过 Payload、Header 和Secret(密钥)创建JWT(令牌)并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。

简化后的步骤如下:

image.png

  1. 用户向服务器发送用户名、密码以及验证码用于登陆系统。
  2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token
  3. 用户以后每次向后端发请求都在 Header 中带上这个 Token
  4. 服务端检查 Token,如果验证通过, 就向客户端返回请求的数据。

上述步骤也是传统token的使用步骤,唯一区别是刚刚聊到的 验证方式不同

两点建议:

  1. 建议将 Token 存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。
  2. 请求服务端并携带 Token 的常见做法是将 Token 放在 HTTP Header 的 Authorization 字段中(Authorization: Bearer Token)。

spring-security-jwt-guide[6] 就是一个基于 JWT 来做身份认证的简单案例,感兴趣的可以看看。

如何让JWT失效?

  1. JWT生成的token是无状态的,服务端不存储,即一旦生成在有效期之前一直可用,无法销毁

  2. 如果需要刷新token有效期或者提前失效需要借助缓存、数据库或者redis来自己实现对应逻辑

  3. JWT无状态=>不需要通过存储验证是否正确,本身可以验证

  4. 手动销毁:必须借助于第三方类似黑名单的方式=> 把检测到的非法token添加到黑名单中,每次验证token是否有效要先过黑名单,如果存在黑名单中直接拒绝

如何生成JWT?

可以使用jsonwebtoken npm包去配置生成

const JWT = require('jsonwebtoken')
const algorithm = 'HS256' //算法类型
const secret = '用到的secret' 签名字符串

使用JWT.sign加密

 let token = JWT.sign(
        { username: 'username123', exp: Date.now() + 1000 * 60 }, // payload
        secret, // 签名密钥
        { algorithm } // 签名算法
  )

使用JWT.verify 去解密

//解密token
JWT.verify(token, secret, function (err, decoded) {
    if (!err){
          console.log(decoded.username);  //会输出username123
     }
})

PS: