Posts Spring Security Architecture 내용정리
Post
Cancel

Spring Security Architecture 내용정리

오래전 spring.io에 올라왔던 Spring Security Architecture 의 내용을 정리해 보았다. 읽을 때는 금방 할듯 했는데, 막상 우리말로 적어 보려니 상당히 여럽네. 몇몇 문장은 직역/의역으로 넘기기도 했고.

Spring Security를 프로젝트에 사용해 본 건 몇번 안된다. 제대로 학습 후 사용하기 보다는 급한대로 구글링하면서 적용할 수 밖에 없었다. 또한 Spring Security는 초기 설정하고 구조를 잡고나면 관련해서 거의 볼 일이 없다보니, 다시 사용할 일이 생기면 또 바닥부터 시작할 듯하다.

Authentication and Access Control

애플리케이션 보안이라고 하면 크게 두가지 나눌 수 있다.

  • 인증 : authentication (who are you?)
  • 인가 또는 권한부여 : authorization (what are you allowed to do?)

인증이란 내가(유저) 누구인지를 확인하는 것으로 쉽게 아이디와 비밀번호를 떠올릴 수 있다. 인가(또는 권한부여)는 인증을 완료한 유저에게 권한을 부여할 수 있고, 그 권한에 따라 서비스의 기능(또는 자원 resource)에 접근여부를 관리하는 것이라 볼 수 있겠다. authorization과 관련해서 access control이라는 용어가 등장하기도 하는데, 권한에 따라 자원(기능)에 접근(access) 여부를 제어(control)하는 것이라는 점에서 자연스러운 것으로 보인다.

Authentication

Spring Security에서 authentication에 관한 핵심 인터페이스는 AuthenticationManager이며 단 하나의 메서드만 가지고 있다.

1
2
3
4
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) 
        throws AuthenticationException;
}

이 인터페이스는 authenticate() 메서드에서 아래 3가지 일 중 한가지를 해야한다고 한다.

  1. 입력이 유효한 주체(principal)를 나타내는지 확인 할 수 있는 경우 Authentication을 리턴.
  2. 입력이 유효하지 않은 주체(principal)를 나타내는 것으로 생각되면 AuthenticationException 예외를 던진다.
  3. 결정 할 수 없으면 null을 리턴.

여기서 principal 이라는 단어가 등장하는데, 구글번역과 파파고 모두 ‘주체’라고 번역을 했다. 문장의 문맥상 어떤 의미인지는 알겠는데 막상 사전을 찾아보면 좀 모호하다. 그런데 Authentication 인터페이스를 보면 Principal이라는 인터페이스를 상속받고 있다. 이 인터페이스의 주석을 보면 Spring Security에서의 principal의 의미를 명확히 파악 할 수 있다.

This interface represents the abstract notion of a principal, which can be used to represent any entity, such as an individual, a corporation, and a login id.

그리고 authenticate() 메서드에서 던지는 AuthenticationException는 런타임 예외로 코드 상에서 잡아서 처리되기 보다는, 웹 UI에서는 인증 실패를 나타내는 페이지를 렌더링하고, 백앤드 HTTP 서비스는 401(Unauthorized)응답을 보내는 것 처럼 어플리케이션 단에서 목적에 따라 일반적인 방식으로 처리하라고 한다.

AuthenticationManager의 구현체로는 ProviderManager가 일반적으로 사용되고 이놈은 AuthenticationProvider 인스턴스들에 위임한다 (문장이 좀 깔끔하지 않는데 뒤에 위임과 관련해서 설명이 나온다). AuthenticationProviderAuthenticationManager와 약간 비슷하지만 주어진 Authentication 타입을 지원할 경우 호출자가 조회할 수 있는 추가적인 메서드를 가지고 있다고 한다.

1
2
3
4
5
public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    boolean supports(Class<?> authentication);
}

여담이지만 스프링을 사용하다 보면 인터페이스나 클래스의 이름들이 많이 헷갈린다. 한번 봐서는 머리에 잘 정리가 되지 않는다고 해야하나… AuthenticationManager, ProviderManager, AuthenticationProvider ㅡ.ㅡ;

쨌든 AuthenticationManager의 구현체인 ProviderManager 클래스를 보면 AuthenticationProvider타입의 리스트를 가지고 있다. 그리고 authenticate() 메서드에서 이 리스트를 돌면서 AuthenticationProvider 인스턴스의 authenticate()를 호출하므로써 위임을 해주고 있는 것을 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    Authentication result = null;
    ...
    for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
            continue;
        }
        ...
        try {
            result = provider.authenticate(authentication);
            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        }
        ...
    }
    ...
}

