From c193e24f86c1c8039af90bcb1da55d920abd28f6 Mon Sep 17 00:00:00 2001 From: kwonhee1 Date: Thu, 30 Oct 2025 13:17:33 +0900 Subject: [PATCH 1/2] save' --- .../NextLevel/demo/config/SecurityConfig.java | 65 +++++++++++++++++++ .../demo/config/SecurityRequestMatcher.txt | 44 +++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/main/java/NextLevel/demo/config/SecurityRequestMatcher.txt diff --git a/src/main/java/NextLevel/demo/config/SecurityConfig.java b/src/main/java/NextLevel/demo/config/SecurityConfig.java index 945f879..1eaed61 100644 --- a/src/main/java/NextLevel/demo/config/SecurityConfig.java +++ b/src/main/java/NextLevel/demo/config/SecurityConfig.java @@ -14,20 +14,32 @@ import NextLevel.demo.user.service.LoginService; import NextLevel.demo.util.jwt.JWTUtil; import jakarta.persistence.EntityManager; +import jakarta.servlet.http.HttpServletRequest; +import java.util.function.Supplier; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authorization.AuthorityAuthorizationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.Customizer; 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.AuthorizeHttpRequestsConfigurer; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.servlet.HandlerExceptionResolver; @Configuration @@ -81,8 +93,61 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/api1/**").hasRole("USER") .requestMatchers("/social/**").hasRole("SOCIAL") .requestMatchers("/admin/**").hasRole("ADMIN") + .requestMatchers("/admin/**").access(new AuthorizationManager() { + @Override + public AuthorizationDecision check( + Supplier authentication, + RequestAuthorizationContext object + ) { + return null; + } + }) .anyRequest().denyAll() // 그 외 요청은 모두 거절 ) + .authorizeHttpRequests( + new Customizer.AuthorizationManagerRequestMatcherRegistry>() { + @Override + public void customize( + AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry authorizationManagerRequestMatcherRegistry) { + authorizationManagerRequestMatcherRegistry.requestMatchers() + } + }) + .authorizeHttpRequests(auth-> + auth.requestMatchers("/api1/**").access(new AuthorizationManager() { + @Override + public void verify(Supplier authentication, + RequestAuthorizationContext object) { + AuthorizationManager.super.verify(authentication, object); + } + @Override + public AuthorizationDecision check(Supplier authentication, + RequestAuthorizationContext object) { + withRoleHierarchy(AuthorityAuthorizationManager + .hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role })) + } + }) + ) + .authorizeHttpRequests( + new Customizer.AuthorizationManagerRequestMatcherRegistry>() { + @Override + public void customize( + AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry authorizationManagerRequestMatcherRegistry) { + authorizationManagerRequestMatcherRegistry.requestMatchers(new RequestMatcher() { + @Override + public boolean matches(HttpServletRequest request) { + return false; + } + }).access(new AuthorizationManager() { + + @Override + public AuthorizationDecision check(Supplier authentication, + RequestAuthorizationContext object) { + return null; + } + }); + } + } + ) .oauth2Login(oauth2 -> oauth2 .authorizedClientRepository(new NullAuthorizedClientRepository()) diff --git a/src/main/java/NextLevel/demo/config/SecurityRequestMatcher.txt b/src/main/java/NextLevel/demo/config/SecurityRequestMatcher.txt new file mode 100644 index 0000000..68a20e6 --- /dev/null +++ b/src/main/java/NextLevel/demo/config/SecurityRequestMatcher.txt @@ -0,0 +1,44 @@ +HttpSecurity에 context를 주면서 context에 List로 uri별 권한 설정을 쌓게 됨 + context : AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry + uri별 권한 설정 : RequestMatcherEntry {RequestMatcher matcher, AuthorizationManager manager} + +RequestMatcher : url 기반의 정보 +AuthorizationManger : authentication을 가지고 AuthorizationDecision을 반환함 + +HttpSecurity.build() 실행시 + AuthorizeHttpRequestsConfigurer.configure() 실행 + 저장된 모든 uri별 권한 설정을 AuthorizationFilter 필터 한개로 변환하고 security filter chain에 등록함 + AuthorizationFilter를 만들 때 AuthorizationManager를 생성자로 넘겨줌 + List>>를 AuthorizationManager 으로 변환함 (by RequestMatcherDelegatingAuthorizationManager.class 생성자) (filter에서는 HttpServletRequest만 사용하기 때문 by gpt) + AuthorizationManager.authorize 함수를 톧해 권한을 설정함 + +AuthorizationFilter 에서는 AuthorizationManager를 가지고 모든 요청을 url과 권한을 가지고 판단함 + AuthorizationManager.authorize 함수를 실행시킴 + +1. HttpSecurity에서 authorizeHttpRequests() 함수 실행 + AuthorizeHttpRequestsConfigure.class 반환 + AuthorizeHttpRequestsConfigure내부 class AuthorizationManagerRequestMatcherRegistry, AuthorizedUrl를 반복하며url 입력, manager 입력을 받는다 + 입력 받은 RequestMatcher와 AuthorizationManager를 RequestMatherEntry로 두고 RequestMatcherDelegatingAuthorizationManager.Builder에 쌓는다 (builder에서는 List>>으로 저장함) +2. HttpSecurity.build() 함수 실행시 + AuthorizeHttpRequestConfigure.configure()함수 실행됨 + RequestMatcherDelegatingAuthorizationManager.Builder.build()를 통해 AuthorizationManager 생성 + (형변환은 하지 않음 RequestMatcherDelegatingAuthorizationManager의 내부 변수 mappings가 List>> 형태임 + AuthorizationManager authorizationManager 가지는 Authorization 생성 / filter chain 등록 +3. 매 요청에 AuthorizationFilter 작동 + 매 요청 마다 List>>를 순회함 + 입력 받은 RequestMatherEntry.RequestMatcher를 통해 url 검증 + RequestMatherEntry.AuthorizationManager를 통해 authentic 검증 (AuthorizationManager.authorize() 함수 호출) +4. 매 요청 마다 발생하는 uri별 Exception을 다르게 처리하기 위해 authorize함수를 override하여 throw CustomException을 처리 예정 + 문제 발생 check함수에서 throw를 맘대로 던져도 되는가? + 다행히 AuthorizationFilter는 boolean값인 AuthorizationResult을 반환하는 check함수에서 Exception을 반환하는 verify로 변환을 준비중이다 + 아직 변환되지는 않았지만 문제되 점은 크게 많지 않아 보임 + 1. this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, result); 문장 실행 안됨 + AuthenticFilter내부 변수 AuthenticationEventPublisher eventPublisher에 저장중이다 + publisher는 filter를 생성하는 AuthorizeHttpRequestsConfigure에서 부터 내려왔으며 ApplicationContext에 저장된 객체이다 + 실패 횟수, 로그 등에 사용 되는 event임 (건너 뛰는 것은 좋지 않지만 다른 방법이 없으면 무시하겠음) + 해결 방법 탐색 + 1. event 무시하고 그냥 throw 던지기 + 2. 깔끔하게 AuthorizationFilter를 직접 구현하여 실패시도 알맞은 publishAuthorizationEvent를 발행하게 한다 + AuthorizationFilter를 생성하는 AuthorizeHttpRequestsConfigure을 상속해야 하는데 하필 AuthorizeHttpRequestsConfigure은 final class이다 (불가능) + 3. 매우 더럽게 HttpSecurity부터 override하고, AuthorizeHttpRequestsConfigure의 모든 interface를 구현한다 (사실 복 붙이라 직접 구현은 아니겠지만) (싫음) + 깔금하게 log event따위 무시하고 throw 던지기 (언젠가 security의 버전이 올라가며 AuthorizationManager.check가 완전히 사라진다면 무시된 event에 대한 코드도 수정이 되어있을 것으로 예상) From 6bebb5b04502d8323a042a5ea37a3fffe15ac27f Mon Sep 17 00:00:00 2001 From: kwonhee1 Date: Mon, 3 Nov 2025 23:48:20 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix=20(Security=20Config):=20uri=20?= =?UTF-8?q?=EB=B3=84=20error=20=EB=B0=9C=EC=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit public : 모두 접근 가능 로그인 안함 : 401, 00401, No authentication please login social이 User 접근 : 403, 00403, Additional data required 다른 사람이 admin 접근 : 401, 10001, not admin --- .../CustomAuthorizationDeniedException.java | 26 ++++ .../NextLevel/demo/config/SecurityConfig.java | 120 +++++++++--------- 2 files changed, 84 insertions(+), 62 deletions(-) create mode 100644 src/main/java/NextLevel/demo/config/CustomAuthorizationDeniedException.java diff --git a/src/main/java/NextLevel/demo/config/CustomAuthorizationDeniedException.java b/src/main/java/NextLevel/demo/config/CustomAuthorizationDeniedException.java new file mode 100644 index 0000000..ee8c16d --- /dev/null +++ b/src/main/java/NextLevel/demo/config/CustomAuthorizationDeniedException.java @@ -0,0 +1,26 @@ +package NextLevel.demo.config; + +import NextLevel.demo.exception.ErrorCode; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.authorization.AuthorizationResult; + +public class CustomAuthorizationDeniedException extends AuthorizationDeniedException { + + private ErrorCode errorCode; + + public CustomAuthorizationDeniedException( + ErrorCode errorCode + ) { + super(errorCode.errorMessage, new AuthorizationResult() { + @Override + public boolean isGranted() { + return false; + } + }); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/NextLevel/demo/config/SecurityConfig.java b/src/main/java/NextLevel/demo/config/SecurityConfig.java index 1eaed61..c096c23 100644 --- a/src/main/java/NextLevel/demo/config/SecurityConfig.java +++ b/src/main/java/NextLevel/demo/config/SecurityConfig.java @@ -9,41 +9,36 @@ import NextLevel.demo.oauth.OAuthFailureHandler; import NextLevel.demo.oauth.OAuthSuccessHandler; import NextLevel.demo.oauth.SocialLoginService; +import NextLevel.demo.role.UserRole; import NextLevel.demo.user.repository.UserHistoryRepository; import NextLevel.demo.user.repository.UserRepository; import NextLevel.demo.user.service.LoginService; import NextLevel.demo.util.jwt.JWTUtil; import jakarta.persistence.EntityManager; -import jakarta.servlet.http.HttpServletRequest; +import java.util.Collection; import java.util.function.Supplier; -import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.authorization.AuthorityAuthorizationManager; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authorization.AuthorizationDecision; -import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.Customizer; 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.AuthorizeHttpRequestsConfigurer; -import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.authentication.logout.LogoutFilter; -import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.servlet.HandlerExceptionResolver; @Configuration @EnableWebSecurity +@Slf4j public class SecurityConfig { private final JWTUtil jwtUtil; @@ -90,64 +85,59 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/login/**").permitAll() .requestMatchers("/public/**").permitAll() .requestMatchers("/payment/**").permitAll() - .requestMatchers("/api1/**").hasRole("USER") .requestMatchers("/social/**").hasRole("SOCIAL") - .requestMatchers("/admin/**").hasRole("ADMIN") - .requestMatchers("/admin/**").access(new AuthorizationManager() { + //.requestMatchers("/api1/**").hasRole("USER") + .requestMatchers("/api1/**").access(new AuthorizationManager() { @Override - public AuthorizationDecision check( + public AuthorizationDecision check(Supplier authentication, RequestAuthorizationContext object) { + verify(authentication, object); + return new AuthorizationDecision(true); + } + @Override + public void verify( Supplier authentication, RequestAuthorizationContext object ) { - return null; + if(authentication.get() instanceof AnonymousAuthenticationToken) + throw new CustomAuthorizationDeniedException(ErrorCode.NO_AUTHENTICATED); + + Collection authorities = authentication.get().getAuthorities(); + + if (authorities.containsAll(UserRole.USER.getAuthorities())) + return; + + if (authorities.containsAll(UserRole.SOCIAL.getAuthorities())) + throw new CustomAuthorizationDeniedException(ErrorCode.NEED_ADDITIONAL_DATA); + + throw new CustomException(ErrorCode.SIBAL_WHAT_IS_IT, "not social, admin, user, anonymous"); } }) + // .requestMatchers("/admin/**").hasRole("ADMIN") + .requestMatchers("/admin/**").access(new AuthorizationManager() { + @Override + public AuthorizationDecision check(Supplier authentication, + RequestAuthorizationContext object) { + verify(authentication, object); + return new AuthorizationDecision(true); + } + + @Override + public void verify( + Supplier authentication, + RequestAuthorizationContext object + ) { + if(authentication.get() instanceof AnonymousAuthenticationToken) + throw new CustomAuthorizationDeniedException(ErrorCode.NO_AUTHENTICATED); + + Collection authorities = authentication.get().getAuthorities(); + if (authorities.containsAll(UserRole.ADMIN.getAuthorities())) + return; + + throw new CustomAuthorizationDeniedException(ErrorCode.NOT_ADMIN); + } + }) .anyRequest().denyAll() // 그 외 요청은 모두 거절 ) - .authorizeHttpRequests( - new Customizer.AuthorizationManagerRequestMatcherRegistry>() { - @Override - public void customize( - AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry authorizationManagerRequestMatcherRegistry) { - authorizationManagerRequestMatcherRegistry.requestMatchers() - } - }) - .authorizeHttpRequests(auth-> - auth.requestMatchers("/api1/**").access(new AuthorizationManager() { - @Override - public void verify(Supplier authentication, - RequestAuthorizationContext object) { - AuthorizationManager.super.verify(authentication, object); - } - @Override - public AuthorizationDecision check(Supplier authentication, - RequestAuthorizationContext object) { - withRoleHierarchy(AuthorityAuthorizationManager - .hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role })) - } - }) - ) - .authorizeHttpRequests( - new Customizer.AuthorizationManagerRequestMatcherRegistry>() { - @Override - public void customize( - AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry authorizationManagerRequestMatcherRegistry) { - authorizationManagerRequestMatcherRegistry.requestMatchers(new RequestMatcher() { - @Override - public boolean matches(HttpServletRequest request) { - return false; - } - }).access(new AuthorizationManager() { - - @Override - public AuthorizationDecision check(Supplier authentication, - RequestAuthorizationContext object) { - return null; - } - }); - } - } - ) .oauth2Login(oauth2 -> oauth2 .authorizedClientRepository(new NullAuthorizedClientRepository()) @@ -168,8 +158,14 @@ public AuthorizationDecision check(Supplier authentication, new CustomException(ErrorCode.NO_AUTHENTICATED)); }) .accessDeniedHandler((request, response, accessDeniedException)-> { - accessDeniedException.printStackTrace(); - handlerExceptionResolver.resolveException(request, response, null, new CustomException(ErrorCode.NEED_ADDITIONAL_DATA)); + if(accessDeniedException instanceof CustomAuthorizationDeniedException) { + ErrorCode errorCode = ((CustomAuthorizationDeniedException) accessDeniedException).getErrorCode(); + handlerExceptionResolver.resolveException(request, response, null, new CustomException(errorCode)); + } + else{ + accessDeniedException.printStackTrace(); + handlerExceptionResolver.resolveException(request, response, null, new CustomException(ErrorCode.SIBAL_WHAT_IS_IT, accessDeniedException.getMessage())); + } }) )