[Spring Security] 인가 흐름 및 절차

2023. 3. 18. 11:50Spring/Security

⚠️ 스프링 시큐리티의 인가는 인증 절차를 먼저 이해하는 편이 권장되므로 https://somuchthings.tistory.com/197 을 먼저 읽고 오는 것을 추천한다.

1. 인가(Authorization)란?

인가(Authorization)는 인증된(authenticated) 사용자가 요청한 자원에 대해 접근할 권한이 있는지를 판단하는 절차이다. 간단히 예를 들면 회원 사용자는 관리자 페이지에는 접근하지 못하도록 해야 한다는 규칙이 있다. 회원 사용자는 분명 서비스를 이용하도록 인증되었지만 관리자 자원에 대한 인가는 받지 못하는 것이다.

2. 스프링 시큐리티의 인가 흐름

앞선 필터를 모두 지나서 맨 마지막에 인가 처리를 담당하는 FilterSecurityInterceptor에 도달한다. 이 클래스는 FilterInvocationSecurityMeatadataSource라는 필드에 해당 자원에 대한 권한을 가지고 있다. 그리고 doFilter() 를 호출할 때 내부의 invoke()를 호출하여 실질적인 인가 처리를 한다. 인자로는 FilterInvocation이라는 타입이 전달되는데 이 클래스는 FilterChain, HttpServletRequest, HttpServletResponse를 필드로 가지는 일종의 래퍼 클래스다.

invoke() 에서는 AccessDecisionManager에게 실제 인가 처리를 위임한다. AccessDecisionManager는 내부의 AccessDecisionVoter들에게 인증 정보, 요청 정보, 권한 정보를 넘겨주어 인가 여부를 요청한다. 각 AccessDecisionVoter들은 각자 인가 여부를 반환하고 AccessDecisionManager는 그 결과에 따라 최종 인가 여부를 결정한다.

인가가 허용되면 다시 FilterSecurityInterceptor의 정상 흐름으로 돌아간다. 인가가 거부되면 예외가 발생하고 ExceptionTranslationFilter에게 던진다.

FilterSecurityInterceptor

스프링 시큐리티의 인가 처리는 필터 중 맨 마지막에 위치한 FilterSecurityInterceptor에서 처리한다. 인증된 사용자에 대해 특정 요청의 승인 또는 거부 여부를 최종적으로 결정하는 역할을 하며 HTTP 자원의 보안 처리를 담당한다.

 

FilterSecurityInterceptor의 doFilter()
FilterSecurityInterceptor의 invoke()

__spring_security_filterSecuritiyInterceptor_filterApplied 라는 속성을 체크하는 것은 만약 FilterSecurityInterceptor를 여러 개 사용해서 인가 처리를 할 경우 앞쪽 FilterSecurityInterceptor에서 인가 처리를 한 경우 뒤에서는 다시 하지 않도록 하기 위해서이다.

beforeInvocation에선 인가 처리를 하기 전에 할 작업들을 처리한다. 해당 자원에 대한 권한이 있는지, 인증 여부 등을 처리한다. 그리고 필터 체인을 가져온 뒤 doFilter()를 호출해 인가 여부를 처리한다. 그리고 인증된 Authentication 객체, 권한 정보 등을 가지고 있는 InterceptorStatusToken으로 반환한다. 이때 권한 정보가 없다면, 즉 모두에게 허용된 자원이라면 null이 반환된다.

AccessDecisionManager

인증 정보, 요청 정보, 권한 정보를 이용해서 사용자의 자원 접근을 허용할 것인지 거부할 것인지를 최종 결정하는 역할을 하는 인터페이스다. 여러 Voter를 가질 수 있으며 decide()를 호출해 voter들로부터 접근 허용, 거부, 보류에 해당하는 값을 반환받고 판단 및 결정한다. 거부로 판단되면 예외를 발생시킨다.

 

AccessDecisionManager

