JWT

1. 产生原因

为了限制 HTTP API 资源访问,如一个用户更改另一个用户的密码,而保护 API 的困难在于请求是无状态的,通过 API 本身是无法知道有两个请求是否来自同一用户。

2. JWT

JWT(Json Web Token) 是一种用于在网络应用之间安全地传输信息的开放标准。它通过将用户信息以 JSON 格式加密并封装在一个 token 中,然后将该 token 发送给服务端进行验证,从而实现身份验证和授权。

JWT 特别适用于分布式站点的单点登录(SSO)场景,它本质上是一种令牌格式,与终端设备、服务器、传输类型都无关,它只规范了令牌的格式而已。

3. 加密解密流程

  1. 生成:客户端登录成功后,服务器使用用户的信息(如用户ID、用户名等)以及服务器端的密钥,通过特定的加密算法生成 JWT
  2. 发送:客户端请求时携带 JWT 发送给服务器,通常是通过 HTTP 请求的头部 Authorization 字段发送。
  3. 服务器验证:服务器收到 JWT 后,校验格式并解析出头部和载荷部分,再使用存储在服务器端的密钥和相同的加密算法,对头部和载荷进行签名验证来确认请求的合法性
  4. 响应处理:当 JWT 验证成功后,服务器才正式响应请求

4. 认证区别

  • 传统 Token :登录成功后,服务端生成一个随机 Token 并分配给用户,同时在服务端(如数据库或缓存)保存一份记录,用户在后续的请求中需携带它,服务端会进行验证
  • JWT:登录成功后,服务端使用 JWT 生成一个随机 Token 并分配给用户,但不存储它,用户在后续的请求中需携带它,服务端通过 JWT 进行验证

5. 组成

JWT 包含三个部分:头部(Header)、载荷(Payload)、签名(Signature),以句点分隔,分别使用 Base64url 编码。

jwt.io 可用于在线测试

1. Header

HeaderToken 的元信息,头部承载两部分信息,由 json 格式经 Base64url(对 Base64 编码后的字符串,用 - 替代 +,用 _ 替代/) 编码得到。

  • 声明令牌类型 typ: 这里是 JWT
  • 声明加密的算法 alg: 通常直接使用 HMAC SHA256
1
2
3
4
5
6
7
# 定义 Header
{
"typ": "JWT",
"alg": "HS256"
}
# 编码得到
# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

2. Payload

Payload 存放有效信息,如用户ID、用户名等,也可以包含其他自定义信息,载荷的内容是经过Base64url 编码,能够被解码。它可以包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

1. 标准中注册的声明 (建议但不强制使用)

  • iss: JWT 签发者
  • sub: JWT 所面向的用户
  • aud: 接收 JWT 的一方
  • exp: JWT 的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该 JWT 都是不可用的.
  • iat: JWT 的签发时间
  • jti: JWT 的唯一身份标识,主要用来作为一次性 Token,从而回避重放攻击。

2. 公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。其可在客户端解密,不建议添加敏感信息。

3. 私有的声明

私有声明是提供者和消费者所共同定义的声明。其可在客户端解密,不建议添加敏感信息。

1
2
3
4
5
6
7
8
9
# 定义 Payload
{
"id": 1001,
"name": "gaop",
"phone": 123456789,
"address": "zh-cn"
}
# 编码得到
# eyJpZCI6MTAwMSwibmFtZSI6Imdhb3AiLCJwaG9uZSI6MTIzNDU2Nzg5LCJhZGRyZXNzIjoiemgtY24ifQ

3. Signature

用于验证 Token 的合法性,签名是由三部分并结合特定的加密算法生成:

  • Header: Base64url 后的头部
  • Payload: Base64url 后的载荷
  • Secret: 密钥
1
2
3
4
5
// Header + Payload,确保了签名对于此特定令牌是唯一的
// Secret 保存在认证服务器端,防止客户端伪造令牌,三者再进行哈希计算
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), gaop)
// 编码得到
// jrgCJ4Nhybc6DxO9leQZOhQuYkMSIOEvWcsIffTtfUk

6. 攻击方式

1. 修改加密算法伪造 Token

JWT 中最常用的两种算法为 HMAC 和 RSA,若某程序在 JWT 传输过程中使用 RSA 算法,同时使用密钥 private_src 对 JWT 进行签名,公钥 public_abc 对签名进行验证。获取公钥后,将 JWT 的加密算法修改为 HMAC,同时使用获取到的公钥作为算法的密钥,对 token 进行签名,发送到服务器端,服务器端会将 RSA 的公钥视为当前算法(HMAC)的密钥,而 HMAC 是对称加密算法,故服务器使用密钥 abc 对接收到的签名进行验证,从而造成 token 伪造问题。

2. None 算法攻击绕过验证

在 Header 中指定 alg 为 None,同时不添加 Signature,服务器在验证JWT时会认为这个JWT是不需要签名的,从而跳过了对签名的验证,直接信任 JWT 中的信息,实现伪造身份绕过服务器验证。

3. kid 参数

kidJWT 头部中的一个可选参数,全称为 Key ID,它用于指定加密算法所使用的密钥。这可能导致以下问题:

  • 任意文件读取:系统并不会验证 kid 参数路径指向的文件是否是有效的密钥文件。因此,在没有对参数进行过滤或验证的情况下,可造成任意文件读取
1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "../../etc/passwd"
}
  • SQL 注入:kid 参数也可能从数据库中提取数据,故存在 SQL 注入攻击的风险
1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "user1' || union select 'users' -- "
}
  • 命令注入:若服务器后端使用的是 Ruby,在读取密钥文件时使用了 open 函数,通过构造参数可能实现命令注入
1
2
3
4
5
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/path/to/key_file|whoami"
}