JWT Token實現方法及步驟詳解
1. 前言
Json Web Token (JWT) 近幾年是前後端分離常用的 Token 技術,是目前最流行的跨域身份驗證解決方案。你可以通過文章 一文了解web無狀態會話token技術JWT 來了解 JWT。今天我們來手寫一個通用的 JWT 服務。DEMO 獲取方式在文末,實現在 jwt 相關包下
2. spring-security-jwt
spring-security-jwt 是 Spring Security Crypto 提供的 JWT 工具包 。
<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 :
/** * 構建 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 。
3.2 生成 RSA 金鑰並進行簽名
為了生成 JWT Token 我們還需要使用 RSA 演算法來進行簽名。這裡我們使用 JDK 提供的證書管理工具 Keytool 來生成 RSA 證書 ,格式為 jks 格式。
生成證書命令參考:
```shell script 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` 我們要作為配置使用要記下來。我們要使用下面定義的這個類來讀取證書
```java 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 生成的 -keypass 值 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,String> additional) { String accessToken = jwtToken(aud,jwtProperties.getAccessExpDays(),roles,additional); String refreshToken = jwtToken(aud,jwtProperties.getRefreshExpDays(),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 ,然後比對並驗證是否有效(包括是否過期)。
/** * 解碼 並校驗簽名 過期不予解析 * * @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
6. 總結
今天我們利用 spring-security-jwt 手寫了一套 JWT 邏輯。無論對你後續結合 Spring Security 還是 Shiro 都十分有借鑑意義。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。