구현체

1. AffirmativeBased

  • 여러 개의 Voter 중 하나라도 접근을 승인하면 접근을 허용한다.
  • 일반적으로 가장 많이 사용된다.

 

2. ConsensusBased

  • 다수결에 의해 최종 결정을 내린다.
  • 동표일 경우 기본적으로 접근을 허용하지만 allowIfEqualGrantedDeniedDecisions를 false로 설정할 경우 접근을 거부한다.

 

3. UnanimousBased

  • 모든 Voter가 만장일치로 승인해야 접근을 허용한다.

가장 많이 사용되는 AffirmativeBased의 코드를 보자.

 

AffirmativeBased

decisionVoter를 순회하다가 result가 만약 1이라면, 즉 Voter가 인가를 허용하면 바로 return한다. 즉, 하나의 Voter라도 1이 반환되면 바로 결정을 마친다. 만약 return이 되지 않고 deny가 증가한 상태로 순회가 종료되면 AccessDeniedException이 발생하는 것을 볼 수 있다. 그리고 deny가 0보다 많지 않은데 순회가 종료되었다면 이는 보류 처리를 위해 checkAllowIfAllAbstainDecsions()를 호출하는 것을 볼 수 있다. 만약 보류 허용을 하지 않았다면 이 역시 AccessDeniedException이 발생한다.

 

AccessDecisionVoter

판단을 하는 역할을 하는 인터페이스다. 스프링 시큐리티에서는 기본 구현체로 RoleVoter를 제공한다.

 

AccessDecsionVoter
RoleVoter

Voter가 권한 허용 과정에서 판단하는 자료

  • Authentication - 인증 정보
  • FilterInvocation - 요청 정보(antMatcher("/user"))
  • ConfigAttributes - 권한 정보(hasRole("user"))

결정 방식

  • ACCESS_GRANTED  - 접근 허용(1)
  • ACCESS_DENIED     - 접근 거부(-1)
  • ACCESS_ABSTAIN    - 접근 보류(0)
    • Voter가 해당 요청에 대해 결정을 내릴 수 없는 경우

처리 과정

  1. SecurityContext에 Authentication 객체가 있는지, 즉 유저가 인증되었는지 확인한다. 객체가 없다면 AuthenticationException을 발생시키고 ExceptionTranslationFilter에게 예외를 던진다.
  2. SecurityMetadataSource가 유저가 요청한 자원에 접근하기 위해 필요한 권한 정보를 조회해서 전달한다. 권한 정보가 없는 경우 모든 유저에게 허용된 자원이므로 접근을 허용하며 권한 심사를 하지 않는다.
  3. AccessDecisionManager는 Authentication 객체, 권한 정보 등을 AccessDecisionVoter 들에게 전달하여 심의를 요청하고 승인 여부를 응답받으면 그에 따라 접근 여부를 판단한다.
  4. 접근이 승인되면 자원에 대한 접근을 허용하고, 거부되면 AccessDeniedException을 발생시켜 ExceptionTranslationFilter 에게 예외를 던진다.

3. 흐름 정리

  1. 요청이 FilterSecuritiyInterceptor에 도달하면 FilterSecurityInterceptor는 AccessDecisionManager에게 인가 처리를 위임한다.
  2. AccessDecisionManager는 decide(authentication, filterInvocation, configAttributes) 를 통해 Authentication, FilterInvocation, ConfigAttributes를 AccessDecisionVoter들에게 넘겨주고 접근에 대한 판단을 요청한다.
  3. 각 Voter들은 인가 여부를 판단하고 그 결과를 반환한다.
  4. AccessDecisionManager의 구현체에 따라 인가 여부를 판단한다.
    • 인가가 허용되면 FilterSecurityInterceptor의 흐름으로 돌아간다.
    • 인가가 거부되면 AccessDeniedException 예외를 발생시키며 ExceptionTranslationFilter로 예외를 던진다.