JWT 簡介
什麼是 JWT
JWT 是 JSON Web Token 的縮寫,是爲了在網絡應用環境間傳遞聲明而執行的一種基於 JSON
的開放標準((RFC 7519)。定義了一種簡潔的,自包含的方法用於通訊雙方之間以 JSON
對象的形式安全的傳遞信息。由於數字簽名的存在,這些信息是可信的,JWT 可使用 HMAC
算法或者是 RSA
的公私祕鑰對進行簽名。javascript
JWT請求流程
- 用戶使用帳號和密碼發起 POST 請求;
- 服務器使用私鑰建立一個 JWT;
- 服務器返回這個 JWT 給瀏覽器;
- 瀏覽器將該 JWT 串在請求頭中像服務器發送請求;
- 服務器驗證該 JWT;
- 返回響應的資源給瀏覽器。
JWT 的主要應用場景
身份認證在這種場景下,一旦用戶完成了登陸,在接下來的每一個請求中包含 JWT,能夠用來驗證用戶身份以及對路由,服務和資源的訪問權限進行驗證。因爲它的開銷很是小,能夠輕鬆的在不一樣域名的系統中傳遞,全部目前在單點登陸(SSO)中比較普遍的使用了該技術。 信息交換在通訊的雙方之間使用 JWT 對數據進行編碼是一種很是安全的方式,因爲它的信息是通過簽名的,能夠確保發送者發送的信息是沒有通過僞造的。java
JWT 數據結構
JWT 是由三段信息構成的,將這三段信息文本用 .
鏈接一塊兒就構成了 JWT 字符串。git
JWT 的三個部分依次爲頭部:Header,負載:Payload 和簽名:Signature。算法
Header
Header 部分是一個 JSON 對象,描述 JWT 的元數據,一般是下面的樣子。spring
{ "alg": "HS256", "typ": "JWT" }
上面代碼中,alg
屬性表示簽名的算法(algorithm),默認是 HMAC SHA256(寫成 HS256);typ
屬性表示這個令牌(token)的類型(type),JWT 令牌統一寫爲 JWT
。shell
最後,將上面的 JSON 對象使用 Base64URL 算法轉成字符串。數據庫
Payload
Payload 部分也是一個 JSON 對象,用來存放實際須要傳遞的有效信息。有效信息包含三個部分:json
- 標準中註冊的聲明
- 公共的聲明
- 私有的聲明
標準中註冊的聲明 (建議但不強制使用) :api
- iss (issuer):簽發人
- exp (expiration time):過時時間,必需要大於簽發時間
- sub (subject):主題
- aud (audience):受衆
- nbf (Not Before):生效時間
- iat (Issued At):簽發時間
- jti (JWT ID):編號,JWT 的惟一身份標識,主要用來做爲一次性
token
,從而回避重放攻擊。
公共的聲明 :瀏覽器
公共的聲明能夠添加任何的信息,通常添加用戶的相關信息或其餘業務須要的必要信息。但不建議添加敏感信息,由於該部分在客戶端可解密。
私有的聲明 :
私有聲明是提供者和消費者所共同定義的聲明,通常不建議存放敏感信息,由於 base64
是對稱解碼的,意味着該部分信息能夠歸類爲明文信息。
這個 JSON 對象也要使用 Base64URL 算法轉成字符串。
Signature
Signature 部分是對前兩部分的簽名,防止數據篡改。
首先,須要指定一個密鑰(secret)。這個密鑰只有服務器才知道,不能泄露給用戶。而後,使用 Header 裏面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出簽名之後,把 Header、Payload、Signature 三個部分拼成一個字符串,每一個部分之間用"點"(.
)分隔,就能夠返回給用戶。
Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。這個算法跟 Base64 算法基本相似,但有一些小的不一樣。
JWT 做爲一個令牌(token),有些場合可能會放到 URL(好比 api.example.com/?token=xxx
)。Base64 有三個字符 +
、 /
和 =
,在 URL 裏面有特殊含義,因此要被替換掉:=
被省略、+
替換成 -
,/
替換成 _
。這就是 Base64URL 算法。
JWT 的使用方式
客戶端收到服務器返回的 JWT 以後須要在本地作保存。此後,客戶端每次與服務器通訊,都要帶上這個 JWT。通常的的作法是放在 HTTP 請求的頭信息 Authorization
字段裏面。
Authorization: Bearer <token>
這樣每一個請求中,服務端就能夠在請求頭中拿到 JWT 進行解析與認證。
JWT 的特性
-
JWT 默認是不加密,但也是能夠加密的。生成原始 Token 之後,能夠用密鑰再加密一次。
-
JWT 不加密的狀況下,不能將祕密數據寫入 JWT。
-
JWT 不只能夠用於認證,也能夠用於交換信息。有效使用 JWT,能夠下降服務器查詢數據庫的次數。
-
JWT 的最大缺點是,因爲服務器不保存 session 狀態,所以沒法在使用過程當中廢止某個 token,或者更改 token 的權限。也就是說,一旦 JWT 簽發了,在到期以前就會始終有效,除非服務器部署額外的邏輯。
-
JWT 自己包含了認證信息,一旦泄露,任何人均可以得到該令牌的全部權限。爲了減小盜用,JWT 的有效期應該設置得比較短。對於一些比較重要的權限,使用時應該再次對用戶進行認證。
-
爲了減小盜用,JWT 不該該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。
基於 nimbus-jose-jwt 簡單封裝
nimbus-jose-jwt 是最受歡迎的 JWT 開源庫,基於Apache 2.0開源協議,支持全部標準的簽名(JWS)和加密(JWE)算法。nimbus-jose-jwt 支持使用對稱加密(HMAC)和非對稱加密(RSA)兩種算法來生成和解析 JWT 令牌。
下面咱們對 nimbus-jose-jwt 進行簡單的封裝,提供如下功能的支持:
- 支持使用 HMAC 和 RSA 算法生成和解析 JWT 令牌
- 支持私有信息直接做爲 Payload,以及標準信息+私有信息做爲 Payload。內置支持後者。
- 提供工具類及可擴展接口,方便自定義擴展開發。
pom 中添加依賴
首先咱們在 pom.xml 中引入 nimbus-jose-jwt 的依賴。
<dependency> <groupid>com.nimbusds</groupid> <artifactid>nimbus-jose-jwt</artifactid> <version>8.20</version> </dependency>
JwtConfig
這個類用於統一管理相關的參數配置。
public class JwtConfig { // JWT 在 HTTP HEADER 中默認的 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 標準信息:簽發人 - iss private String issuer; // JWT 標準信息:主題 - sub private String subject; // JWT 標準信息:受衆 - aud private String audience; // JWT 標準信息:生效時間 - nbf,將來多長時間內生效 private long notBeforeIn; // JWT 標準信息:生效時間 - nbf,具體哪一個時間生效 private long notBeforeAt; // JWT 標準信息:過時時間 - exp,將來多長時間內過時 private long expiredIn; // JWT 標準信息:過時時間 - exp,具體哪一個時間過時 private long expiredAt; }
hmacKey
字段用於支持 HMAC 算法,只要該字段不爲空,則使用該值做爲 HMAC 的密鑰對 JWT 進行簽名與驗證。
jksFileName
、jksPassword
、certPassword
三個字段用於支持 RSA 算法,程序將讀取證書文件做爲 RSA 密鑰對 JWT 進行簽名與驗證。
其餘幾個字段用於設置 Payload 中須要攜帶的標準信息。
JwtService
JwtService 是提供 JWT 簽名與驗證的接口,內置了 HMACJwtServiceImpl 提供 HMAC 算法的實現和 RSAJwtServiceImpl 提供 RSA 算法的實現。兩種算法在獲取密鑰的方式上是有差異的,這裏也提出來成了接口方法。後續若是要自定義實現,只須要再寫一個具體實現類。
public interface JwtService { /** * 獲取 key * * @return */ Object genKey(); /** * 對信息進行簽名 * * @param payload * @return */ String sign(String payload); /** * 驗證並返回信息 * * @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<string> 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 的實現類中比較簡潔,由於主要的方法都在 JwtUtils 中提供了。以下是 Payload 中只包含私有信息時,兩種算法的簽名與驗證明現。可使用這些方法方便的實現本身的擴展。
/** * 使用 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(); // 創建一個載荷 Payload Payload payload = new Payload(info); // 將頭部和載荷結合在一塊兒 JWSObject jwsObject = new JWSObject(jwsHeader, payload); // 創建一個密匙 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) ); // 進行加密 jwsObject.sign(signer); return jwsObject.serialize(); } catch (JOSEException e) { e.printStackTrace(); throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e); } } /** * 使用 HMAC 算法驗證 token(Payload 中只包含私有信息) * * @param token * @param key * @return */ public static String verifyDirectByHMAC(String token, String key) { try { JWSObject jwsObject = JWSObject.parse(token); // 創建一個解鎖密匙 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 算法驗證 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); // 驗證數據 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
定義統一的異常類,能夠屏蔽 nimbus-jose-jwt 以及其餘諸如加載證書錯誤拋出的異常,而且在其餘項目集成咱們封裝好的庫的時候,方便的進行異常處理。
在 JwtService 實現的不一樣階段,咱們封裝了不一樣的 JwtException 子類,來方便外部根據須要作對應的處理。如異常是 KeyGenerateException,則處理成服務器處理錯誤;如異常是 TokenVerifyException,則處理成 Token 驗證失敗,無權限。
JwtContext
JWT 用於用戶認證,常常在 Token 驗證完成後,程序中須要獲取到當前登陸的用戶信息, JwtContext 中提供了經過線程局部變量保存信息的方法。
public class JwtContext { private static final String KEY_TOKEN = "token"; private static final String KEY_PAYLOAD = "payload"; private static ThreadLocal<map<object, object>> context = new ThreadLocal<>(); private JwtContext() {} public static void set(Object key, Object value) { Map<object, object> locals = context.get(); if (locals == null) { locals = new HashMap<>(); context.set(locals); } locals.put(key, value); } public static Object get(Object key) { Map<object, object> locals = context.get(); if (locals != null) { return locals.get(key); } return null; } public static void remove(Object key) { Map<object, object> locals = context.get(); if (locals != null) { locals.remove(key); if (locals.isEmpty()) { context.remove(); } } } public static void removeAll() { Map<object, object> locals = context.get(); if (locals != null) { locals.clear(); } context.remove(); } public static void setToken(String token) { set(KEY_TOKEN, token); } public static String getToken() { return (String) get(KEY_TOKEN); } public static void setPayload(Object payload) { set(KEY_PAYLOAD, payload); } public static Object getPayload() { return get(KEY_PAYLOAD); } }
@AuthRequired
在項目實戰中,並非全部 Controller 中的方法都必須傳 Token,經過 @AuthRequired 註解來區分方法是否須要校驗 Token。
/** * 應用於 Controller 中的方法,標識是否攔截進行 JWT 驗證 */ @Target({ElementType.METHOD, ElementType.TYPE}) public @interface AuthRequired { boolean required() default true; }
Spring Boot 集成 JWT 實例
有了上面封裝好的庫,咱們在 SpringBoot 項目中集成 JWT。建立好 Spring Boot 項目後,咱們編寫下面主要的類。
JwtDemoInterceptor
在 Spring Boot 項目中,經過自定義 HandlerInterceptor 的實現類能夠對請求和響應進行攔截,咱們新建 JwtDemoInterceptor 類進行攔截。
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; /** * 預處理回調方法,實現處理器的預處理(如檢查登錄),第三個參數爲響應的處理器,自定義 Controller * 返回值: * true 表示繼續流程(如調用下一個攔截器或處理器); * false 表示流程中斷(如登陸檢查失敗),不會繼續調用其餘的攔截器或處理器,此時咱們須要經過 response 來產生響應。 */ @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); // 設置線程局部變量中的 token JwtContext.setToken(token); JwtContext.setPayload(payload); return true; } /** * 後處理回調方法,實現處理器的後處理(但在渲染視圖以前),此時咱們能夠經過 modelAndView(模型和視圖對象)對模型數據進行處理或對視圖進行處理,modelAndView 也可能爲null。 */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } /** * 整個請求處理完畢回調方法,即在視圖渲染完畢時回調,如性能監控中咱們能夠在此記錄結束時間並輸出消耗時間,還能夠進行一些資源清理,相似於 try-catch-finally 中的 finally * 但僅調用處理器執行鏈中 preHandle 返回 true 的攔截器的 afterCompletion。 */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { JwtContext.removeAll(); } }
preHandle
、postHandle
、afterCompletion
三個方法的具體做用,能夠看代碼上的註釋。
preHandle
中這段代碼中的邏輯以下:
- 攔截被 @AuthRequired 註解的方法,只要不是
required = false
都會進行 Token 的校驗。 - 從請求中解析出 Token,對 Token 進行驗證。若是驗證異常,會在方法中拋出異常。
- Token 驗證經過,會在線程局部變量中設置相關信息,以便後續程序獲取處理。
afterCompletion
中這段代碼對線程變量進行了清理。
InterceptorConfig
定義 InterceptorConfig,經過 @Configuration 註解,Spring 會加載該類,並完成裝配。
addInterceptors
方法中設置攔截器,並攔截全部請求。
jwtDemoConfig
方法中注入 JwtConfig,並設置了 HMACKey。
jwtDemoService
方法會根據注入的 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
方法對用戶信息進行簽名並返回 Token;因爲 @AuthRequired(required = false)
攔截器將不會對其進行攔截。
verify
方法在 Token 經過驗證後,獲取解析出的信息並返回。
用 Postman 進行測試
訪問 sign 接口,返回簽名 Token。
在 Header 中添加 Token 信息,請求 verify 接口,返回用戶信息。
測試 RSA 算法實現
上面咱們只設置了 JwtConfig 的 hmacKey 參數,使用的是 HMAC 算法進行簽名和驗證。本節咱們演示 RSA 算法進行簽名和驗證的實現。
生成簽名文件
使用 Java 自帶的 keytool 工具能夠方便的生成證書文件。
➜ resources git:(master) ✗ keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks 輸入密鑰庫口令: 密鑰庫口令過短 - 至少必須爲 6 個字符 輸入密鑰庫口令: ronjwt 再次輸入新口令: ronjwt 您的名字與姓氏是什麼? [Unknown]: ron 您的組織單位名稱是什麼? [Unknown]: ron 您的組織名稱是什麼? [Unknown]: ron 您所在的城市或區域名稱是什麼? [Unknown]: Xiamen 您所在的省/市/自治區名稱是什麼? [Unknown]: Fujian 該單位的雙字母國家/地區代碼是什麼? [Unknown]: CN CN=ron, OU=ron, O=ron, L=Xiamen, ST=Fujian, C=CN是否正確? [否]: 是 輸入 <jwt> 的密鑰口令 (若是和密鑰庫口令相同, 按回車): Warning: JKS 密鑰庫使用專用格式。建議使用 "keytool -importkeystore -srckeystore jwt.jks -destkeystore jwt.jks -deststoretype pkcs12" 遷移到行業標準格式 PKCS12。
文件生成後,複製到項目的 resources 目錄下。
設置 JwtConfig 參數
修改上節 InterceptorConfig 中的 jwtDemoConfig
方法,這是 jksFileName、jksPassword、certPassword 3 個參數。
@Bean public JwtConfig jwtDemoConfig() { JwtConfig jwtConfig = new JwtConfig(); // jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb"); jwtConfig.setJksFileName("jwt.jks"); jwtConfig.setJksPassword("ronjwt"); jwtConfig.setCertPassword("ronjwt"); return jwtConfig; }
不要設置 hmacKey 參數,不然會加載 HMACJwtServiceImpl。由於 JwtUtils#obtainJwtService
方法實現以下:
/** * 獲取內置 JwtService 的工廠方法。 * * 優先採用 HMAC 算法實現 * * @param jwtConfig * @return */ public static JwtService obtainJwtService(JwtConfig jwtConfig) { if (!JwtUtils.isEmpty(jwtConfig.getHmacKey())) { return new HMACJwtServiceImpl(jwtConfig); } return new RSAJwtServiceImpl(jwtConfig); }
這樣就能夠進行 RSA 算法簽名與驗證的測試了。運行程序並使用 Postman 測試,請自行查看區別。
- End -
本文只是 Spring Boot 集成 JWT 的第一篇,在後續咱們還將繼續對這個庫進行封裝,構建 spring-boot-starter,自定義 @Enable 註解來方便在項目中引入。
第二篇已更新,《一行代碼就能夠實現 Jwt 登陸認證,爽呆了》
請關注個人公衆號:精進Java(ID:craft4j),第一時間獲取知識動態。
若是你對項目的完整源碼感興趣,能夠在公衆號中回覆 jwt
來獲取。