Json web token (JWT), 根据官网的定义,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT的特点
- 体积小,因而传输速度快
- 传输方式多样,可以通过URL/POST参数/HTTP头部等方式传输
- 严格的结构化。它自身(在 payload 中)就包含了所有与用户相关的验证消息,如用户可访问路由、访问有效期等信息,服务器无需再去连接数据库验证信息的有效性,并且 payload 支持为你的应用而定制化。
- 支持跨域验证,可以应用于单点登录。
1. 格式
一个 JWT token 看起来是这样的:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJqdGkiOiI0MzIzY2YwOS0yNTE3LTRhMTAtYWU0MC1hZmFkNDIxZTM2MjMiLCJ1c2VySWQiOiIxMjEwIiwiaWF0IjoxNTA4MzgwNTA4LCJleHAiOjE1MDg0NjY5MDgsImF1ZCI6IiIsImlzcyI6IiIsInN1YiI6IiJ9.oEZquQ55hkORTyj2FlWbp_JZHuIK6gcKn9S8kbVEEUvQamF9J-3n8RgfhS06yh4RNO-URpQsYqzbiTg5ddojKg
这个字符串由 . 分为三部分:头部、载荷、签名。其中头部和载荷两部分是BASE64编码后的字符串,可以通过BASE64解码得到原始的JSON数据。
1.1. 头部
头部包含了一些元数据,至少会表明 token 类型以及 签名方法。比如
{
"typ" : "JWT",
"alg" : "HS256"
}
type: 必需。token 类型,JWT 表示是 JSON Web Token.
alg: 必需。token 所使用的签名算法,通常用HS256,可用的值:
- HS256 HMAC SHA-256算法
- HS384 HMAC SHA-384算法
- HS512 HMAC SHA-512算法
- RS256 RSASSA SHA-256算法
- RS384 RSASSA SHA-384算法
- RS512 RSASSA SHA-512算法
- ES256 ECDSA P-256曲线 SHA-256算法
- ES384 ECDSA P-384曲线 SHA-384算法
- ES512 ECDSA P-512曲线 SHA-512算法
将上述的JSON字符串使用BASE64加密之后就构成了第一部分:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9
1.2. 载荷
如载荷包含了一些跟这个token有关的重要信息。这些有效信息包含三个部分:
公共的声明 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。它可以包括下列信息:
- iss: issuer该JWT的签发者,可选
- sub: subject该JWT所面向的用户,可选
- aud: audience接收该JWT的一方,可选
- exp(expires): 什么时候过期,Unix时间戳,可选
- iat(issued at): 在什么时候签发,UNIX时间戳,可选
- nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟;可选
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击;可选
下面是一个例子:
{
"jti": "4323cf09-2517-4a10-ae40-afad421e3623",
"userId": "1210",
"iat": 1508380508,
"exp": 1508466908,
"aud": "",
"iss": "",
"sub": ""
}
将载荷的字符串使用BASE64加密之后就构成了第二部分:
eyJqdGkiOiI0MzIzY2YwOS0yNTE3LTRhMTAtYWU0MC1hZmFkNDIxZTM2MjMiLCJ1c2VySWQiOiIxMjEwIiwiaWF0IjoxNTA4MzgwNTA4LCJleHAiOjE1MDg0NjY5MDgsImF1ZCI6IiIsImlzcyI6IiIsInN1YiI6IiJ9
1.3. 签名
jwt的第三部分是一个签名信息。将上面的两个编码后的字符串都用.连接在一起(头部在前),就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJqdGkiOiI0MzIzY2YwOS0yNTE3LTRhMTAtYWU0MC1hZmFkNDIxZTM2MjMiLCJ1c2VySWQiOiIxMjEwIiwiaWF0IjoxNTA4MzgwNTA4LCJleHAiOjE1MDg0NjY5MDgsImF1ZCI6IiIsImlzcyI6IiIsInN1YiI6IiJ9
然后在将上面的字符串使用相应的加密方法(头部里指定)加密 最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。如果我们用 secret 作为密钥的话,那么就可以得到我们加密后的内容:
oEZquQ55hkORTyj2FlWbp_JZHuIK6gcKn9S8kbVEEUvQamF9J-3n8RgfhS06yh4RNO-URpQsYqzbiTg5ddojKg
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJqdGkiOiI0MzIzY2YwOS0yNTE3LTRhMTAtYWU0MC1hZmFkNDIxZTM2MjMiLCJ1c2VySWQiOiIxMjEwIiwiaWF0IjoxNTA4MzgwNTA4LCJleHAiOjE1MDg0NjY5MDgsImF1ZCI6IiIsImlzcyI6IiIsInN1YiI6IiJ9.oEZquQ55hkORTyj2FlWbp_JZHuIK6gcKn9S8kbVEEUvQamF9J-3n8RgfhS06yh4RNO-URpQsYqzbiTg5ddojKg
签名实际上是对头部以及载荷内容进行加密。如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。这样就能保证token不会被篡改。
2. 服务端校验
服务端收到TOKEN之后可以按照下面的步骤对TOKEN的有效性进行校验:
- 将TOKEN按照格式拆分为三段
- 校验载荷中的数据是否有效,例如TOKEN是否过期,签发者是否一致等
- 根据头部中指定的签名算法对前两部分做签名
- 将服务器生成的签名与调用方传入的签名比较,两个签名相同才能说明这个TOKEN是合法的
3. 优点
- 跨域友好: 只需要在请求头加上token就可以实现跨域访问
- 无状态: 用户状态分散到了客户端中,分散了服务器端的存储压力
- 更适合CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可
- 解耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可
- 适应多种终端: 在Android和IOS使用TOKEN认证机制要比Cookie简单得多
- 避免CSRF攻击: 不再依赖于Cookie,就不需要考虑对CSRF的防范
- 性能更好: 一次网络往返时间(通过数据库查询session信息)要比做一次HMACSHA256计算 的Token验证和解析要费时得多
4. 缺点
- 安全性: Payload是base64编码,不能存储敏感信息
- 一次性: 一旦签发一个jwt,在到期之前就会始终有效,无法中途废弃;Jwt到期后不能自动续签,要改变jwt的有效时间,就要签发新的jwt。虽然可以通过服务端存储JWT来实现,但JWT就变成有状态了,需要根据业务取舍。所以jwt适合做 restful api 认证,但不适合做服务端的状态管理
5. 如何使用
5.1. 尽可能保护你的 jwt 不被泄露
使用HTTPS加密应用,返回 jwt 给客户端时设置 httpOnly=true 并且使用 cookie 而不是 LocalStorage 存储 jwt,这样可以防止 XSS 攻击和 CSRF 攻击
为什么会遭到攻击可以查看这篇文章
5.2. secret不使用统一值
jwt的secret可以设计成和用户相关的,而不是一个所有用户公用的统一值,这样在用户修改密码后可以通过修改密钥让之前的jwt失效
5.3. 不要存储敏感信息
jwt中仅存储加密后的用户标识符。
5.4. 续签
jwt不支持续签,无法像session一样自动刷新有效期,假设JWT30分钟失效,如果用户一直在进行连续操作的时候突然显示token失效,重新登录无疑会让用户抓狂,有几种解决方式需要自行取舍
- 像OAuth2一样提供一个refreshToken
- 在redis存储过期时间,这会导致jwt变成有状态