详细理解 JWT(Json Web Token)
在分布式微服务架构中,特别是跨域访问的情况下,通常会使用 JWT 技术来实现安全认证。
JSON Web Tokens 是一种开放的、行业标准的 RFC7519 规范,用于安全地表示双方之间的声明。
JWT 官网,JWT Introduction,Auth0 > JWT。
JWT 介绍及结构
学一门技术,优先阅读官方文档,才能系统地了解并理解其概念和应用。
JWT 简介
JWT 是 Json Web Token 的简写,是一种开放标准(RFC 7519),它定义了一种简洁独立的数据规范,用于在各方之间作为 JSON 对象安全地传输信息,这些信息可通过数字签名进行验签和信任。
JWT 可以使用密钥(如,HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
虽然 JWT 可以加密以在各方之间提供保密,但我们将专注于签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则隐藏其他方的声明。当使用公钥/私钥对签名令牌时,签名还证明只有持有私钥的一方是签署它的一方。
JWT 使用场景
- Authorization:授权,这是 JWT 最常见的方案。一旦用户登录,后续每个请求都会包含 JWT,允许用户访问授予该令牌的路由、服务和资源。SSO(Single Sign On - 单点登录)是 JWT 广泛应用的场景,开销小,且能在不同的域中轻松使用;其它一些一次性验证场景,如邮件激活用户等。
- Information Exchange:信息交换,JSON Web 令牌是在各方之间安全传输信息的好方法。因为 JWT 可以签名。例如,使用 公钥/私钥对,可以确信息是持有私钥的人发送的。另外,签名是对头和消息体(有效负载)计算得到的,可以验证内容是否被篡改。
JWT 数据结构
JWT 数据结构由三部分组成,分别是:Header、Payload、Signature,使用点号分隔(.
),所以 JWT 通常显示如下格式:xxxxx.yyyyy.zzzzz。
- Header:消息头
Header 通常由两部分组成:Token 类型,即 JWT,以及使用的签名算法,如 HMAC、SHA256 或 RSA。如下示例:然后对这串 header json 使用 Base64Url 编码1
2
3
4{
"alg": "HS256",
"typ": "JWT"
} - Payload:消息体
Payload 指有效负载(消息体),包含关于实体(通常为用户)和其他数据的声明。声明有三种类型:注册声明,公开声明,私人声明。- Registered claims:注册声明,一组预先定义的声明,非强制的,但建议提供一组有用的,可互操作的声明。其中包括:ISS(发行人),EXP(过期时间),SUB(主题)、AUD(受众)等。
注意:声明名称只有三个字符,因为 JWT 意味着紧溱。 - Public claims:公开声明,由使用 JWT 的人随意定义。但为了避免冲突,最好使用 IANA JSON Web Token 注册表中的声明名称,或者将其定义为包含在一个防止冲突命名空间的 URI。
- Private claims:私人声明,这是为了使用各方之间共享信息而创建的自定义声明。
然后对这串 payload json 使用 Base64Url 编码。1
2
3
4
5{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意:对于已签名的令牌,这些信息虽然可以防止被篡改,但任何人都可以读取。除非 JWT 是加密的,否则不要将敏感信息放在 Header 或 Payload 中。 - Registered claims:注册声明,一组预先定义的声明,非强制的,但建议提供一组有用的,可互操作的声明。其中包括:ISS(发行人),EXP(过期时间),SUB(主题)、AUD(受众)等。
- Signature:签名
要创建签名,必须获取已编码的 Header、已编码的 Payload,一个密钥,和在 Header 中指定的算法,并对其进行签名。
例如,如果使用 HMAC SHA256 算法,将按以下方式创建签名:签名用于验证消息是否在传输过程中被篡改,对于使用私钥签名的令牌,还可以验证 JWT 的发送者是否可信息。1
+ "." +base64UrlEncode(payload), secret)
JWT 数据组合
JWT 最终输出的是三个由点(.
) 分隔的Base64-URL 字符串,这些点可以在 HTML 和 HTTP 环境中轻松传递。
下面显示了一个JWT,包含了 Header 和 Payload 编码,并使用密钥签名:
可以使用 JWT.IO 调试器来解码、验证和生成 JWT,如下示例:
JWT 工作原理
在身份验证中,当用户使用其凭据成功登录时,将返回 JSON Web Token。 由于 Token 就是凭证,因此必须非常小心以防止出现安全问题,通常会对令牌设置过期时间。
每当用户想要访问受保护的路由或资源时,用户代理应该使用 Bearer 模式发送 JWT,Token 通常在 Authorization 头中。 如下所示:
1 | Authorization: Bearer <token> |
在某些情况下,可以是无状态授权机制。 服务器的受保护路由将在 Authorization 头中检查有效的 JWT,如果存在,则允许用户访问受保护的资源。 如果 JWT 包含必要的数据,则可以减少查询数据库以进行某些操作的需要。
如果在 Authorization 头中发送 Token,则跨域资源共享(CORS)将不会成为问题,因为它不使用 cookies。
下图显示了如何获取 JWT 并用于访问 API 或资源:
- 应用或客户端向受权服务器请求受权。
- 授权服务器向应用程序返回访问令牌(Token)。
- 应用程序使用访问令牌访问资源服务器中受保护资源。
- 资源服务器对令牌进行核验,包括对令牌自身的核验和向授权服务器请求核验。
注意:对于签名的令牌,令牌中包含的所有信息都是对外公开的,即使外界无法修改它,所以不应该将秘密信息放在令牌中。
关于授权,JSON Web Token 允许粒度安全性,即能够在令牌中指定一组特定权限,从而提高可调试性。
JWT 优点
先谈谈 JSON Web Tokens(JWT)与 Simple Web Tokens(SWT)和 Security Assertion Markup Language Tokens(SAML)相比的好处。
由于 JSON 比 XML 更简洁,所以它被编码时,它的大小也更小,使得 JWT 比 SAML 更紧凑。这使得JWT 成为在 HTML 和 HTTP 环境中传递的一个很好的选择。
在安全方面,SWT 只能使用 HMAC 算法通过共享密钥对称签名。但是,JWT 和 SAML 令牌可以使用 X.509 证书形式的公钥/私钥对进行签名。与简单的 JSON 签名相比,使用 XML 数字签名来签名 XML 而不会引入模糊的安全漏洞非常困难。
JSON 解析器在大多数编程语言中很常见,因为它们直接映射到对象。相反,XML 没有一个自然的文档映射到对象。这使得使用 JWT 比使用 SAML 断言更容易。
关于使用,JWT 用于互联网规模。这突出了在多个平台(尤其是移动平台)上客户端处理 JSON Web Token 的便利性。
JWT 缺点
- JWT 生成严重依赖于密钥和生成算法,并且是硬编码存中,或存在外部配置文件中,这样密钥增加了泄漏的风险,威胁系统安全。
- JWT 使用 Base64 编码,并没有加密,不参存储敏感数据,而 Session 信息存服务器,相对更安全。
- JWT 是无状态一次性的,一旦签发,无法中途废弃,只能重新签发,但旧的未过期,仍可使用。
- 若不控制 JWT 签名的有效载荷(payload)中的数据量,则数据可能非常大,增加网络开销,远比只是很短的字符串 sessionId 开销大得多。
JWT 安全使用
清除已泄漏的 Token:将 JWT 在服务端(Redis)存储一份,若发现令牌异常,则从服务端将此 Token 清除,当用户发起请求时,强制用户重新进行身份验证。这种处理方式相对就比较重了,与 JWT 轻量级验证有些违背,但也不失为一种可行的选择。
敏感操作增加二次验证,如手机验证码,扫二维码等方式,确认操作者是用户本人,若验证不通过,则终止操作,同时要求重新验证用户身份信息。
地域检查:检查用户请求所在的地域,若短时间内在多个地域活动,则终止当前请求,强制用户重新进行身份认证,签发新的 Token,并提醒(或要求)用户重置密码。
监控请求频率:如果 JWT 密钥被盗,攻击者伪造用户身份,可能会高频次对系统发送请求,以窃取用户数据。
可以监控单位时间内的用户请求次数,若超出阀值则认为异常,服务端终止请求并清除该用户的 Token ,转到认证中心对用户身份进行验证。
客户端环境检测:可以 Token 与设备的机器码进行绑定,并存储在服务端中,当客户端发起请求时,可以先校验机器码,如果不匹配则视为非法,终止请求。
将废弃的 JWT 加入黑名单:重新签发 JWT 后,将旧的未过期的 Token 加入黑名单(Redis)避免再次使用。
JWT 续签:最简单的方式是在每次请求时刷新 JWT,或计算剩余有效时间小于某个值时就刷新 JWT,在HTTP 请求时返回一个新的 JWT。此方法暴力且不优雅。
另一种方式是在 Redis 中为每个 JWT 设置过期时间,每次访问时刷新 JWT 的过期时间。
JWT 使用建议
- JWT 是无状态的,只有过期时间,无法主动使其失效,因此 Payload 中的 exp 过期时间不要设的太长。
- JWT 是无状态的,适用于无状态的 Rest API,适用于移动端,前后端分离的 Web 端。
- JWT 的 Base64Url 编码是为了 token 存在于 url 中, Base64Url 解码后是明文,不可存放敏感数据。
- 在服务端开启 HttpOnly,预防 XSS 攻击。
- 若要预防重放攻击,可以增加 jti(JWT ID)来作为唯一验证。
- JWT 最好用于一次性授权 Token 的设计,时效短。
- 在实际应用中,强烈建议走 HTTPS 协议,对 JWT 串进行二次加密。
使用 KeyProvider
通过使用 KeyProvider,可以在运行时更改用于验证令牌签名或为 RSA 或 ECDSA 算法签署新令牌的密钥。 这是通过实现 RSAKeyProvider 或 ECDSAKeyProvider 方法达到的:
- getPublicKeyById(String kid):在令牌签名验证期间调用,返回用于验证令牌的密钥。如果使用钥匙旋转,例如 JWK,它可以使用ID获取正确的旋转钥匙(或始终返回相同的钥匙)。
- **getPrivateKey()**:在令牌签名期间调用,返回将用于签名 JWT 的密钥。
- **getPrivateKeyId()**:在令牌签名期间调用,返回 getPrivateKey() 方法返回的密钥的 ID。此值优先于 jwtcreator.builder withkeyid(string) 方法中的设置。如果不需要设置 kid 值,请避免使用 KeyProvider 实例化算法。
下面的示例展示了如何使用 JWkstore,一个虚构的 JWK 集实现。对于使用 JWKS 的简单密钥旋转,请尝试 JWKS RSA Java 库。
1 | final JwkStore jwkStore = new JwkStore("{JWKS_FILE_HOST}"); |
JWT 实现 java-jwt
在 JWT 官网的 Libraries 里可以看到分别为不同语言提供了 JWT 标准的实现库,甚至一种语言有多个类似的库,只是支持的签名算法和检验略有区别。
JWT 的 Java 实现有:Auth0 > java-jwt,jjwt,jose4j,参考库官网或 GitHub Wiki 学习对其使用。
添加依赖
1 | <dependency> |
选择算法
算法定义如何对签名和验证令牌。
当使用 RSA 或 ECDSA 算法并且只需要对 JWT 进行签名时,可以通过传递空值来避免指定公钥。当只需要验证 JWT 时,也可以对私钥进行同样的操作。
使用静态密码或密钥:
1 | //HMAC |
创建并签署令牌
首先要创建一个 JWTCreator 实例,调用 JWT.create() 并使用 Builder 模式自定义需要加入到 Token 中的声明,最后调用 sign() 入传入 Algorithm 实例。
HS256 示例:
1 | try { |
RS256 示例:
1 | RSAPublicKey publicKey = //Get the key instance |
验证令牌
首先调用 JWT.require() 创建 JWTVerifier 实例,并且传入 Algorithm 实例。如果需要验证令牌具有特定声明值,可使用 Builder 来定义。最后调用 *verifier.verify(token)*。
HS256 示例:
1 | String token = ""; |
RS256 示例:
1 | String token = ""; |
如果 Token 含有无效的签名,或未满足声明要求,则会抛出:JWTVerificationException 异常。
时间校验
JWT Token 可能包含 DateNumber 类型字段,可用于验证:
- iat(Issued At):toke 签发时间,必须小于当前时间。
- exp(Expires At):token 过期时间,必须大于当前时间。
- nbf(Not Before):token 开始生效日期,在此日期之前不可用。
验证令牌时,将自动对时间进行验证,从而在值无效时引发 JWTverificationException 。如果上面的任何字段不存在,则在验证中不检验这些字段。
给 Token 指定一个额外有效期的窗口期(相当延长有时间),使用 JWTVerifier 生成器中的 acceptLeeway() 方法并传递一个正秒值。适用于上面列出的每一项。
过期的窗口期是最终实现是将当前时间往回推 N 秒,相对延长过期时间。
1 | JWTVerifier verifier = JWT.require(algorithm) |
还可以为的日期声明指定自定义值,只覆盖该声明的默认值。
1 | JWTVerifier verifier = JWT.require(algorithm) |
如果您需要在 lib/app 中测试此行为,请将 Verification 实例强制转换为 BaseVerification,以获得接受自定义 Clock 的verification.build() 方法的可见性。如下示例:
1 | BaseVerification verification = (BaseVerification) JWT.require(algorithm) |
解码令牌
1 | String token = ""; |
如果 Token 存无效的语法,或不是 JSON ,则抛出 JWTDecodeException 异常。
java-jwt 示例
示例中使用 FastJson 和 HuTools 库。
1 |
|
JWT 实现 jjwt
JJWT 旨在成为最容易使用和理解的库,用于在 JVM 和 Android 上创建和验证 JSON Web Token(JWT)。
JJWT 是一个开源的纯 Java 实现,完全基于 JWT,JWS,JWE,JWK 和 JWA RFC 规范,该库由 Okta 的高级架构师 Les Hazlewood 创建,JJWT 还添加了一些不属于规范的便利扩展,例如 JWT 压缩和声明实施。
添加依赖
1 | <!--jjwt--> |
快速开始
1 | import io.jsonwebtoken.Jwts; |
断言 jwt
1 | assert Jwts.parser().setSigningKey(key).parseClaimsJws(jws).getBody().getSubject().equals("Joe"); |
创建 Keys
1 | SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //or HS384 or HS512 |
创建 JWS
使用 Jwts.builder() 创建 wtBuilder 实例,可以使用 Builder 模式设置消息头,消息体(声明),签名算法,最后调用 compact() 方法。
1
2
3
4String jws = Jwts.builder() // (1)
.setSubject("Bob") // (2)
.signWith(key) // (3)
.compact(); // (4)设置 Header,jjwt 默认会设置两个头信息,分别是
alg
和zip
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//设置头
String jws = Jwts.builder()
.setHeaderParam("kid", "myKeyId")
// ... etc ...
// Header 实例
Header header = Jwts.header();
populate(header); //implement me
String jws = Jwts.builder()
.setHeader(header)
// ... etc ...
// Header Map
Map<String,Object> header = getMyHeaderMap(); //implement me
String jws = Jwts.builder()
.setHeader(header)
// ... etc ...设置 Payload
- setIssuer: sets the iss (Issuer) Claim
- setSubject: sets the sub (Subject) Claim
- setAudience: sets the aud (Audience) Claim
- setExpiration: sets the exp (Expiration Time) Claim
- setNotBefore: sets the nbf (Not Before) Claim
- setIssuedAt: sets the iat (Issued At) Claim
- setId: sets the jti (JWT ID) Claim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32String jws = Jwts.builder()
.setIssuer("me")
.setSubject("Bob")
.setAudience("you")
.setExpiration(expiration) //a java.util.Date
.setNotBefore(notBefore) //a java.util.Date
.setIssuedAt(new Date()) // for example, now
.setId(UUID.randomUUID()) //just an example id
//自定义 Claims 声明
String jws = Jwts.builder()
.claim("hello", "world")
//创建 Claims 实例
Claims claims = Jwts.claims();
populate(claims); //implement me
String jws = Jwts.builder()
.setClaims(claims)
//创建 Claims Map
Map<String,Object> claims = getMyClaimsMap(); //implement me
String jws = Jwts.builder()
.setClaims(claims)
//设置算法密钥
String jws = Jwts.builder()
.signWith(key)
.compact();
//覆盖默认算法,如果设置的 RSA PrivateKey,JJWT默认自动选择 RS256算法
.signWith(privateKey, SignatureAlgorithm.RS512)
.compact();
读取 JWS
使用 Jwts.parser() 方法创建 JwtParser 实例,指定 SecretKey 或 非对称 PublicKey 用于验证 JWS 签名,最后调用 parseClaimsJws(String) 方法。
解析(验证) JWS
1
2
3
4
5
6
7
8
9
10Jws<Claims> jws;
try {
jws = Jwts.parser() // (1)
.setSigningKey(key) // (2)
.parseClaimsJws(jwsString); // (3)
// we can safely trust the JWT
catch (JwtException ex) { // (4)
// we *cannot* use the JWT as intended by its creator
}验证密钥 Key
1
2
3
4
5
6
7
8
9//使用单个密钥
Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwsString);
//非对称签名,私钥签名,公钥验证
Jwts.parser()
.setSigningKey(publicKey)
.parseClaimsJws(jwsString);如果想要使用不同的 Keys,就不要调用 setSigningKey() 方法,而是实现 SigningKeyResolver 接口,传入实例到 JwtParser。
1
2
3
4SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();
Jwts.parser()
.setSigningKeyResolver(signingKeyResolver) // <----
.parseClaimsJws(jwsString);继承 SigningKeyResolverAdapter 适配器,实现 resolveSigningKey(JwsHeader, Claims) 方法:
1
2
3
4
5
6public class MySigningKeyResolver extends SigningKeyResolverAdapter {
public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
// implement me
}
}
自定义压缩
JJWT 默认对生成的 jws 进行了压缩,压缩后的 jws 是非标准的 JWT,因此,创建和解析都需要使用 JJWT 库。
压缩 JWT
1 | Jwts.builder() |
解析 JWT 设置压缩解析器
1 | CompressionCodecResolver ccr = new MyCompressionCodecResolver(); |
自定义压缩实现
1 | public class MyCompressionCodecResolver implements CompressionCodecResolver { |
自定 Base64
1 | //编码 |
其它参考
详细理解 JWT(Json Web Token)
http://blog.gxitsky.com/2019/05/20/ArchitectureDesign-Distrbuted-app-jwt-token/