개발자의 오르막

[SpringSecurity #07] AccessDecisionManager 본문

SpringFrameWork/SpringSecurity

[SpringSecurity #07] AccessDecisionManager

계단 2020. 9. 28. 00:42

# 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);
			}
		}
	}

  위의 소스 부분에서 예외에 대한 분기처리가 이루어진다. ( 인증에 대한 에러, 인가에 대한 에러 )

 

Comments