文章

JWT 简单教程

JSON Web Token(缩写 JWT)是目前最流行的身份验证解决方案,本文介绍它的原理和用法。

用户身份认证方案

session 流程

  1. 客户端使用用户名密码登录。
  2. 服务端验证通过后在当前会话(session)中保存相关数据,比如用户基本信息等,返回 session_id,写入用户 cookie。
  3. 客户端的后续请求将 session_id 放在 cookie 里传回服务端。
  4. 服务端通过 session_id 找到前期保存的数据,确认用户身份。

token 流程

  1. 客户端使用用户名密码登录
  2. 服务端验证通过后生成 token 返回给客户端,客户端保存 token 在本地
  3. 客户端的后续都请求带上 token, 一般放在 header 里面
  4. 服务端通过验证 token 来确认用户身份

两种方案流程上看好像相差无几,但实际应用过程中还是有很大的区别的:

  1. 使用 session 方案存在跨域问题 因为 session 是由服务端维护的,并且是通过 cookie 传递的,如果是单一服务或者单一域名,使用 session 是完全没有问题的,但是如果是多台服务器集群或者需要在多个域名之间实现单点登录,使用 session 就需要较大的工作量了,因为必须做到 session 持久化,将 session 写入数据库或者其他持久层。各个服务在接收到请求后,都通过持久层获取 session 数据来确认用户身份。
  2. session 保存在服务端,token 保存在客户端 session 方案只有 session_id 是保存在客户端,其他信息都是保存在服务端,而 token 方案则相反,服务端不保存任何信息,生成 token 之后,由客户端自行保存,每次请求都发回服务端进行校验。这种方案也可以有效减轻服务端的压力。

使用 JWT 的场景

  • 验证:用户登录之后签发一个 token,后续的请求通过 token 来校验用户身份,广泛使用 JWT 实现单点登录,因为开销小并且可以很好的解决跨域问题。

  • 信息交换:服务端与客户端在进行通讯时,使用 JWT 可以保证数据安全传输,因为生成 JWT 需要加上签名,服务端可以通过验证 token 的签名来确认客户端身份,同时,因为签名是通过 JWT 的头部和负载计算出来了,所以同样可以验证 JWT 的内容是否被篡改。

JWT 的数据结构

由三部分组成

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

一个完整的 JWT 数据就是用 . 将三部分拼接起来: Header.Payload.Signature

Header(头部)

头部一般由两部分组成:token 的类型(JWT token统一写为 JWT)和加密算法,例如:

1
2
3
4
{
    "alg":"HS256",
    "typ":"JWT"
}

将上面的 json 对象用 Base64URL 算法转成字符串,作为 JWT 的第一部分。

Payload(负载)

负载用来存放实际需要传递的有效数据,包括签发者、签发时间、过期时间等,JWT 官方规定了7个字段,供开发者使用

1
2
3
4
5
6
7
1. iss (issuer):签发人
2. exp (expiration time):过期时间
3. sub (subject):主题
4. aud (audience):受众
5. nbf (Not Before):生效时间
6. iat (Issued At):签发时间
7. jti (JWT ID):编号

除了官方规定的字段,开发者也可以使用自己定义的私有字段。例如:

1
2
3
4
5
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

将负载的 json 对象用 Base64Url 进行编码,作为 JWT 的第二部分。

另外,对于 JWT 来说,信息虽然可以防止被篡改,但任何人都可以读取。除非做了额外的加密处理,否则不要将秘密信息放在 JWT 的负载或头部中。

Signature(签名)

签名部分是采用 Header 中指定的签名算法以及一个私有的秘钥(这个秘钥只有服务器知道)对 Header 和 Payload 进行签名后生成的字符串,防止数据被篡改。

例如,使用 HMAC SHA256 算法创建签名:

1
2
3
4
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

计算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用点(.)分隔,就是一个完整的 JWT 了。

JWT 的工作原理

在身份验证过程中,当用户使用它们的凭证(如用户名、密码)成功登录后时,服务端返回一个 JWT, 客户端接收到这个 JWT 后将其保存在本地,前端可以存储在 cookie、localStorage, Android 端可以存储在 SharePreference、SQLite, iOS可以存储在 NSUserDefault。在之后的每一个请求中都带上这个 JWT。服务端接收到请求之后,校验 JWT 是否正确以及过期,再根据不同的情况给客户端不同的响应。 注意,如果把 JWT 放在 cookie 中发送的话不能跨域,放在 HTTP 请求头部或者参数上传递则可以跨域。推荐的做法是放在 HTTP 请求头部的 Authorization 字段中。

1
Authorization: Bearer <JWT>

Base64URL

前面提到,Header 和 Payload 都需要使用 Base64URL 进行编码。这个算法跟 Base64 算法基本类似,但有一些小的不同。 JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。

总结

  • JWT 是一个可以用来解决跨域身份认证的方案。
  • session 和 JWT 有各自的应用场景,JWT 更适合移动端应用以及前后端分离应用。
  • JWT 默认是不加密的,所以不要把私密的数据写入 JWT。
  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,最好使用 HTTPS 协议传输。
本文由作者按照 CC BY 4.0 进行授权