From 6d5649535fd74b3df8edb6a9eb0e65b14a3b9e2e Mon Sep 17 00:00:00 2001 From: ckx6954 Date: Tue, 10 Dec 2024 12:21:40 +0900 Subject: [PATCH] =?UTF-8?q?session=20=EB=A7=8C=EB=A3=8C=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EB=B3=80=EA=B2=BD,=20@Guest=20annotation=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../localhost/common/annotation/Guest.java | 12 +++++ .../common/config/SecurityConfig.java | 30 +++++------ .../common/exception/code/UserErrorCode.java | 4 ++ .../CustomDynamicAuthorizationManager.java | 53 +++++++++++++++---- ...stomSessionInformationExpiredStrategy.java | 32 +++++++++++ .../controller/common/TestController.java | 9 ++-- 6 files changed, 110 insertions(+), 30 deletions(-) create mode 100644 src/main/java/io/company/localhost/common/annotation/Guest.java create mode 100644 src/main/java/io/company/localhost/common/security/session/CustomSessionInformationExpiredStrategy.java diff --git a/src/main/java/io/company/localhost/common/annotation/Guest.java b/src/main/java/io/company/localhost/common/annotation/Guest.java new file mode 100644 index 0000000..3988d76 --- /dev/null +++ b/src/main/java/io/company/localhost/common/annotation/Guest.java @@ -0,0 +1,12 @@ +package io.company.localhost.common.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Guest { +} diff --git a/src/main/java/io/company/localhost/common/config/SecurityConfig.java b/src/main/java/io/company/localhost/common/config/SecurityConfig.java index 7b31b2b..f6a0d5a 100644 --- a/src/main/java/io/company/localhost/common/config/SecurityConfig.java +++ b/src/main/java/io/company/localhost/common/config/SecurityConfig.java @@ -1,36 +1,36 @@ package io.company.localhost.common.config; +import io.company.localhost.common.filter.WebCorsFilter; +import io.company.localhost.common.security.dsl.RestApiDsl; +import io.company.localhost.common.security.handler.MemberAuthFailureHandler; +import io.company.localhost.common.security.handler.MemberAuthSuccessHandler; +import io.company.localhost.common.security.handler.RestAccessDeniedHandler; +import io.company.localhost.common.security.handler.RestAuthenticationEntryPointHandler; +import io.company.localhost.common.security.service.CustomRememberMeServices; +import io.company.localhost.common.security.service.MemberPrincipalDetailService; +import io.company.localhost.common.security.session.AuthenticationSessionControlStrategy; +import io.company.localhost.common.security.session.CustomSessionInformationExpiredStrategy; +import io.company.localhost.common.security.session.CustomSessionRegistryImpl; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.intercept.RequestAuthorizationContext; - import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy; -import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy; - -import io.company.localhost.common.security.dsl.RestApiDsl; -import io.company.localhost.common.security.handler.RestAuthenticationEntryPointHandler; -import io.company.localhost.common.filter.WebCorsFilter; -import io.company.localhost.common.security.handler.MemberAuthFailureHandler; -import io.company.localhost.common.security.handler.MemberAuthSuccessHandler; -import io.company.localhost.common.security.handler.RestAccessDeniedHandler; -import io.company.localhost.common.security.service.CustomRememberMeServices; -import io.company.localhost.common.security.service.MemberPrincipalDetailService; -import io.company.localhost.common.security.session.AuthenticationSessionControlStrategy; -import io.company.localhost.common.security.session.CustomSessionRegistryImpl; -import lombok.RequiredArgsConstructor; @Configuration @EnableWebSecurity +@EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { @@ -74,7 +74,7 @@ public class SecurityConfig { .sessionAuthenticationStrategy(sessionControlStrategy()) // 세션 제어 전략 설정 .maximumSessions(MAX_SESSION) // 최대 세션 수 설정 .maxSessionsPreventsLogin(MAX_SESSION_PRENT) // 최대 세션 수 초과 시 로그인 방지 여부 - .expiredSessionStrategy(new SimpleRedirectSessionInformationExpiredStrategy(INVALID_URL)) // 세션 만료 시 리디렉션 + .expiredSessionStrategy(new CustomSessionInformationExpiredStrategy()) // 세션 만료 시 401 .sessionRegistry(sessionRegistry()) // 세션 레지스트리 설정 ) .csrf(csrf -> csrf.ignoringRequestMatchers("/api/**")) // CSRF 비활성화 diff --git a/src/main/java/io/company/localhost/common/exception/code/UserErrorCode.java b/src/main/java/io/company/localhost/common/exception/code/UserErrorCode.java index fe59260..b4a8498 100644 --- a/src/main/java/io/company/localhost/common/exception/code/UserErrorCode.java +++ b/src/main/java/io/company/localhost/common/exception/code/UserErrorCode.java @@ -21,4 +21,8 @@ public enum UserErrorCode implements ErrorCode { public ApiResponse getApiResponse() { return ApiResponse.error(this.getHttpStatus() , this.getMessage()); } + + public ApiResponse getApiResponse(String message) { + return ApiResponse.error(this.getHttpStatus() , message); + } } diff --git a/src/main/java/io/company/localhost/common/security/manager/CustomDynamicAuthorizationManager.java b/src/main/java/io/company/localhost/common/security/manager/CustomDynamicAuthorizationManager.java index d4c3222..4932bb7 100644 --- a/src/main/java/io/company/localhost/common/security/manager/CustomDynamicAuthorizationManager.java +++ b/src/main/java/io/company/localhost/common/security/manager/CustomDynamicAuthorizationManager.java @@ -1,10 +1,12 @@ package io.company.localhost.common.security.manager; -import java.util.List; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import org.checkerframework.common.reflection.qual.Invoke; +import io.company.localhost.common.annotation.Guest; +import io.company.localhost.common.security.mapper.MapBasedUrlRoleMapper; +import io.company.localhost.common.security.service.DynamicAuthorizationService; +import io.company.localhost.vo.MemberVo; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; @@ -16,12 +18,14 @@ import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; -import io.company.localhost.common.security.mapper.MapBasedUrlRoleMapper; -import io.company.localhost.common.security.service.DynamicAuthorizationService; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; @Component @RequiredArgsConstructor @@ -29,6 +33,7 @@ public class CustomDynamicAuthorizationManager implements AuthorizationManager>> mappings; private static final AuthorizationDecision DENY = new AuthorizationDecision(false); private final HandlerMappingIntrospector handlerMappingIntrospector; + private final RequestMappingHandlerMapping requestMappingHandlerMapping; // 클래스 초기화 후 동적으로 URL-권한 매핑을 설정 @PostConstruct @@ -45,6 +50,17 @@ public class CustomDynamicAuthorizationManager implements AuthorizationManager authentication, RequestAuthorizationContext object) { + HttpServletRequest request = object.getRequest(); + + Authentication auth = authentication.get(); + + // @Guest 메서드인지 확인 + if (isGuestRequest(request) && !(auth.getPrincipal() instanceof MemberVo)) { + boolean allowGuest = !auth.isAuthenticated() || "anonymousUser".equals(auth.getPrincipal()); + return new AuthorizationDecision(allowGuest); + } + + for (RequestMatcherEntry> mapping : this.mappings) { RequestMatcher matcher = mapping.getRequestMatcher(); @@ -68,6 +84,24 @@ public class CustomDynamicAuthorizationManager implements AuthorizationManager authentication, RequestAuthorizationContext object) { AuthorizationManager.super.verify(authentication, object); @@ -76,7 +110,6 @@ public class CustomDynamicAuthorizationManager implements AuthorizationManager authentication, RequestAuthorizationContext object) { return (AuthorizationDecision)authorize(authentication,object); diff --git a/src/main/java/io/company/localhost/common/security/session/CustomSessionInformationExpiredStrategy.java b/src/main/java/io/company/localhost/common/security/session/CustomSessionInformationExpiredStrategy.java new file mode 100644 index 0000000..d1a394b --- /dev/null +++ b/src/main/java/io/company/localhost/common/security/session/CustomSessionInformationExpiredStrategy.java @@ -0,0 +1,32 @@ +package io.company.localhost.common.security.session; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.company.localhost.common.exception.code.UserErrorCode; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.security.web.session.SessionInformationExpiredEvent; +import org.springframework.security.web.session.SessionInformationExpiredStrategy; + +import java.io.IOException; + +public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { + // 세션 만료 시 401 상태 코드 설정 + jakarta.servlet.http.HttpServletResponse response = event.getResponse(); + + // HTTP 상태 코드를 401 (Unauthorized)로 설정 + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + // 응답의 콘텐츠 타입을 JSON으로 설정 + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + String message = "세션이 만료되었습니다."; + + // 사용자 정의 에러 코드와 메시지를 JSON 형식으로 응답 + response.getWriter().write(mapper.writeValueAsString(UserErrorCode.NOT_AUTH_USER.getApiResponse(message))); + } +} diff --git a/src/main/java/io/company/localhost/controller/common/TestController.java b/src/main/java/io/company/localhost/controller/common/TestController.java index ab1c4a7..52e805d 100644 --- a/src/main/java/io/company/localhost/controller/common/TestController.java +++ b/src/main/java/io/company/localhost/controller/common/TestController.java @@ -1,15 +1,14 @@ package io.company.localhost.controller.common; -import org.springframework.web.bind.annotation.*; - +import io.company.localhost.common.annotation.Guest; import io.company.localhost.common.annotation.ParameterCheck; import io.company.localhost.common.annotation.ReqMap; import io.company.localhost.common.dto.MapDto; import io.company.localhost.common.response.ApiResponse; - import io.company.localhost.service.TestService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; @Slf4j @RequestMapping("/api/test/") @@ -30,11 +29,11 @@ public class TestController { //MyBatis TEST @GetMapping("getCong") - public ApiResponse getCong() { + public ApiResponse getCong() { return ApiResponse.ok(testService.getCong()); } - + @Guest @ParameterCheck @GetMapping("reqTest1") public ApiResponse reqTest1(@ReqMap MapDto map) {