package com.kamco.cd.training.config; import com.kamco.cd.training.auth.CustomAuthenticationProvider; import com.kamco.cd.training.auth.JwtAuthenticationFilter; import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; 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.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.firewall.HttpFirewall; import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain( org.springframework.security.config.annotation.web.builders.HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter, CustomAuthenticationProvider customAuthenticationProvider) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .formLogin(form -> form.disable()) // /monitor 에서 Basic 인증을 쓰려면 disable 하면 안됨 .httpBasic(basic -> {}) .logout(logout -> logout.disable()) .authenticationProvider(customAuthenticationProvider) .authorizeHttpRequests( auth -> auth // monitor .requestMatchers("/monitor/health", "/monitor/health/**") .permitAll() .requestMatchers("/monitor/**") .authenticated() // Basic으로 인증되게끔 // mapsheet .requestMatchers("/api/mapsheet/**") .permitAll() .requestMatchers(HttpMethod.POST, "/api/mapsheet/upload") .permitAll() // test role .requestMatchers("/api/test/admin") .hasRole("ADMIN") .requestMatchers("/api/test/label") .hasAnyRole("ADMIN", "LABELER") .requestMatchers("/api/test/review") .hasAnyRole("ADMIN", "REVIEWER") // common permit .requestMatchers("/error") .permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**") .permitAll() .requestMatchers( "/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/swagger-ui/**", "/v3/api-docs/**", "/api/upload/chunk-upload-dataset", "/api/upload/chunk-upload-complete", "/download_progress_test.html", "/api/models/download/**") .permitAll() .requestMatchers("/api/members/*/password") .authenticated() // default .anyRequest() .authenticated()) // JWT 필터는 앞단에 .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** CORS 설정 - application.yml에서 환경별로 관리 */ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성 // application.yml에서 환경별로 설정된 도메인 사용 config.setAllowedOriginPatterns(List.of("*")); // 도메인 허용 config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정 config.setExposedHeaders(List.of("Content-Disposition", "Authorization")); config.setMaxAge(3600L); // Preflight 요청 캐시 (1시간) UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); /** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */ source.registerCorsConfiguration("/**", config); // CORS 정책을 등록 return source; } @Bean public HttpFirewall httpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowUrlEncodedSlash(true); firewall.setAllowUrlEncodedDoubleSlash(true); firewall.setAllowUrlEncodedPercent(true); firewall.setAllowSemicolon(true); return firewall; } /** 완전 제외(필터 자체를 안 탐) */ @Bean public WebSecurityCustomizer webSecurityCustomizer() { return (web) -> web.ignoring().requestMatchers("/api/mapsheet/**"); } }