在上一篇文章介紹 youlai-mall 項目中,經過整合Spring Cloud Gateway、Spring Security OAuth二、JWT等技術實現了微服務下統一認證受權平臺的搭建。最後在文末留下一個值得思考問題,就是如何在註銷、修改密碼、修改權限場景下讓JWT失效?因此在這篇文章來對方案和實現進行補充。想親身體驗的小夥伴們能夠了解下 youlai-mall 項目和Spring Cloud實戰系列往期文章。html
Spring Cloud實戰系列往期文章vue
JWT最大的一個優點在於它是無狀態的,自身包含了認證鑑權所須要的全部信息,服務器端無需對其存儲,從而給服務器減小了存儲開銷。java
可是無狀態引出的問題也是可想而知的,它沒法做廢未過時的JWT。舉例說明註銷場景下,就傳統的cookie/session認證機制,只須要把存在服務器端的session刪掉就OK了。可是JWT呢,它是不存在服務器端的啊,好的那我刪存在客戶端的JWT行了吧。額,社會本就複雜別再欺騙本身了好麼,被你在客戶端刪掉的JWT仍是能夠經過服務器端認證的。git
首先明確一點JWT失效的惟一途徑就是等過時,就是說不借助外力的狀況下,沒法達到某些場景下須要主動使JWT失效的目的。而外力則是在服務器端存儲着JWT的狀態,在請求資源時添加判斷邏輯,這與JWT特性無狀態是相互矛盾的存在。可是,你要知道若是你選擇走上了JWT這條路,那就沒得選了。若是你有好的方式,但願你來打我臉。github
如下就JWT在某些場景須要失效的簡單方案整理以下:web
1. 白名單方式redis
認證經過時,把JWT緩存到Redis,註銷時,從緩存移除JWT。請求資源添加判斷JWT在緩存中是否存在,不存在拒絕訪問。這種方式和cookie/session機制中的會話失效刪除session基本一致。json
2. 黑名單方式小程序
註銷登陸時,緩存JWT至Redis,且緩存有效時間設置爲JWT的有效期,請求資源時判斷是否存在緩存的黑名單中,存在則拒絕訪問。
白名單和黑名單的實現邏輯差很少,黑名單不需每次登陸都將JWT緩存,僅僅在某些特殊場景下須要緩存JWT,給服務器帶來的壓力要遠遠小於白名單的方式。
如下演示在退出登陸時經過添加至黑名單的方式實現JWT失效
邏輯很明確,在調用退出登陸接口時將JWT緩存到Redis的黑名單中,而後在網關作斷定請求頭的JWT是否在黑名單內作對應的處理。
登出接口/oauth/logout的主要邏輯把JWT添加至Redis黑名單緩存中,但不必把整個JWT字符串都存儲下來,JWT的載體中有個jti(JWT ID)字段聲明爲JWT提供了惟一的標識符。JWT解析的結構以下:
既然有這麼個字段能做爲JWT的惟一標識,從JWT解析出jti以後將其存儲到黑名單中做爲判別依據,相較於存儲完整的JWT字符串減小了存儲開銷。另外咱們只需保證JWT在其有效期內用戶登出後失效就能夠了,JWT有效期過了黑名單也就沒有存在的必要,因此咱們這裏還須要設置黑名單的過時時間,否則黑名單的數量會無休止的愈來愈多,這是咱們不想看到的。
@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(); } }
從請求頭提取JWT,解析出惟一標識jti,而後判斷該標識是否存在黑名單列表裏,若是是直接返回響應token失效的提示信息。
/** * 全局過濾器 黑名單token過濾 */ @Component @Slf4j @AllArgsConstructor public class AuthGlobalFilter implements GlobalFilter, Ordered { private RedisTemplate redisTemplate; @SneakyThrows @Override public Mono<Void> 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個接口
1. 登陸訪問資源
2. 退出登陸再次訪問資源
退出成功查看redis緩存黑名單列表
再次訪問登陸用戶信息以下:
能夠看到退出登陸後再次使用原JWT請求提示「token無效或已過時」
3. youlai-mall項目退出登陸演示
上面報「token無效或已過時」的響應碼是"A0230",這個對應的是Java開發手冊【泰山版】的錯誤碼
打開以前搭建好的前端管理平臺youlai-mall-admin-web,修改src/util/request.js文件中的無效token的響應碼爲「A0230」,這樣在token無效的狀況下提示從新登陸
演示經過第三方接口調試工具調用註銷接口讓JWT失效,而後再次刷新頁面請求資源會由於JWT的失效而跳轉到登陸頁。
JWT是JSON風格輕量級的受權和身份認證規範,可實現無狀態、分佈式應用的統一認證鑑權。可是事物每每具備兩面性,有利必有弊,由於JWT的無狀態,自生成後不借助外界條件惟一失效的方式就是過時。然而藉助的外界的條件後JWT便有狀態了的,也就是沒有所謂嚴格意義上的無狀態,其實也沒必要糾結於此,由於瑕不掩瑜。在白名單和黑名單的實現方式,這裏選擇了後者狀態性更小的黑名單方式。仍是文中提到過的一句話,若是你有更好的實現方式,歡迎留言告知,不勝感激!
本篇是暫階段的Spring Cloud實戰的最終章了,也就是說基於Spring Boot +Spring Cloud+ Element-UI搭建的先後端分離基礎權限框架已經搭建完成。後面計劃寫使用此基礎框架整合uni-app跨平臺前端框架開發一套商城小程序,但願你們給個關注或star,感謝感謝~
本篇完整代碼下載地址: