Spring Cloud實戰 | 最七篇:Spring Cloud Gateway+Spring Security OAuth2集成統一認證授權平台下實現注銷使JWT失效方案


一. 前言

在上一篇文章介紹 youlai-mall 項目中,通過整合Spring Cloud Gateway、Spring Security OAuth2、JWT等技術實現了微服務下統一認證授權平台的搭建。最后在文末留下一個值得思考問題,就是如何在注銷、修改密碼、修改權限場景下讓JWT失效?所以在這篇文章來對方案和實現進行補充。想親身體驗的小伙伴們可以了解下 youlai-mall 項目和Spring Cloud實戰系列往期文章。

youlai-mall項目地址

項目文章

后台微服務

  1. Spring Cloud實戰 | 第一篇:Windows搭建Nacos服務
  2. Spring Cloud實戰 | 第二篇:Spring Cloud整合Nacos實現注冊中心
  3. Spring Cloud實戰 | 第三篇:Spring Cloud整合Nacos實現配置中心
  4. Spring Cloud實戰 | 第四篇:Spring Cloud整合Gateway實現API網關
  5. Spring Cloud實戰 | 第五篇:Spring Cloud整合OpenFeign實現微服務之間的調用
  6. Spring Cloud實戰 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT實現微服務統一認證授權
  7. Spring Cloud實戰 | 最七篇:Spring Cloud Gateway+Spring Security OAuth2集成統一認證授權平台下實現注銷使JWT失效方案
  8. Spring Cloud實戰 | 最八篇:Spring Cloud +Spring Security OAuth2+ Vue前后端分離模式下無感知刷新實現JWT續期
  9. Spring Cloud實戰 | 最九篇:Spring Security OAuth2認證服務器統一認證自定義異常處理
  10. Spring Cloud實戰 | 第十篇 :Spring Cloud + Nacos整合Seata 1.4.1最新版本實現微服務架構中的分布式事務,進階之路必須要邁過的檻
  11. Spring Cloud實戰 | 第十一篇 :Spring Cloud Gateway網關實現對RESTful接口權限和按鈕權限細粒度控制

后台管理前端

  1. vue-element-admin實戰 | 第一篇: 移除mock接入微服務接口,搭建SpringCloud+Vue前后端分離管理平台
  2. vue-element-admin實戰 | 第二篇: 最小改動接入后台實現根據權限動態加載菜單

微信小程序

  1. vue+uni-app商城實戰 | 第一篇:從0到1快速開發一個商城微信小程序,無縫接入Spring Cloud OAuth2認證授權登錄

應用部署

  1. Docker實戰 | 第一篇:Linux 安裝 Docker
  2. Docker實戰 | 第二篇:Docker部署nacos-server:1.4.0
  3. Docker實戰 | 第三篇:IDEA集成Docker插件實現一鍵自動打包部署微服務項目,一勞永逸的技術手段值得一試
  4. Docker實戰 | 第四篇:Docker安裝Nginx,實現基於vue-element-admin框架構建的項目線上部署
  5. Docker實戰 | 第五篇:Docker啟用TLS加密解決暴露2375端口引發的安全漏洞,被黑掉三台雲主機的教訓總結

二. 解決方案

JWT最大的一個優勢在於它是無狀態的,自身包含了認證鑒權所需要的所有信息,服務器端無需對其存儲,從而給服務器減少了存儲開銷。

但是無狀態引出的問題也是可想而知的,它無法作廢未過期的JWT。舉例說明注銷場景下,就傳統的cookie/session認證機制,只需要把存在服務器端的session刪掉就OK了。但是JWT呢,它是不存在服務器端的啊,好的那我刪存在客戶端的JWT行了吧。額,社會本就復雜別再欺騙自己了好么,被你在客戶端刪掉的JWT還是可以通過服務器端認證的。

首先明確一點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解析的結構如下:

既然有這么個字段能作為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();
    }
}

2. 網關(youlai-gateway)的全局過濾器

從請求頭提取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. 注銷后JWT失效測試

測試流程涉及到以下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,感謝感謝~

本篇完整代碼下載地址

youlai-mall

youlai-mall-admin-web


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM