스프링 시큐리티를 사용하는 프로젝트를 진행하는 도중 동적으로 권한을 처리해야하는 경우가 생겼다
기존에 공부할때는 특정 접근 경로에 대해서 어떠한 권한(혹은 역할)이 있는지를 판단하는 것이 전부였고 간단한 상황에서만 사용이 가능했다
문제 상황
게시글을 작성한 사용자, 해당 게시글에 접근이 허락된 사용자들에 대해서 권한 처리 문제
게시글을 작성한 사용자에 대해서 기존 방식대로 권한을 주게 되면 ROLE_WRITER와 같이 주면 된다requestMatchers("/post/**").hasRole("WRITER")
하지만 이 방식대로 권한을 주게되면 한가지 문제점이 있다 1번 글 작성자가 2번 글에 대해서 권한을 가질 수 있게 된다는 점이다
예를들어 1번글 작성자가 /post/1/update
경로로 접속을 하는 것은 문제가 되지 않는다 하지만 /post/2/update
에 대해 접근을 하게 된다면? 이를 허용을 하게 된다면 많은 문제가 생길 것이다
이를 해결하기 위해서 이전에 몇가지 고민을 하게 되었다
고민내용
- 각 Controller에서 post 아이디를 가지고 작성자를 비교한다
- -> 필요한 Controller에 모두 추가를 해줘야 함 지저분해지고 불필요한 검증 과정이 추가됨
- AOP 혹인 Interceptor를 사용해서 검증 로직을 분리한다
- -> 이렇게 하면 Security를 사용해서 권한 처리를 하지 않음 특정 경로는 Security 특정 경로는 기본 Spring 기능을 사용해서 하기에는 인증 처리가 분리되는 느낌
- 로그인 성공시 JWT Claim값으로 사용자가 작성한 글의 아이디 값을 넣어주고 JWT 검증 filter에서 우선적으로 작성한 글이 하나라도 있다면 WRITER 권한을 주고 Interceptor에서 requestUri를 파싱해서 일치하는 id값이 있는지를 한 번더 확인 후 넘긴다
- -> Security의 AuthorizationFilter를 사용하게 되어 위의 문제를 해결 한 듯 보이지만 결국 Interceptor에서 재 확인 처리가 필요하고 만약 로그인 후(JWT가 이미 발행이 된 시점) 글을 작성하게 되거나 특정 게시글에 접근이 허용이 된다면 토큰 내부에 글의 아이디를 넣어줄 수 없음
해결방안
몇가지 고민을 해봤지만 문제를 해결할 수 없고 Security의 기능을 사용하지 않고 1, 2번 방법을 사용할 생각을 가졌고 그래도 혹시나 하는 마음에 AuthorizationFilter내부를 확인하면서 AuthorizationManager
에 대해서 알게 되었고 해당 방법을 사용해서 해결을 하기로 했다
AuthorizationFilter의 내부에서 어떻게 동작하는지 간단하게 정리해둔 글이다 이전버전과 현 버전의 차이를 알아보기 위함이 주였기도 하고 이때는 이렇게 동적으로 관리가 되어야 하는 상황에 대해서도 알지 못했어서 한번에 기억해 내기가 어려웠다
구현 전 바탕 지식
위 글에서 보다시피 AuthorizationFilter는 RequestMatcherDelegatingAuthorizationManager
를 사용하여 권한 확인을 하게 되고
RequestMatcherDelegatingAuthorizationManager
는 내부에
private final List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> mappings;
를 가지고 있어 loop를 돌며 권한 확인을 하게 되는 것이다
// RequestMatcherDelegatingAuthorizationManager.Builder
public static final class Builder {
private final List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> mappings = new ArrayList<>();
public Builder add(RequestMatcher matcher, AuthorizationManager<RequestAuthorizationContext> manager) {
this.mappings.add(new RequestMatcherEntry<>(matcher, manager));
return this;
}
public Builder mappings(
Consumer<List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>>> mappingsConsumer) {
mappingsConsumer.accept(this.mappings);
return this;
}
public RequestMatcherDelegatingAuthorizationManager build() {
return new RequestMatcherDelegatingAuthorizationManager(this.mappings);
}
}
위 빌더를 통해서 RequestMatcherDelegatingAuthorizationManager
가 생성이 되는데 보면 빌더 내부적으로 RequestMacherEntry로 감싸서 넣어주므로 우리가 할 일은 AuthorizationManager의 구현체를 만들고 적용을 해주면 되는 것이다
적용 방법
기존에 hasRole
와 동일하게 이번에는 access
를 사용하여 적용을 해주면 된다
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize ->
authorize
.requestMatchers("/post/*").access(authorizationManager)
);
return http.build();
}
}
AuthorizationManager 구현
public class RecruitmentAuthorizationManagerForApplimentMember implements AuthorizationManager<RequestAuthorizationContext> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
boolean access = 권한_확인_로직();
return new AuthorizationDecision(access);
}
}
위 권한 확인 로직에서 승인을 원하는 특정 조건에서는 true를 아니라면 false를 넘겨주면 된다
이로써 동적으로 권한을 처리할 수 있게 되었다
참고
이 부분은 굳이 알 필요는 없고 단순히 authorizationManager가 어떻게 등록이 되는지에 대한 과정이다
access()
나 hasRole()
메소드를 통해서 권한처리, 역할을 넣어주게 되면 내부적으로 AuthorizeHttpRequestsConfigurer.addMapping()
을 통해서 위에서 봤던 RequestMatcherDelegatingAuthorizationManager.Builder
의 add()
메소드를 실행시켜 주고 securityFilterChain()에서 http.build()를 실행 할 때 builder.build를 실행시켜 넘겨주게 된다
http.build()
@Override
protected final O doBuild() throws Exception {
synchronized (this.configurers) {
this.buildState = BuildState.INITIALIZING;
beforeInit();
init();
this.buildState = BuildState.CONFIGURING;
beforeConfigure();
configure(); // -> 이 시점에 각종 securityConfigurer파일들의 설정정보가 등록되게 된다
this.buildState = BuildState.BUILDING;
O result = performBuild();
this.buildState = BuildState.BUILT;
return result;
}
}
'공부기록 > Spring Security' 카테고리의 다른 글
Controller에서 Security 정보 가져오기 (0) | 2023.11.14 |
---|---|
[추가] 스프링 시큐리티 OAuth 로그인 처리 방법 2 (0) | 2023.02.22 |
[추가] 스프링 시큐리티 OAuth 로그인 처리 방법 1 (2) | 2023.02.22 |
[실습] 스프링 시큐리티 OAuth2 Login 2 (2) | 2023.02.21 |
[실습] 스프링 시큐리티 OAuth2 Login 1 (0) | 2023.02.21 |