# JWT(JSON Web Token)

JWT(Json web token),是为了在网络应用环境声明而执行的一种基于 JSON 的开放标准。

# 1. Session 的弊病和 JWT 的起源

http 协议本身是无状态的协议』这是所有问题的起点和关键。在这个前提下,服务器为了弄明白『这次请求和上次请求是不是同一个人/客户端发来的?』,在第一次发起请求时,服务器(Tomcat)会创建一个 Session 对象,以表示有人/客户端开始了一次会话,并将这个 Session 对象对应的 id(SessionID)返回给客户端浏览器。在后续的请求中,客户端再发出的请求都需要携带这个 SessionID,以表示当前请求和之前的请求是在同一个会话中。

传统的 Seesion 认证存在的问题:

  1. 用户请求规模大之后增加服务器内存开销;

  2. 不利于服务端搭建集群。

当然,可以有一些其他的方案(比如使用 Redis 实现 Session 共享)来解决上述 Session 的两个问题,但是 JWT 则是完全提供了另一种不同的思路:『服务端不负责存储用户信息』。

JWT 的认证流程:

  1. 客户端发送用户名和密码至服务端进行认证;

  2. 服务端认证通过后,生成一个具有唯一性标识的字符串:Token;

  3. 服务端将 Token 发还给客户端,客户端后续的请求都需要带上这个 Token;

  4. 服务端再次收到客户端请求时,认证这个 Token 是否是自己(服务端)当初生成且未经篡改过的。如果没毛病,那么就认为该用户曾经通过了登陆认证的,本次请求该干嘛干嘛。

JWT 和 SessionID 相比表面上看起来好像并没有多大区别,以前服务端回传的是叫 SessionID 的字符串,现在回传的是叫 Token 的字符串,但是它代表着『保存用户信息』这个责任从后端转移到了前端。

另外,相较于 SessionID 这样的纯 ID 字符串,JWT 的 Token 中多多少少还是能携带一些数据的。

# 2. JWT 的组成

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

这个 JWT 的标准形式为『头部.荷载.签名』。

例如:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b20iLCJleHAiOjE1NjY5MjA4MzIsImlhdCI6MTU2NjkxNzIzMiwianRpIjoiMGM4ODRhNzAtOTdkNy00MTczLTgyYzItMTcyNzA2ZmMyZDU4In0.IKO9rBpLz-u2m2gA1S2gR-8CFn1Z1qs-AZvW55A1SoY

需要说明的有 2 点:

  1. JWT 的头部和荷载部分的内容是 JSON,在 JWT 中对于 JSON 的属性(键值对)有一个专门的称呼:Claim 。

  2. JWT 是有一套规范的,在规范中定义了一些常见的 Claim 的名称,以方便大家使用。再此之外的 Claim 可以自由命名。

# 头部(Header)

JWT 都有一个头部,头部用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个 JSON 对象。

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

typealg 是放在头部的最常见的 2 个 标准 Claim ,通常它俩的值都是固定的。

  • type 用来表示当前字符串是什么类型的字符串,毫无疑问,它的值必然就是 JWT
  • alg 的值表示当前的字符串是使用什么算法加密生成的。通常这个值都是由我们使用的 Java 库来自动填充的(取决于你使用的加密算法),我们无需专门考虑它的值。

上述示例表达的含义就是:当前信息是一个 JWT,且是被 HS256 算法加密过的。

当然,一个 JWT 的头部信息真正的样子并不是上面这个样子,未来,它会被加密算法加密。上例中的头部内容未来会变成 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

这个内容其实是被 Base64 编解码算法编码,而不是真正的加密。所以,它可以变反向地解码回来。

除了头部的标准 Claim(不止上述 2 个),有需要的话,你还可以向头部添加任意的 Claim(它们的名字就是由你自己任意定义了),这些 Claim 被称为 private Claims 。

不过,一般情况下,我们不会向头部中添加 private Claims ,即便是要加,也是加到了荷载部分。

# 荷载(Payload)部分

JWT 的荷载是 JWT 的最重要部分,它就代表着 JWT 的信息内容。JWT 的荷载部分和头部部分一样,其具体内容也是一个 JSON 格式字符串。例如:

{
    "iss": "tommy",
    "aud": "jerry",
    "iat": 1627484243323,
    "xxx": "hello",
    "yyy": "world",
    "zzz": "goodbye",
}

和头部一样,荷载部分也有一些标准 Claim ,常见的有:

claim 说明
iss jwt 签发者
sub jw t所面向的用户
aud 接收 jwt 的一方
exp jwt 的过期时间,这个过期时间必须要大于签发时间
nbf 定义在什么时间之前,该jwt都是不可用的.
iat jwt 的签发时间
jti jwt 的唯一身份标识

