Spring Boot 集成 JWT 實(shí)現(xiàn)用戶登錄認(rèn)證
JWT 簡介
什么是 JWT
JWT 是 JSON Web Token 的縮寫,是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于 JSON 的開放標(biāo)準(zhǔn)((RFC 7519)。定義了一種簡潔的,自包含的方法用于通信雙方之間以 JSON 對象的形式安全的傳遞信息。因?yàn)閿?shù)字簽名的存在,這些信息是可信的,JWT 可以使用 HMAC 算法或者是 RSA 的公私秘鑰對進(jìn)行簽名。
JWT請求流程

用戶使用賬號(hào)和密碼發(fā)起 POST 請求; 服務(wù)器使用私鑰創(chuàng)建一個(gè) JWT; 服務(wù)器返回這個(gè) JWT 給瀏覽器; 瀏覽器將該 JWT 串在請求頭中像服務(wù)器發(fā)送請求; 服務(wù)器驗(yàn)證該 JWT; 返回響應(yīng)的資源給瀏覽器。
JWT 的主要應(yīng)用場景
身份認(rèn)證在這種場景下,一旦用戶完成了登錄,在接下來的每個(gè)請求中包含 JWT,可以用來驗(yàn)證用戶身份以及對路由,服務(wù)和資源的訪問權(quán)限進(jìn)行驗(yàn)證。由于它的開銷非常小,可以輕松的在不同域名的系統(tǒng)中傳遞,所有目前在單點(diǎn)登錄(SSO)中比較廣泛的使用了該技術(shù)。 信息交換在通信的雙方之間使用 JWT 對數(shù)據(jù)進(jìn)行編碼是一種非常安全的方式,由于它的信息是經(jīng)過簽名的,可以確保發(fā)送者發(fā)送的信息是沒有經(jīng)過偽造的。
JWT 數(shù)據(jù)結(jié)構(gòu)
JWT 是由三段信息構(gòu)成的,將這三段信息文本用 . 連接一起就構(gòu)成了 JWT 字符串。
JWT 的三個(gè)部分依次為頭部:Header,負(fù)載:Payload 和簽名:Signature。