AuthenticationProvider 의 support() 메서드에 Class<?> 인자는 authenticate() 메서드로 전달 되어질 녀석을 지원하는지 여부만 확인하기 위한 실제 Class<? extends Authentication> 이다. 바로 위에서 살펴봤듯이 ProviderManagerAuthenticationProviders 체인에 위임하므로써 여러 다른 인증 메커니즘을 지원할 수 있다.

ProviderManager에는 모든 Provider들이 null을 리턴할 경우 사용할 ProviderManager(parent라고 부르자)를 지정할 수 있다(없을 수 있음). 만약 이 ProviderManager도 사용할 수 없는 경우 AuthenticationException이 발생한다.

보호하고자 하는 리소스(protected resources)의 논리적 그룹이 있고(에를 들어 /api/** 의 경로 패턴과 일치하는 모든 웹 리소스) 이들 각 그룹마다 자체 전용 AuthenticationManager가 있을 수 있다. 이들 각각은 ProviderManager이고 parent(위 문단 참조)를 공유한다. 그러면 parent는 모든 Provider에 대한 대체(fallback) 역할을 하는 일종의 ‘global’ 리소스가 된다.

An `AuthenticationManager` hierachy using `ProviderManager`

Customizing Authentication Managers

항상 그렇듯이 Spring Security 역시 설정을 쉽게 해주는 핼퍼를 제공한다. 가장 일반적으로 사용되는 핼퍼는 in-memory, JDBC, LDAP user details 설정이나 custom UserDetailsService를 추가하는데 유용한 AuthenticationManagerBuilder이다. 아래는 global(parent) AuthenticationManager를 설정하는 예제다.

1
2
3
4
5
6
7
8
9
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
    ... // web stuff here

    @Autowired
    public void initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
        builder.jdbcAuthentication().dataSource(dataSource).withUser("dave").password("secret").roles("USER");
    }
}

위 설정에서의 AuthenticationManagerBuilder는 @Bean의 메서드에서 @Authworied 되어 있음을 유의해야한다. 이것이 global(parent) AuthenticationManager를 빌드하게 만다는 것이다. 이와 반대로 아래와 같이 설정 한다면:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
    @Autowired
    DataSource dataSource;
    ... // web stuff here

    @Override
    public void configure(AuthenticationManagerBuilder builder) {
        builder.jdbcAuthentication().dataSource(dataSource).withUser("dave").password("secret").roles("USER");
    }
}

configurer의 메서드를 @OverrideAuthenticationManagerBuilder는 global 의 자식인 “local” AuthenticationManager로 빌드하는데만 사용된다. Spring Boot 애플리케이션에서 다른 빈으로 global 을 @Authwoired 할 수 있지만, 명시적으로 직접 노출하지 않는 한 “local”을 이용해서 그렇게 할 수는 없다.

Spring Boot는 default global AuthenticationManager를 제공하는데, 하나의 유저만을 가지고 있다.

The default AuthenticationManager has a single user (‘user’ username and random password, printed at INFO level when the application starts up)

Autorization or Access Control

인증이 성공하면 권한부여를 할 차례인데, 여기서의 핵심 전략은 AccessDecisionManager이다. 프레임워크에서 3가지 구현체를 제공하는데 3가지 모두 AccessDecisionVoter 체인에 위임한다. (이건 ProviderManager에서의 위임방식과 유사하다)

정리하면 인증이 완료된 이후 특정 리소스에 접근할 때 권한에 따라 접근을 허용할 것인지 여부를 결정하는 역할을 한다고 볼 수 있겠다. 그리고 제공되는 구현체 3가지를 보자.

  • AffirmativeBased : AssessDecisionVoter 중 하나라도 허용하는 경우 접근 권한을 부여
  • ConsensusBased : 다수결에 따라
  • UnanimousBased : 만장일치에 따라

대략 그림이 좀 그려지는 것 같은데… 기본적인 권한부여 전략을 가지고 그 전략을 구체적으로 결정하는데 사용할 여러 Voter들을 가질 수 있다. (Voter(유권자), vote(투표) 개인적으로는 확~ 와닫지는 않네;) 그런데 여러 voter의 역할 또는 여러개를 가져야하는 이유가 당장 떠오르지 않는다.

AccessDecisionVoter는 principal을 나타내는 Authentication과 접근하고자 하는 자원인 Object , 그리고 이 Object와 관련된 설정 속성인 ConfigAttributes에 대해서 고려한다. (즉, 이 인터페이스의 메서드 인자로서 이런 정보들을 이용한다는 의미)

1
2
3
4
5
public interface AccessDecisionVoter<S> {
    boolean supports(ConfigAttribute attribute);
	boolean supports(Class<?> clazz);
	int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}

Object는 제네릭 타입으로 유저가 액세스하려는 모든 것을 의미한다고 보자. 웹 리소스 또는 자바 클래스의 메서드가 가장 일반적인 케이스이다. (이와 관련해서 이 블로그 Spring Security로 Security 서비스 구축하기 2 내용을 보고 조금 이해가 되었다.) ConfigAttributes는 액세스에 필요한 권한 수준을 결정하는 secure Object의 일부 메타 데이터이다. ConfigAttributes는 인터페이스며 String을 반환하는 메서드 하나만 가진다. 이 문자열은 리소스 소유자가 의도하는 방식으로 인코딩하여 누가 액세스 할 수 있는지에 대한 규칙(rule)을 표현한다. 일반적인 ConfigAttribute는 사용자 role의 이름을ROLE_ADMIN 또는 ROLE_AUDIT)이고 종종 특수한 포멧을(ROLE_ 접두사 같은) 갖거나 평가해야하는 expression을 나타낸다.

대부분은 기본 AccessDecisionManagerAffirmativeBased를 그냥 사용한다. 이와 관련한 모든 사용자 정의(customization)은 voters에 새로운 voter를 추가하거나 기존 voter의 작동방식을 수정하는 등으로 이뤄진다.

ConfigAttributes으로 Spring Expression Language(SpEL) 표현식을 사용하는게 일반적이다. 예를 들어 isFullyAuthenticated() && hasRole('FOO'). 이는 표현식을 처리하고 이에 대한 context를 작성할 수 있는 AccessDecisionVoter에 의해 지원된다. 처리 할 수 있는 표현식의 범위를 확장하려면 SecurityExpressionRoot의 구현이 필요하고 때로는 SecurityExpressionHandler도 필요하다.

Web Security

UI와 HTTP 백엔드를 위한 웹 계층의 Spring Security는 서블릿 Filters를 기반으로 한다. 따라서 Filter의 역할을 먼저 살펴 보자. 아래 그림은 단일 HTTP 요청에 대한 핸들러의 일반적인 계층을 보여준다.

Filters

클라이언트가 앱에 요청을 보내고 컨테이너는 요청 URI의 경로를 기반으로 어떤 필터와 어떤 서블릿을 적용할지 결정한다. 하나의 서블릿이 단일 요청을 처리하지만 필터는 체인을 형성하므로 순서가 지정되며 실제로 요청 자체를 처리하려는 경우 필터가 나머지 체인을 거부 할 수 있다. 필터는 다운스트림 필터와 서블릿을 사용해서 요청과 응답을 수정할 수도 있다. 필터 체인의 순서는 매우 중요하며 Spring Boot는 두 가지 메커니즘을 통해 이를 관리한다. 하나는 Filter 타입의 @Beans@Order를 붙이거나 Orderd를 구현하는 것이고, 다른 하나는 API의 일부로 순서를 가지는 FilterRegistrationBean의 일부가 되는 것이다. 일부 이미 만들어진 필터는 서로 상대적인 순서를 나타내는데 도움이 되도록 상수를 정의한다. (예로 Spring Session의 SessionRepositoryFilterInteger.MIN_VALUE + 50의 값을 가지는 DEFAULT_ORDER를 가지며, 이는 체인의 앞단에 있고자 하지만 그 앞에 다른 필터를 배제하지는 않는다.)

Spring Security는 필터 체인에 단일 Filter로 설치되고 타입은 FilterChainProxy이다. Spring Boot 앱에서 security 필터는 ApplicationContext@Bean이고 모든 요청에 적용되도록 설치된다. SecurityProperties.DEFAULT_FILTER_ORDER에 의해 정의된 위치에 설치되며 FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER 값을 이용해서 초기화 되고 있다.(참고로 REQUEST_WRAPPER_FILTER_MAX_ORDER는 2.1.0 버전부터 Deprecated 되었고 OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER로 변경 되었다.)

1
public static final int DEFAULT_FILTER_ORDER = OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100;

컨테이너의 관점에서 Spring Security는 단일 필터지만 내부적으로는 각각 특별한 역할(role)을 하는 추가적인 필터들이 있다. 아래 그림을 참조:

Spring Security is a single physical Filter but delegates processing to a chain of internal filters

사실은 security 필터에서 간접적인 계층이 하나 더 있다. 일반적으로 DelegatingFilterProxy로 설치되며 Spring @Bean 일 필요는 없다. 이 Proxy는 항상 @Bean인 FilterChainProxy에 위임하며 일반적으로 springSecurityFilterChain라는 고정된 이름을 사용한다. 필터들의 체인으로 내부적으로 정리된 모든 보안 로직을 포함하는 FilterChainProxy다. 모든 필터는 동일한 API를 가지고 있으며(모두 서블릿 규약의 Filter 인터페이스를 구현하고 있음) 나머지 체인에 대해 거부 할 기회가 있다. (좀 더 자세한 내용은 Spring Security Reference를 한번 읽어보자. 그런데 내가 읽은건 좀 지난 버전이고 이 링크는 최신 버전인데… 내용이 좀 달라보이긴한다.)

Spring Security 필터는 필터 체인 목록을 포함하고 일치하는 첫 번째 체인에 요청을 보낸다. 아래 그림은 요청 경로 일치에 따라 발생하는 처리를 보여준다. 이건 일반적인 방법이긴 하지만 유일한 방법은 아니다.

The Spring Security FilterChainProxy dispatches requests to the first chain that matches

커스텀 security 설정이 없는 바닐라 Spring Boot 애플리케이션은 여러개의(n) 필터 체인이 있다. 첫 번째 (n-1) 체인은 /css/**/images/** 같은 정적 리소스 패턴과 오류 뷰 /error를 무시한다. 마지막 체인은 모든 경로 /**와 일치하며, 권한 부여, 예외 처리, 세션처리, 헤더 쓰기 등에 대한 로직을 포함하여 더 활성화 된다. 이 체인에는 기본적으로 총 11개의 필터가 있지만 일반적으로 사용자가 어떤 필터를 언제 사용하는지 고민 할 필요가 없다.

Creating and Customizing Filter Chains

Spring Boot 앱의 기본 fallback 필터 체인은 순서가 SecurityProperties.BASIC_AUTH_ORDER로 사전정의 되어있다.security.basic.enabled = false를 설정하여 완전히 끌 수도 있고, 더 낮은 순서로 다른 규칙을 정의 할 수도 있다. 이를 위해 WebSecurityConfigurerAdapter (또는 WebSecurityConfigurer) 타입의 @Bean을 추가하고 @Order를 클래스에 붙여라.

1
2
3
4
5
6
7
8
9
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/foo/**")
        ...;
    }
}

이 빈은 Spring Security가 새로운 필터 체인을 추가하고 순서를 fallback 이전으로 지정하고 있다.

많은 애플리케이션은 다른 리소스와 비교 할 때 한 리소스 집합에 대해 완전히 다른 액세스 규칙을 가지고 있다. 예를 들어 UI 및 backing API를 호스팅하는 애플리케이션은 UI부분에 대해서는 로그인 페이지로 리다이렉션되는 쿠키 기반 인증을, API 부분에서는 인증되지 않은 요청에 대한 401 응답을 사용하는 토큰 기반 인증을 지원 할 수 있다. 각 자원 세트에는 고유한 순서와 자체 요청 matcher가 있는 자체 WebSecurityConfigurerAdapter가 있다. 일치하는 규칙이 겹치면 가장 빠른 순서의 필터 체인이 우선한다.

Request Matching for Dispatch and Authorization

보안 필터 체인(또는 WebSecurityConfigurerAdapter)에는 HTTP 요청에 적용할 것 인가를 결정하는데 사용되는 request matcher가 있다. 특정 필터 체인을 적용하기로 결정하면 다른 필터 체인이 적용되지 않는다. 그러나 필터 체인 내에서 HttpSecurity 설정에 추가로 matcher를 설정하여 권한 부여를 보다 세밀하게 제어 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
      .authorizeRequests()
        .antMatchers("/foo/bar").hasRole("BAR")
        .antMatchers("/foo/spam").hasRole("SPAM")
        .anyRequest().isAuthenticated();
  }
}

Spring Security를 설정 할 때 저지르는 가장 흔한 실수 중 하나는 이러한 matcher가 다른 프로세스들에 적용된다는 사실을 잊는 것이다. 하나는 전체 필터 체인에 대한 request matcher이며 다른 하나는 적용 할 액세스 규칙을 선택하는 것이다.

Combining Application Security Rules with Actuator Rules

Spring Boot Actuator를 사용할 때 이 역시 기본적으로 보안에 안전하다. Spring Security가 적용된 애플리케이션에 Actuator를 추가하면 actuator endpoint에만 적용되는 필터 체인이 추가된다. actuator endpoint에만 일치하는 request matcher로 정의 되며, 디폴트 SecurityProperties fallback 필터보다 5만큼 낮은 ManagementServerProperties.BASIC_AUTH_ORDER의 순위를 가진다. 따라서 ballback 전에 참조된다.

만약 actuator endpoint에도 나의 security rule을 적용하려면 actuator 순위보다 빠른 순위로 필터 체인을 추가하면 된다. 그게 아니고 actuator endpoint에 대해서는 디폴트 security 세팅을 더 선호한다면 내가 추가할 필터 체인의 순위를 actuator와 fallback 사이의 순위로 추가하면 된다. (예: ManagementServerProperties.BASIC_AUTH_ORDER + 1)

1
2
3
4
5
6
7
8
9
@Configuration
@Order(ManagementServerProperties.BASIC_AUTH_ORDER + 1)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/foo/**")
        ...;
    }
}

Method Security

Spring Security는 자바 메서드 실행에 대해서도 접근 rule의 적용을 지원한다. 접근 rule을 정의 하는 방식은 동일하지만 코드상에서의 위치가 다르다. 메서드 보안을 활성화 시키기 위해서는:

1
2
3
4
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

그리고 메서드 레벨에:

1
2
3
4
5
6
7
@Service
public class MyService {
    @Secured("ROLE_USER")
    public String secure() {
        return "Hello Security";
    }
}

위 클래스는 보안 메서드를 가지고 있는 서비스다. 위 타입의 @Bean을 만들면 프록시가 적용된다. 따라서 이 메서드가 호출되면 실재 메서드 호출 전에 security 인터셉터를 통하게 된다. 만약 접근이 거부되면 AccessDeniedException가 발생한다.

메서드에 보안관련 제약을 강제하기 위한 어노테이션이 더 있다. 특히 @PreAuthorize, @PostAuthorize를 사용하여 메서드 매개 변수에 대한 참조를 포함하는 표현식을 작성할 수 있다.

Working with Threads

Spring Security는 기본적으로 스레드 바운드다. 기본적인 빌딩 블럭은 Authentication을 가지는 SecurityContext다 (유저가 로그인하면 명시적으로 authenticatedAuthentication이 된다). SecurityContextHoler의 static 메서드를 통해 언제든지 SecurityContext에 접근하고 조작 할 수 있다. 이는 단순히 ThreadLocal을 조작하는 것 이다.

1
2
3
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);

이런 코드는 유저 애플리케이션 코드에 일반적인 것은 아니지만, 사용자 정의 인증 필터를 작성하기 위해 유용하다.

웹 endpoint에서 현재 인증된 사용자에 대한 접근이 필요한 경우 @RequestMapping에서 메서드 매개 변수를 사용할 수 있다.

1
2
3
4
@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
    ... // do stuff with user
}

위 코드는 SecurityContext에서 현재 Authentication를 가져오고 거기서 getPrincipal()메서드를 호출하여 메서드 파라미터를 생성하게 된다. Authentication에서 Principal의 타입은 인증 유효성을 검사하는데 사용되는 AuthenticationManager에 따라 달라진다. 그러니 사용자 데이터에 대한 type-safe 한 참조를 얻는데 유용한 방식이다.

Spring Security가 사용 중인 경우 HttpServletRequestPrincipalAuthentication 타입이므로 직접 사용할 수도 있다.

1
2
3
4
5
6
@RequestMapping("/foo")
public String foo(Principal principal) {
    Authentication authentication = (Authentication) principal;
    User = (User) authentication.getPrincipal();
    ... // do stuff with user
}

이는 Spring Security가 사용되지 않는 상태에서 코드를 작성할 때 유용하다. (Authentication 클래스를 로딩하는데 더 방어적이어야 한다.)

Processing Secure Methods Asynchronously

SecurityContex는 스레드 바운드이므로 @Async가 붙은 메서드와 같이 비동기로 백그라운드에서 처리되는 secure 메서드를 호출해야 한다면 해당 컨텍스트도 전파되어야 한다. 이는 백그라운드에서 실행되는 작업 (Runnable, Callable 등)과 SecurityContext를 래핑하는 것으로 요약할 수 있다. Spring Security는 RunnableCallable에 대한 wrapper와 같은 몇 가지 도우미(helper)를 제공한다. SecurityContext@Async 메서드를 전파하려면 AsyncConfigurer를 설정하고 Executor가 옳은지 확인해야한다.

1
2
3
4
5
6
7
@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
    @Override
    public Executor getAsyncExecutor() {
        return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
    }
}
This post is licensed under CC BY 4.0 by the author.

Jackson UnrecognizedPropertyException - Unrecognized field

System call 이란?

Comments powered by Disqus.