Skip to content

Commit e013101

Browse files
authored
Merge pull request #66 from DMU-NextLevel/feat/security
Feat/security
2 parents 82203e4 + 6bebb5b commit e013101

3 files changed

Lines changed: 139 additions & 8 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package NextLevel.demo.config;
2+
3+
import NextLevel.demo.exception.ErrorCode;
4+
import org.springframework.security.authorization.AuthorizationDeniedException;
5+
import org.springframework.security.authorization.AuthorizationResult;
6+
7+
public class CustomAuthorizationDeniedException extends AuthorizationDeniedException {
8+
9+
private ErrorCode errorCode;
10+
11+
public CustomAuthorizationDeniedException(
12+
ErrorCode errorCode
13+
) {
14+
super(errorCode.errorMessage, new AuthorizationResult() {
15+
@Override
16+
public boolean isGranted() {
17+
return false;
18+
}
19+
});
20+
this.errorCode = errorCode;
21+
}
22+
23+
public ErrorCode getErrorCode() {
24+
return errorCode;
25+
}
26+
}

src/main/java/NextLevel/demo/config/SecurityConfig.java

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,36 @@
99
import NextLevel.demo.oauth.OAuthFailureHandler;
1010
import NextLevel.demo.oauth.OAuthSuccessHandler;
1111
import NextLevel.demo.oauth.SocialLoginService;
12+
import NextLevel.demo.role.UserRole;
1213
import NextLevel.demo.user.repository.UserHistoryRepository;
1314
import NextLevel.demo.user.repository.UserRepository;
1415
import NextLevel.demo.user.service.LoginService;
1516
import NextLevel.demo.util.jwt.JWTUtil;
1617
import jakarta.persistence.EntityManager;
17-
import lombok.RequiredArgsConstructor;
18+
import java.util.Collection;
19+
import java.util.function.Supplier;
20+
import lombok.extern.slf4j.Slf4j;
1821
import org.springframework.beans.factory.annotation.Autowired;
1922
import org.springframework.beans.factory.annotation.Qualifier;
2023
import org.springframework.context.annotation.Bean;
2124
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
26+
import org.springframework.security.authorization.AuthorizationDecision;
27+
import org.springframework.security.authorization.AuthorizationManager;
2228
import org.springframework.security.config.Customizer;
2329
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
2430
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
2531
import org.springframework.security.config.http.SessionCreationPolicy;
26-
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
27-
import org.springframework.security.crypto.password.PasswordEncoder;
28-
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
32+
import org.springframework.security.core.Authentication;
33+
import org.springframework.security.core.GrantedAuthority;
2934
import org.springframework.security.web.SecurityFilterChain;
35+
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
3036
import org.springframework.security.web.authentication.logout.LogoutFilter;
3137
import org.springframework.web.servlet.HandlerExceptionResolver;
3238

3339
@Configuration
3440
@EnableWebSecurity
41+
@Slf4j
3542
public class SecurityConfig {
3643

3744
private final JWTUtil jwtUtil;
@@ -78,9 +85,57 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
7885
.requestMatchers("/login/**").permitAll()
7986
.requestMatchers("/public/**").permitAll()
8087
.requestMatchers("/payment/**").permitAll()
81-
.requestMatchers("/api1/**").hasRole("USER")
8288
.requestMatchers("/social/**").hasRole("SOCIAL")
83-
.requestMatchers("/admin/**").hasRole("ADMIN")
89+
//.requestMatchers("/api1/**").hasRole("USER")
90+
.requestMatchers("/api1/**").access(new AuthorizationManager<RequestAuthorizationContext>() {
91+
@Override
92+
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
93+
verify(authentication, object);
94+
return new AuthorizationDecision(true);
95+
}
96+
@Override
97+
public void verify(
98+
Supplier<Authentication> authentication,
99+
RequestAuthorizationContext object
100+
) {
101+
if(authentication.get() instanceof AnonymousAuthenticationToken)
102+
throw new CustomAuthorizationDeniedException(ErrorCode.NO_AUTHENTICATED);
103+
104+
Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
105+
106+
if (authorities.containsAll(UserRole.USER.getAuthorities()))
107+
return;
108+
109+
if (authorities.containsAll(UserRole.SOCIAL.getAuthorities()))
110+
throw new CustomAuthorizationDeniedException(ErrorCode.NEED_ADDITIONAL_DATA);
111+
112+
throw new CustomException(ErrorCode.SIBAL_WHAT_IS_IT, "not social, admin, user, anonymous");
113+
}
114+
})
115+
// .requestMatchers("/admin/**").hasRole("ADMIN")
116+
.requestMatchers("/admin/**").access(new AuthorizationManager<RequestAuthorizationContext>() {
117+
@Override
118+
public AuthorizationDecision check(Supplier<Authentication> authentication,
119+
RequestAuthorizationContext object) {
120+
verify(authentication, object);
121+
return new AuthorizationDecision(true);
122+
}
123+
124+
@Override
125+
public void verify(
126+
Supplier<Authentication> authentication,
127+
RequestAuthorizationContext object
128+
) {
129+
if(authentication.get() instanceof AnonymousAuthenticationToken)
130+
throw new CustomAuthorizationDeniedException(ErrorCode.NO_AUTHENTICATED);
131+
132+
Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
133+
if (authorities.containsAll(UserRole.ADMIN.getAuthorities()))
134+
return;
135+
136+
throw new CustomAuthorizationDeniedException(ErrorCode.NOT_ADMIN);
137+
}
138+
})
84139
.anyRequest().denyAll() // 그 외 요청은 모두 거절
85140
)
86141