Header
Header 部分是一個(gè) JSON 對象,描述 JWT 的元數(shù)據(jù),通常是下面的樣子。
{
??"alg":?"HS256",
??"typ":?"JWT"
}
上面代碼中,alg 屬性表示簽名的算法(algorithm),默認(rèn)是 HMAC SHA256(寫成 HS256);typ 屬性表示這個(gè)令牌(token)的類型(type),JWT 令牌統(tǒng)一寫為 JWT。
最后,將上面的 JSON 對象使用 Base64URL 算法轉(zhuǎn)成字符串。
Payload
Payload 部分也是一個(gè) JSON 對象,用來存放實(shí)際需要傳遞的有效信息。有效信息包含三個(gè)部分:
標(biāo)準(zhǔn)中注冊的聲明 公共的聲明 私有的聲明
標(biāo)準(zhǔn)中注冊的聲明 (建議但不強(qiáng)制使用) :
iss (issuer):簽發(fā)人 exp (expiration time):過期時(shí)間,必須要大于簽發(fā)時(shí)間 sub (subject):主題 aud (audience):受眾 nbf (Not Before):生效時(shí)間 iat (Issued At):簽發(fā)時(shí)間 jti (JWT ID):編號(hào),JWT 的唯一身份標(biāo)識(shí),主要用來作為一次性 token,從而回避重放攻擊。
公共的聲明 :
公共的聲明可以添加任何的信息,一般添加用戶的相關(guān)信息或其他業(yè)務(wù)需要的必要信息。但不建議添加敏感信息,因?yàn)樵摬糠衷诳蛻舳丝山饷堋?/p>
私有的聲明 :
私有聲明是提供者和消費(fèi)者所共同定義的聲明,一般不建議存放敏感信息,因?yàn)?base64 是對稱解碼的,意味著該部分信息可以歸類為明文信息。
這個(gè) JSON 對象也要使用 Base64URL 算法轉(zhuǎn)成字符串。
Signature
Signature 部分是對前兩部分的簽名,防止數(shù)據(jù)篡改。
首先,需要指定一個(gè)密鑰(secret)。這個(gè)密鑰只有服務(wù)器才知道,不能泄露給用戶。然后,使用 Header 里面指定的簽名算法(默認(rèn)是 HMAC SHA256),按照下面的公式產(chǎn)生簽名。
HMACSHA256(base64UrlEncode(header)?+?"."?+?base64UrlEncode(payload),?secret)
算出簽名以后,把 Header、Payload、Signature 三個(gè)部分拼成一個(gè)字符串,每個(gè)部分之間用"點(diǎn)"(.)分隔,就可以返回給用戶。
Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。這個(gè)算法跟 Base64 算法基本類似,但有一些小的不同。
JWT 作為一個(gè)令牌(token),有些場合可能會(huì)放到 URL(比如 api.example.com/?token=xxx)。Base64 有三個(gè)字符 +、 / 和 =,在 URL 里面有特殊含義,所以要被替換掉:= 被省略、+ 替換成 -,/ 替換成 _ 。這就是 Base64URL 算法。
JWT 的使用方式
客戶端收到服務(wù)器返回的 JWT 之后需要在本地做保存。此后,客戶端每次與服務(wù)器通信,都要帶上這個(gè) JWT。一般的的做法是放在 HTTP 請求的頭信息 Authorization 字段里面。
Authorization:?Bearer?
這樣每個(gè)請求中,服務(wù)端就可以在請求頭中拿到 JWT ?進(jìn)行解析與認(rèn)證。
JWT 的特性
JWT 默認(rèn)是不加密,但也是可以加密的。生成原始 Token 以后,可以用密鑰再加密一次。
JWT 不加密的情況下,不能將秘密數(shù)據(jù)寫入 JWT。
JWT 不僅可以用于認(rèn)證,也可以用于交換信息。有效使用 JWT,可以降低服務(wù)器查詢數(shù)據(jù)庫的次數(shù)。
JWT 的最大缺點(diǎn)是,由于服務(wù)器不保存 session 狀態(tài),因此無法在使用過程中廢止某個(gè) token,或者更改 token 的權(quán)限。也就是說,一旦 JWT 簽發(fā)了,在到期之前就會(huì)始終有效,除非服務(wù)器部署額外的邏輯。
JWT 本身包含了認(rèn)證信息,一旦泄露,任何人都可以獲得該令牌的所有權(quán)限。為了減少盜用,JWT 的有效期應(yīng)該設(shè)置得比較短。對于一些比較重要的權(quán)限,使用時(shí)應(yīng)該再次對用戶進(jìn)行認(rèn)證。
為了減少盜用,JWT 不應(yīng)該使用 HTTP 協(xié)議明碼傳輸,要使用 HTTPS 協(xié)議傳輸。
基于 nimbus-jose-jwt 簡單封裝
nimbus-jose-jwt 是最受歡迎的 JWT 開源庫,基于Apache 2.0開源協(xié)議,支持所有標(biāo)準(zhǔn)的簽名(JWS)和加密(JWE)算法。nimbus-jose-jwt 支持使用對稱加密(HMAC)和非對稱加密(RSA)兩種算法來生成和解析 JWT 令牌。
下面我們對 nimbus-jose-jwt 進(jìn)行簡單的封裝,提供以下功能的支持:
支持使用 HMAC 和 RSA 算法生成和解析 JWT 令牌 支持私有信息直接作為 Payload,以及標(biāo)準(zhǔn)信息+私有信息作為 Payload。內(nèi)置支持后者。 提供工具類及可擴(kuò)展接口,方便自定義擴(kuò)展開發(fā)。
pom 中添加依賴
首先我們在 pom.xml 中引入 nimbus-jose-jwt 的依賴。
<dependency>
??<groupId>com.nimbusdsgroupId>
??<artifactId>nimbus-jose-jwtartifactId>
??<version>8.20version>
dependency>
JwtConfig
這個(gè)類用于統(tǒng)一管理相關(guān)的參數(shù)配置。
public?class?JwtConfig?{
????//?JWT?在?HTTP?HEADER?中默認(rèn)的?KEY
????private?String?tokenName?=?JwtUtils.DEFAULT_TOKEN_NAME;
????//?HMAC?密鑰,用于支持?HMAC?算法
????private?String?hmacKey;
????//?JKS?密鑰路徑,用于支持?RSA?算法
????private?String?jksFileName;
????//?JKS?密鑰密碼,用于支持?RSA?算法
????private?String?jksPassword;
????//?證書密碼,用于支持?RSA?算法
????private?String?certPassword;
????//?JWT?標(biāo)準(zhǔn)信息:簽發(fā)人?-?iss
????private?String?issuer;
????//?JWT?標(biāo)準(zhǔn)信息:主題?-?sub
????private?String?subject;
????//?JWT?標(biāo)準(zhǔn)信息:受眾?-?aud
????private?String?audience;
????//?JWT?標(biāo)準(zhǔn)信息:生效時(shí)間?-?nbf,未來多長時(shí)間內(nèi)生效
????private?long?notBeforeIn;
????
????//?JWT?標(biāo)準(zhǔn)信息:生效時(shí)間?-?nbf,具體哪個(gè)時(shí)間生效
????private?long?notBeforeAt;
????//?JWT?標(biāo)準(zhǔn)信息:過期時(shí)間?-?exp,未來多長時(shí)間內(nèi)過期
????private?long?expiredIn;
????//?JWT?標(biāo)準(zhǔn)信息:過期時(shí)間?-?exp,具體哪個(gè)時(shí)間過期
????private?long?expiredAt;
}??
hmacKey 字段用于支持 HMAC 算法,只要該字段不為空,則使用該值作為 HMAC 的密鑰對 JWT 進(jìn)行簽名與驗(yàn)證。
jksFileName、jksPassword、certPassword 三個(gè)字段用于支持 RSA 算法,程序?qū)⒆x取證書文件作為 RSA 密鑰對 JWT 進(jìn)行簽名與驗(yàn)證。
其他幾個(gè)字段用于設(shè)置 Payload 中需要攜帶的標(biāo)準(zhǔn)信息。
JwtService
JwtService 是提供 JWT 簽名與驗(yàn)證的接口,內(nèi)置了 HMACJwtServiceImpl 提供 HMAC 算法的實(shí)現(xiàn)和 RSAJwtServiceImpl 提供 RSA 算法的實(shí)現(xiàn)。兩種算法在獲取密鑰的方式上是有差別的,這里也提出來成了接口方法。后續(xù)如果要自定義實(shí)現(xiàn),只需要再寫一個(gè)具體實(shí)現(xiàn)類。
public?interface?JwtService?{
????/**
?????*?獲取?key
?????*
?????*?@return
?????*/
????Object?genKey();
????/**
?????*?對信息進(jìn)行簽名
?????*
?????*?@param?payload
?????*?@return
?????*/
????String?sign(String?payload);
????/**
?????*?驗(yàn)證并返回信息
?????*
?????*?@param?token
?????*?@return
?????*/
????String?verify(String?token);
}
public?class?HMACJwtServiceImpl?implements?JwtService?{
????private?JwtConfig?jwtConfig;
????public?HMACJwtServiceImpl(JwtConfig?jwtConfig)?{
????????this.jwtConfig?=?jwtConfig;
????}
????@Override
????public?String?genKey()?{
????????String?key?=?jwtConfig.getHmacKey();
????????if?(JwtUtils.isEmpty(key))?{
????????????throw?new?KeyGenerateException(JwtUtils.KEY_GEN_ERROR,?new?NullPointerException("HMAC?need?a?key"));
????????}
????????return?key;
????}
????@Override
????public?String?sign(String?info)?{
????????return?JwtUtils.signClaimByHMAC(info,?genKey(),?jwtConfig);
????}
????@Override
????public?String?verify(String?token)?{
????????return?JwtUtils.verifyClaimByHMAC(token,?genKey(),?jwtConfig);
????}
}
public?class?RSAJwtServiceImpl?implements?JwtService?{
????private?JwtConfig?jwtConfig;
????private?RSAKey?rsaKey;
????public?RSAJwtServiceImpl(JwtConfig?jwtConfig)?{
????????this.jwtConfig?=?jwtConfig;
????}
????private?InputStream?getCertInputStream()?throws?IOException?{
????????//?讀取配置文件中的證書路徑
????????String?jksFile?=?jwtConfig.getJksFileName();
????????if?(jksFile.contains("://"))?{
????????????//?從本地文件讀取
????????????return?new?FileInputStream(new?File(jksFile));
????????}?else?{
????????????//?從?classpath?讀取
????????????return?getClass().getClassLoader().getResourceAsStream(jwtConfig.getJksFileName());
????????}
????}
????@Override
????public?RSAKey?genKey()?{
????????if?(rsaKey?!=?null)?{
????????????return?rsaKey;
????????}
????????InputStream?is?=?null;
????????try?{
????????????KeyStore?keyStore?=?KeyStore.getInstance(KeyStore.getDefaultType());
????????????is?=?getCertInputStream();
????????????keyStore.load(is,?jwtConfig.getJksPassword().toCharArray());
????????????Enumeration?aliases?=?keyStore.aliases();
????????????String?alias?=?null;
????????????while?(aliases.hasMoreElements())?{
????????????????alias?=?aliases.nextElement();
????????????}
????????????RSAPrivateKey?privateKey?=?(RSAPrivateKey)?keyStore.getKey(alias,?jwtConfig.getCertPassword().toCharArray());
????????????Certificate?certificate?=?keyStore.getCertificate(alias);
????????????RSAPublicKey?publicKey?=?(RSAPublicKey)?certificate.getPublicKey();
????????????rsaKey?=?new?RSAKey.Builder(publicKey).privateKey(privateKey).build();
????????????return?rsaKey;
????????}?catch?(IOException?|?CertificateException?|?UnrecoverableKeyException
????????????????|?NoSuchAlgorithmException?|?KeyStoreException?e)?{
????????????e.printStackTrace();
????????????throw?new?KeyGenerateException(JwtUtils.KEY_GEN_ERROR,?e);
????????}?finally?{
????????????if?(is?!=?null)?{
????????????????try?{
????????????????????is.close();
????????????????}?catch?(IOException?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????}
????????}
????}
????@Override
????public?String?sign(String?payload)?{
????????return?JwtUtils.signClaimByRSA(payload,?genKey(),?jwtConfig);
????}
????@Override
????public?String?verify(String?token)?{
????????return?JwtUtils.verifyClaimByRSA(token,?genKey(),?jwtConfig);
????}
}
JwtUtils
JwtService 的實(shí)現(xiàn)類中比較簡潔,因?yàn)橹饕姆椒ǘ荚?JwtUtils 中提供了。如下是 Payload 中只包含私有信息時(shí),兩種算法的簽名與驗(yàn)證實(shí)現(xiàn)。可以使用這些方法方便的實(shí)現(xiàn)自己的擴(kuò)展。
???/**
?????*?使用?HMAC?算法簽名信息(Payload?中只包含私有信息)
?????*
?????*?@param?info
?????*?@param?key
?????*?@return
?????*/
????public?static?String?signDirectByHMAC(String?info,?String?key)?{
????????try?{
????????????JWSHeader?jwsHeader?=?new?JWSHeader.Builder(JWSAlgorithm.HS256)
????????????????????.type(JOSEObjectType.JWT)
????????????????????.build();
????????????//?建立一個(gè)載荷?Payload
????????????Payload?payload?=?new?Payload(info);
????????????//?將頭部和載荷結(jié)合在一起
????????????JWSObject?jwsObject?=?new?JWSObject(jwsHeader,?payload);
????????????//?建立一個(gè)密匙
????????????JWSSigner?jwsSigner?=?new?MACSigner(key);
????????????//?簽名
????????????jwsObject.sign(jwsSigner);
????????????//?生成?token
????????????return?jwsObject.serialize();
????????}?catch?(JOSEException?e)?{
????????????e.printStackTrace();
????????????throw?new?PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR,?e);
????????}
????}
????/**
?????*?使用?RSA?算法簽名信息(Payload?中只包含私有信息)
?????*
?????*?@param?info
?????*?@param?rsaKey
?????*?@return
?????*/
????public?static?String?signDirectByRSA(String?info,?RSAKey?rsaKey)?{
????????try?{
????????????JWSSigner?signer?=?new?RSASSASigner(rsaKey);
????????????JWSObject?jwsObject?=?new?JWSObject(
????????????????????new?JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKey.getKeyID()).build(),
????????????????????new?Payload(info)
????????????);
????????????//?進(jìn)行加密
????????????jwsObject.sign(signer);
????????????return?jwsObject.serialize();
????????}?catch?(JOSEException?e)?{
????????????e.printStackTrace();
????????????throw?new?PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR,?e);
????????}
????}
????/**
?????*?使用?HMAC?算法驗(yàn)證?token(Payload?中只包含私有信息)
?????*
?????*?@param?token
?????*?@param?key
?????*?@return
?????*/
????public?static?String?verifyDirectByHMAC(String?token,?String?key)?{
????????try?{
????????????JWSObject?jwsObject?=?JWSObject.parse(token);
????????????//?建立一個(gè)解鎖密匙
????????????JWSVerifier?jwsVerifier?=?new?MACVerifier(key);
????????????if?(jwsObject.verify(jwsVerifier))?{
????????????????return?jwsObject.getPayload().toString();
????????????}
????????????throw?new?TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR,?new?NullPointerException("Payload?can?not?be?null"));
????????}?catch?(JOSEException?|?ParseException?e)?{
????????????e.printStackTrace();
????????????throw?new?TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR,?e);
????????}
????}
????/**
?????*?使用?RSA?算法驗(yàn)證?token(Payload?中只包含私有信息)
?????*
?????*?@param?token
?????*?@param?rsaKey
?????*?@return
?????*/
????public?static?String?verifyDirectByRSA(String?token,?RSAKey?rsaKey)?{
????????try?{
????????????RSAKey?publicRSAKey?=?rsaKey.toPublicJWK();
????????????JWSObject?jwsObject?=?JWSObject.parse(token);
????????????JWSVerifier?jwsVerifier?=?new?RSASSAVerifier(publicRSAKey);
????????????//?驗(yàn)證數(shù)據(jù)
????????????if?(jwsObject.verify(jwsVerifier))?{
????????????????return?jwsObject.getPayload().toString();
????????????}
????????????throw?new?TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR,?new?NullPointerException("Payload?can?not?be?null"));
????????}?catch?(JOSEException?|?ParseException?e)?{
????????????e.printStackTrace();
????????????throw?new?TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR,?e);
????????}
????}
JwtException
定義統(tǒng)一的異常類,可以屏蔽 nimbus-jose-jwt 以及其他諸如加載證書錯(cuò)誤拋出的異常,并且在其他項(xiàng)目集成我們封裝好的庫的時(shí)候,方便的進(jìn)行異常處理。
在 JwtService 實(shí)現(xiàn)的不同階段,我們封裝了不同的 JwtException 子類,來方便外部根據(jù)需要做對應(yīng)的處理。如異常是 KeyGenerateException,則處理成服務(wù)器處理錯(cuò)誤;如異常是 TokenVerifyException,則處理成 Token 驗(yàn)證失敗,無權(quán)限。
JwtContext
JWT 用于用戶認(rèn)證,經(jīng)常在 Token 驗(yàn)證完成后,程序中需要獲取到當(dāng)前登錄的用戶信息, JwtContext 中提供了通過線程局部變量保存信息的方法。
public?class?JwtContext?{
????private?static?final?String?KEY_TOKEN?=?"token";
????private?static?final?String?KEY_PAYLOAD?=?"payload";
????private?static?ThreadLocal@AuthRequired
在項(xiàng)目實(shí)戰(zhàn)中,并不是所有 Controller 中的方法都必須傳 Token,通過 @AuthRequired 注解來區(qū)分方法是否需要校驗(yàn) Token。
/**
?*?應(yīng)用于?Controller?中的方法,標(biāo)識(shí)是否攔截進(jìn)行?JWT?驗(yàn)證
?*/
@Target({ElementType.METHOD,?ElementType.TYPE})
public?@interface?AuthRequired?{
????boolean?required()?default?true;
}
Spring Boot 集成 JWT 實(shí)例
有了上面封裝好的庫,我們在 SpringBoot 項(xiàng)目中集成 JWT。創(chuàng)建好 Spring Boot 項(xiàng)目后,我們編寫下面主要的類。
JwtDemoInterceptor
在 Spring Boot 項(xiàng)目中,通過自定義 HandlerInterceptor 的實(shí)現(xiàn)類可以對請求和響應(yīng)進(jìn)行攔截,我們新建 JwtDemoInterceptor 類進(jìn)行攔截。
public?class?JwtDemoInterceptor?implements?HandlerInterceptor?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(JwtDemoInterceptor.class);
????private?static?final?String?PREFIX_BEARER?=?"Bearer?";
????@Autowired
????private?JwtConfig?jwtConfig;
????@Autowired
????private?JwtService?jwtService;
????/**
?????*?預(yù)處理回調(diào)方法,實(shí)現(xiàn)處理器的預(yù)處理(如檢查登陸),第三個(gè)參數(shù)為響應(yīng)的處理器,自定義?Controller
?????*?返回值:
?????*?true?表示繼續(xù)流程(如調(diào)用下一個(gè)攔截器或處理器);
?????*?false?表示流程中斷(如登錄檢查失敗),不會(huì)繼續(xù)調(diào)用其他的攔截器或處理器,此時(shí)我們需要通過?response?來產(chǎn)生響應(yīng)。
?????*/
????@Override
????public?boolean?preHandle(HttpServletRequest?request,?HttpServletResponse?response,?Object?handler)?throws?Exception?{
????????//?如果不是映射到方法直接通過
????????if(!(handler?instanceof?HandlerMethod)){
????????????return?true;
????????}
????????HandlerMethod?handlerMethod?=?(HandlerMethod)?handler;
????????Method?method?=?handlerMethod.getMethod();
????????//?檢查是否有?@AuthRequired?注解,有且?required()?為?false?則跳過
????????if?(method.isAnnotationPresent(AuthRequired.class))?{
????????????AuthRequired?authRequired?=?method.getAnnotation(AuthRequired.class);
????????????if?(!authRequired.required())?{
????????????????return?true;
????????????}
????????}
????????String?token?=?request.getHeader(jwtConfig.getTokenName());
????????logger.info("token:?{}",?token);
????????if?(StringUtils.isEmpty(token)?||?token.trim().equals(PREFIX_BEARER.trim()))?{
????????????return?true;
????????}
????????token?=?token.replace(PREFIX_BEARER,?"");
????????String?payload?=?jwtService.verify(token);
????????//?設(shè)置線程局部變量中的?token
????????JwtContext.setToken(token);
????????JwtContext.setPayload(payload);
????????return?true;
????}
????/**
?????*?后處理回調(diào)方法,實(shí)現(xiàn)處理器的后處理(但在渲染視圖之前),此時(shí)我們可以通過?modelAndView(模型和視圖對象)對模型數(shù)據(jù)進(jìn)行處理或?qū)σ晥D進(jìn)行處理,modelAndView?也可能為null。
?????*/
????@Override
????public?void?postHandle(HttpServletRequest?request,?HttpServletResponse?response,?Object?handler,?ModelAndView?modelAndView)?throws?Exception?{
????}
????/**
?????*?整個(gè)請求處理完畢回調(diào)方法,即在視圖渲染完畢時(shí)回調(diào),如性能監(jiān)控中我們可以在此記錄結(jié)束時(shí)間并輸出消耗時(shí)間,還可以進(jìn)行一些資源清理,類似于?try-catch-finally?中的?finally
?????*?但僅調(diào)用處理器執(zhí)行鏈中?preHandle?返回?true?的攔截器的?afterCompletion。
?????*/
????@Override
????public?void?afterCompletion(HttpServletRequest?request,?HttpServletResponse?response,?Object?handler,?Exception?ex)?throws?Exception?{
????????JwtContext.removeAll();
????}
}
preHandle、postHandle、afterCompletion 三個(gè)方法的具體作用,可以看代碼上的注釋。
preHandle 中這段代碼中的邏輯如下:
攔截被 @AuthRequired 注解的方法,只要不是 required = false都會(huì)進(jìn)行 Token 的校驗(yàn)。從請求中解析出 Token,對 Token 進(jìn)行驗(yàn)證。如果驗(yàn)證異常,會(huì)在方法中拋出異常。 Token 驗(yàn)證通過,會(huì)在線程局部變量中設(shè)置相關(guān)信息,以便后續(xù)程序獲取處理。
afterCompletion 中這段代碼對線程變量進(jìn)行了清理。
InterceptorConfig
定義 InterceptorConfig,通過 @Configuration 注解,Spring 會(huì)加載該類,并完成裝配。
addInterceptors 方法中設(shè)置攔截器,并攔截所有請求。
jwtDemoConfig 方法中注入 JwtConfig,并設(shè)置了 HMACKey。
jwtDemoService 方法會(huì)根據(jù)注入的 JwtConfig 配置,生成具體的 JwtService,這里是 HMACJwtServiceImpl。
@Configuration
public?class?InterceptorConfig?implements?WebMvcConfigurer?{
????@Override
????public?void?addInterceptors(InterceptorRegistry?registry)?{
????????registry.addInterceptor(jwtDemoInterceptor()).addPathPatterns("/**");
????}
????@Bean
????public?JwtDemoInterceptor?jwtDemoInterceptor()?{
????????return?new?JwtDemoInterceptor();
????}
????@Bean
????public?JwtConfig?jwtDemoConfig()?{
????????JwtConfig?jwtConfig?=?new?JwtConfig();
????????jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb");
????????return?jwtConfig;
????}
????@Bean
????public?JwtService?jwtDemoService()?{
????????return?JwtUtils.obtainJwtService(jwtDemoConfig());
????}
}
編寫測試 Controller
@RestController
public?class?UserController?{
????@Autowired
????private?ObjectMapper?objectMapper;
????@Autowired
????private?JwtService?jwtService;
????@GetMapping("/sign")
????@AuthRequired(required?=?false)
????public?String?sign()?throws?JsonProcessingException?{
????????UserDTO?userDTO?=?new?UserDTO();
????????userDTO.setName("fatfoo");
????????userDTO.setPassword("112233");
????????userDTO.setSex(0);
????????String?payload?=?objectMapper.writeValueAsString(userDTO);
????????return?jwtService.sign(payload);
????}
????@GetMapping("/verify")
????public?UserDTO?verify()?throws?IOException?{
????????String?payload?=?(String)?JwtContext.getPayload();
????????return?objectMapper.readValue(payload,?UserDTO.class);
????}
}
sign 方法對用戶信息進(jìn)行簽名并返回 Token;由于 @AuthRequired(required = false) 攔截器將不會(huì)對其進(jìn)行攔截。
verify 方法在 Token 通過驗(yàn)證后,獲取解析出的信息并返回。
用 Postman 進(jìn)行測試
訪問 sign 接口,返回簽名 Token。

