package com.kamco.cd.kamcoback.config; import com.kamco.cd.kamcoback.auth.CustomAuthenticationProvider; import com.kamco.cd.kamcoback.auth.JwtAuthenticationFilter; import java.util.List; import lombok.RequiredArgsConstructor; 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.builders.HttpSecurity; 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.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.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthenticationProvider customAuthenticationProvider; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .formLogin(form -> form.disable()) .httpBasic(basic -> basic.disable()) .logout(logout -> logout.disable()) .authenticationProvider(customAuthenticationProvider) .authorizeHttpRequests( auth -> auth // 맵시트 영역 전체 허용 (우선순위 최상단) .requestMatchers("/api/mapsheet/**") .permitAll() // 업로드 명시적 허용 .requestMatchers(HttpMethod.POST, "/api/mapsheet/upload") .permitAll() // ADMIN만 접근 .requestMatchers("/api/test/admin") .hasRole("ADMIN") // ADMIN, LABELER 접근 .requestMatchers("/api/test/label") .hasAnyRole("ADMIN", "LABELER") // ADMIN, REVIEWER 접근 .requestMatchers("/api/test/review") .hasAnyRole("ADMIN", "REVIEWER") .requestMatchers("/error") .permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**") .permitAll() // preflight 허용 .requestMatchers( "/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/swagger-ui/**", "/api/members/*/password", "/v3/api-docs/**") .permitAll() .anyRequest() .authenticated()) .addFilterBefore( jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. ; return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } /** * CORS 설정 * * @return */ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성 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 등 자격증명 포함 요청을 허용할지 설정 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(new AntPathRequestMatcher("/api/mapsheet/**")); } }