@@ -103,8 +158,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
103158
new CustomException(ErrorCode.NO_AUTHENTICATED));
104159
})
105160
.accessDeniedHandler((request, response, accessDeniedException)-> {
106-
accessDeniedException.printStackTrace();
107-
handlerExceptionResolver.resolveException(request, response, null, new CustomException(ErrorCode.NEED_ADDITIONAL_DATA));
161+
if(accessDeniedException instanceof CustomAuthorizationDeniedException) {
162+
ErrorCode errorCode = ((CustomAuthorizationDeniedException) accessDeniedException).getErrorCode();
163+
handlerExceptionResolver.resolveException(request, response, null, new CustomException(errorCode));
164+
}
165+
else{
166+
accessDeniedException.printStackTrace();
167+
handlerExceptionResolver.resolveException(request, response, null, new CustomException(ErrorCode.SIBAL_WHAT_IS_IT, accessDeniedException.getMessage()));
168+
}
108169
})
109170
)
110171

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
HttpSecurity에 context를 주면서 context에 List로 uri별 권한 설정을 쌓게 됨
2+
context : AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry
3+
uri별 권한 설정 : RequestMatcherEntry {RequestMatcher matcher, AuthorizationManager<RequestAuthorizationContext> manager}
4+
5+
RequestMatcher : url 기반의 정보
6+
AuthorizationManger<RequestAuthorizationContext> : authentication을 가지고 AuthorizationDecision을 반환함
7+
8+
HttpSecurity.build() 실행시
9+
AuthorizeHttpRequestsConfigurer.configure() 실행
10+
저장된 모든 uri별 권한 설정을 AuthorizationFilter 필터 한개로 변환하고 security filter chain에 등록함
11+
AuthorizationFilter를 만들 때 AuthorizationManager를 생성자로 넘겨줌
12+
List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>>를 AuthorizationManager<HttpServletRequest> 으로 변환함 (by RequestMatcherDelegatingAuthorizationManager.class 생성자) (filter에서는 HttpServletRequest만 사용하기 때문 by gpt)
13+
AuthorizationManager<HttpServletRequest>.authorize 함수를 톧해 권한을 설정함
14+
15+
AuthorizationFilter 에서는 AuthorizationManager<HttpServletRequest>를 가지고 모든 요청을 url과 권한을 가지고 판단함
16+
AuthorizationManager.authorize 함수를 실행시킴
17+
18+
1. HttpSecurity에서 authorizeHttpRequests() 함수 실행
19+
AuthorizeHttpRequestsConfigure.class 반환
20+
AuthorizeHttpRequestsConfigure내부 class AuthorizationManagerRequestMatcherRegistry, AuthorizedUrl를 반복하며url 입력, manager 입력을 받는다
21+
입력 받은 RequestMatcher와 AuthorizationManager를 RequestMatherEntry로 두고 RequestMatcherDelegatingAuthorizationManager.Builder에 쌓는다 (builder에서는 List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>>으로 저장함)
22+
2. HttpSecurity.build() 함수 실행시
23+
AuthorizeHttpRequestConfigure.configure()함수 실행됨
24+
RequestMatcherDelegatingAuthorizationManager.Builder.build()를 통해 AuthorizationManager<HttpServletRequest> 생성
25+
(형변환은 하지 않음 RequestMatcherDelegatingAuthorizationManager<HttpServletRequest>의 내부 변수 mappings가 List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> 형태임
26+
AuthorizationManager<HttpServletRequest> authorizationManager 가지는 Authorization 생성 / filter chain 등록
27+
3. 매 요청에 AuthorizationFilter 작동
28+
매 요청 마다 List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>>를 순회함
29+
입력 받은 RequestMatherEntry.RequestMatcher를 통해 url 검증
30+
RequestMatherEntry.AuthorizationManager를 통해 authentic 검증 (AuthorizationManager.authorize() 함수 호출)
31+
4. 매 요청 마다 발생하는 uri별 Exception을 다르게 처리하기 위해 authorize함수를 override하여 throw CustomException을 처리 예정
32+
문제 발생 check함수에서 throw를 맘대로 던져도 되는가?
33+
다행히 AuthorizationFilter는 boolean값인 AuthorizationResult을 반환하는 check함수에서 Exception을 반환하는 verify로 변환을 준비중이다
34+
아직 변환되지는 않았지만 문제되 점은 크게 많지 않아 보임
35+
1. this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, result); 문장 실행 안됨
36+
AuthenticFilter내부 변수 AuthenticationEventPublisher eventPublisher에 저장중이다
37+
publisher는 filter를 생성하는 AuthorizeHttpRequestsConfigure에서 부터 내려왔으며 ApplicationContext에 저장된 객체이다
38+
실패 횟수, 로그 등에 사용 되는 event임 (건너 뛰는 것은 좋지 않지만 다른 방법이 없으면 무시하겠음)
39+
해결 방법 탐색
40+
1. event 무시하고 그냥 throw 던지기
41+
2. 깔끔하게 AuthorizationFilter를 직접 구현하여 실패시도 알맞은 publishAuthorizationEvent를 발행하게 한다
42+
AuthorizationFilter를 생성하는 AuthorizeHttpRequestsConfigure을 상속해야 하는데 하필 AuthorizeHttpRequestsConfigure은 final class이다 (불가능)
43+
3. 매우 더럽게 HttpSecurity부터 override하고, AuthorizeHttpRequestsConfigure의 모든 interface를 구현한다 (사실 복 붙이라 직접 구현은 아니겠지만) (싫음)
44+
깔금하게 log event따위 무시하고 throw 던지기 (언젠가 security의 버전이 올라가며 AuthorizationManager.check가 완전히 사라진다면 무시된 event에 대한 코드도 수정이 되어있을 것으로 예상)

0 commit comments

Comments
 (0)