目录

JWT java 实践

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并返回(防止他人旧密码登录后继续使用)。

附录