原理在上篇已经说过了,实现起来并不难,你可以自己写一个 jwt 工具类(如果你有兴趣的话)
当然了,重复造轮子不是程序员的风格,我们主张拿来主义!
JWT 官网提供了多种语言的 JWT 库,详情可以参考 https://jwt.io/#debugger 页面下半部分
建议使用 jjwt库 ,它的github地址 https://github.com/jwtk/jjwt
jjwt 版本 0.10.7,它和 0.9.x 有很大的区别,一定要注意!!!
本文分5部分
- 第1部分:以简单例子演示生成、验证、解析 jwt 过程
- 第2部分:介绍 jjwt 的常用方法
- 第3部分:封装一个常用的 jwt 工具类
- 如果只是拿来主义,看到这里就可以了*
- 第4部分:介绍 jjwt 的各种签名算法
- 第5部分:对 jwt 进行安全加密
简单例子
引入 MAVN 依赖
1 | <dependency> |
2 | <groupId>io.jsonwebtoken</groupId> |
3 | <artifactId>jjwt-api</artifactId> |
4 | <version>0.10.7</version> |
5 | </dependency> |
6 | <dependency> |
7 | <groupId>io.jsonwebtoken</groupId> |
8 | <artifactId>jjwt-impl</artifactId> |
9 | <version>0.10.7</version> |
10 | <scope>runtime</scope> |
11 | </dependency> |
12 | <dependency> |
13 | <groupId>io.jsonwebtoken</groupId> |
14 | <artifactId>jjwt-jackson</artifactId> |
15 | <version>0.10.7</version> |
16 | <scope>runtime</scope> |
17 | </dependency> |
一个例子
1 | |
2 | // 生成密钥 |
3 | String key = "0123456789_0123456789_0123456789"; |
4 | SecretKey secretKey = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS256.getJcaName()); |
5 | |
6 | // 1. 生成 token |
7 | String token = Jwts.builder() // 创建 JWT 对象 |
8 | .setSubject("JSON Web Token") // 设置主题(声明信息) |
9 | .signWith(secretKey) // 设置安全密钥(生成签名所需的密钥和算法) |
10 | .compact(); // 生成token(1.编码 Header 和 Payload 2.生成签名 3.拼接字符串) |
11 | System.out.println(token); |
12 | |
13 | //token = token + "s"; |
14 | |
15 | // 2. 验证token,如果验证token失败则会抛出异常 |
16 | try { |
17 | Jwts.parser() |
18 | .setSigningKey(secretKey) |
19 | .parseClaimsJws(token); |
20 | // OK, we can trust this token |
21 | System.out.println("验证成功"); |
22 | } catch (JwtException e) { |
23 | //don't trust the token! |
24 | System.out.println("验证失败"); |
25 | } |
26 | |
27 | // 3. 解析token |
28 | Claims body = Jwts.parser() // 创建解析对象 |
29 | .setSigningKey(secretKey) // 设置安全密钥(生成签名所需的密钥和算法) |
30 | .parseClaimsJws(token) // 解析token |
31 | .getBody(); // 获取 payload 部分内容 |
32 | System.out.println(body); |
输出结果:
1 | eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKU09OIFdlYiBUb2tlbiJ9.QwmY_0qXW4BhAHcDpxz62v3xqkFYbg5lsZQhM2t-kVs |
2 | 验证成功 |
3 | {sub=JSON Web Token} |
常用方法
以下内容建议参考源码获知更多详情
Jwts.builder() 创建了 DefaultJwtBuilder 对象,该对象的常用方法如下:
Header
在 compact() 方法中会自动根据签名算法设置头部信息,当然也可以手动设置
- setHeader(Header header): JwtBuilder
- setHeader(Map<String, Object> header): JwtBuilder
- setHeaderParams(Map<String, Object> params): JwtBuilder
- setHeaderParam(String name, Object value): JwtBuilder
参数 Header 对象 可通过 Jwts.header(); 创建,它简单得就像一个 map (把它当做 map 使用即可)
Payload
至少设置一个 claims,否则在生成签名时会抛出异常
- setClaims(Claims claims): JwtBuilder
- setClaims(Map<String, Object> claims): JwtBuilder
- addClaims(Map<String, Object> claims): JwtBuilder
- setIssuer(String iss): JwtBuilder
- setSubject(String sub): JwtBuilder
- setAudience(String aud): JwtBuilder
- setExpiration(Date exp): JwtBuilder
- setNotBefore(Date nbf): JwtBuilder
- setIssuedAt(Date iat): JwtBuilder
- setId(String jti): JwtBuilder
- claim(String name, Object value: JwtBuilder
参数对象 Claims 同 Header 类似,通过 Jwts.claims() 创建,同样简单得就像一个 map
值得注意的一点是:不要在 setXxx 之后调用 setClaims(Claims claims) 或 setClaims(Map<String, Object> claims),因为这两个方法会覆盖所有已设置的 claim
Signature
- signWith(Key key)
- signWith(Key key, SignatureAlgorithm alg)
- signWith(SignatureAlgorithm alg, byte[] secretKeyBytes)
- signWith(SignatureAlgorithm alg, String base64EncodedSecretKey)
- signWith(SignatureAlgorithm alg, Key key)
以上方法最终就是设置两个对象:key 和 algorithm,分别代表密钥和算法
方法内部生成密钥使用的方法的和演示中的一样
1 | SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName()); |
注意:key 的长度必须符合签名算法的要求(避免生成弱密钥)
HS256:bit 长度要>=256,即字节长度>=32
HS384:bit 长度要>=384,即字节长度>=48
HS512:bit 长度要>=512,即字节长度>=64
在 secret key algorithms 名称中的数字代表了最小bit长度
更多签名算法的详情,请参考签名算法小节
封装 JWT 工具类
1 | package com.liuchuanv.jwt; |
2 | |
3 | import io.jsonwebtoken.*; |
4 | import io.jsonwebtoken.security.SignatureException; |
5 | |
6 | import javax.crypto.spec.SecretKeySpec; |
7 | import java.security.Key; |
8 | import java.util.Date; |
9 | import java.util.Map; |
10 | import java.util.UUID; |
11 | |
12 | /** |
13 | * JSON Web Token 工具类 |
14 | * |
15 | * @author LiuChuanWei |
16 | * @date 2019-12-11 |
17 | */ |
18 | public class JwtUtils { |
19 | |
20 | /** |
21 | * key(按照签名算法的字节长度设置key) |
22 | */ |
23 | private final static String SECRET_KEY = "0123456789_0123456789_0123456789"; |
24 | /** |
25 | * 过期时间(毫秒单位) |
26 | */ |
27 | private final static long TOKEN_EXPIRE_MILLIS = 1000 * 60 * 60; |
28 | |
29 | /** |
30 | * 创建token |
31 | * @param claimMap |
32 | * @return |
33 | */ |
34 | public static String createToken(Map<String, Object> claimMap) { |
35 | long currentTimeMillis = System.currentTimeMillis(); |
36 | return Jwts.builder() |
37 | .setId(UUID.randomUUID().toString()) |
38 | .setIssuedAt(new Date(currentTimeMillis)) // 设置签发时间 |
39 | .setExpiration(new Date(currentTimeMillis + TOKEN_EXPIRE_MILLIS)) // 设置过期时间 |
40 | .addClaims(claimMap) |
41 | .signWith(generateKey()) |
42 | .compact(); |
43 | } |
44 | |
45 | /** |
46 | * 验证token |
47 | * @param token |
48 | * @return 0 验证成功,1、2、3、4、5 验证失败 |
49 | */ |
50 | public static int verifyToken(String token) { |
51 | try { |
52 | Jwts.parser().setSigningKey(generateKey()).parseClaimsJws(token); |
53 | return 0; |
54 | } catch (ExpiredJwtException e) { |
55 | e.printStackTrace(); |
56 | return 1; |
57 | } catch (UnsupportedJwtException e) { |
58 | e.printStackTrace(); |
59 | return 2; |
60 | } catch (MalformedJwtException e) { |
61 | e.printStackTrace(); |
62 | return 3; |
63 | } catch (SignatureException e) { |
64 | e.printStackTrace(); |
65 | return 4; |
66 | } catch (IllegalArgumentException e) { |
67 | e.printStackTrace(); |
68 | return 5; |
69 | } |
70 | } |
71 | |
72 | /** |
73 | * 解析token |
74 | * @param token |
75 | * @return |
76 | */ |
77 | public static Map<String, Object> parseToken(String token) { |
78 | return Jwts.parser() // 得到DefaultJwtParser |
79 | .setSigningKey(generateKey()) // 设置签名密钥 |
80 | .parseClaimsJws(token) |
81 | .getBody(); |
82 | } |
83 | |
84 | /** |
85 | * 生成安全密钥 |
86 | * @return |
87 | */ |
88 | public static Key generateKey() { |
89 | return new SecretKeySpec(SECRET_KEY.getBytes(), SignatureAlgorithm.HS256.getJcaName()); |
90 | } |
91 | } |
测试代码如下:
1 | //Map<String, Object> map = new HashMap<String, Object>(); |
2 | //map.put("userId", 1002); |
3 | //map.put("userName", "张晓明"); |
4 | //map.put("age", 12); |
5 | //map.put("address", "山东省青岛市李沧区"); |
6 | //String token = JwtUtils.createToken(map); |
7 | //System.out.println(token); |
8 | |
9 | String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ZWM2NWNhNC0wZjVmLTRlOTktOTI5NS1mYWUyN2UwODIzYzQiLCJpYXQiOjE1NzY0OTI4NjYsImV4cCI6MTU3NjQ5NjQ2NiwiYWRkcmVzcyI6IuWxseS4nOecgemdkuWym-W4guadjuayp-WMuiIsInVzZXJOYW1lIjoi5byg5pmT5piOIiwidXNlcklkIjoxMDAyLCJhZ2UiOjEyfQ.6Z18aIA6y52ntQkV3BwlYiVK3hL3R2WFujjTmuvimww"; |
10 | int result = JwtUtils.verifyToken(token); |
11 | System.out.println(result); |
12 | |
13 | Map<String, Object> map = JwtUtils.parseToken(token); |
14 | System.out.println(map); |
输出结果:
1 | 0 |
2 | {jti=4ec65ca4-0f5f-4e99-9295-fae27e0823c4, iat=1576492866, exp=1576496466, address=山东省青岛市李沧区, userName=张晓明, userId=1002, age=12} |
签名算法
12 种签名算法
JWT 规范定义了12种标准签名算法:3种 secret key 算法和9种非对称密钥算法
HS256: HMAC using SHA-256HS384: HMAC using SHA-384HS512: HMAC using SHA-512ES256: ECDSA using P-256 and SHA-256ES384: ECDSA using P-384 and SHA-384ES512: ECDSA using P-521 and SHA-512RS256: RSASSA-PKCS-v1_5 using SHA-256RS384: RSASSA-PKCS-v1_5 using SHA-384RS512: RSASSA-PKCS-v1_5 using SHA-512PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512
根据算法名称可分为四类:HSxxx(secret key 算法)、ESxxx、RSxxx、PSxxx
HSxxx、ESxxx 中的 xxx 表示算法 key 最小 Bit 长度
RSxxx、PSxxx 中的 xxx 表示算法 key 最小 Byte 长度
规定key的最小长度是为了避免因 key 过短生成弱密钥
生成密钥
jjwt 生成 secret key 两种方法
1 | String key = "1234567890_1234567890_1234567890"; |
2 | // 1. 根据key生成密钥(会根据字节参数长度自动选择相应的 HMAC 算法) |
3 | SecretKey secretKey1 = Keys.hmacShaKeyFor(key.getBytes()); |
4 | // 2. 根据随机数生成密钥 |
5 | SecretKey secretKey2 = Keys.secretKeyFor(SignatureAlgorithm.HS256); |
方法 Keys.hmacShaKeyFor(byte[]) 内部也是 new SecretKeySpec(bytes, alg.getJcaName()) 来生成密钥的
方法 Keys.secretKeyFor(SignatureAlgorithm) 内部使用 KeyGenerator.generateKey() 生成密钥
jjwt 也提供了非对称密钥对的生成方法
1 | // 1. 使用jjwt提供的方法生成 |
2 | KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512 |
3 | |
4 | // 2. 手动生成 |
5 | int keySize = 1024; |
6 | // RSA算法要求有一个可信任的随机数源 |
7 | SecureRandom secureRandom = new SecureRandom(); |
8 | // 为RSA算法创建一个KeyPairGenerator对象 |
9 | KeyPairGenerator keyPairGenerator = null; |
10 | try { |
11 | keyPairGenerator = KeyPairGenerator.getInstance("RSA"); |
12 | } catch (NoSuchAlgorithmException e) { |
13 | e.printStackTrace(); |
14 | } |
15 | // 利用上面的随机数据源初始化这个KeyPairGenerator对象 |
16 | keyPairGenerator.initialize(keySize, secureRandom); |
17 | // 生成密钥对 |
18 | KeyPair keyPair2 = keyPairGenerator.generateKeyPair(); |
- Keys.keyPairFor(SignatureAlgorithm) 会根据算法自动生成相应长度的
- signWith(secretKey) 会根据密钥长度自动选择相应算法,也可以指定任意算法(指定的算法不受密钥长度限制,可任意选择,即用 RS256生成的密钥,可以 signWith(secretKey, SignatureAlgorithm.RS512),但是 JJWT 并不建议这么做)
- 在加密时使用 keyPair.getPrivate() ,解密时使用 keyPair.getPublic()
不同密钥生成token
以上都是使用同一密钥签名生成所有的token,下面我们使用不同的密钥
这一个特性可以应用于不同用户/角色使用不同的密钥生成的 token,帮助你更好的构建权限系统
首先在 Header(或 claims)中设置一个 keyId
定义一个类,继承 SigningKeyResolverAdapter,并重写 resolveSigningKey() 或 resolveSigningKeyBytes() 方法
1public class MySigningKeyResolver extends SigningKeyResolverAdapter {23public Key resolveSigningKey(JwsHeader header, Claims claims) {4// 除了从 header 中获取 keyId 外,也可以从 claims 中获取(前提是在 claims 中设置了 keyId 声明)5String keyId = header.getKeyId();6// 根据 keyId 查找相应的 key7Key key = lookupVerificationKey(keyId);8return key;9}1011public Key lookupVerificationKey(String keyId) {12// TODO 根据 keyId 获取 key,比如从数据库中获取13// 下面语句仅做演示用,绝对不可用于实际开发中!!!14String key = "qwertyuiopasdfghjklzxcvbnm2019_" + keyId;15return Keys.hmacShaKeyFor(key.getBytes());16}17}解析时,不再调用 setSigningKey(SecretKey) ,而是调用 setSigningKeyResolver(SigningKeyResolver)
1// 生成密钥2// TODO 此处 keyId 仅做演示用,实际开发中可以使用 UserId、RoleId 等作为 keyId3String keyId = new Long(System.currentTimeMillis()).toString();4System.out.println("keyId=" + keyId);56String key = "qwertyuiopasdfghjklzxcvbnm2019_" + keyId;7SecretKey secretKey = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS256.getJcaName());89// 1. 生成 token10String token = Jwts.builder()11.setHeaderParam(JwsHeader.KEY_ID, keyId) // 设置 keyId(当然也可以在 claims 中设置)12.setSubject("JSON Web Token")13.signWith(secretKey)14.compact();15System.out.println("token=" + token);1617// 2. 验证token18// token 使用了不同的密钥生成签名,在解析时就不用调用 setSigningKey(SecretKey) 了19// 而是调用 setSigningKeyResolver(SigningKeyResolver)20try {21Jwts.parser()22.setSigningKeyResolver(new MySigningKeyResolver())23.parseClaimsJws(token);24// OK, we can trust this token25System.out.println("token验证成功");26} catch (JwtException e) {27//don't trust the token!28System.out.println("token验证失败");29}
安全加密
敬请期待 …..