JWT Token實(shí)現(xiàn)方法及步驟詳解
1. 前言
Json Web Token (JWT) 近幾年是前后端分離常用的 Token 技術(shù),是目前最流行的跨域身份驗(yàn)證解決方案。你可以通過文章 一文了解web無狀態(tài)會話token技術(shù)JWT 來了解 JWT。今天我們來手寫一個(gè)通用的 JWT 服務(wù)。DEMO 獲取方式在文末,實(shí)現(xiàn)在 jwt 相關(guān)包下
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>
核心類只有一個(gè): org.springframework.security.jwt.JwtHelper 。它提供了兩個(gè)非常有用的靜態(tài)方法。
3. JWT 編碼
JwtHelper 提供的第一個(gè)靜態(tài)方法就是 encode(CharSequence content, Signer signer) 這個(gè)是用來生成jwt的方法 需要指定 payload 跟 signer 簽名算法。payload 存放了一些可用的不敏感信息:
- iss jwt簽發(fā)者
- sub jwt所面向的用戶
- aud 接收jwt的一方
- iat jwt的簽發(fā)時(shí)間
- exp jwt的過期時(shí)間,這個(gè)過期時(shí)間必須要大于簽發(fā)時(shí)間 iat
- jti jwt的唯一身份標(biāo)識,主要用來作為一次性token,從而回避重放***
除了以上提供的基本信息外,我們可以定義一些我們需要傳遞的信息,比如目標(biāo)用戶的權(quán)限集 等等。切記不要傳遞密碼等敏感信息 ,因?yàn)?JWT 的前兩段都是用了 BASE64 編碼,幾乎算是明文了。
3.1 構(gòu)建 JWT 中的 payload
我們先來構(gòu)建 payload :
/**
* 構(gòu)建 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簽發(fā)者
**/
private String iss;
/**
* jwt所面向的用戶
**/
private String sub;
/**
* 接收jwt的一方
**/
private String aud;
/**
* jwt的過期時(shí)間,這個(gè)過期時(shí)間必須要大于簽發(fā)時(shí)間
**/
private LocalDateTime exp;
/**
* jwt的簽發(fā)時(shí)間
**/
private LocalDateTime iat = LocalDateTime.now();
/**
* 權(quán)限集
*/
private Set<String> roles = new HashSet<>();
/**
* jwt的唯一身份標(biāo)識,主要用來作為一次性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 我們可以很方便來構(gòu)建 JWT 所需要的 payload json 字符串傳遞給 encode(CharSequence content, Signer signer) 中的 content 。
3.2 生成 RSA 密鑰并進(jìn)行簽名
為了生成 JWT Token 我們還需要使用 RSA 算法來進(jìn)行簽名。這里我們使用 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` 我們要作為配置使用要記下來。我們要使用下面定義的這個(gè)類來讀取證書
```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 的兩個(gè)要素就完成了。我們可以和之前定義的 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 都是成對出現(xiàn)的,一個(gè)為平常請求攜帶的 accessToken, 另一個(gè)只作為刷新 accessToken 之用的 refreshToken 。而且 refreshToken 的過期時(shí)間要相對長一些。當(dāng) accessToken 失效而refreshToken 有效時(shí),我們可以通過 refreshToken 來獲取新的 Jwt Token對 ;當(dāng)兩個(gè)都失效就用戶就必須重新登錄了。
生成 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對 會在返回給前臺的同時(shí)放入緩存中。過期策略你可以選擇分開處理,也可以選擇以refreshToken 的過期時(shí)間為準(zhǔn)。
4. JWT 解碼以及驗(yàn)證
JwtHelper 提供的第二個(gè)靜態(tài)方法是Jwt decodeAndVerify(String token, SignatureVerifier verifier) 用來 驗(yàn)證和解碼 Jwt Token 。我們獲取到請求中的token后會解析出用戶的一些信息。通過這些信息去緩存中對應(yīng)的token ,然后比對并驗(yàn)證是否有效(包括是否過期)。
/**
* 解碼 并校驗(yàn)簽名 過期不予解析
*
* @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對象 ,方便后續(xù)的操作。
5. 配置
我們將 JWT 的可配置項(xiàng)抽出來放入 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簽發(fā)者
**/
private String iss;
/**
* jwt所面向的用戶
**/
private String sub;
/**
* access jwt token 有效天數(shù)
*/
private int accessExpDays;
/**
* refresh jwt token 有效天數(shù)
*/
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 編碼/解碼驗(yàn)證 Jwt Token 對 ,通過 JwtTokenStorage 來處理 Jwt Token 緩存。緩存這里我用了Spring Cache Ehcache 來實(shí)現(xiàn),你也可以切換到 Redis 。相關(guān)單元測試參見 DEMO
6. 總結(jié)
今天我們利用 spring-security-jwt 手寫了一套 JWT 邏輯。無論對你后續(xù)結(jié)合 Spring Security 還是 Shiro 都十分有借鑒意義。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java Set集合及其子類HashSet與LinkedHashSet詳解
這篇文章主要介紹了Java Set集合及其子類HashSet與LinkedHashSet詳解,文章通過Set集合存儲原理展開文章主題相關(guān)介紹,感興趣的小伙伴可以參考一下2022-06-06
java學(xué)生管理系統(tǒng)界面簡單實(shí)現(xiàn)(全)
這篇文章主要為大家詳細(xì)介紹了java學(xué)生管理系統(tǒng)界面的簡單實(shí)現(xiàn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01
SpringBoot整合JWT實(shí)戰(zhàn)教程
JWT(JSON?Web?Token)是一種用于身份驗(yàn)證和授權(quán)的開放標(biāo)準(zhǔn)(RFC?7519),它使用JSON格式傳輸信息,可以在不同系統(tǒng)之間安全地傳遞數(shù)據(jù),這篇文章主要介紹了SpringBoot整合JWT實(shí)戰(zhàn)教程,需要的朋友可以參考下2023-06-06
如何解決executors線程池創(chuàng)建的線程不釋放的問題
這篇文章主要介紹了如何解決executors線程池創(chuàng)建的線程不釋放的問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
SpringBoot實(shí)現(xiàn)發(fā)送電子郵件
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)發(fā)送電子郵件,電子郵件是—種用電子手段提供信息交換的通信方式,是互聯(lián)網(wǎng)應(yīng)用最廣的服務(wù)。通過網(wǎng)絡(luò)的電子郵件系統(tǒng),用戶可以非??焖俚姆绞?,與世界上任何一個(gè)角落的網(wǎng)絡(luò)用戶聯(lián)系,下面就來看看SpringBoot如何實(shí)現(xiàn)發(fā)送電子郵件吧2022-01-01

