S SmartDocs
Chuỗi bài: Springboot java 151 dòng · Cập nhật 2026-08-14

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();
    }
}

Bài viết liên quan

Springboot java EN Cập nhật 2026-08-14

BookstoreApiApplication.java

BookstoreApiApplication.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/BookstoreApiApplication.java).

Đọc bài viết →
Springboot java EN Cập nhật 2026-08-14

DataInitializer.java

DataInitializer.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/DataInitializer.java).

Đọc bài viết →
Springboot java EN Cập nhật 2026-08-14

AuthController.java

AuthController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/AuthController.java).

Đọc bài viết →
Springboot java EN Cập nhật 2026-08-14

BookController.java

BookController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/BookController.java).

Đọc bài viết →
Springboot java EN Cập nhật 2026-08-14

ApiErrorResponse.java

ApiErrorResponse.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/dto/ApiErrorResponse.java).

Đọc bài viết →
Springboot java EN Cập nhật 2026-08-14

AuthResponse.java

AuthResponse.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/dto/AuthResponse.java).

Đọc bài viết →