session 만료 방식 변경, @Guest annotation 추가

This commit is contained in:
ckx6954 2024-12-10 12:21:40 +09:00
parent b255a24a2c
commit 6d5649535f
6 changed files with 110 additions and 30 deletions

View File

@ -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 {
}

View File

@ -1,36 +1,36 @@
package io.company.localhost.common.config; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy; 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 @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor @RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
@ -74,7 +74,7 @@ public class SecurityConfig {
.sessionAuthenticationStrategy(sessionControlStrategy()) // 세션 제어 전략 설정 .sessionAuthenticationStrategy(sessionControlStrategy()) // 세션 제어 전략 설정
.maximumSessions(MAX_SESSION) // 최대 세션 설정 .maximumSessions(MAX_SESSION) // 최대 세션 설정
.maxSessionsPreventsLogin(MAX_SESSION_PRENT) // 최대 세션 초과 로그인 방지 여부 .maxSessionsPreventsLogin(MAX_SESSION_PRENT) // 최대 세션 초과 로그인 방지 여부
.expiredSessionStrategy(new SimpleRedirectSessionInformationExpiredStrategy(INVALID_URL)) // 세션 만료 리디렉션 .expiredSessionStrategy(new CustomSessionInformationExpiredStrategy()) // 세션 만료 401
.sessionRegistry(sessionRegistry()) // 세션 레지스트리 설정 .sessionRegistry(sessionRegistry()) // 세션 레지스트리 설정
) )
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/**")) // CSRF 비활성화 .csrf(csrf -> csrf.ignoringRequestMatchers("/api/**")) // CSRF 비활성화

View File

@ -21,4 +21,8 @@ public enum UserErrorCode implements ErrorCode {
public ApiResponse<?> getApiResponse() { public ApiResponse<?> getApiResponse() {
return ApiResponse.error(this.getHttpStatus() , this.getMessage()); return ApiResponse.error(this.getHttpStatus() , this.getMessage());
} }
public ApiResponse<?> getApiResponse(String message) {
return ApiResponse.error(this.getHttpStatus() , message);
}
} }

View File

@ -1,10 +1,12 @@
package io.company.localhost.common.security.manager; package io.company.localhost.common.security.manager;
import java.util.List; import io.company.localhost.common.annotation.Guest;
import java.util.function.Supplier; import io.company.localhost.common.security.mapper.MapBasedUrlRoleMapper;
import java.util.stream.Collectors; import io.company.localhost.common.security.service.DynamicAuthorizationService;
import io.company.localhost.vo.MemberVo;
import org.checkerframework.common.reflection.qual.Invoke; import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager; 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.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.security.web.util.matcher.RequestMatcherEntry;
import org.springframework.stereotype.Component; 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.handler.HandlerMappingIntrospector;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import io.company.localhost.common.security.mapper.MapBasedUrlRoleMapper; import java.util.List;
import io.company.localhost.common.security.service.DynamicAuthorizationService; import java.util.function.Supplier;
import jakarta.annotation.PostConstruct; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@ -29,6 +33,7 @@ public class CustomDynamicAuthorizationManager implements AuthorizationManager<R
List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> mappings; List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> mappings;
private static final AuthorizationDecision DENY = new AuthorizationDecision(false); private static final AuthorizationDecision DENY = new AuthorizationDecision(false);
private final HandlerMappingIntrospector handlerMappingIntrospector; private final HandlerMappingIntrospector handlerMappingIntrospector;
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
// 클래스 초기화 동적으로 URL-권한 매핑을 설정 // 클래스 초기화 동적으로 URL-권한 매핑을 설정
@PostConstruct @PostConstruct
@ -45,6 +50,17 @@ public class CustomDynamicAuthorizationManager implements AuthorizationManager<R
// 요청에 대해 권한을 확인하는 메소드 // 요청에 대해 권한을 확인하는 메소드
@Override @Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, RequestAuthorizationContext object) { public AuthorizationResult authorize(Supplier<Authentication> 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<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) { for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
RequestMatcher matcher = mapping.getRequestMatcher(); RequestMatcher matcher = mapping.getRequestMatcher();
@ -68,6 +84,24 @@ public class CustomDynamicAuthorizationManager implements AuthorizationManager<R
} }
} }
// @Guest가 적용된 요청인지 확인
private boolean isGuestRequest(HttpServletRequest request) {
try {
HandlerExecutionChain handlerExecutionChain = requestMappingHandlerMapping.getHandler(request);
if (handlerExecutionChain != null) {
Object handler = handlerExecutionChain.getHandler();
if (handler instanceof HandlerMethod handlerMethod) {
return handlerMethod.getMethod().isAnnotationPresent(Guest.class); }
}
} catch (Exception e) {
return false;
}
return false;
}
@Override @Override
public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) { public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
AuthorizationManager.super.verify(authentication, object); AuthorizationManager.super.verify(authentication, object);
@ -76,7 +110,6 @@ public class CustomDynamicAuthorizationManager implements AuthorizationManager<R
//deprecated 됬는디 넣으면 세팅이 //deprecated 됬는디 넣으면 세팅이
@Deprecated @Deprecated
@Invoke
@Override @Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) { public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
return (AuthorizationDecision)authorize(authentication,object); return (AuthorizationDecision)authorize(authentication,object);

View File

@ -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)));
}
}

View File

@ -1,15 +1,14 @@
package io.company.localhost.controller.common; 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.ParameterCheck;
import io.company.localhost.common.annotation.ReqMap; import io.company.localhost.common.annotation.ReqMap;
import io.company.localhost.common.dto.MapDto; import io.company.localhost.common.dto.MapDto;
import io.company.localhost.common.response.ApiResponse; import io.company.localhost.common.response.ApiResponse;
import io.company.localhost.service.TestService; import io.company.localhost.service.TestService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j @Slf4j
@RequestMapping("/api/test/") @RequestMapping("/api/test/")
@ -30,11 +29,11 @@ public class TestController {
//MyBatis TEST //MyBatis TEST
@GetMapping("getCong") @GetMapping("getCong")
public ApiResponse getCong() { public ApiResponse<?> getCong() {
return ApiResponse.ok(testService.getCong()); return ApiResponse.ok(testService.getCong());
} }
@Guest
@ParameterCheck @ParameterCheck
@GetMapping("reqTest1") @GetMapping("reqTest1")
public ApiResponse<?> reqTest1(@ReqMap MapDto map) { public ApiResponse<?> reqTest1(@ReqMap MapDto map) {