在 Header 中添加 Token 信息,請求 verify 接口,返回用戶信息。

測試 RSA 算法實(shí)現(xiàn)
上面我們只設(shè)置了 JwtConfig 的 hmacKey 參數(shù),使用的是 HMAC 算法進(jìn)行簽名和驗(yàn)證。本節(jié)我們演示 RSA 算法進(jìn)行簽名和驗(yàn)證的實(shí)現(xiàn)。
生成簽名文件
使用 Java 自帶的 keytool 工具可以方便的生成證書文件。
???resources?git:(master)???keytool?-genkey?-alias?jwt?-keyalg?RSA?-keystore?jwt.jks
輸入密鑰庫口令:
密鑰庫口令太短?-?至少必須為?6?個(gè)字符
輸入密鑰庫口令:?ronjwt
再次輸入新口令:?ronjwt
您的名字與姓氏是什么?
??[Unknown]:??ron
您的組織單位名稱是什么?
??[Unknown]:??ron
您的組織名稱是什么?
??[Unknown]:??ron
您所在的城市或區(qū)域名稱是什么?
??[Unknown]:??Xiamen
您所在的省/市/自治區(qū)名稱是什么?
??[Unknown]:??Fujian
該單位的雙字母國家/地區(qū)代碼是什么?
??[Unknown]:??CN
CN=ron,?OU=ron,?O=ron,?L=Xiamen,?ST=Fujian,?C=CN是否正確?
??[否]:??是
輸入??的密鑰口令
?(如果和密鑰庫口令相同,?按回車):
Warning:
JKS?密鑰庫使用專用格式。建議使用?"keytool?-importkeystore?-srckeystore?jwt.jks?-destkeystore?jwt.jks?-deststoretype?pkcs12"?遷移到行業(yè)標(biāo)準(zhǔn)格式?PKCS12。
文件生成后,復(fù)制到項(xiàng)目的 resource 目錄下。
設(shè)置 JwtConfig 參數(shù)
修改上節(jié) InterceptorConfig 中的 jwtDemoConfig 方法,這是 jksFileName、jksPassword、certPassword 3 個(gè)參數(shù)。
@Bean
public?JwtConfig?jwtDemoConfig()?{
????JwtConfig?jwtConfig?=?new?JwtConfig();
//????????jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb");
????jwtConfig.setJksFileName("jwt.jks");
????jwtConfig.setJksPassword("ronjwt");
????jwtConfig.setCertPassword("ronjwt");
????return?jwtConfig;
}
不要設(shè)置 hmacKey 參數(shù),否則會(huì)加載 HMACJwtServiceImpl。因?yàn)?JwtUtils#obtainJwtService 方法實(shí)現(xiàn)如下:
/**
?*?獲取內(nèi)置?JwtService?的工廠方法。
?*
?*?優(yōu)先采用?HMAC?算法實(shí)現(xiàn)
?*
?*?@param?jwtConfig
?*?@return
?*/
public?static?JwtService?obtainJwtService(JwtConfig?jwtConfig)?{
????if?(!JwtUtils.isEmpty(jwtConfig.getHmacKey()))?{
????????return?new?HMACJwtServiceImpl(jwtConfig);
????}
????return?new?RSAJwtServiceImpl(jwtConfig);
}
這樣就可以進(jìn)行 RSA 算法簽名與驗(yàn)證的測試了。運(yùn)行程序并使用 Postman 測試,請自行查看區(qū)別。
END
有熱門推薦?
1.?面試官靈魂一問: MySQL 的 delete、truncate、drop 有什么區(qū)別?
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)

