SecurityConfig.java
Springboot/bookstore-api/src/main/java/com/example/bookstore/config/SecurityConfig.java
package com.example.bookstore.config;
import com.example.bookstore.dto.ApiErrorResponse;
import com.example.bookstore.security.JwtAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security 主設定檔——認證與授權規則都集中在這裡。
*
* 【兩個核心名詞】
* 認證 Authentication:「你是誰?」 → 驗帳號密碼、驗 JWT
* 授權 Authorization :「你能做什麼?」→ 依角色決定能存取哪些 API
*
* 【401 與 403 的差別】(超常考!)
* 401 Unauthorized:沒通過「認證」——沒帶 token 或 token 無效
* 403 Forbidden :通過認證但沒通過「授權」——登入了,但角色權限不足
*/
@Configuration
@EnableWebSecurity // 啟用 Spring Security 的 Web 安全功能
@EnableMethodSecurity // 啟用方法層級授權(@PreAuthorize 等註解)
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final ObjectMapper objectMapper; // Jackson 物件,用來手動輸出 JSON
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
ObjectMapper objectMapper) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.objectMapper = objectMapper;
}
/**
* 安全過濾器鏈(Security Filter Chain)——所有安全規則的核心。
* 每個 HTTP 請求都會依序通過這條「過濾器鏈」再到達 Controller。
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. 關閉 CSRF 防護。
// CSRF 攻擊利用的是「瀏覽器自動帶 Cookie」的特性;
// 本專案是無狀態 REST API,用 Header 帶 JWT、不用 Cookie,
// 因此不受 CSRF 影響,可以安全關閉。
.csrf(csrf -> csrf.disable())
// 2. Session 管理設為 STATELESS(無狀態)。
// 伺服器完全不建立、不使用 HttpSession,
// 每個請求都只靠 JWT 自我證明身分——這是 Token 認證的精髓。
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 3. URL 授權規則(由上而下逐條比對,先中先贏)
.authorizeHttpRequests(auth -> auth
// 註冊與登入:所有人(含未登入)都可存取
.requestMatchers("/api/auth/**").permitAll()
// 查詢書籍(GET):開放訪客瀏覽
.requestMatchers(HttpMethod.GET, "/api/books/**").permitAll()
// 刪除書籍:僅限 ADMIN 角色
// (hasRole("ADMIN") 會自動比對 "ROLE_ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/books/**").hasRole("ADMIN")
// 其餘書籍操作(POST / PUT / PATCH):USER 或 ADMIN 皆可
.requestMatchers("/api/books/**").hasAnyRole("USER", "ADMIN")
// 沒列到的任何請求:一律要求登入
.anyRequest().authenticated())
// 4. 自訂「安全相關錯誤」的回應格式(預設是 HTML 錯誤頁,
// 對 REST API 不友善,改成統一的 JSON)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(unauthorizedEntryPoint()) // 401
.accessDeniedHandler(forbiddenHandler())) // 403
// 5. 把我們的 JWT 過濾器插在「帳密認證過濾器」之前,
// 讓每個請求先經過 JWT 驗證
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* 401 Unauthorized 的 JSON 回應。
* 觸發時機:匿名(或 token 無效的)請求存取需要登入的 API。
*/
private AuthenticationEntryPoint unauthorizedEntryPoint() {
return (request, response, authException) -> writeErrorJson(
response, HttpStatus.UNAUTHORIZED,
"尚未登入或 Token 無效,請先登入取得有效的 Token",
request.getRequestURI());
}
/**
* 403 Forbidden 的 JSON 回應。
* 觸發時機:已登入,但角色權限不足(例如 USER 想刪除書籍)。
*/
private AccessDeniedHandler forbiddenHandler() {
return (request, response, accessDeniedException) -> writeErrorJson(
response, HttpStatus.FORBIDDEN,
"權限不足,無法執行此操作",
request.getRequestURI());
}
/** 手動把 ApiErrorResponse 寫成 JSON 回應(過濾器層無法用 @RestControllerAdvice) */
private void writeErrorJson(HttpServletResponse response, HttpStatus status,
String message, String path) throws java.io.IOException {
response.setStatus(status.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
ApiErrorResponse body = ApiErrorResponse.of(status, message, path);
response.getWriter().write(objectMapper.writeValueAsString(body));
}
/**
* 密碼編碼器:BCrypt。
* - 單向雜湊:無法從雜湊值反推出原始密碼
* - 自動加鹽(salt):同一組密碼每次產生的雜湊值都不同
* - 計算成本可調:暴力破解的代價極高
* 註冊時用它加密;登入時 Spring Security 用它比對。
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 認證管理器:登入 API 會呼叫它來驗證帳號密碼。
* 它背後會使用我們的 CustomUserDetailsService(載入使用者)
* 與上面的 PasswordEncoder(比對密碼)。
*/
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
Related articles
BookstoreApiApplication.java
BookstoreApiApplication.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/BookstoreApiApplication.java).
Read article →DataInitializer.java
DataInitializer.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/DataInitializer.java).
Read article →AuthController.java
AuthController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/AuthController.java).
Read article →BookController.java
BookController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/BookController.java).
Read article →ApiErrorResponse.java
ApiErrorResponse.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/dto/ApiErrorResponse.java).
Read article →AuthResponse.java
AuthResponse.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/dto/AuthResponse.java).
Read article →