From 709edd0345b2ecf9bb66ac1477288fc3630ed330 Mon Sep 17 00:00:00 2001 From: htlee Date: Sat, 14 Feb 2026 21:54:01 +0900 Subject: [PATCH] =?UTF-8?q?fix(security):=20=EC=9D=B8=EC=A6=9D=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20401=20=EC=9D=91=EB=8B=B5=20+=20CORS=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthenticationEntryPoint 추가: 미인증 요청에 403 대신 401 반환 - AccessDeniedHandler 추가: 권한 부족 시 403 + JSON body 반환 - CORS 설정 범위를 /api/** → /** 로 확장하여 에러 응답에도 CORS 헤더 포함 - exposedHeaders에 Authorization 추가 Co-Authored-By: Claude Opus 4.6 --- .../com/gcsc/guide/config/SecurityConfig.java | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gcsc/guide/config/SecurityConfig.java b/src/main/java/com/gcsc/guide/config/SecurityConfig.java index f6c7b90..2cc3f9c 100644 --- a/src/main/java/com/gcsc/guide/config/SecurityConfig.java +++ b/src/main/java/com/gcsc/guide/config/SecurityConfig.java @@ -1,10 +1,13 @@ 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; @@ -14,7 +17,9 @@ 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 @@ -22,6 +27,7 @@ import java.util.List; 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; @@ -46,6 +52,30 @@ public class SecurityConfig { .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); @@ -58,11 +88,12 @@ public class SecurityConfig { 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("/api/**", config); + source.registerCorsConfiguration("/**", config); return source; } }