1. 前言
Json Web Token (JWT
) 近几年是前后端分离常用的 Token 技术,是目前最流行的跨域身份验证解决方案。今天我们来手写一个通用的 JWT
服务。DEMO 获取方式在文末,实现在 jwt 相关包下
2. spring-security-jwt
spring-security_jwt
是Spring Security Crypto提供的JWT
工具包
1 2 3 4 5
| <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>${spring-security-jwt.version}</version> </dependency>
|
核心类只有一个:org.springframework.security.jwt.JwtHelper
。它提供了两个非常有用的静态方法。
3. JWT编码
JwtHelper
提供的第一个静态方法就是 encode(CharSequence content, Signer signer)
这个是用来生成jwt的方法 需要指定 payload 跟 signer 签名算法。payload 存放了一些可用的不敏感信息:
iss
jwt签发者
sub
jwt所面向的用户
aud
接收jwt的一方
iat
jwt的签发时间
exp
jwt的过期时间,这个过期时间必须要大于签发时间iat
jti
jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
除了以上提供的基本信息外,我们可以定义一些我们需要传递的信息,比如目标用户的权限集 等等。切记不要传递密码等敏感信息 ,因为 JWT
的前两段都是用了 BASE64
编码,几乎算是明文了。
3.1 构建JWT中的payload
我们先来构建payload:
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
|
public class JwtPayloadBuilder {
private Map<String, String> payload = new HashMap<>();
private Map<String, String> additional;
private String iss;
private String sub;
private String aud;
private LocalDateTime exp;
private LocalDateTime iat = LocalDateTime.now();
private Set<String> roles = new HashSet<>();
private String jti = IdUtil.simpleUUID();
public JwtPayloadBuilder iss(String iss) { this.iss = iss; return this; }
public JwtPayloadBuilder sub(String sub) { this.sub = sub; return this; }
public JwtPayloadBuilder aud(String aud) { this.aud = aud; return this; }
public JwtPayloadBuilder roles(Set<String> roles) { this.roles = roles; return this; }
public JwtPayloadBuilder expDays(int days) { Assert.isTrue(days > 0, "jwt expireDate must after now"); this.exp = this.iat.plusDays(days); return this; }
public JwtPayloadBuilder additional(Map<String, String> additional) { this.additional = additional; return this; }
public String builder() { payload.put("iss", this.iss); payload.put("sub", this.sub); payload.put("aud", this.aud); payload.put("exp", this.exp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); payload.put("iat", this.iat.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); payload.put("jti", this.jti);
if (!CollectionUtils.isEmpty(additional)) { payload.putAll(additional); } payload.put("roles", JSONUtil.toJsonStr(this.roles)); return JSONUtil.toJsonStr(JSONUtil.parse(payload));
}
}
|
通过建造类 JwtPayloadBuilder
我们可以很方便来构建 JWT
所需要的 payload json
字符串传递给 encode(CharSequence content, Signer signer)
中的 content
。
3.2 生成 RSA 密钥并进行签名
为了生成 JWT Token 我们还需要使用 RSA 算法来进行签名。 这里我们使用 JDK 提供的证书管理工具 Keytool 来生成 RSA 证书 ,格式为 jks
格式。
生成证书命令参考:
1
| keytool -genkey -alias felordcn -keypass felordcn -keyalg RSA -storetype PKCS12 -keysize 1024 -validity 365 -keystore d:/keystores/felordcn.jks -storepass 123456 -dname "CN=(Felord), OU=(felordcn), O=(felordcn), L=(zz), ST=(hn), C=(cn)"
|
其中 -alias felordcn -storepass 123456
我们要作为配置使用要记下来。我们要使用下面定义的这个类来读取证书
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 44 45 46 47 48 49 50 51 52 53
| package cn.felord.spring.security.jwt;
import org.springframework.core.io.ClassPathResource;
import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyStore; import java.security.PublicKey; import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.RSAPublicKeySpec;
class KeyPairFactory {
private KeyStore store;
private final Object lock = new Object();
KeyPair create(String keyPath, String keyAlias, String keyPass) { ClassPathResource resource = new ClassPathResource(keyPath); char[] pem = keyPass.toCharArray(); try { synchronized (lock) { if (store == null) { synchronized (lock) { store = KeyStore.getInstance("jks"); store.load(resource.getInputStream(), pem); } } } RSAPrivateCrtKey key = (RSAPrivateCrtKey) store.getKey(keyAlias, pem); RSAPublicKeySpec spec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent()); PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(spec); return new KeyPair(publicKey, key); } catch (Exception e) { throw new IllegalStateException("Cannot load keys from store: " + resource, e); }
} }
|
获取了 KeyPair
就能获取公私钥 生成 Jwt
的两个要素就完成了。我们可以和之前定义的 JwtPayloadBuilder
一起封装出生成 Jwt Token 的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private String jwtToken(String aud, int exp, Set<String> roles, Map<String, String> additional) { String payload = jwtPayloadBuilder .iss(jwtProperties.getIss()) .sub(jwtProperties.getSub()) .aud(aud) .additional(additional) .roles(roles) .expDays(exp) .builder(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RsaSigner signer = new RsaSigner(privateKey); return JwtHelper.encode(payload, signer).getEncoded(); }
|
通常情况下 Jwt Token 都是成对出现的,一个为平常请求携带的 accessToken
, 另一个只作为刷新 accessToken
之用的 refreshToken
。而且 refreshToken
的过期时间要相对长一些。当 accessToken
失效而refreshToken
有效时,我们可以通过 refreshToken
来获取新的 Jwt Token对 ;当两个都失效就用户就必须重新登录了。
生成 Jwt Token对 的方法如下:
1 2 3 4 5 6 7 8 9 10 11
| public JwtTokenPair jwtTokenPair(String aud, Set<String> roles, Map<String, String> additional) { String accessToken = jwtToken(aud, jwtProperties.getAccessExpDays(), roles, additional); String refreshToken = jwtToken(aud, jwtProperties.getRefreshExpDays(), roles, additional);
JwtTokenPair jwtTokenPair = new JwtTokenPair(); jwtTokenPair.setAccessToken(accessToken); jwtTokenPair.setRefreshToken(refreshToken); jwtTokenStorage.put(jwtTokenPair, aud); return jwtTokenPair; }
|
通常 Jwt Token对 会在返回给前台的同时放入缓存中。过期策略你可以选择分开处理,也可以选择以refreshToken
的过期时间为准。
4. JWT解码以及验证
JwtHelper
提供的第二个静态方法是Jwt decodeAndVerify(String token, SignatureVerifier verifier)
用来 验证和解码 Jwt Token 。我们获取到请求中的token后会解析出用户的一些信息。通过这些信息去缓存中对应的token ,然后比对并验证是否有效(包括是否过期)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public JSONObject decodeAndVerify(String jwtToken) { Assert.hasText(jwtToken, "jwt token must not be bank"); RSAPublicKey rsaPublicKey = (RSAPublicKey) this.keyPair.getPublic(); SignatureVerifier rsaVerifier = new RsaVerifier(rsaPublicKey); Jwt jwt = JwtHelper.decodeAndVerify(jwtToken, rsaVerifier); String claims = jwt.getClaims(); JSONObject jsonObject = JSONUtil.parseObj(claims); String exp = jsonObject.getStr(JWT_EXP_KEY);
if (isExpired(exp)) { throw new IllegalStateException("jwt token is expired"); } return jsonObject; }
|
上面我们将有效的 Jwt Token 中的 payload
解析为 JSON对象 ,方便后续的操作。
5. 配置
我们将 JWT 的可配置项抽出来放入 JwtProperties
如下:
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
|
@Data @ConfigurationProperties(prefix=JWT_PREFIX) public class JwtProperties { static final String JWT_PREFIX= "jwt.config";
private boolean enabled;
private String keyLocation;
private String keyAlias;
private String keyPass;
private String iss;
private String sub;
private int accessExpDays;
private int refreshExpDays; }
|
然后我们就可以配置JWT的javaConfig
如下:
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
|
@EnableConfigurationProperties(JwtProperties.class) @ConditionalOnProperty(prefix = "jwt.config",name = "enabled") @Configuration public class JwtConfiguration {
@Bean public JwtTokenStorage jwtTokenStorage() { return new JwtTokenCacheStorage(); }
@Bean public JwtTokenGenerator jwtTokenGenerator(JwtTokenStorage jwtTokenStorage, JwtProperties jwtProperties) { return new JwtTokenGenerator(jwtTokenStorage, jwtProperties); }
}
|
然后你就可以通过 JwtTokenGenerator
编码/解码验证 Jwt Token 对 ,通过 JwtTokenStorage
来处理 Jwt Token 缓存。缓存这里我用了Spring Cache Ehcache 来实现,你也可以切换到 Redis 。相关单元测试参见 DEMO
6. 总结
今天我们利用 spring-security-jwt
手写了一套 JWT 逻辑。无论对你后续结合 Spring Security 还是 Shiro 都十分有借鉴意义。下一篇我们会讲解 JWT 结合Spring Security。
本节代码分支在day05
转载自@felord.cn