package com.gcsc.guide.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.gcsc.guide.auth.JwtAuthenticationFilter; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; 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.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.time.LocalDateTime; import java.util.List; import java.util.Map; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final ObjectMapper objectMapper; @Value("${app.cors.allowed-origins:http://localhost:5173,https://guide.gc-si.dev}") private List allowedOrigins; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers( "/api/auth/**", "/api/health", "/actuator/health", "/h2-console/**", "/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**" ).permitAll() .requestMatchers("/api/admin/**").authenticated() .anyRequest().authenticated() ) .exceptionHandling(ex -> ex .authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); objectMapper.writeValue(response.getOutputStream(), Map.of( "status", 401, "error", "Unauthorized", "message", "인증이 필요합니다. JWT 토큰을 Authorization 헤더에 포함하세요.", "timestamp", LocalDateTime.now().toString() )); }) .accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); objectMapper.writeValue(response.getOutputStream(), Map.of( "status", 403, "error", "Forbidden", "message", "접근 권한이 없습니다.", "timestamp", LocalDateTime.now().toString() )); }) ) .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(allowedOrigins); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setExposedHeaders(List.of("Authorization")); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }