티스토리 뷰

Dev/Spring

Spring Security Basic

마이스토리 2017. 6. 5. 16:33

이 글은 spring.io에서 제공하는 스프링 시큐리티 topical guide를 번역한 글입니다.

원문 : https://spring.io/guides/topicals/spring-security-architecture/


스프링 시큐리티 아키텍쳐


이 가이드는 스프링 시큐리티에 대한 프레임워크의 기본 개념 및 디자인 관점을 제공하는 입문서이다. 

어플리케이션 보안에 대한 가장 기본적인 것을 다루지만, 스프링 시큐리티를 경험한 개발자들이 애매하게 느끼는 부분들을 명확하게 해소해 줄 수 있다. 이를 위해 필터와 메소드 어노테이션을 이용한 웹 어플리케이션 보안을 다룰 것이다.

보안 어플리케이션이 어떻게 작동하는지, 어떻게 커스트마이징될 수 있는지, 아니면 단지 어플리케이션 보안에 대한 개념을 알고 싶다면 이 가이드를 활용해라.


이 가이드는 기본적인 문제 그 이상의 복잡한 문제해결에 대한 매뉴얼로 작성된 것은 아니지만, 초보자 뿐만 아니라 전문가에게도 유용할 수 있다.

스프링 부트는 보안의 기본 동작 및 설정을 제공해주고 전체적인 아키텍쳐에 어떻게 적용되는지 이해하는데 도움을 주기 때문에 참고되지만,

스프링 부트가 없어도 기본적인 원리는 어플리케이션에 동일하게 적용된다.


인증과 접근제어 (Authentication and Access Control)

어플리케이션 보안은 대개 2개의 독립적인 문제로 요약된다. 인증(Authentication:Who are you?) 와 인가(Authorization: What are you allowed to do?).

어떤 사람은 인가(Authorization) 대신에 접근제어(Accesc Control) 이라고도 말해서 혼란을 야기할 수도 있지만... 인가가 더 광범위 하기에 그 뜻을 이해하는데는 유용할 수도 있다.

스프링 시큐리티는 인증과 인가를 분리해서 디자인되어 있고, 각각에 대한 전략과 확장 포인트를 가지고 있다,.


인증(Authentication)

인증에 대한 주요 전략 인터페이스는 AuthenticationManager이고, 이는 단 한개의 메소드만 가지고 있다.

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

}

AuthenticationManager는 authenticate() 메소드를 통해 다음 3가지 중의 하나를 할 수 있다.


  1. 유효한 principal이면 Authentication을 리턴한다.
  2. 유효하지 않으면 AuthenticationException을 throw한다.
  3. 결정하지 못하면 null을 리턴한다.
AuthenticationException은 런타임 예외이다. 이는 대개 어플리케이션의 스타일과 목적에 따라 일반적인 방법으로 핸들링될 수도 있고, 직접 예외를 catch해서 핸들링할 수 있다. 예를 들면, 인증이 실패했다는 웹 페이지를 보여주던지, 401 응답을 리턴할 수 있다.(WWW-Authentication 헤더는 컨텍스트에 따라 있을 수도 없을 수도 있음.)

AuthenticationManager의 가장 일반적인 구현체는 ProviderManager이고, 이는 일련의 AuthenticationProverdier 인스턴스들에게 위임한다.
AuthenticationProvider는 AuthenticationManager와 조금 다르다. AuthenticationProvider는 호출자자 해당 인증유형을 지원하는지 확인할 수 있는 메소드를 제공한다.

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);

}
Class<?> 아큐먼트는 실제로는 Class<? extends Authentication> 이다. (authenticate() 메소드에 전달될 어떤 것을 지원하는지 여부를 묻는다.)
ProviderManager는 일련의 AuthenticationProvider들에게 위힘함으로써 동일 어플리케이션 내에서 여러 다른 인증 메커니즘을 제공할 수 있다.
ProviderManager가 특정 인증 유형을 인식하지 못한다면 그 인증은 그냥 스킵될 것이다.

