1. 程式人生 > >Spring Cloud實戰 | 最終篇:Spring Cloud Gateway+Spring Security OAuth2整合統一認證授權平臺下實現登出使JWT失效方案

Spring Cloud實戰 | 最終篇:Spring Cloud Gateway+Spring Security OAuth2整合統一認證授權平臺下實現登出使JWT失效方案

# 一. 前言 在上一篇文章介紹 [youlai-mall](https://github.com/hxrui/youlai-mall) 專案中,通過整合Spring Cloud Gateway、Spring Security OAuth2、JWT等技術實現了微服務下統一認證授權平臺的搭建。最後在文末留下一個值得思考問題,就是如何在登出、修改密碼、修改許可權場景下讓JWT失效?所以在這篇文章來對方案和實現進行補充。想親身體驗的小夥伴們可以瞭解下 [youlai-mall](https://github.com/hxrui/youlai-mall) 專案和Spring Cloud實戰系列往期文章。 **[youlai-mall專案地址](https://github.com/hxrui/youlai-mall)** **Spring Cloud實戰系列往期文章** 1. [Spring Cloud實戰 | 第一篇:Windows搭建Nacos服務 ](https://www.cnblogs.com/haoxianrui/p/13581881.html) 2. [Spring Cloud實戰 | 第二篇:Spring Cloud整合Nacos實現註冊中心](https://www.cnblogs.com/haoxianrui/p/13584204.html) 3. [Spring Cloud實戰 | 第三篇:Spring Cloud整合Nacos實現配置中心](https://www.cnblogs.com/haoxianrui/p/13585125.html) 4. [Spring Cloud實戰 | 第四篇:Spring Cloud整合Gateway實現API閘道器](https://www.cnblogs.com/haoxianrui/p/13608650.html) 5. [Spring Cloud實戰 | 第五篇:Spring Cloud整合OpenFeign實現微服務之間的呼叫](https://www.cnblogs.com/haoxianrui/p/13615592.html) 6. [Spring Cloud實戰 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT實現微服務統一認證授權](https://www.cnblogs.com/haoxianrui/p/13719356.html) 7. [vue-element-admin實戰 | 第一篇: 移除mock接入後臺,搭建有來商城youlai-mall前後端分離管理平臺](https://www.cnblogs.com/haoxianrui/p/13624548.html) 8. [vue-element-admin實戰 | 第二篇: 最小改動接入後臺實現根據許可權動態載入選單](https://www.cnblogs.com/haoxianrui/p/13676619.html) # 二. 解決方案 JWT最大的一個優勢在於它是**無狀態**的,自身包含了認證鑑權所需要的所有資訊,伺服器端無需對其儲存,從而給伺服器減少了儲存開銷。 但是無狀態引出的問題也是可想而知的,它無法作廢未過期的JWT。舉例說明登出場景下,就傳統的cookie/session認證機制,只需要把存在伺服器端的session刪掉就OK了。但是JWT呢,它是不存在伺服器端的啊,好的那我刪存在客戶端的JWT行了吧。額,社會本就複雜別再欺騙自己了好麼,被你在客戶端刪掉的JWT還是可以通過伺服器端認證的。 ![](https://i.loli.net/2020/09/27/Vd7KRprwuJz16CE.png) 首先明確一點JWT失效的唯一途徑就是等過期,就是說不借助外力的情況下,無法達到某些場景下需要主動使JWT失效的目的。而外力則是在伺服器端儲存著JWT的狀態,在請求資源時新增判斷邏輯,這與JWT特性無狀態是相互矛盾的存在。但是,你要知道如果你選擇走上了JWT這條路,那就沒得選了。如果你有好的方式,希望你來打我臉。 以下就JWT在某些場景需要失效的簡單方案整理如下: **1. 白名單方式** 認證通過時,把JWT快取到Redis,登出時,從快取移除JWT。請求資源新增判斷JWT在快取中是否存在,不存在拒絕訪問。這種方式和cookie/session機制中的會話失效刪除session基本一致。 **2. 黑名單方式** 登出登入時,快取JWT至Redis,且快取有效時間設定為JWT的有效期,請求資源時判斷是否存在快取的黑名單中,存在則拒絕訪問。 白名單和黑名單的實現邏輯差不多,黑名單不需每次登入都將JWT快取,僅僅在某些特殊場景下需要快取JWT,給伺服器帶來的壓力要遠遠小於白名單的方式。 # 三. 黑名單方式實現 以下演示在退出登入時通過新增至黑名單的方式實現JWT失效 邏輯很明確,在呼叫退出登入介面時將JWT快取到Redis的黑名單中,然後在閘道器做判定請求頭的JWT是否在黑名單內做對應的處理。 ### 1. 認證中心(youlai-auth)退出登入介面 登出介面/oauth/logout的主要邏輯把JWT新增至Redis黑名單快取中,但沒必要把整個JWT字串都儲存下來,JWT的載體中有個jti(JWT ID)欄位宣告為JWT提供了唯一的識別符號。JWT解析的結構如下: ![](https://i.loli.net/2020/09/19/8SuirOcdvGt3ACm.png) 既然有這麼個欄位能作為JWT的唯一標識,從JWT解析出jti之後將其儲存到黑名單中作為判別依據,相較於儲存完整的JWT字串減少了儲存開銷。另外我們只需保證JWT在其有效期內使用者登出後失效就可以了,JWT有效期過了黑名單也就沒有存在的必要,所以我們這裡還需要設定黑名單的過期時間,不然黑名單的數量會無休止的越來越多,這是我們不想看到的。 ``` java @Api(tags = "認證中心") @RestController @RequestMapping("/oauth") @AllArgsConstructor public class AuthController { private RedisTemplate redisTemplate; @DeleteMapping("/logout") public Result logout(HttpServletRequest request) { String payload = request.getHeader(AuthConstants.JWT_PAYLOAD_KEY); JSONObject jsonObject = JSONUtil.parseObj(payload); String jti = jsonObject.getStr("jti"); // JWT唯一標識 long exp = jsonObject.getLong("exp"); // JWT過期時間戳(單位:秒) long currentTimeSeconds = System.currentTimeMillis() / 1000; if (exp < currentTimeSeconds) { // token已過期 return Result.custom(ResultCode.INVALID_TOKEN_OR_EXPIRED); } redisTemplate.opsForValue().set(AuthConstants.TOKEN_BLACKLIST_PREFIX + jti, null, (exp - currentTimeSeconds), TimeUnit.SECONDS); return Result.success(); } } ``` ### 2. 閘道器(youlai-gateway)的全域性過濾器 從請求頭提取JWT,解析出唯一標識jti,然後判斷該標識是否存在黑名單列表裡,如果是直接返回響應token失效的提示資訊。 ``` /** * 全域性過濾器 黑名單token過濾 */ @Component @Slf4j @AllArgsConstructor public class AuthGlobalFilter implements GlobalFilter, Ordered { private RedisTemplate redisTemplate; @SneakyThrows @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER); if (StrUtil.isBlank(token)) { return chain.filter(exchange); } token = token.replace(AuthConstants.JWT_TOKEN_PREFIX, Strings.EMPTY); JWSObject jwsObject = JWSObject.parse(token); String payload = jwsObject.getPayload().toString(); // 黑名單token(登出、修改密碼)校驗 JSONObject jsonObject = JSONUtil.parseObj(payload); String jti = jsonObject.getStr("jti"); // JWT唯一標識 Boolean isBlack = redisTemplate.hasKey(AuthConstants.TOKEN_BLACKLIST_PREFIX + jti); if (isBlack) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.OK); response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); response.getHeaders().set("Access-Control-Allow-Origin", "*"); response.getHeaders().set("Cache-Control", "no-cache"); String body = JSONUtil.toJsonStr(Result.custom(ResultCode.INVALID_TOKEN_OR_EXPIRED)); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8"))); return response.writeWith(Mono.just(buffer)); } ServerHttpRequest request = exchange.getRequest().mutate() .header(AuthConstants.JWT_PAYLOAD_KEY, payload) .build(); exchange = exchange.mutate().request(request).build(); return chain.filter(exchange); } @Override public int getOrder() { return 0; } } ``` ### 3. 登出後JWT失效測試 測試流程涉及到以下3個介面 ![](https://i.loli.net/2020/09/26/gQm39Z8KjY4yAG1.png) **1. 登入訪問資源** - http://localhost:9999/youlai-auth/oauth/token - http://localhost:9999/youlai-admin/users/me ![](https://i.loli.net/2020/09/26/vTf1B5lVsNoZGjJ.png) **2. 退出登入再次訪問資源** - http://localhost:9999/youlai-auth/oauth/logout - http://localhost:9999/youlai-admin/users/me 退出成功檢視redis快取黑名單列表 ![](https://i.loli.net/2020/09/26/sBtTDa4ZWJObFMU.png) 再次訪問登入使用者資訊如下: ![](https://i.loli.net/2020/09/26/xBwAYrSEJP5ivhQ.png) 可以看到退出登入後再次使用原JWT請求提示“token無效或已過期” **3. youlai-mall專案退出登入演示** 上面報“token無效或已過期”的響應碼是"A0230",這個對應的是Java開發手冊【泰山版】的錯誤碼 ![](https://i.loli.net/2020/09/27/ruOvYfkbFdWaq3n.png) 開啟之前搭建好的前端管理平臺[youlai-mall-admin-web](https://github.com/hxrui/youlai-mall-admin-web),修改src/util/request.js檔案中的無效token的響應碼為“A0230”,這樣在token無效的情況下提示重新登入 ![](https://i.loli.net/2020/09/27/QpPlOqoD1nre7ZR.png) 演示通過第三方介面除錯工具呼叫登出介面讓JWT失效,然後再次重新整理頁面請求資源會因為JWT的失效而跳轉到登入頁。 ![](https://i.loli.net/2020/09/27/FHqm5M2b7iUyDzL.gif) # 四. 總結 JWT是JSON風格輕量級的授權和身份認證規範,可實現無狀態、分散式應用的統一認證鑑權。但是事物往往具有兩面性,有利必有弊,因為JWT的無狀態,自生成後不借助外界條件唯一失效的方式就是過期。然而藉助的外界的條件後JWT便有狀態了的,也就是沒有所謂嚴格意義上的無狀態,其實也不必糾結於此,因為瑕不掩瑜。在白名單和黑名單的實現方式,這裡選擇了後者狀態性更小的黑名單方式。還是文中提到過的一句話,如果你有更好的實現方式,歡迎留言告知,不勝感激! 本篇是暫階段的Spring Cloud實戰的最終章了,也就是說基於Spring Boot +Spring Cloud+ Element-UI搭建的前後端分離基礎許可權框架已經搭建完成。後面計劃寫使用此基礎框架整合uni-app跨平臺前端框架開發一套商城小程式,希望大家給個關注或star,感謝感謝~ **本篇完整程式碼下載地址**: [youlai-mall](https://github.com/hxrui/youlai-mall) [youlai-mall-admin-web](https://github.com/hxrui/youlai-mall-admin-web)