From 4dd5deb1256d9f7c687361e0797936b5e12f4c24 Mon Sep 17 00:00:00 2001 From: songhyeonpk Date: Thu, 20 Mar 2025 16:02:59 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20Security=20OAuth2=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0,=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 - .../server/infrastructure/security/.gitkeep | 0 src/main/resources/application-security.yml | 25 +++++-------------- 3 files changed, 6 insertions(+), 20 deletions(-) delete mode 100644 src/main/java/com/ftm/server/infrastructure/security/.gitkeep diff --git a/build.gradle b/build.gradle index fe44ae1..128397b 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' // Security - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.session:spring-session-data-redis' diff --git a/src/main/java/com/ftm/server/infrastructure/security/.gitkeep b/src/main/java/com/ftm/server/infrastructure/security/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/application-security.yml b/src/main/resources/application-security.yml index 0bf6e8f..d424991 100644 --- a/src/main/resources/application-security.yml +++ b/src/main/resources/application-security.yml @@ -3,22 +3,9 @@ spring: activate: on-profile: "security" - security: - oauth2: - client: - registration: - kakao: - client-id: ${KAKAO_CLIENT_ID} - client-secret: ${KAKAO_CLIENT_SECRET} - client-authentication-method: client_secret_post - authorization-grant-type: authorization_code - redirect-uri: "{baseUrl}/login/oauth2/code/kakao" - client-name: Kakao - provider: kakao - provider: - kakao: - authorization-uri: https://kauth.kakao.com/oauth/authorize - token-uri: https://kauth.kakao.com/oauth/token - user-info-uri: https://kapi.kakao.com/v2/user/me - user-info-authentication-method: header - user-name-attribute: id \ No newline at end of file + kakao: + clinet-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + redirect-uri: "${BASE_URL}/api/auth/kakao/callback" + token-uri: "https://kauth.kakao.com/oauth/token" + user-info-uri: "https://kapi.kakao.com/v2/user/me" \ No newline at end of file From 1b5f6e94b17514531aec4ca241d97cdeba4542ce Mon Sep 17 00:00:00 2001 From: songhyeonpk Date: Thu, 20 Mar 2025 16:03:30 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20Security=20=EA=B8=B0=EB=B3=B8=20con?= =?UTF-8?q?fig=20=ED=8C=8C=EC=9D=BC=20=EB=B0=8F=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/SecurityConfig.java | 109 ++++++++++++++++++ .../handler/PermissionDeniedHandler.java | 33 ++++++ .../handler/SecurityResponseHandler.java | 28 +++++ .../handler/UnauthenticatedAccessHandler.java | 30 +++++ 4 files changed, 200 insertions(+) create mode 100644 src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java create mode 100644 src/main/java/com/ftm/server/infrastructure/security/handler/PermissionDeniedHandler.java create mode 100644 src/main/java/com/ftm/server/infrastructure/security/handler/SecurityResponseHandler.java create mode 100644 src/main/java/com/ftm/server/infrastructure/security/handler/UnauthenticatedAccessHandler.java diff --git a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java new file mode 100644 index 0000000..4a01ff4 --- /dev/null +++ b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java @@ -0,0 +1,109 @@ +package com.ftm.server.infrastructure.security; + +import com.ftm.server.infrastructure.security.handler.PermissionDeniedHandler; +import com.ftm.server.infrastructure.security.handler.UnauthenticatedAccessHandler; +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.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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableWebSecurity(debug = true) +@RequiredArgsConstructor +public class SecurityConfig { + + private final UnauthenticatedAccessHandler unauthenticatedAccessHandler; + private final PermissionDeniedHandler permissionDeniedHandler; + + // CORS 에서 허용할 HTTP 메서드 목록 + public static final List CORS_ALLOWED_METHODS = + List.of( + HttpMethod.GET, + HttpMethod.POST, + HttpMethod.PUT, + HttpMethod.PATCH, + HttpMethod.DELETE, + HttpMethod.HEAD); + + // CORS 에서 허용할 도메인 목록 + public static final List CORS_ALLOWED_ORIGINS = + List.of( + "http://localhost:8080", // 로컬 환경 서버 도메인 + "https://dev-api.fittheman.site", // 개발 환경 서버 도메인 + "https://fittheman.site"); // 개발 환경 클라이언트 도메인 + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // csrf 비활성화 + .csrf(AbstractHttpConfigurer::disable) + // http basic 비활성화 + .httpBasic(AbstractHttpConfigurer::disable) + // 폼로그인 비활성화 + .formLogin(AbstractHttpConfigurer::disable) + // 로그아웃 비활성화 + .logout(AbstractHttpConfigurer::disable) + // 세션 관리 + .sessionManagement( + session -> + session.sessionFixation() + .migrateSession() // 세션 고정 보호 + .maximumSessions(1) // 동시 로그인 1개 제한 + .maxSessionsPreventsLogin(false)) // 기존 세션 만료 후 새 로그인 허용 + // 예외 핸들링 + .exceptionHandling( + exception -> + exception + // 인증되지 않은 요청 예외 처리 + .authenticationEntryPoint(unauthenticatedAccessHandler) + // 접근 권한 부족 예외 처리 + .accessDeniedHandler(permissionDeniedHandler)) + // cors 설정 + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + // 경로 인가 설정 + .authorizeHttpRequests( + authorize -> { + authorize + // 정적 리소스 경로 허용 + .requestMatchers("/docs/**") + .permitAll(); + + // TODO: 요청 허용 특정 API 추가 (회원가입, 로그인 등) + + // 그 외 모든 요청은 인증 필요 + authorize.anyRequest().authenticated(); + }); + + return http.build(); + } + + // Password 암호화 설정 + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + // CORS 설정 + @Bean + public UrlBasedCorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedHeader("*"); + CORS_ALLOWED_ORIGINS.forEach(config::addAllowedOriginPattern); + CORS_ALLOWED_METHODS.forEach(config::addAllowedMethod); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return source; + } +} diff --git a/src/main/java/com/ftm/server/infrastructure/security/handler/PermissionDeniedHandler.java b/src/main/java/com/ftm/server/infrastructure/security/handler/PermissionDeniedHandler.java new file mode 100644 index 0000000..5de0c40 --- /dev/null +++ b/src/main/java/com/ftm/server/infrastructure/security/handler/PermissionDeniedHandler.java @@ -0,0 +1,33 @@ +package com.ftm.server.infrastructure.security.handler; + +import com.ftm.server.common.response.ApiResponse; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.context.SecurityContextPersistenceFilter; +import org.springframework.stereotype.Component; + +/** 인증은 되었지만 접근 권한이 없는 경우 예외처리하는 핸들러 */ +@Component +@RequiredArgsConstructor +public class PermissionDeniedHandler implements AccessDeniedHandler { + + private final SecurityResponseHandler securityResponseHandler; + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + securityResponseHandler.sendResponse( + response, ApiResponse.fail(ErrorResponseCode.NOT_AUTHORIZATION)); + + SecurityContextPersistenceFilter filter = new SecurityContextPersistenceFilter(); + } +} diff --git a/src/main/java/com/ftm/server/infrastructure/security/handler/SecurityResponseHandler.java b/src/main/java/com/ftm/server/infrastructure/security/handler/SecurityResponseHandler.java new file mode 100644 index 0000000..bd4e3c2 --- /dev/null +++ b/src/main/java/com/ftm/server/infrastructure/security/handler/SecurityResponseHandler.java @@ -0,0 +1,28 @@ +package com.ftm.server.infrastructure.security.handler; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ftm.server.common.response.ApiResponse; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** 시큐리티 필터단에서 응답을 처리하는 핸들러 */ +@Component +@RequiredArgsConstructor +public class SecurityResponseHandler { + + private final ObjectMapper objectMapper; + + public void sendResponse(HttpServletResponse response, ApiResponse apiResponse) + throws IOException { + String jsonResponse = objectMapper.writeValueAsString(apiResponse); + + response.setStatus(apiResponse.getStatus()); + response.setContentType(APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(jsonResponse); + } +} diff --git a/src/main/java/com/ftm/server/infrastructure/security/handler/UnauthenticatedAccessHandler.java b/src/main/java/com/ftm/server/infrastructure/security/handler/UnauthenticatedAccessHandler.java new file mode 100644 index 0000000..fbc2c2b --- /dev/null +++ b/src/main/java/com/ftm/server/infrastructure/security/handler/UnauthenticatedAccessHandler.java @@ -0,0 +1,30 @@ +package com.ftm.server.infrastructure.security.handler; + +import com.ftm.server.common.response.ApiResponse; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +/** 인증이 필요한 요청에서 인증되지 않은 유저가 요청할 경우 예외처리하는 핸들러 */ +@Component +@RequiredArgsConstructor +public class UnauthenticatedAccessHandler implements AuthenticationEntryPoint { + + private final SecurityResponseHandler securityResponseHandler; + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) + throws IOException, ServletException { + securityResponseHandler.sendResponse( + response, ApiResponse.fail(ErrorResponseCode.NOT_AUTHENTICATED)); + } +}