개발자의 오르막
[SpringSecurity #07] AccessDecisionManager 본문
# Access Control 결정을 내리는 인터페이스로, 구현체 3가지를 기본으로 제공한다.
- AffirmativeBased : 여러 Voter 중에 한명이라도 허용하면 허용, 기본전략
- ConsensusBased : 다수결
- UnanimousBased : 만장일치
- AffirmativeBased
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
- 위의 코드는 Request 시에 거치는 소스이며, 이때 voter 를 통해 진행된다.
인가 여부는 result 값으로 반환된 값에 따라 진행되며, 이 값은 아래에 정의되어 있다.
public interface AccessDecisionVoter<S> {
// ~ Static fields/initializers
// =====================================================================================
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
# AccessDecisionVoter
- 해당 Authentication 이 특정한 Object에 접근할 때 필요한 ConfigAttributes 를 만족하는지 확인
( ConfigAttributes → .permitAll(), .hasRole("") )
- WebExpressionVoter : 웹 시큐리티에서 사용하는 기본 구현체, ROLE_XXXX 가 매치하는지 확인
- RoleHierarchyVoter : 계층형 Role 지원, ADMIN > MANAGER > USER
그렇다면 Admin 권한을 갖고 있는 계정이 User 만 들어갈 수 있는 페이지도 들어가려면
어떻게 해야할까?
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/", "/info", "/account/**").permitAll()
.mvcMatchers("/admin").hasRole("ADMIN")
.mvcMatchers("/user").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
위처럼 config 를 지정하면, ADMIN 권한을 갖고 있는 계정이 /user URL 로 접근할 때 403 에러가 난다.
왜냐하면 Security 는 현재 ADMIN, USER 라는 권한을 갖고 있는지만 검사할 뿐, 문맥상 의미를 파악하지
못하고 있기 때문이다. (ADMIN 권한을 최고권한으로 지정한다는 의미 = 문맥상 의미)
이를 해결하기 위해서는 2가지 방법이 있다.
1. ADMIN 롤을 리턴할 때 USER 롤도 같이 리턴한다.
2. Hierarchy Role 를 이해하는 AccessDecisionManager 를 사용한다.
- SecurityConfig.class ( 2번 경우 )
public AccessDecisionManager accessDecisionManager() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
// handler 도 같은 걸 쓰고 있는데, roleHierarchy 설정을 추가해준 것 뿐임
handler.setRoleHierarchy(roleHierarchy);
WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
webExpressionVoter.setExpressionHandler(handler);
List<AccessDecisionVoter<? extends Object>> voters = Arrays.asList(webExpressionVoter);
return new AffirmativeBased(voters);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/", "/info", "/account/**").permitAll()
.mvcMatchers("/admin").hasRole("ADMIN")
.mvcMatchers("/user").hasRole("USER")
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager());
http.formLogin();
http.httpBasic();
}
2번을 구현하기 위해서는 인가할 때 쓰는 AccessDecisionManager 에 roleHierarchy 를 set 해주면 된다.
이는 간단하게 설정을 하지 못함으로, 실제 시큐리티에서 쓰는 AccessDecisionManager 객체를 리턴해서
커스텀한 부분을 재정의하면 해결될 수 있다.
밑에 configure 메소드에서 .accessDecisionManager() 를 통해 우리가 재정의한 객체를 주입하는 걸로 설정한 후
accessDecisionManager 메소드를 통해 재정의한다.
위의 소스에서는 단순히 우리가 기존에 쓰는 handler 를 로드하고, setRoleHierarchy 를 해준 것 밖에 없다.
그러면 ADMIN 권한만 갖고 있는 계정도 /user 에 접근할 수 있다.
이는 아래처럼 handler 에 RoleHierarchy 만 set 해주는 방식으로 간결하게 refactoring 해줄 수도 있다.
public SecurityExpressionHandler expressionHandler() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
// handler 도 같은 걸 쓰고 있는데, roleHierarchy 설정을 추가해준 것 뿐임
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/", "/info", "/account/**").permitAll()
.mvcMatchers("/admin").hasRole("ADMIN")
.mvcMatchers("/user").hasRole("USER")
.anyRequest().authenticated()
.expressionHandler(expressionHandler());
// .accessDecisionManager(accessDecisionManager());
http.formLogin();
http.httpBasic();
}
그러면 AccessDecisionManager 는 어디에서 사용될까?
# FilterSecurityInterceptor
- AccessDecisionManager를 사용하여 Access Control 또는 예외처리 하는 필터
대부분의 경우 FilterChainProxy에 제일 마지막 필터로 들어가 있다.
- FilterSecurityInterceptor → AbstractSecurityInterceptor 에서 accessDecisionManager를 통해 실행된다.
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
위의 부분이 권한을 확인해주는 부분이다. 모든 Request 마다 실행되며, 통과하기 위해서는 인가가 필요하다.
config 파일에서 설정했던 조건들은 attribute 안에 들어가고, 인증 정보는 authenticated 에 들어간다.
object 는 chain, request, response 에 대한 정보를 가지고 있다.
인가가 실패되었을 때는 catch 부분에서 FailureEvent 를 타게 된다.
# ExceptionTranslationFilter
- 필터 체인에서 발생하는 AccessDeniedException 과 AuthenticationException을 처리하는 필터
- AuthenticationException 발생 시
AuthenticationEntryPoint 실행
AbstractSecurityInterceptor 하위 클래스 (예, FilterSecurityInterceptor) 에서 발생하는 예외처리
UsernamePasswordAuthenticationFilter에서 발생한 인증에러는?
- UsernamePasswordAuthenticationFilter의 상위 클래스인
AbstractPreAuthenticatedProcessingFilter 에서 인증 실패가 떨어지면
unsucceessfulAuthentication(request, response, failed) 로 들어가게 된다.
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}
rememberMeServices.loginFail(request, response);
failureHandler.onAuthenticationFailure(request, response, failed);
}
- AccessDeniedException 발생 시
익명 사용자 : AuthenticationEntryPoint 실행
익명 사용자가 아니면 AccessDeniedHandler 에게 위임
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
위의 소스 부분에서 예외에 대한 분기처리가 이루어진다. ( 인증에 대한 에러, 인가에 대한 에러 )
'SpringFrameWork > SpringSecurity' 카테고리의 다른 글
[SpringSecurity #09] Spring Security Filter (0) | 2020.10.02 |
---|---|
[SpringSecurity #08] 스프링시큐리티 아키텍처 정리 (0) | 2020.10.02 |
[SpringSecurity #06] 스프링 시큐리티 Filter와 FilterChainProxy (0) | 2020.09.27 |
[SpringSecurity #05] Authentication 와 SecurityContextHolder (0) | 2020.09.27 |
[SpringSecurity #04] SecurityContextHolder 와 Authentication (0) | 2020.09.21 |