종종 한 어플리케이션은 제한된 자원들의 논리적인 그룹들을 가지고 있고.(예를 들면, /api/** path에 해당되는 웹 리소스들),
각 그룹은 그 그룹만을 위한 AuthenticationManager를 가질 수 있다.
각각은 ProviderManager이며, 부모를 공유한다. 그 부모는 일종의 글로벌 리소스로 모든 ProviderManager에 대한 대체 역할은 한다.




Figure 1. An AuthenticationManager hierarchy using ProviderManager


Custimizing Authentication Managers

스프링 시큐리티는 일반적인 인증관리 특징들을 빠르게 설정할 수 있는 헬퍼드을 제공한다.

가장 많이 사용하는 헬퍼는 AuthenticationManagerBuilder 이고, 메모리, JDBC, LDAP 사용자 정보를 셋팅하고 커스텀 UserDetailService를 추가할 수 있다.


다음은 AuthenticationManager의 예이다.


@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

   ... // web stuff here

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

}

AuthenticationManagerBuilder는 @Autowired 된다. 이는 전역(부모) AuthenticationManager를 설정한다.


만약, 아래와 같이 한다면...


@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

  @Autowired
  DataSource dataSource;

   ... // web stuff here

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

}

이렇게 configure 메소드를 @Override하면, AuthenticationManagerBuilder은 단지, 전역(glocal) AuthenticationManager의 한 자식인 지역(local) AuthenticationManager를 생생하는 데 사용된다. 

스프링 부트 어플리케이션에서 전역 빈를 다른 빈에 주입하기 위해서는 @Autowired 를 사용할 수 있지만, 지역 빈은 명시적으로 선억하지 않는 이상 그렇게 할 수 없다.


스프링부트는 별도의 AuthenticationManager를 선언하지 않으면 기본 전역 AuthentiionManager(with just one user)를 제공한다.

커스텀 global AuthenticationManager가 필요하지 않다면 이 디폴트만으로 충분하고,

필요하다면 지역적으로 보호가 필요한 자원에 대해서만 AuthenticationManager를 추가할 수 있다.


Authentication or AccessControl

인증이 되었다면, 다음 인가를 처리할 수 있고, 이것의 핵심은 AccessDecisionManager 이다.

프레임워크에서 제공되는 3가지 구현체가 있고. 이 3가지는 AccessDecisionVoter 체인에 위임한다. ( ProviderManager가 AuthenticationProviders 들에게 위임하는 것과 유사.)


AccesDecisionVoter는 Authentication(representing a pricipal), 보안 Object, ConfigAttrubutes 를 참고한다.


boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

int vote(Authentication authentication, S object,
        Collection<ConfigAttribute> attributes);

Object는 완전히 generic하며, 웹 리소스나 메소드일 수 있다.

ConfigAttribute는 보안object에 접근가능한 퍼미션 레벨을 판단할 수 있는 메타정보를 제공한다.

ConfigAttribute는 단 하나의 메소드만을 가지는 인터페이스이며, 이 메소드는 누가 접근권한을 가지는지를 표현하는 문자열을 리턴한다. 대개 ROLE_ADMIN, ROLE_AUDIT 등과 같이 사용자 role명이며, 종종 ROLE_ 접두사와 같은 특별한 포맷을 가지거나 평가할 필요가 있는 표현식을 가진다.


대게는 기본 AccessDecisionManager를 사용하고, 이것은 AffirmativeBased  이다. (voter가 거부하지 않으면 허가되는)

새로운 voter를 추가하거나 기존의 내용을 수정하여 커스마이징 할 수 있다.


ConfigAttrubutes는 보통 SpEL(Spring Expression Lagnuage)를 사용하는 게 일반적이다. (예, isFullyAuthenticated() && hasRole('FOO') )

이는 표현식을 처리하고 컨텍스트를 작성할 수 있는 AccessDecisionVoter가 자원한다.

처리할 수 있는 표현식 범위를 확장하기 위해서는 SecurityExpressionRoot 및 SecurityExcpressionHandler의 커스트마아징이 필요하다.


 

Web Security

웹 티어에서의 스프링 시큐리티는 서블릿 필터에 기반한다. 그래서, 일반적으로 먼저 필터들의 역할을 살펴보는 것이 도움이 된다.

아래 그림은 하나의 HTTP요청를 처리하는 전형적인 계층을 표현한다.



클라이언트가 요청을 보내면 컨테이너는 요청 URI의 경로에 기초해서 어떤 필터와 서블릿이 적용될지 결정한다.

하나의 서블릿이 하나의 요청을 처리하지만, 필터들은 체인을 형성하고 순서가 정해진다.

사실 필터는 요청 자체를 저리하기 원한다면 체인의 나머지를 거부할 수 있다.

필터는 요청 또는 응답을 수정할 수 있다.

필터의 순서는 매우 중요하다. 스프링 부트는 2 메커니즘으로 이를 관리한다.

하나는 필터 빈에 @Order 를 사용하거나 Ordered를 구현한다. 

다른 하나는 API일부로써 순서를 가지는 FiterRegistraionBean의 일부가 되는 것이다.


일부 기성 필터들은 자신의 상수를 정의하여 서로에 대한 순서를 알려준다.

예를 들면, Spring Session 의 SessionRepositoryFilter는 Integer.MIN_VALUE + 50의 DEFAULT_ORDER를 가지며, 체인의 초기 단계에 있는 것을 좋아하지만, 이전에 오는 다른 필터를 배재하지 않는다.


스프링 시큐리티는 하나의 Filter로 설치되고, 이는 FilterChainProxy로 완성된다.

스프링 부트에서 security filter는 ApplicationContext에 잇는 @Bean 이고, 모든 요청에 대해 적용된다.

SecurityProperties.DEFAULT_FILTER_ORDER로 정의된 위치에 설치되고, 이는 FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER에 의해 고정된다.?


컨테이너에서 보면 Spring Security는 단일 필터이지만, 그 안에는 특별한 역할을 하는 추가 필터들이 있다.





실제로 시큐리티 필터에는 간접적인 계층이 하나 더 있다. 일반적으로 컨테이너에는 DelegatingFilterProxy로 설치되며 Spring @Bean일 필요는 없다.

프록시는 항상 @Bean인 FilterChainProxy에 위임한다. 보통 springSecurityFilterChain 이라는 고정된 이름이다.

FilterChainProxy는 내부적으로 필터 체인으로 정렬된 모든 보안 논리를 포함한다.

모든 필터에는 동일한 API가 있고(서블릿 스펙의 필터 인터페이스 구현), 나머지 필터는 모두 거부할 수 있다.


동일한 최상위 FilterChainProxy와 컨테이너에 알려지지 않은 모은 것? 에 스프링 시큐리티에 의해 관리되는 여러 개의 필터 체인이 있을 수 있다.

스프링 시큐리티 필터는 필터체인 목록을 포함하고, 클라이언트 요청을 그와 매치되는 첫번째 필터에 dispatch한다.

아래 그림은 요청 path 매칭에 기반한 dispatch 상황을 나타낸다.(/foo/** 는 /** 보다 이전에 매칭된다.)

이는 일반적인 방법이지만 요청을 매칭하는 유일한 방법은 아니다.

이 dispatch 프로세서의 중요한 점은 요청을 처리하는 체인은 하나의 체인이라는 것이다.




 사용자 정의 보안 설정이 없는 순수 스프링 부트 어플리케이션에는 여러 개의 필터 체인이 있고 보통 6개 다.

첫 번째 체인은 /css/**, /images/** 와 같은 정적인 리소스 패턴과 오류 뷰 /error 를 무시하기 위한 것이다. (경로는 SecurityProperties환경설정 에서 security.ignored 설정으로 정해질 수 있다.)

마지막 체인은 모든 경로 /** 에 매치되며, 인증, 권한부여, 예외처리, 세션처리, 헤더 쓰기 등을 위한 로직을 포함한다. 기본적으로 이 체인에는 총 11개의 필터가 있지만 일반적으로 사용자가 어떤 필터를 사용하고 언제 사용하는지에 대해 일 필요는 없다.


스프링 시큐리티 내부의 모든 필터가 컨테이너에 알려지지 않는다는 것이 중요하다. 특히, Spring Boot 어플리케이션에서.

필터 유형의 모든 @Bean은 기본적으로 컨테이너에 자동으로 등록된다. 따라서 사용자 정의 필터를 보안 체인에 추가하려면 컨테이너를 @Bean으로 만들거나 컨테이너 등록을 명시적으로 비활성화하는 FilterRegistrationBean으로 포장하지 말아야 한다.???


Creating and Customizing Filter Chains

스프링 부트 내의 기본 폴백 필터 체인 (/** 요청에 매칭되는) 은 SecurityProperties.BASIC_AUTH_ORDER 로 정해진 순서를 갖는다.

security.basic.enabled=false 로 설정을 끌 수 있고, 더 낮은 순위의 규칙을 정의할 수 있다.

그렇게 하려면 WebSecurityConfigurerAdapter(또는 WebSecurityConfigurer) 유형의 @Bena 을 추가하고 @Order를 사용하면 된다.

예)

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
     ...;
  }
}

이 빈은 스프링 시큐리티에 새로운 필터를 추가하고 폴백 이전에 순서를 지정할 것이다.


많은 어플리케이션은 자원들마다 완전히 다른 액세스 룰을 가지고 있다.

예를 들면, UI 파트는 로그인 페이지로 리다이렉트 하는 쿠키 기반의 인증처리를 가지고, API 파트는 404 응답을 리턴하는 토큰 기반의 인증처리를 가질 수 있다.


각각의 리소스 셋은 각자의 WebSecurityConfigurerAdapter를 가지고, 그것은 유일한 순서와 그만의 request matcher를 가진다.

매칭 출이 중복되면 순서가 먼저인 필터체인을 탄다.


Request Matching for Dispatch and Authorization

보안 필터 체인(동일하게 WebSecurityConfigurerAdapter)은 http요청을 처리할 지를 결정하는 request matcher를 가지고 있다.

특정 필터 체인이 결정되면 다른 체인은 적용되지 않는다.

하지만, 필터 체인 내에서 HttpSecurity configurer 에 추가적인 matcher를 설정하여 세분화된 권한 제어를 할 수 있다.

예0

@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();
  }
}

Combining Application Security Rules with Actuator Roles

스프링 시큐리티 어플리케이션에 스프링 액츄에이터를 추가하면 자동으로 액츄에이터 endpoints에 추가 필터 체인이 생기며, 기본 SecurityProeprties폴백 필터보다 5 작은 순서(ManagementServerProperties.BASIC_AUTH_ORDER)로 적용되므로 폴백 전에 참조된다.


엑츄에이터 기본보안설정을 선호하는 경우는 액츄에이터보다 +1 로 순서 설정.

@Configuration
@Order(ManagementServerProperties.BASIC_AUTH_ORDER + 1)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
     ...;
  }
}

# 웹 티어의 Spring Security는 현재 서블릿 API에 묶여 있으므로, 임베디드 또는 기타 서블리 컨테이너에서 앱을 실행할 때만 적용된다.

 그러나, Spring MVC나 나머지 Spring 웹 스택에는 연결되어 있지 않으므로, 모든 서블릿 애플리케이션( 예: JAX-RS를 사용하는 앱) 에 적용할 수 있다.


Method Security

메소드 보안을 위해서는 먼저 아래와 같이 설정해야 한다.

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

그리고, 해당 보안 메소드에 @Secured 어노테이션을 사용한다.


@Service
public class MyService {

  @Secured("ROLE_USER")
  public String secure() {
    return "Hello Security";
  }

}

caller가 권한이 없다면 AccessDeniedException 이 발생한다.


@PreAuthorize 및 @PostAuthorize 어노테이션으로 메서드 매개변수 및 반환값에 대한 참조가 포함된 식을 작성할 수 있다.


# 웹 보안과 메소드 보안을 같이 사용하는 것이 드문 일은 아니다.

필터 체인은 인증 및 로그인 페이지로의 리다이렉션 등과 같은 사용자 경험 기능을 제공하며, 메소드 보안은 보다 세부적인 수준의 보호기능을 제공한다.


Working with Threads

스프링 시큐리티는 근본적으로 thread bound 이다. 왜냐하면, 현재의 인증된 principa;을 다양한 downstream consumer들이 이용할 수 있게 해야하기 때문이다. 기본 빌딩 블록은 Authentication을 포함 할 수 있는 SecurityContext이다. (사용자가 로그인하면 명시적으로 인증된 Authentication이 된다.)

SecurityContextHolder의 정적 메소드를 통해 항상 SecurityContext에 액세스하고 조작할 수 있다. SecurityContextHolder는 ThreadLocal.


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

코드에서 이 작업을 하는 것은 일반적이지 않지만, 사용자 정의 인증필터를 작성해야 하는 경우는 유용할 수 있다.


웹 엔드포인트에서 현재 인증된 사용자엑 액세스해야하는 경우 @RequestMapping에서 메소드 매개변수로 사용할 수 있다.


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

이 어노테이션은 현재 Authentication을 SEcurityContext에서 꺼내서 getPrincipal() 메소드를 호출하여 메소드 매개변수를 생성한다. Principal의 유형은 인증을 확인하는데 사용되는 AuthenticationManager에 따라 다르므로 사용자 데이타에 대한 type safe 한 참조를 얻는 것이 유용하다.


스프링 시큐리티가 사용중이면, HttpServletReqeust의 Principal은 Authentication 유형이 될 것이므로 직접 사용할 수도 있다.


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

이것은 스프링 시큐리티가 사용되지 않을 때 작동하는 코드를 작성해야 할 때 유요할 수 있다.(Authentiction 클래스를 로드하는 것에 대해 좀 더 방어해야한다.)


Processing Secure Methods Asynchronously

SecurityContext는 쓰레드 바운드이므로 보안 메서드를 호출하는 백그라운드를 수행하려는 경우 @Async를 사용하면 컨텍스트가 전달되는 시 확인해야 한다. 이것은 백그라운드에서 실행되는 작업(Runnable, Callable)으로 SecurityContext를 래핑하는 것으로 귀결된다.

스프링 시큐리티는 Runnable과 Callable에 대한 래퍼와 같은 좀 더 쉬운 헬퍼를 제공한다.

SecurityContext를 @Async메소드에 전파하려면 AsyncConfigureer를 제공하고 Executor가 올바른 유형인지 확인해야 한다.


@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

  @Override
  public Executor getAsyncExecutor() {
    return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
  }

}




댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31