SpringBoot整合JWT的實現示例
目錄
- 一. JWT簡介
- 二. 實現JWT(SpringBoot方式整合)
- JWT總結
一. JWT簡介
1. 什麼是JWT?
JWT(ONWeb Token)是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準。
它將使用者資訊加密到token裡,伺服器不儲存任何使用者資訊。伺服器通過使用儲存的金鑰驗證token的正確性,只要正確即通過驗證;應用場景如使用者登入。JWT詳細講解請見 :https://github.com/jwtk/jjwt
2. 為什麼使用JWT?
隨著技術的發展,分散式web應用的普及,通過session管理使用者登入狀態成本越來越高,因此慢慢發展成為token的方式做登入身份校驗,然後通過token去取redis中的快取的使用者資訊,隨著之後jwt的出現,校驗方式更加簡單便捷化,無需通過redis快取,而是直接根據token取出儲存的使用者資訊,以及對token可用性校驗,單點登入更為簡單。
3. 傳統Cookie+Session與JWT對比
① 在傳統的使用者登入認證中,因為http是無狀態的,所以都是採用session方式。使用者登入成功,服務端會保證一個session,當然會給客戶端一個sessionId,客戶端會把sessionId儲存在cookie中,每次請求都會攜帶這個sessionId。
cookie+session這種模式通常是儲存在記憶體中,而且服務從單服務到多服務會面臨的session共享問題,隨著使用者量的增多,開銷就會越大。而JWT不是這樣的,只需要服務端生成token,客戶端儲存這個token,每次請求攜帶這個token,服務端認證解析就可。
② JWT方式校驗方式更加簡單便捷化,無需通過redis快取,而是直接根據token取出儲存的使用者資訊,以及對token可用性校驗,單點登入,驗證token更為簡單。
4. JWT的組成(3部分)
第一部分為頭部(header),第二部分我們稱其為載荷(payload),第三部分是簽證(signature)。【中間用 . 分隔】
一個標準的JWT生成的token格式如下:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI1IiwiaWF0IjoxNTY1NTk3MDUzLCJleHAiOjE1NjU2MDA2NTN9.qesdk6aeFEcNafw5WFm-TwZltGWb1Xs6oBEk5QdaLzlHxDM73IOyeKPF_iN1bLvDAlB7UnSu-Z-Zsgl_dIlPiw
5. JWT驗證流程和特點
驗證流程:
① 在頭部資訊中宣告加密演算法和常量, 然後把header使用json轉化為字串
② 在載荷中宣告使用者資訊,同時還有一些其他的內容;再次使用json 把載荷部分進行轉化,轉化為字串
③ 使用在header中宣告的加密演算法和每個專案隨機生成的secret來進行加密, 把第一步分字串和第二部分的字串進行加密, 生成新的字串。詞字串是獨一無二的。
④ 解密的時候,只要客戶端帶著JWT來發起請求,服務端就直接使用secret進行解密。
特點:
① 三部分組成,每一部分都進行字串的轉化
② 解密的時候沒有使用,僅僅使用的是secret進行解密
③ JWT的secret千萬不能洩密!
6. JWT優缺點
優點:
①.可擴充套件性好
應用程式分散式部署的情況下,Session需要做多機資料共享,通常可以存在資料庫或者Redis裡面。而JWT不需要。
②. 無狀態
JWT不在服務端儲存任何狀態。RESTful API的原則之一是無狀態,發出請求時,總會返回帶有引數的響應,不會產生附加影響。使用者的認證狀態引入這種附加影響,這破壞了這一原則。另外JWT的載荷中可以儲存一些常用資訊,用於交換資訊,有效地使用 JWT,可以降低伺服器查詢資料庫的次數。
缺點:
① 安全性:由於JWT的payload是使用Base64編碼的,並沒有加密,因此JWT中不能儲存敏感資料。而Session的資訊是存在服務端的,相對來說更安全。
② 效能:JWT太長。由於是無狀態使用JWT,所有的資料都被放到JWT裡,如果還要進行一些資料交換,那載荷會更大,經過編碼之後導致JWT非常長,Cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在LocalStorage裡面。並且使用者在系統中的每一次Http請求都會把JWT攜帶在Header裡面,Http請求的Header可能比Body還要大。而SessionId只是很短的一個字串,因此使用JWT的Http請求比使用Session的開銷大得多。
③ 一次性:無狀態是JWT的特點,但也導致了這個問題,JWT是一次性的。想修改裡面的內容,就必須簽發一個新的JWT。即缺陷是一旦下發,服務後臺無法拒絕攜帶該jwt的請求(如踢除使用者)
(1)無法廢棄:通過JWT的驗證機制可以看出來,一旦簽發一個JWT,在到期之前就會始終有效,無法中途廢棄。例如你在payload中儲存了一些資訊,當資訊需要更新時,則重新簽發一個JWT,但是由於舊的jwt還沒過期,拿著這個舊的JWT依舊可以登入,那登入後服務端從JWT中拿到的資訊就是過時的。為了解決這個問題,我們就需要在服務端部署額外的邏輯,例如設定一個黑名單,一旦簽發了新的JWT,那麼舊的就加入黑名單(比如存到redis裡面),避免被再次使用。
(2)續簽:如果你使用jwt做會話管理,傳統的Cookie續簽方案一般都是框架自帶的,Session有效期30分鐘,30分鐘內如果有訪問,有效期被重新整理至30分鐘。一樣的道理,要改變JWT的有效時間,就要簽發新的JWT。最簡單的一種方式是每次請求重新整理JWT,即每個HTTP請求都返回一個新的JWT。這個方法不僅暴力不優雅,而且每次請求都要做JWT的加密解密,會帶來效能問題。另一種方法是在Redis中單獨為每個JWT設定過期時間,每次訪問時重新整理JWT的過期時間。
可以看出想要破解JWT一次性的特性,就需要在服務端儲存jwt的狀態。但是引入 redis 之後,就把無狀態的jwt硬生生變成了有狀態了,違背了JWT的初衷。而且這個方案和Session都差不多了。
二. Java實現JWT(SpringBoot方式整合)
1. Maven依賴與application.yml配置
<!-- JWT依賴 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
server: port: 8080 spring: application: name: springboot-jwt config: jwt: # 加密金鑰 secret: abcdefg1234567 # token有效時長 expire: 3600 # header 名稱 header: token
2. 編寫JwtConfig
package com.example.config; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Date; /** * JWT的token,區分大小寫 */ @ConfigurationProperties(prefix = "config.jwt") @Component public class JwtConfig { private String secret; private long expire; private String header; /** * 生成token * @param subject * @return */ public String createToken (String subject){ Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + expire * 1000);//過期時間 return Jwts.builder() .setHeaderParam("typ","JWT") .setSubject(subject) .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS512,secret) .compact(); } /** * 獲取token中註冊資訊 * @param token * @return */ public Claims getTokenClaim (String token) { try { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); }catch (Exception e){ // e.printStackTrace(); return null; } } /** * 驗證token是否過期失效 * @param expirationTime * @return */ public boolean isTokenExpired (Date expirationTime) { return expirationTime.before(new Date()); } /** * 獲取token失效時間 * @param token * @return */ public Date getExpirationDateFromToken(String token) { return getTokenClaim(token).getExpiration(); } /** * 獲取使用者名稱從token中 */ public String getUsernameFromToken(String token) { return getTokenClaim(token).getSubject(); } /** * 獲取jwt釋出時間 */ public Date getIssuedAtDateFromToken(String token) { return getTokenClaim(token).getIssuedAt(); } // --------------------- getter & setter --------------------- public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public long getExpire() { return expire; } public void setExpire(long expire) { this.expire = expire; } public String getHeader() { return header; } public void setHeader(String header) { this.header = header; } }
3. 配置攔截器
package com.example.interceptor; import com.example.config.JwtConfig; import io.jsonwebtoken.Claims; import io.jsonwebtoken.SignatureException; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class TokenInterceptor extends HandlerInterceptorAdapter { @Resource private JwtConfig jwtConfig ; @Override public boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws SignatureException { /** 地址過濾 */ String uri = request.getRequestURI() ; if (uri.contains("/login")){ return true ; } /** Token 驗證 */ String token = request.getHeader(jwtConfig.getHeader()); if(StringUtils.isEmpty(token)){ token = request.getParameter(jwtConfig.getHeader()); } if(StringUtils.isEmpty(token)){ throw new SignatureException(jwtConfig.getHeader()+ "不能為空"); } Claims claims = null; try{ claims = jwtConfig.getTokenClaim(token); if(claims == null || jwtConfig.isTokenExpired(claims.getExpiration())){ throw new SignatureException(jwtConfig.getHeader() + "失效,請重新登入。"); } }catch (Exception e){ throw new SignatureException(jwtConfig.getHeader() + "失效,請重新登入。"); } /** 設定 identityId 使用者身份ID */ request.setAttribute("identityId",claims.getSubject()); return true; } }
註冊攔截器到SpringMvc
package com.example.config; import com.example.interceptor.TokenInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration public class WebConfig implements WebMvcConfigurer { @Resource private TokenInterceptor tokenInterceptor ; public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tokenInterceptor).addPathPatterns("/**"); } }
4. 編寫統一異常處理類
package com.example.config; import io.jsonwebtoken.SignatureException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.yuyi.full.handler.exception.ExceptionInfoBO; import org.yuyi.full.handler.exception.ResultBO; import org.yuyi.full.handler.exception.ResultTool; @RestControllerAdvice public class PermissionHandler { @ExceptionHandler(value = { SignatureException.class }) @ResponseBody public ResultBO<?> authorizationException(SignatureException e){ return ResultTool.error(new ExceptionInfoBO(1008,e.getMessage())); } }
5.編寫測試介面
package com.example.controller; import com.alibaba.fastjson.JSONObject; import com.example.config.JwtConfig; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.yuyi.full.handler.exception.ResultBO; import org.yuyi.full.handler.exception.ResultTool; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @RestController public class TokenController { @Resource private JwtConfig jwtConfig ; /** * 使用者登入介面 * @param userName * @param passWord * @return */ @PostMapping("/login") public ResultBO<?> login (@RequestParam("userName") String userName,@RequestParam("passWord") String passWord){ JSONObject json = new JSONObject(); /** 驗證userName,passWord和資料庫中是否一致,如不一致,直接return ResultTool.errer(); 【這裡省略該步驟】*/ // 這裡模擬通過使用者名稱和密碼,從資料庫查詢userId // 這裡把userId轉為String型別,實際開發中如果subject需要存userId,則可以JwtConfig的createToken方法的引數設定為Long型別 String userId = 5 + ""; String token = jwtConfig.createToken(userId) ; if (!StringUtils.isEmpty(token)) { json.put("token",token) ; } return ResultTool.success(json) ; } /** * 需要 Token 驗證的介面 */ @PostMapping("/info") public ResultBO<?> info (){ return ResultTool.success("info") ; } /** * 根據請求頭的token獲取userId * @param request * @return */ @GetMapping("/getUserInfo") public ResultBO<?> getUserInfo(HttpServletRequest request){ String usernameFromToken = jwtConfig.getUsernameFromToken(request.getHeader("token")); return ResultTool.success(usernameFromToken) ; } /* 為什麼專案重啟後,帶著之前的token還可以訪問到需要info等需要token驗證的介面? 答案:只要不過期,會一直存在,類似於redis */ }
用PostMan測試工具測試一下,訪問登入介面,當對賬號密碼驗證通過時,則返回一個token給客戶端:
當直接去訪問info介面時,會返回token為空的自定義異常:
當在請求頭加上正確token時,則攔截器驗證通過,可以正常訪問到介面:
當在請求頭加入一個錯誤token,則會返回token失效的自定義異常:
接下來測試一下獲取使用者資訊,因為這裡存的subject為userId,所以直接返回上面寫死的假資料5:
JWT總結
1. 基於JSON,所以JWT是可以進行跨語言支援的,像JAVA,,Node.JS,等很多語言都可以使用。
2. payload部分,需要時JWT可以儲存一些其他業務邏輯所必要的非敏感資訊。
3. 體積小巧,便於傳輸;JWT的構成非常簡單,位元組佔用很小,所以它是非常便於傳輸的。它不需要在服務端儲存會話資訊,所以它易於應用的擴充套件。
到此這篇關於SpringBoot整合JWT的實現示例的文章就介紹到這了,更多相關SpringBoot整合JWT內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!