iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Vibe Coding

做一個團購後端,順便搞懂那些事系列 第 10 篇

Day 10|token 過期之後,哪些請求該擋?

  • 分享至 

  • xImage
  •  

access token 的效期是 15 分鐘,假使使用者 10:00 登入,10:15 token 過期,10:16 使用者的 App 仍夾著這顆舊 token 送出三個請求:

10:16 的請求 結果 是否正確
換新 token(POST /auth/refresh) 放行,換發成功 正確
連 WebSocket 收通知(STOMP CONNECT) 拒絕連線 正確
登出(POST /login/logout) 修正前:401,登出失敗 錯誤,已修正

同一過期 token,三種結局,差別在於這三個入口後面還有沒有人把關。


情境一: 換 token 為什麼要放行?

Refresh Token 是拿來「重新取得身分憑證」的,所以 /auth/refresh 本身必須能讓「帶著過期 Access Token 的請求」通過 Security。

直覺寫法會在 JwtAuthenticationFilter 發現 Access token 過期時,直接回 401:

if (!valid(token)) {
    response.setStatus(401);
    return;                 // chain 中止,後面都不跑
}

問題在於這裡其實有兩個不同的判斷,發生在 filter chain 的不同位置:JwtAuthenticationFilter 判斷「有沒有身分」,AuthorizationFilter 依白名單判斷「這個 API 需不需要身分」。前者若在後者之前就結束請求,AuthorizationFilter 根本沒機會查到 /auth/refresh 在白名單裡,換發端點就跟著被擋,從下圖看流程,等於在第一關就被擋下來,根本走不到換發流程。而 Access token 過期,正是該用 Refresh token 換新的時候。換發端點進不去,使用者只能重新登入。

https://ithelp.ithome.com.tw/upload/images/20260919/201686674QWPGDrwJB.png

最後的結論就是驗不過也不擋,失敗時 ifPresent 什麼都不做,doFilter 寫在 if 外面,不論成功還是失敗都會往下走:

String header = request.getHeader("Authorization");

if (header != null && header.startsWith("Bearer ")) {
    tokenAuthenticator.tryAuthenticate(header.substring(7))
            .ifPresent(auth -> SecurityContextHolder.getContext().setAuthentication(auth));
}

filterChain.doFilter(request, response);

情境二: WebSocket 為什麼反而要當場擋?

同一個 TokenAuthenticator,WebSocket 入口的處理相反,驗不過直接丟例外:

Authentication auth = tokenAuthenticator.tryAuthenticate(header.substring(7))
        .orElseThrow(() -> new MessageDeliveryException("JWT 驗證失敗"));
accessor.setUser(auth);

/ws/** 在 HTTP 層屬於白名單,握手不檢查身分;WebSocket 這一層也沒有授權層,只掛了這一個攔截器,WebSocket 連線上,只有建立連線的那一刻會檢查身分。


情境三: 登出為什麼曾被誤擋?

登出 API /login/logout 原本不在白名單內。10:15 access token 過期後,使用者在 10:16 按登出,反而因驗證未通過而登出失敗,以下是修正前的流程:

https://ithelp.ithome.com.tw/upload/images/20260919/20168667cCDee7NkwE.png

事實上,過期的Access token根本不會走進AuthService。

登出的邏輯在 AuthService(參考下方程式碼),AuthService會先嘗試把 access token 加入黑名單,再把 body 裡的 refresh token 從 Redis 刪掉。

public void logout(String refreshTokenId, String accessToken) {
    // Blacklist access token in Redis
    if (accessToken != null) {
        try {
            Claims claims = jwtUtils.parseToken(accessToken);
            String jti = claims.getId();
            if (jti != null) {
                long remaining = jwtUtils.getRemainingSeconds(claims);
                tokenRedisService.blacklistAccessToken(jti, remaining);
            }
        } catch (Exception e) {
            // Access token may already be expired — ignore
        }
    }

    // Delete refresh token from Redis
    if (refreshTokenId != null) {
        tokenRedisService.deleteRefreshToken(refreshTokenId);
    }
}

登出真正需要的是 refresh token,登出不需要再靠有效的 access token 證明身分,因此把 /login/logout 加入白名單:

public static final String[] WHITE_LIST = {
        "/login/create_token",
        "/login/logout",
        "/auth/refresh",
        "/order/get_all_shops",
        "/ws/**",
        "/swagger-ui.html",
        "/swagger-ui/**",
        "/v3/api-docs",
        "/v3/api-docs/**"
};

修改後的流程:
https://ithelp.ithome.com.tw/upload/images/20260919/20168667A6V1uaI2Xo.png

白名單讓登出走得進 Service;黑名單只處理還有效的 access token。


上一篇
Day 9|JWT 怎麼測?先分定義清楚誰該負責任!
下一篇
Day 11|10:05 撤了客服,10:06 舊 token 為什麼還能看全部訂單
系列文
做一個團購後端,順便搞懂那些事 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言