package com.kamco.cd.kamcoback.config; import com.kamco.cd.kamcoback.auth.CustomAuthenticationProvider; import com.kamco.cd.kamcoback.auth.JwtAuthenticationFilter; import com.kamco.cd.kamcoback.auth.MenuAuthorizationManager; import com.kamco.cd.kamcoback.common.download.DownloadPaths; import jakarta.servlet.DispatcherType; 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.configurers.AbstractHttpConfigurer; 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.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; private final MenuAuthorizationManager menuAuthorizationManager; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) .authenticationProvider(customAuthenticationProvider) .authorizeHttpRequests( auth -> auth // .requestMatchers("/chunk_upload_test.html").authenticated() .requestMatchers("/monitor/health", "/monitor/health/**") .permitAll() // 맵시트 영역 전체 허용 (우선순위 최상단) .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") // ASYNC/ERROR 재디스패치는 막지 않기 (다운로드/스트리밍에서 필수) .dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.ERROR) .permitAll() // 다운로드는 인증 필요 .requestMatchers(HttpMethod.GET, DownloadPaths.PATTERNS) .authenticated() // 메뉴 등록 ADMIN만 가능 .requestMatchers(HttpMethod.POST, "/api/menu/auth") .hasAnyRole("ADMIN") // 에러 경로는 항상 허용 (이미 있지만 유지) .requestMatchers("/error") .permitAll() // preflight 허용 .requestMatchers(HttpMethod.OPTIONS, "/**") .permitAll() .requestMatchers( "/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/swagger-ui/**", "/v3/api-docs/**", "/chunk_upload_test.html", "/download_progress_test.html", "/api/model/file-chunk-upload", "/api/upload/file-chunk-upload", "/api/upload/chunk-upload-complete", "/api/change-detection/**", "/api/layer/map/**", "/api/layer/tile-url", "/api/layer/tile-url-year") .permitAll() // 로그인한 사용자만 가능 IAM .requestMatchers( "/api/user/**", "/api/my/menus", "/api/common-code/**", "/api/members/*/password", "/api/training-data/label/**", "/api/training-data/review/**") .authenticated() // 나머지는 메뉴권한 .anyRequest() .access(menuAuthorizationManager)) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } /** CORS 설정 */ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(List.of("*")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); config.setExposedHeaders(List.of("Content-Disposition")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } @Bean public HttpFirewall httpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowUrlEncodedSlash(true); firewall.setAllowUrlEncodedDoubleSlash(true); firewall.setAllowUrlEncodedPercent(true); firewall.setAllowSemicolon(true); return firewall; } }