除了标准 Claim 你也可以向荷载部分添加任何你对有用的 Claim 。

和头部一样,上述示例中的荷载内容真正的样子是:

eyJhdWQiOiJqZXJyeSIsImlzcyI6InRvbW15IiwieXl5Ijoid29ybGQiLCJ4eHgiOiJoZWxsbyIsInp6eiI6Imdvb2RieWUiLCJpYXQiOjE2Mjc0ODQyNDN9

和头部部分一样,真正的荷载部分也是经过 Base64 算法编码的。所以,它也是可以反向解码回来的。

# 签名(Signature)部分

签名部分是用前面的头部和荷载部分的内容生成的。

将上面的两个编码后的字符串都用句号 . 连接在一起(头部在前,荷载在后),再使用 HS256 算法进行加密。

之所以是 HS256 算法,是因为要与头部中所生成的加密算法呼应。当然,你也可以不使用 HS256 算法,那么与之对应的你的头部中的加密算法信息也要作响应的调整。

在加密过程中,需要提供一个秘钥(secret), 例如,以 secret 作为秘钥,上述的头部和荷载部分生成的签名就是:

uaaPNwE6n5aLztIUcjS7-DiPl9en-MKLaW5i2QkP5vY

最终,上例中的整个 JWT 的内容就是:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJqZXJyeSIsImlzcyI6InRvbW15IiwieXl5Ijoid29ybGQiLCJ4eHgiOiJoZWxsbyIsInp6eiI6Imdvb2RieWUiLCJpYXQiOjE2Mjc0ODQyNDN9.uaaPNwE6n5aLztIUcjS7-DiPl9en-MKLaW5i2QkP5vY

一旦服务端生成并发回这个 JWT 字符串之后,后续用户的请求就应该带上这个 JWT,以证明自己成功登陆过 :

http://.../xxx.do?jwt-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJqZXJyeSIsImlzcyI6InRvbW15IiwieXl5Ijoid29ybGQiLCJ4eHgiOiJoZWxsbyIsInp6eiI6Imdvb2RieWUiLCJpYXQiOjE2Mjc0ODQyNDN9.uaaPNwE6n5aLztIUcjS7-DiPl9en-MKLaW5i2QkP5vY

# 3. JWT 的安全性和注意事项

JWT 的签名部分杜绝了 JWT 被篡改的可能。它从两点实现了这个功能:

  1. 被篡改后的荷载部分加上头部后与签名部分对不上(因为签名部分使用之前没改过的内容生成的),这种情况下就是非法的 JWT;

  2. 前端不知道后端生成 JWT 时使用的是什么秘钥。理论上而言,无法重新生成新的签名部分。

在 JWT 中,『不应该在载荷里面加入任何敏感的数据』。在上面的例子中,我们传输的是用户的 User ID 。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。

理论上来说,只要使用 HTTP 协议都有可能被人拦截/观测到你在网络上所发送的数据。这种情况下,使用什么方案都防不住信息的泄露。这也是现在越来越鼓励使用 https(http+ssl)的原因。

# 4. Java 操作 JWT

参见《操作 JWT:nimbus-jose-jwt 库》笔记

# 5. JWT Token 使用技巧

  1. 可以将用户的 Http 请求的 UserAgent 信息也放入 JWT 荷载部分。

    这样做的好处是在一定程度上 JWT Token 被盗用。一旦在另一个客户端使用同一个 token 发出 http 请求,http 请求的 UserAgent(大概率)会和前一个客户端不一样。

    按照这个思路,客户端的 IP 信息也可以放入 JWT 荷载部分。

  2. 服务端不需要考虑客户端将收到的 JWT Token 放哪。

    Cookie 还是 local storage、session storage ?这是客户端(前端开发工程师、IOS/Andriod 客户端开发工程师)考虑的事情。

  3. 前端向后端传递 JWT token 是,可以放在 http header 里,也可以拼在 url 里,也可以放 cookie 里。

# 其它

不要试图用jwt去代替session。

这种模式下其实传统的 session+cookie 机制工作的更好,jwt 因为其无状态和分布式,事实上只要在有效期内,是无法作废的,用户的签退更多是一个客户端的签退,服务端 token 仍然有效,你只要使用这个 token ,仍然可以登陆系统。另外一个问题是续签问题,使用 token ,无疑令续签变得十分麻烦,当然你也可以通过 redis 去记录 token 状态,并在用户访问后更新这个状态,但这就是硬生生把 jwt 的无状态搞成有状态了,而这些在传统的 session+cookie 机制中都是不需要去考虑的。

这种场景下,考虑高可用,我更加推荐采用分布式的 session 机制,现在已经有很多的成熟框架可供选择了(比如 spring session)。

https://jwt.io/#debugger-io