JWT (JSON Web Token) 指的是一种规范,这种规范允许我们使用 JWT 在两个组织之间传递安全可靠的信息。
JWS (JSON Web Signature) 和 JWE (JSON Web Encryption) 是 JWT 规范的两种不同实现,我们平时最常使用的实现就是 JWS 。
nimbus-jose-jwt
推荐 nimbus-jose-jwt 而非 java-jwt 及 jjwt。
1
2
3
4
5
6
|
<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.25.6</version>
</dependency>
|
加密算法
-
对称加密 指的是使用相同的秘钥来进行加密和解密,如果你的秘钥不想暴露给解密方,考虑使用非对称加密。在加密方和解密方是同一个人(或利益关系紧密)的情况下可以使用它。
-
非对称加密 指的是使用公钥和私钥来进行加密解密操作。对于加密操作,公钥负责加密,私钥负责解密,对于签名操作,私钥负责签名,公钥负责验证。非对称加密在 JWT 中的使用显然属于签名操作。在加密方和解密方是不同人(或不同利益方)的情况下可以使用它。
1
|
JWSAlgorithm algorithm = JWSAlgorithm.HS256
|
核心 API 介绍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
JWSHeader jwsHeader =
new JWSHeader.Builder(algorithm) // 加密算法
.type(JOSEObjectType.JWT) // 静态常量
.build();
// 获取头部信息的 Base64 形式
jwsHeader.getParsedBase64URL();
Payload payload = new Payload("hello world"); // 这里还可以传 JSON 串,或 Map 。
payload.toBase64URL();
// 签名(sign)
JWSSigner jwsSigner = new MACSigner(secret);
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 进行签名(根据前两部分生成第三部分)
jwsObject.sign(jwsSigner);
String token = jwsObject.serialize();
|
1
2
3
4
5
|
JWSVerifier jwsVerifier = new MACVerifier(secret);
JWSObject jwsObject = JWSObject.parse(token);
if (!jwsObject.verify(jwsVerifier)) {
throw new RuntimeException("token 签名不合法!");
}
|
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
|
@Data
public class Claims {
// "主题"
private String sub;
// "签发时间"
private Long iat;
// 过期时间
private Long exp;
// JWT的ID
private String jti;
// "用户名称"
private String username;
// "用户拥有的权限"
//private List<String> authorities;
}
ObjectMapper mapper = new ObjectMapper(); // 这里使用的是 Jackson 库
// 将负载信息封装到Payload中
Payload payload = new Payload(mapper.writeValueAsString(claims));
|
非对称加密(RSA)
要使用 RSA ,我们需要生成一个『证书文件』,这里将使用 Java 自带的 keytool 工具来生成 jks 证书文件,该工具在 JDK 的 bin 目录下。
1
|
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
|
将证书文件 jwt.jks 复制到项目的 resource 目录下,然后需要从证书文件中读取 RSAKey
1
2
3
4
5
6
|
<!-- Spring Security RSA 含有相关工具类 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-rsa</artifactId>
<!-- spring-cloud-commons-dependencies 已含有版本信息 -->
</dependency>
|
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
|
public RSAKey generateRsaKey() {
// 从 classpath 下获取 RSA 秘钥对
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
KeyPair keyPair = keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
// 获取 RSA 公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 获取 RSA 私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
return rsaKey;
}
RSAKey rsaKey = generateRsaKey();
// JWS 头
JWSHeader jwsHeader = new JWSHeader
.Builder(JWSAlgorithm.RS256) // 指定 RSA 算法
.type(JOSEObjectType.JWT)
.build();
// JWS 荷载
Payload payload = new Payload("hello world");
// JWS 签名
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
JWSSigner jwsSigner = new RSASSASigner(rsaKey, true); // rsaKey 生成签名器
jwsObject.sign(jwsSigner);
// JWT/JWS 字符串
String jwt = jwsObject.serialize();
System.out.println(jwt);
|
解密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// JWT/JWS 字符串转 JWSObject 对象
String token = "...";
JWSObject jwsObject = JWSObject.parse(token);
// 根据公要生成验证器
RSAKey rsaKey = generateRsaKey();
RSAKey publicRsaKey = rsaKey.toPublicJWK();
System.out.println(publicRsaKey); // show 公钥
JWSVerifier jwsVerifier = new RSASSAVerifier(publicRsaKey);
// 使用校验器校验 JWSObject 对象的合法性
if (!jwsObject.verify(jwsVerifier)) {
throw new RuntimeException("token签名不合法!");
}
// 拆解 JWT/JWS,获得荷载中的内容
String payload = jwsObject.getPayload().toString();
System.out.println(payload); // show 荷载
|
HMAC 保护的 JWT
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
32
33
34
35
36
37
38
39
40
41
42
43
|
import java.security.SecureRandom;
import java.util.Date;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;
// Generate random 256-bit (32-byte) shared secret
SecureRandom random = new SecureRandom();
byte[] sharedSecret = new byte[32];
random.nextBytes(sharedSecret);
// Create HMAC signer
JWSSigner signer = new MACSigner(sharedSecret);
// Prepare JWT with claims set
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("ynthm")
.issuer("https://ynthm.com")
.expirationTime(new Date(new Date().getTime() + 60 * 1000))
.build();
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
// Apply the HMAC protection
signedJWT.sign(signer);
// Serialize to compact form, produces something like
// eyJhbGciOiJIUzI1NiJ9.SGVsbG8sIHdvcmxkIQ.onO9Ihudz3WkiauDO2Uhyuz0Y18UASXlSc1eS0NkWyA
String s = signedJWT.serialize();
// On the consumer side, parse the JWS and verify its HMAC
signedJWT = SignedJWT.parse(s);
JWSVerifier verifier = new MACVerifier(sharedSecret);
assertTrue(signedJWT.verify(verifier));
// Retrieve / verify the JWT claims according to the app requirements
assertEquals("ynthm", signedJWT.getJWTClaimsSet().getSubject());
assertEquals("https://ynthm.com", signedJWT.getJWTClaimsSet().getIssuer());
assertTrue(new Date().before(signedJWT.getJWTClaimsSet().getExpirationTime()));
|
认证流程
要实现认证功能,很容易就会想到JWT或者session。基于session和基于JWT的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而JWT是保存在客户端的。
基于session的认证流程
- 用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个session并保存到数据库
- 服务器为用户生成一个sessionId,并将具有sesssionId的cookie放置在用户浏览器中,在后续的请求中都将带有这个cookie信息进行访问
- 服务器获取cookie,通过获取cookie中的sessionId查找数据库判断当前请求是否有效
基于JWT的认证流程
- 用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个token并保存到数据库
- 前端获取到token,存储到cookie或者local storage中,在后续的请求中都将带有这个token信息进行访问
- 服务器获取token值,通过查找数据库判断当前token是否有效
优缺点
JWT保存在客户端,在分布式环境下不需要做额外工作。而session因为保存在服务端,分布式环境下需要实现多机数据共享 session一般需要结合Cookie实现认证,所以需要浏览器支持cookie,因此移动端无法使用session认证方案
安全性
JWT的payload使用的是base64编码的,因此在JWT中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全
token 相关
- 生成的 token 中带有过期时间(续期使用),token 失效由 redis 进行管理
- Claim 中不包含敏感信息
- 更新密码,删除缓存,生成新的token并返回(防止他人旧密码登录后继续使用)。
附录