JwtAuthenticationFilter.java
Springboot/bookstore-api/src/main/java/com/example/bookstore/security/JwtAuthenticationFilter.java
package com.example.bookstore.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* JWT 認證過濾器——整個 JWT 機制的「守門員」。
*
* 【運作位置】
* 它被插在 Spring Security 過濾器鏈(Filter Chain)中,
* 「每一個」HTTP 請求進入 Controller 之前都會先經過這裡。
*
* 【工作流程】
* 請求 → 取出 Header 中的 token → 驗證 → 合法就把使用者放入
* SecurityContext(本次請求的「已登入名單」)→ 放行給後續授權檢查
*
* 繼承 OncePerRequestFilter:保證同一請求只執行一次過濾邏輯。
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
/** Authorization Header 的標準前綴,例如:Authorization: Bearer eyJhb... */
private static final String BEARER_PREFIX = "Bearer ";
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtService jwtService,
UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// ---------- 步驟 1:從 Header 取出 Authorization ----------
String authHeader = request.getHeader("Authorization");
// 沒帶 token 或格式不對 → 直接放行。
// 注意:「放行」不代表「允許存取」!後面的授權規則會判斷
// 這個「匿名請求」能不能存取目標 API(不能就回 401)。
if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
filterChain.doFilter(request, response);
return;
}
// ---------- 步驟 2:去掉 "Bearer " 前綴,取得純 token ----------
String token = authHeader.substring(BEARER_PREFIX.length());
String username;
try {
// 解析 token 取得帳號;若 token 無效(簽章錯誤、過期、格式壞掉)
// 會拋出例外
username = jwtService.extractUsername(token);
} catch (Exception e) {
// token 無效 → 當作匿名請求放行,受保護的 API 之後自然會回 401
filterChain.doFilter(request, response);
return;
}
// ---------- 步驟 3:載入使用者並二次驗證 token ----------
// 條件「SecurityContext 尚未有認證資訊」可避免重複處理
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 從資料庫載入使用者(含最新的角色權限)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
// ---------- 步驟 4:建立「已認證」物件並放入 SecurityContext ----------
// 三個參數:使用者資料、憑證(JWT 模式不需要,給 null)、權限清單
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
// 附加請求細節(來源 IP、Session ID 等),供稽核使用
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 關鍵一步!放入 SecurityContext 後,Spring Security
// 就視這個請求為「已登入」,後續授權規則據此判斷角色權限
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
// ---------- 步驟 5:放行,讓請求繼續往下走 ----------
filterChain.doFilter(request, response);
}
}
관련 글
BookstoreApiApplication.java
BookstoreApiApplication.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/BookstoreApiApplication.java).
글 읽기 →DataInitializer.java
DataInitializer.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/DataInitializer.java).
글 읽기 →SecurityConfig.java
SecurityConfig.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/SecurityConfig.java).
글 읽기 →AuthController.java
AuthController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/AuthController.java).
글 읽기 →BookController.java
BookController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/BookController.java).
글 읽기 →ApiErrorResponse.java
ApiErrorResponse.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/dto/ApiErrorResponse.java).
글 읽기 →