Json Web Token (JWT
) 近几年是先后端分离经常使用的 Token 技术,是目前最流行的跨域身份验证解决方案。你能够经过文章 一文了解web无状态会话token技术JWT 来了解 JWT
。今天咱们来手写一个通用的 JWT
服务。DEMO 获取方式在文末,实如今 jwt
相关包下java
spring-security-jwt
是 Spring Security Crypto 提供的 JWT
工具包 。web
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>${spring-security-jwt.version}</version> </dependency>
核心类只有一个: org.springframework.security.jwt.JwtHelper
。它提供了两个很是有用的静态方法。算法
JwtHelper
提供的第一个静态方法就是 encode(CharSequence content, Signer signer)
这个是用来生成jwt的方法 须要指定 payload 跟 signer 签名算法。payload 存放了一些可用的不敏感信息:spring
iss
jwt签发者sub
jwt所面向的用户aud
接收jwt的一方iat
jwt的签发时间exp
jwt的过时时间,这个过时时间必需要大于签发时间 iat
jti
jwt的惟一身份标识,主要用来做为一次性token,从而回避重放攻击除了以上提供的基本信息外,咱们能够定义一些咱们须要传递的信息,好比目标用户的权限集 等等。切记不要传递密码等敏感信息 ,由于 JWT
的前两段都是用了 BASE64
编码,几乎算是明文了。json
咱们先来构建 payload :后端
/** * 构建 jwt payload * * @author Felordcn * @since 11:27 2019/10/25 **/ public class JwtPayloadBuilder { private Map<String, String> payload = new HashMap<>(); /** * 附加的属性 */ private Map<String, String> additional; /** * jwt签发者 **/ private String iss; /** * jwt所面向的用户 **/ private String sub; /** * 接收jwt的一方 **/ private String aud; /** * jwt的过时时间,这个过时时间必需要大于签发时间 **/ private LocalDateTime exp; /** * jwt的签发时间 **/ private LocalDateTime iat = LocalDateTime.now(); /** * 权限集 */ private Set<String> roles = new HashSet<>(); /** * jwt的惟一身份标识,主要用来做为一次性token,从而回避重放攻击 **/ 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)); } }
经过建造类 JwtClaimsBuilder
咱们能够很方便来构建 JWT
所须要的 payload json
字符串传递给 encode(CharSequence content, Signer signer)
中的 content
。跨域
为了生成 JWT Token 咱们还须要使用 RSA 算法来进行签名。 这里咱们使用 JDK 提供的证书管理工具 Keytool 来生成 RSA 证书 ,格式为 jks
格式。缓存
生成证书命令参考:springboot
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
咱们要做为配置使用要记下来。咱们要使用下面定义的这个类来读取证书app
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; /** * KeyPairFactory * * @author Felordcn * @since 13:41 2019/10/25 **/ class KeyPairFactory { private KeyStore store; private final Object lock = new Object(); /** * 获取公私钥. * * @param keyPath jks 文件在 resources 下的classpath * @param keyAlias keytool 生成的 -alias 值 felordcn * @param keyPass keytool 生成的 -storepass 值 felordcn * @return the key pair 公私钥对 */ 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 的方法:
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对 的方法以下:
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
的过时时间为准。
JwtHelper
提供的第二个静态方法是Jwt decodeAndVerify(String token, SignatureVerifier verifier)
用来 验证和解码 Jwt Token 。咱们获取到请求中的token后会解析出用户的一些信息。经过这些信息去缓存中对应的token ,而后比对并验证是否有效(包括是否过时)。
/** * 解码 并校验签名 过时不予解析 * * @param jwtToken the jwt token * @return the jwt claims */ 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
以下:
/** * Jwt 在 springboot application.yml 中的配置文件 * * @author Felordcn * @since 15 :06 2019/10/25 */ @Data @ConfigurationProperties(prefix=JWT_PREFIX) public class JwtProperties { static final String JWT_PREFIX= "jwt.config"; /** * 是否可用 */ private boolean enabled; /** * jks 路径 */ private String keyLocation; /** * key alias */ private String keyAlias; /** * key store pass */ private String keyPass; /** * jwt签发者 **/ private String iss; /** * jwt所面向的用户 **/ private String sub; /** * access jwt token 有效天数 */ private int accessExpDays; /** * refresh jwt token 有效天数 */ private int refreshExpDays; }
而后咱们就能够配置 JWT 的 javaConfig
以下:
/** * JwtConfiguration * * @author Felordcn * @since 16 :54 2019/10/25 */ @EnableConfigurationProperties(JwtProperties.class) @ConditionalOnProperty(prefix = "jwt.config",name = "enabled") @Configuration public class JwtConfiguration { /** * Jwt token storage . * * @return the jwt token storage */ @Bean public JwtTokenStorage jwtTokenStorage() { return new JwtTokenCacheStorage(); } /** * Jwt token generator. * * @param jwtTokenStorage the jwt token storage * @param jwtProperties the jwt properties * @return the jwt token generator */ @Bean public JwtTokenGenerator jwtTokenGenerator(JwtTokenStorage jwtTokenStorage, JwtProperties jwtProperties) { return new JwtTokenGenerator(jwtTokenStorage, jwtProperties); } }
而后你就能够经过 JwtTokenGenerator
编码/解码验证 Jwt Token 对 ,经过 JwtTokenStorage
来处理 Jwt Token 缓存。缓存这里我用了Spring Cache Ehcache 来实现,你也能够切换到 Redis 。相关单元测试参见 DEMO
今天咱们利用 spring-security-jwt
手写了一套 JWT 逻辑。不管对你后续结合 Spring Security 仍是 Shiro 都十分有借鉴意义。下一篇咱们会讲解 JWT 结合Spring Security ,敬请关注公众号:Felordcn 来及时获取资料。
本次的 DEMO 也可经过关注公众号回复 day05 获取。
关注公众号:Felordcn获取更多资讯