이번에는 스프링시큐리티에서 어떻게 인가가 이뤄지고, 어떤 라이프사이클을 갖는지 알아보려고 한다.
시큐리티를 모르고 읽으면 어려울 것 같다. 이전 포스팅을 읽고보면 좋을 것 같다. - 스프링 시큐리티와 인증
인가란 리소스에 대한 접근 권한 및 정책을 지정하는 기능이다
유저의 Request가 우리의 리소스에 접근할 수 있는지 확인하는 절차다.
ex) 관리자 페이지 접근, 게시판 글쓰기 등에서 보통 인가절차를 밟는다. 우리가 로그인 이후, 해당 리소스를 접근할 수 있는 건. 보통 로그인 되어있음을 Session에 저장하거나, JWT 토큰을 이용한다. 그래서 사용자는 로그인이되어있음을 느낄 수 있다. 인증이 선행되는 걸 알 수 있다.
권리나 권력 또는 직권이 미치는 범위.
Request한 유저의 권한을 검사한다. 인가절차에서 요소로 사용된다.
ex) 관리자 페이지를 접근할 때, Request한 유저가 관리자인지 확인한다.
유튜브에 잘 나와 있다. - 토니의 인증과 인가
@Configuration
@EnableWebSecurity
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.headers()
.disable()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth").permitAll()
.antMatchers("/api/user/join").permitAll()
.antMatchers("/api/**").hasRole(Role.USER.name())
.accessDecisionManager(accessDecisionManager())
.anyRequest().permitAll()
.and()
.formLogin()
.disable();
http
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
위 클래스에서 WebSecurityConfigurerAdapter
를 상속하면서 configure()
를 구현하고 있다. 이 부분이 시큐리티의 메인설정이라고 볼 수 있다. (어렵다. 필요한 부분만 살펴보자)
우리가 눈 여겨봐야 할 곳은 authorizeRequests()
부분과 accessDecisionManager(accessDecisionManager())
이 부분이다.
한 눈에 알수있는부분은 authorizeRequests()을 통해 URL에 따른 권한승인을 할 수 있는 것으로 보인다.
.antMatchers("/api/auth").permitAll()
.antMatchers("/api/user/join").permitAll()
위와 같이 로그인/회원가입 같은 부분은 모든 유저들에게 허용할 수 있도록 해야 한다. 로그인/회원가입할 때 권한이필요하면 아무도 서비스를 이용할 수 없을 것 같다😭
.antMatchers("/api/**").hasRole(Role.USER.name())
코드를보면 URL패턴(ANT 문법)에 맞게 적절한 권한을 줄 수 있는 걸로 보인다.
보통 위처럼 모든 권한을 주고 나서, 권한이 불필요한 URL을 따로 제외하는 게 방어적인 프로그래밍이라고 생각한다.
그럼 지금 서비스는 "/api/auth", "/api/user/join"를 제외하고 모두 Role.User의 권한을 갖고 있어야 접근할 수 있다.
.accessDecisionManager(accessDecisionManager())
를 보면 accessDecisionManager()를 통해 만들어 둔 빈을 주입하는 걸 볼 수 있다. 그럼 구현체를 봐 보자.
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
decisionVoters.add(new WebExpressionVoter());
decisionVoters.add(connectionBasedVoter());
return new UnanimousBased(decisionVoters);
}
UnanimousBased
라는 AccessDecisionManager
의 구현체를 리턴하는 메서드고, AccessDecisionManager
는 Voter들의 리스트를 참조하고 있다.
Voter는 투표자라는 뜻으로, N명의 투표자의 투표를 통해 인가가 이뤄진다.
투표의 결과는 AccessDecisionManager
의 전략에 따라서 달라지며 AccessDecisionManager를 자세히 알아보겠다.
인가를 추가로 하는 요소를 알아보자.
먼저 Voter를 알아보면, 투표자라는 뜻으로 멤버변수로 GRANTED, ABSTAIN, DENINED이 있다.
각 Voter객체는 로직에 따라 요청에 따라 권한을 승인, 무효, 거절로 투표할 수 있다.
왠지 추측으로는 AccessDisicionManager는 여러 개의 Voter들의 vote()를 통해서 ACCESS의 결과들의 합을 구할 수 있을 것 같다. '그 합의 값이 양수이면 인증된 유저라고 생각할 수 있지 않을까'? 라는 생각이 들었다.
실제 AccessDisicionManager는 얼추 비슷했다. 다음은 그들의 구현체들을 알아보겠다.
위 사진에서 AccessDecisionManager과 Voter의 관계를 볼 수 있다.
FilterSecurityInterceptor
필터가 호출해준다.AffirmativeBased
기반 Manager와 WebExpressionVoter
의 Voter 1개로 인가처리가 진행된다.디시전매니저는 시큐리티에 제공하는 건 3가지가 있다. 어떤 게 있는지 알아놓고, 적재적소 사용할 수 있어야 한다.
또, 단순 decide 로직은 어렵지 않아서 커스터마이징하여 사용할 수도 있을 것 같다.
위 처럼 AccessDisicionManager은 다양한 전략이 존재하는데, 어떤 구현체들이 있는지 알아보자.
allowIfEqualGrantedDeniedDecisions
을 통해서 동점일 때 처리를 어떻게 할지 전략을 정할 수 있다. 거기에 따른 동점전략 setter가 존재한다.즉 우리는 상황에 맞는 AccessDisicionManager를 사용하여 투표전략을 어떻게 정할지 선택한다. 도메인 상황에 따라 Voter로 인증에 구체적인 로직을 넣을 수 있다. 그 Voter들을 Manager에 주입하여 사용하면 된다.
(권한을 등록하는 사진)
디폴트 Voter인 WebExpressionVoter
를 알아보자.
현재 인증된 사용자가 가지고 있는 권한이 ROLE_XXXX와 매치되는지 확인한다. 즉 실질적으로 WebExpressionVoter을 통해서 권한을 확인하는 걸 알 수 있다.
(위 사진에 등록된 권한이 Request User의 권한이 일치하는지 확인한다.)
WebExpressionVoter
의 구현체다. weca 객체를 통해 권한을 확인하고, 권한이 일치하면 1, 아니면 -1을 주는 모습을 볼 수 있었다.
(@Override를 안 쓰고, 인터페이스의 static변수 활용하지 않는 부분이 코드에서 아쉽다..ㅎㅎ)
AccessDicisionManager
가 디폴트 Voter인 WebExpressionVoter
에서 실질적으로 Request의 권한을 확인하는걸 볼 수 있었다. 또 이런 디시전매니저는 FilterSecurityInterceptor
필터가 호출해준다.근데 Voter에서 어느정도 비즈니스로직이 들어 갈 것 같다. '너무 많은 Voter를 사용하여 인가부분을 위임하면 프로그램의 흐름을 읽기 어렵지 않을까?' 라는 생각이 든다.
스프링 AOP에서도 중복코드와 비즈니스코드를 잘 분리할 수 있지만, 로직의 순서나 코드의 분리 측면에서 이해하기 힘든 단점이 있다고 생각했는데, Voter로 비슷함을 느꼈다.
인가를 다루려니 생각보다 '내가 제대로 알고 있는 게 맞을까?'라고 생각하며 더 공부하여 포스팅할 수 있었다. 아직도 두루뭉술한 부분이 많은 것 같고, 부족한 내용은 더 퇴고하여 글을 더 성장시키고 싶다.
시큐리티 쉽지 않다. 너