10. Web Application Security

10.1 The Security Filter Chain

Spring Security는 표준 서블릿 필터에 기반하고 있다. 다른 프레임워크(Spring MVC같은)에 대한 의존성이 없으므로 특정 웹 기술에 대해 종속되지 않는다.

Spring Security는 내부적으로 필터 체인을 유지한다. 각 필터들은 고유의 역할을 가지고 있고, 서비스들이 어떤 요구사항이 있는지에 따라 필터를 추가하거나 삭제할 수 있다. 필터의 순서는 필터 사이에 의존성이 있으므로 중요하다. 네임스페이스 구성을 사용하고 있다면 필터가 자동으로 구성되며 스프링 빈을 명시 적으로 정의 할 필요는 없다. 하지만 네임스페이스에서 지원하지 않는 기능을 쓰거나, 커스터마이징된 버전을 사용해서 Security Filter Chain을 전체적으로 제어하고 싶을 때는 빈으로 등록하면 된다.

10.1.1 DelegatingFilterProxy

서블릿 필터를 사용할 때는 web.xml 에 명시적으로 선언해야한다. 그렇지않으면 서블릿 컨테이너에서 무시된다. Spring Security에서 필터 클래스는 애플리케이션 컨텍스트에 정의 된 스프링 빈 이기도 하므로 Spring의 풍부한 의존성 주입 기능과 라이프 사이클 인터페이스를 이용할 수 있다. Spring의 DelegatingFilterProxy는 web.xml과 애플리케이션 컨텍스트 사이의 링크를 제공한다.

DelegatingFilterProxy를 사용할 때 web.xml 파일에 다음과 같이 정의힌다.

<filter>
  <filter-name>myFilter</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
  <filter-name>myFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

필터는 실제로는 DelegatingFilterProxy이며 실제로 필터 로직을 구현한 클래스는 아니다. DelegatingFilterProxy가 하는 일은 Filter 메소드를 Spring Application Context에서 얻은 Bean에 위임하는 것이다. 이를 통해 빈은 Spring 웹 애플리케이션 컨텍스트 라이프 사이클 지원 및 구성 유연성의 이점을 누릴 수 있다. 빈은 javax.servlet.Filter를 구현해야하며 filter-name 요소의 이름과 동일한 이름을 가져야 한다. 자세한 정보는 DelegatingFilterProxy에 대한 Javadoc을 참조.

10.1.2 FilterChainProxy

생략

<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<constructor-arg>
    <list>
    <sec:filter-chain pattern="/restful/**" filters="
        securityContextPersistenceFilterWithASCFalse,
        basicAuthenticationFilter,
        exceptionTranslationFilter,
        filterSecurityInterceptor" />
    <sec:filter-chain pattern="/**" filters="
        securityContextPersistenceFilterWithASCTrue,
        formLoginFilter,
        exceptionTranslationFilter,
        filterSecurityInterceptor" />
    </list>
</constructor-arg>
</bean>

ASC: allowSessionCreation. false일 경우 jsessionid를 만들지 않음. 대규모 볼륨의 애플리케이션일때 활용

Bypassing the Filter Chain

filters = "none"으로 설정하면 모든 접근을 허용함. 대신 이때 SecurityContextHolder에 값이 없을 것이다

10.1.3 Filter Ordering

아래와 같은 순서이어야 함

  • ChannelProcessingFilter : 다른 프로토콜로 리다이렉트가 필요할 경우가 있을 수 있으므로
  • SecurityContextPersistenceFilter : SecurityContextSecurityContextHolder에 세팅되어 함 (HttpSession 사용)
  • ConcurrentSessionFilter : SecurityContextHolder를 참조하여 SessionRegistry에 변경사항을 반영함
  • Authentication processing mechanism들. UsernamePasswordAuthenticationFilter, CasAuthenticationFilter, BasicAuthenticationFilter 등. SecurityContextHolder이 올바르게 수정되어 유효한 Authentication request token을 포함할수 있도록.
  • SecurityContextHolderAwareRequestFilter : servlet container에 Spring Security aware HttpServletRequestWrapper를 넣어주기 위해서.
  • JaasApiIntegrationFilter: JaasAuthenticationTokenSecurityContextHolder에 있을 경우 JaasAuthenticationToken에 있는 Subject으로서 FilterChain을 처리할 것이다
  • RememberMeAuthenticationFilter : .... 생략
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor

10.1.4 Request Matching and HttpFirewall

Spring Security에서 사용자 지정 패턴이 검사되는 지점

  • FilterChainProxy : 어떤 필터 체인으로 Request를 통과시킬지 결정
  • FilterSecurityInterceptor : 어떤 보안 제약사항이 Request에 적용 될 지를 결정

지정했던 패턴이 검사 될 때 어떤 URL 값이 사용되는지에 대한 메커니즘을 이해하는 것은 중요하다.

서블릿 스펙에 의해, HttpServletRequest에서 getter 메서드를 통해 contextPath, servletPath, pathInfo, queryString에 접근 가능하다. Spring Security는 오로지 애플리케이션 내부 경로를 보호하는 것에만 관심이 있으므로, contextPath는 무시된다. 불행히도 서블릿 스펙은 servletPathpathInfo에 어떤 URI가 정확히 들어가는지에 대한 정의가 없다. 예를 들어, RFC2396에 의하면 URL의 각 path segment에는 파라미터가 포함 될 수 있다. 해당 스펙에는 이런값들이 servletPath와 pathInfo에 포함되어야 하는지가 명확하지 않고, 서블릿 컨테이너들마다 동작이 다르다. path parameter를 strip하지 않는 컨테이너들에 애플리케이션이 디플로이 되면 공격자는 패턴매칭을 이용해 공격할 수 있다. 다른 URL 변형 또한 가능하다. 예를 들어 경로 변경 시퀸스(/../ 등과 같이)나 다중 포워드 슬래시(//)가 패턴 매칭을 실패시킬수 있다. 컨테이너마다 이런 항목에 대한 처리를 해주는것도 안해주는 것도 있다. 이것을 보완하기 위해 FilterChainProxyHttpFirewall 전략을 사용해 request를 래핑한다. 기본적으로 정규화되지 않은 요청은 자동으로 거절되며, 다중 슬래시는 삭제된다. 그렇기 때문에 FilterChainProxy가 security filter chain을 관리하기 위해 사용되는 것이 필수적이다.

servletPathpathInfo는 컨테이너에 의해서 디코딩 되므로, 당신의 애플리케이션은 세미컬론을 포함한 유효 경로를 포함해서는 안된다. 이런 파트들은 매칭 목적을 위해 제거 될 것이다.

위에서 언급했듯이 기본 전략은 Ant-style path이며, 아마 대부분의 유저들에게 최선의 선택일 것이다. 이 전략은 AntPathRequestMatcher에 구현되어 있으며 스프링의 AntPathMatcher를 사용해 servletPathpathInfo 매칭을 수행할 것이며 queryString은 무시된다.

특별한 이유가 있어서 좀 더 파워풀한 매칭 전략이 필요하다면, 정규표현식을 쓸수도 있다. 이 전략은 RegexRequestMatcher에 구현되어 있고, 자세한 내용은 Javadoc을 참조하자.

실전에서는, 웹-애플리케이션 레벨에서의 보안 제약사항에 완전히 의존하기 보다는 서비스 레이어의 메서드 보안을 사용할 것을 추천한다. URL들은 변화하고, 모든 경우의 수를 고려하는 것은 어렵다. 이해하기 쉬운 간단한 ant path들을 직접 제한해 봐야 한다. 언제나 "기본값은 deny' 접근법으로 마지막에 엑세스를 제한시키도록 노력해라.

서비스 레이어에서 정의된 보안은 견고하고 지나치기 어려우므로, 항상 Spring Security의 메서드 보안 옵션을 활용해야 한다.

기본적으로 StrictHttpFirewall이 사용된다. 이 구현은 기본적으로 위험한 요청을 모두 거절한다. 요구사항 비해 너무 stict하다면, 거절할 요청 타입을 커스터마이징 할 수 있다. 하지만 이것들이 공격지점을 노출시킬수 있다는 사실을 주의해야 한다. 만약 Spring MVC의 Matrix 변수를 활용하고 싶다면, xml에 아래와 같이 정의해라.

<b:bean id="httpFirewall"
      class="org.springframework.security.web.firewall.StrictHttpFirewall"
      p:allowSemicolon="true"/>

<http-firewall ref="httpFirewall"/>

자바 설정으로 하려면 StrictHttpFirewall bean을 노출시키면 된다.

@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowSemicolon(true);
    return firewall;
}

StrictHttpFirewallCross Site Tracing(XST), HTTP Verb Tampering 공격을 보호하기 위한 유효 HTTP method들에 대한 화이트 리스트를 제공한다(기본값: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT).

커스터마이징 예:

<b:bean id="httpFirewall"
      class="org.springframework.security.web.firewall.StrictHttpFirewall"
      p:allowedHttpMethods="GET,HEAD"/>

<http-firewall ref="httpFirewall"/>
@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
    return firewall;
}

new MockHttpServletRequest()의 HTTP 메소드는 "" 이므로 new MockHttpServletRequest("GET", "") 으로 생성해 줘야 한다. (이슈번호 SPR_16851)

추천하진 않지만 모든 HTTP 메서드를 오픈해야 한다면 StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)를 사용해라.

10.1.5 Use with other Filter-Based Frameworks

다른 필터기반 프레임워크를 사용한다면 Spring Security filter가 먼저 나오도록 해야 한다. 그래야 다른 필터들이 사용할 수 있도록 SecurityContextHolder가 세팅된다. ex) SiteMesh, Wicket 등

10.1.6 Advanced Namespace Configuration

namespace 기반에서 서로다른 URL 패턴에 대해 다른 설정값 적용시키기. '구체적인' 패턴부터 먼저 선언해줘야 한다.

<!-- Stateless RESTful service using Basic authentication -->
<http pattern="/restful/**" create-session="stateless">
<intercept-url pattern='/**' access="hasRole('REMOTE')" />
<http-basic />
</http>

<!-- Empty filter chain for the login page -->
<http pattern="/login.htm*" security="none"/>

<!-- Additional filter chain for normal users, matching all other requests -->
<http>
<intercept-url pattern='/**' access="hasRole('USER')" />
<form-login login-page='/login.htm' default-target-url="/home.htm"/>
<logout />
</http>

10.2 Core Security Filters

Spring Security에는 웹 애플리케이션에서 항상 사용되는 주요 필터들이 있어서, 사용되는 클래스와 인터페이스들을 먼저 살펴볼것이다. 모든 피처를 다 다루진 않으므로, 전체적인 그림을 얻으려면 Javadoc을 참조해라.

10.2.1 FilterSecurityInterceptor

이미 FilterSecurityInterceptor에 대해서는 8.1.5에서 intercept-url 요소들이 내부적으로 결합되어 사용되는것을 간단히 살펴봤었다. 여기서는 FilterChainProxy와 동반 필터인 ExceptionTranslationFilter를 명시적으로 설정하는 방법에 대해 살펴보겠다.

<bean id="filterSecurityInterceptor"
    class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="securityMetadataSource">
    <security:filter-security-metadata-source>
    <security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/>
    <security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
    </security:filter-security-metadata-source>
</property>
</bean>

FilterSecurityInterceptor는 HTTP 리소스 보안을 처리할 책임이 있다. 이것은 AuthenticationManagerAccessDecisionManager에 대한 레퍼런스를 필요로 한다. 각기 다린 HTTP URL request에 대한 Configuration attributes(ROLE_어쩌구 등.. 8.1.5에서 언급)도 같이 지정 된다.

FilterSecurityInterceptor는 Configuration attributes와 함께 두가지 방법으로 설정 될 수 있다. 하나는 위에서 본것처럼 <filter-security-metadata-source> 요소를 이용한 방식이다. 이것은 namespace 챕터의 http 요소와 유사하지만, <intercept-url> 자식 요소들은 오직 patternaccess 만을 사용한다. 컴마를 통해 각기 다른 configuration attribute가 HTTP URL에 적용된다. 두번째 방식은 나만의 SecurityMetadataSource를 작성하는 것이다. 하지만 이것은 이 문서의 영역을 벗어난다. 어떤 접근법을 사용하든 관계없이, SecurityMetadataSource는 각 secure HTTP URL에 적용되는 configuration attribute를 List<ConfigAttribute>로 반환해야 하는 책임을 갖는다.

FilterSecurityInterceptor.setSecurityMetadataSource()FilterInvocationSecurityMetadataSource 인스턴스를 필요로 한다. 이것은 SecurityMetadataSource의 서브클래스를 나타내는 마커 인터페이스이다. 이것은 단순히 SecurityMetadataSourceFilterInvocation을 이해하고 있다는 것을 나타낸다. 단순함을 위해 앞으론 FilterInvocationSecurityMetadataSourceSecurityMetadataSource라고 언급 하겠다(대부분의 유저에겐 상관이 없을것이다)

네임스페이스 문법에 의해 생성된 SecurityMetadataSource는 특정 FilterInvocation에 대한 configuration attribute들을 pattern 항목에 의해 설정된 request URL에 의해 획득한다. 디폴트로는 모든 표현식을 apache ant path로 취급할 것이고, 복잡한 케이스를 위한 정규표현식 표현 또한 지원된다. request-matcher 항목으로 사용할 패턴 종류를 지정한다. 동일 정의에 여러 문법을 섞어 쓸수는 없다. ant path 대신 정규표현식 필터로 표현된 설정은 아래와 같다.

<bean id="filterInvocationInterceptor"
    class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="runAsManager" ref="runAsManager"/>
<property name="securityMetadataSource">
    <security:filter-security-metadata-source request-matcher="regex">
    <security:intercept-url pattern="\A/secure/super/.*\Z" access="ROLE_WE_DONT_HAVE"/>
    <security:intercept-url pattern="\A/secure/.*\" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
    </security:filter-security-metadata-source>
</property>
</bean>

패턴들은 정의된 순서대로 평가(evaluate)된다. 따라서 더 구체적인 패턴이 덜 구체적인 패턴보다 리스트 상위에 있어야 한다. 예제도 그런식이다. 만약 /secure/가 먼저 나온다면 /secure/super/ 패턴도 해당 패턴에 속하므로 /secure/super/ 항목은 평가가 일어나지 않을 것이다.

10.2.2 ExceptionTranslationFilter

ExceptionTranslationFilter는 보안 필터 스택에서 FilterSecurityInterceptor 위에 위치한다. 그 자체로는 보안 관련한 동작에는 관여하지 않지만, security interceptor에 의해 발생한 예외들을 적절한 HTTP response로 처리한다.

<bean id="exceptionTranslationFilter"
class="org.springframework.security.web.access.ExceptionTranslationFilter">
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
<property name="accessDeniedHandler" ref="accessDeniedHandler"/>
</bean>

<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>

<bean id="accessDeniedHandler"
    class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage" value="/accessDenied.htm"/>
</bean>

AuthenticationEntryPoint

AuthenticationEntryPoint는 인증되지 않은 유저가 HTTP resource를 요청한 경우 호출된다. 콜스택 깊이 security interceptor에 의해 AuthenticationException 또는 AccessDeniedException이 적절히 던져지고, entiry point의 commence를 호출 할 것이다. 여기서 사용자가 인증과정을 시작할 수 있도록 적절한 response를 보내는 작업을 한다. 여기서 우리가 사용한 것이 바로 LoginUrlAuthenticationEntryPoint이고, 이것은 다른 URL(일반적으로 로그인 페이지)로 리다이렉트를 해준다.실제 사용되는 구현체는 당신이 애플리케이션에서 사용되길 원하는 인증 메커니즘에 따라 다를것이다.

AccessDeniedHandler

만약 유저가 이미 인증되어있는데 보호된 리소스에 접근하려고 한다면? 일반적인 상황의 애플리케이션 워크플로우라면 이런일은 없을것이다. 예를들면 어드민 페이지 링크는 해당 롤을 가진 사용자에겐 보여지지 않을 것이다. 하지만 유저가 직접 url을 입력하거나 하는 방식으로 접근이 가능하므로 이런 시나리오에 대한 보호가 필요하다. 그러므로 서비스 레이어 기반 보안이 필요하다.

만약 이미 인증된 유저에게서 AccessDeniedException이 발생했다면, 이것은 퍼미션 부족을 의미할것이다. 이러한 상황에서 ExceptionTranslationFilter는 두번째 전략인 AccessDeniedHandler 전략을 실행시킬것이다. 기본적으로 AccessDeniedHandlerImpl은 클라이언트에게 403(Forbidden) 응답을 보낸다. 이에 대한 대안으로 위에서와 같이 명시적으로 설정을 고쳐서 에러 페이지 URL을 세팅하고, 간단한 "access denied" 페이지로 보내버리거나, MVC controller를 통해 다른 복잡한 작업을 수행하게끔 할 수도 있다. 입맛대로 커스터마이징이 가능하다.

namespace 설정으로도 가능하며, 자세한 내용은 the namespace appendix 참조.

SavedRequests and the RequestCache Interface

ExceptionTranslationFilter의 또다른 책임은 AuthenticationEntryPoint를 호출하기 전 현재의 요청을 저장하는 일이다. 이 작업은 사용자가 인증 작업을 한 후 최초 요청을 다시 복원할 수 있게 해준다(web authentication의 이전 overview를 참조). 일반적인 예로, form 로그인 이후 original URL로 리다이렉트 되는 것이 있다(SavedRequestAwareAuthenticationSuccessHandler)

RequestCacheHttpServletRequest 인스턴스를 저장하고 복원시키는데 필요한 기능들을 캡슐화 했다. 기본적으로 HttpSessionRequestCache가 쓰이며, 요청을 HttpSession에 저장한다. RequestCacheFilter는 유저가 original URL로 리다이렉트 될 때 저장되어있던 request를 복원해주는 작업을 해준다.

일반적인 상황에서 이러한 기능들을 수정할 필요는 없겠지만 저장된 요청을 처리하는 것은 최선의 노력에 의한 것이고 기본 설정으로 다룰 수 없는 상황이 있을수도 있다. 이러한 인터페이스들을 사용함으로써 Spring Security 3.0부터는 완전히 플러거블 해졌다.

10.2.3 SecurityContextPersistenceFilter

Technical Overview에서 웬만한 주요 필터들의 목적들은 모두 다루었다. 그중 FilterChainProxy을 어떻게 설정할 수 있는지 먼저 알아보자. bean이 필요로 하는 기본 설정은 아래와 같다.

<bean id="securityContextPersistenceFilter"
class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/>

앞에서 살펴봤듯이, 이 필터는 두가지 주요 작업을 가지고 있다. HTTP 요청들간의 SecurityContext 컨텐츠를 저장하는 역할과 요청이 완료되었을 때 SecurityContextHolder를 지우는 작업이다. context가 저장된 곳에서 ThreadLocal을 지우는 작업은 필수적인데, 컨테이너가 쓰레드풀을 사용해서 security context가 애먼사람한테 지정되는 경우가 발생할수도 있기 때문이다. (잘못된 credential로 operation 수행)

SecurityContextRepository

Spring Security 3.0부터, security context 를 로딩하고 복원하는 작업은 분리된 Strategy 인터페이스에 위임되었다.

public interface SecurityContextRepository {

SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);

void saveContext(SecurityContext context, HttpServletRequest request,
        HttpServletResponse response);
}

HttpRequestResponseHolder는 유입되는 request와 response 객체에 대한 컨테이너에 불과하다. 리턴된 컨텐츠는 필터 체인에 전달된다.

기본 구현체는 HttpSessionSecurityContextRepository이며, 이것은 security context를 HttpSession attribute에 저장한다. 이 구현에서 가장 중요한 configuration parameter는 allowSessionCreation 이며, 기본값은 true이고 이것이 인증된 유저에 대한 security context를 저장할 필요가 있을 때 세션을 생성할 수 있도록 해준다(인증이 발생하지 않았고 security context 내용이 변경되지 않았다면 생성하지 않을 것이다). 세션이 생성되지 않길 원한다면 이 값을 false로 바꿀 수 있다.

<bean id="securityContextPersistenceFilter"
    class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
<property name='securityContextRepository'>
    <bean class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>
    <property name='allowSessionCreation' value='false' />
    </bean>
</property>
</bean>

대신 NullSecurityContextRepository 구현체를 제공할수도 있는데, request가 이미 세션을 생성 했더라도 Security context가 저장되는 것을 방지한다.

10.2.4. UsernamePasswordAuthenticationFilter

지금까지 Spring Security 웹 설정에 항상 존재하는 주요 3개 필터를 살펴 보았다. 이 3개 필터들은 <http> 요소에 의해 자동으로 생성되며 다른 것들로 대체되지 않는다. 현재 빠진 부분은 사용자의 인증을 담당하는 실제 메커니즘에 관한 부분이다. 이 필터는 가장 대중적이로 쓰이는 인증 필터이며 제일 종종 커스터마이징 되는 필터이기도 하다. 이 필터는 namespace에서 <form-login> 에 사용되는 구현체를 제공한다. 설정 방법은 아래 3단계 이다.

  • 위에서 했던것 처럼, 로그인 페이지의 URL과 함께 LoginUrlAuthenticationEntryPoint를 설정하고, ExceptionTranslationFilter 위에 set 한다.
  • 로그인 페이지를 구현한다(JSP나 MVC controller 사용).
  • application context에 UsernamePasswordAuthenticationFilter 인스턴스를 설정한다.
  • filter chain proxy에 필터 빈을 추가한다. (순서에 유의).

로그인 폼은 간단히 username 와 password input 피드를 포함하고, 필터에 의해 모니터링 되는 URL(기본값은 /login)로 포스팅 된다. 기본 필터 설정은 아래와 같다.

<bean id="authenticationFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>

Application Flow on Authentication Success and Failure

이 필터는 설정된 AuthenticationManager를 호출해서 인증 요청들을 처리한다. 성공/실패 목적지는 각각AuthenticationSuccessHandlerAuthenticationFailureHandler strategy 인터페이스에 의해 컨트롤 된다. 이 필터는 이 동작들을 커스터마이징 할 수 있는 프로퍼티들을 가지고 있다. SimpleUrlAuthenticationSuccessHandler, SavedRequestAwareAuthenticationSuccessHandler, SimpleUrlAuthenticationFailureHandler, ExceptionMappingAuthenticationFailureHandler, DelegatingAuthenticationFailureHandler 등과 같은 표준 구현체들이 제공된다. AbstractAuthenticationProcessingFilter 클래스를 포함해서 Javadoc을 살펴보고 전체적인 그림과 각자 어떻게 동작하는지, 제공되는 기능들은 무엇이 있는지 확인해라.

인증이 성공하면, 결과 Authentication 객체가 SecurityContextHolder에 세팅된다. 설정된 AuthenticationSuccessHandler이 리다이렉트 또는 적절한 목적지로의 포워딩을 위해 호출 될 것이다. 기본적으로 SavedRequestAwareAuthenticationSuccessHandler가 사용되고, 이것은 login 화면으로 이동하기 직전에 요청했던 경로를 목적지로 하여 리다이렉트 한다.

ExceptionTranslationFilter는 유저가 만든 최초 request를 캐싱한다. 사용자가 인증을 수행하면 original URL값을 획득하기 위해 request를 캐싱한다. 원본 request는 재생산되어 대체될 것이다.

인증이 실패하면, 설정된 AuthenticationFailureHandler가 호출 될 것이다.

10.3. Servlet API integration

이 섹션에선 Spring Security 가 어떻게 Servlet API와 어떻게 인티그레이션 되는지 살펴볼 것이다.

10.3.1. Servlet 2.5+ Integration

HttpServletRequest.getRemoteUser()

= SecurityContextHolder.getContext().getAuthentication().getName()
일반적으로 현재 유저의 이름이 반환된다. 현재 사용자의 유저이름을 표시 할 때 유용하다. 추가적으로, 이 값이 null인지 체크함으로써 유저가 인증되었는지 / 익명 사용자인지를 알아낼수 있고, 특정 UI 요소가 보여질지 숨겨질지 여부를 결정하는데 유용하게 쓰일 수 있다. (i.e. 로그아웃 링크가 보여질지 여부).

HttpServletRequest.getUserPrincipal()

= SecurityContextHolder.getContext().getAuthentication()

username / password 기반의 인증일 경우, 일반적으로 이 AuthenticationUsernamePasswordAuthenticationToken 인스턴스이다. 추가적인 유저 정보를 얻을 때 유용하다. 예를 들어, 커스텀 UserDetailsService를 구현했을 경우 아래와 같은 방식으로 추가 정보를 획득 할 수 있다:

Authentication auth = httpServletRequest.getUserPrincipal();
// assume integrated custom UserDetails called MyCustomUserDetails
// by default, typically instance of UserDetails
MyCustomUserDetails userDetails = (MyCustomUserDetails) auth.getPrincipal();
String firstName = userDetails.getFirstName();
String lastName = userDetails.getLastName();

일반적으로 위 내용은 나쁜 예제다. 대신, 중앙집중화 되어 Spring Security와 Servlet API 간의 커플링을 줄이는 쪽으로 고쳐야 한다.

HttpServletRequest.isUserInRole(String)

SecurityContextHolder.getContext().getAuthentication().getAuthorities()을 참조하여 결과를 반환하게 된다. ROLE_ prefix는 제외해서 호출해야 한다.

10.3.2. Servlet 3+ Integration

HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse)

유저가 인증 된 것을 보장하는데 쓰인다. 인증되지 않았다면 설정된 AuthenticationEntryPoint가 동작하여 사용자가 인증을 수행하도록 요구한다. (i.e. 로그인페이지 이동 등)

HttpServletRequest.login(String,String)

현재의 AuthenticationManager를 사용하여 유저를 인증시키는데 사용된다.

try {
httpServletRequest.login("user","password");
} catch(ServletException e) {
// fail to authenticate
}

Spring Security가 로그인 실패 처리를 처리하길 바란다면 ServletException를 catch할 필요가 없다.

HttpServletRequest.logout()

현재 유저를 로그아웃 시키는데 쓰인다.

일반적으로, SecurityContextHolder 제거, HttpSession 무효화, "Remember Me" 인증 제거 등을 의미한다. 하지만, 설정된 LogoutHandler 구현은 Spring Security 설정에 따라 그때그때 다를수 있다. HttpServletRequest.logout()를 호출했더라도, response를 작성해야 할 책임은 남는다는 사실을 꼭 기억해라. 일반적으론 welcome page로 리다이렉트 한다.

AsyncContext.start(Runnable)

credentials이 새 쓰레드에 전파되는것을 보장한다. Spring Security’s 동시성 지원 기능을 사용하면, Spring Security는 AsyncContext.start(Runnable) 동작을 오버라이드 해서 Runnable을 처리 할 때 현재 SecurityContext가 사용되도록 보장해준다.

final AsyncContext async = httpServletRequest.startAsync();
async.start(new Runnable() {
    public void run() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        try {
            final HttpServletResponse asyncResponse = (HttpServletResponse) async.getResponse();
            asyncResponse.setStatus(HttpServletResponse.SC_OK);
            asyncResponse.getWriter().write(String.valueOf(authentication));
            async.complete();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
});

Async Servlet Support

Java 기반 설정일 경우 바로 적용되고, XML 설정일 때는 몇가지 단계를 거쳐야 한다.

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
</web-app>

다음으로 springSecurityFilterChain 이 세팅되어야 한다.

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
    org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>

자세한 동작 원리가 궁금하면 원문을 참조하자.

10.3.3. Servlet 3.1+ Integration

HttpServletRequest#changeSessionId()

서블린 3.1+에서 Session Fixation 공격을 방어하는데 쓰인다.

10.4. Basic and Digest Authentication

Basic 그리고 digest authentication 은 웹 애플리케이션에서 인기있는 대체 인증 메커니즘이다. Basic authentication 은 credential을 매 요청마다 전송하는 stateless 클라이언트들에 자주 쓰인다. form-based 인증과 함께 쓰이는 것이 일반적이다. 그러나, basic authentication은 암호를 평문으로 전송하기 때문에 HTTPS와 같은 암호화된 전송계층 위에서만 사용해야 한다.

10.4.1. BasicAuthenticationFilter

BasicAuthenticationFilter는 HTTP 헤더에 포함되는 basic 인증 credentials를 처리한다. 파폭이나 익스같은 일반적인 user-agent 뿐만 아니라, Hessian와 Burlap 같은 Spring remoting protocols에도 쓰일 수 있다. 표준 스펙은 RFC 1945의 Section 11에 기술되어 있고, BasicAuthenticationFilter은 이 스펙을 따른다. user agent들 사이에서 널리 전파돼 있고, username:password 문자열을 base64로 변환하면 된다는 간편함 때문에 매력적인 접근 방식이 될 수 있다.

Configuration

필터체인에 BasicAuthenticationFilter를 포함시켜야 한다. (다른 협동 컴포넌트들도 포함)

<bean id="basicAuthenticationFilter"
class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
</bean>

<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
<property name="realmName" value="Name Of Your Realm"/>
</bean>

설정된 AuthenticationManager가 각 인증 요청들을 처리한다. 인증이 실패되면, 설정된 AuthenticationEntryPoint가 인증 처리를 재시도 하기 위해 사용된다. 대개 401 응답과 함께 HTTP 기본 인증을 재시도하는 BasicAuthenticationEntryPoint를 쓰게 될 것이다. 인증이 성공하면, 대개는 반환된 Authentication 객체가 SecurityContextHolder에 할당될 것이다.

인증이 성공하든, 지원되지 않는 인증 요청이 HTTP 헤더에 포함되어있어 실패하든, 필터 체인은 계속 처리를 이어 나간다. 필터 체인이 인터럽트 되는 시점은, 오직 인증 실패로 인해 AuthenticationEntryPoint가 호출 되었을 뿐이다.

10.4.2. DigestAuthenticationFilter

DigestAuthenticationFilter는 Basic authentication과는 달리 credential이 평문으로 전송되지 않도록 해준다.

Digest는 안전하다고 여겨지지 않기 때문에 현대 애플리케이션에서 사용해서는 안 된다. 가장 분명한 문제는 암호를 일반 텍스트, 암호화 또는 MD5 형식으로 저장해야 한다는 것이다. 이러한 모든 스토리지 형식은 안전하지 않은 것으로 간주된다. 대신 단방향 적응형 암호 해시(즉, bCrypt, PBKDF2, SCrypt 등)를 사용해야 한다.

Digest Authentication의 핵심은 "nonce" 이다. 이것은 서버가 생성하는 값이다. Spring Security는 다음과 같은 형식을 선택했다.

base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))  
expirationTime: The date and time when the nonce expires, expressed in milliseconds  
key: A private key to prevent modification of the nonce token  

DigestAuthenticatonEntryPoint 에는 nonce 토큰을 생성할때 쓰일 key를 구체화 하는 프로퍼티를 가지고 있으며, nonceValiditySeconds 속성을 통해 만료 시간을 지정할 수 있다(기본값은 300 = 5분). nonce의 유효 여부와 관계 없이, username, password, nonce, 요청된 URI, client 생성 nonce(각 요청마다 user agent가 생성한 랜덤 값), realm name 등의 값을 붙인다음 MD5 해시하는 연산이 적용된다. 암호 등이 틀리면 다른 해시값을 갖는다. 스프링 구현체에서는 만약 단지 서버에서 생성된 nonce가 만료 되었을 경우(다른 값은 유효), DigestAuthenticationEntryPoint"stale=true" 헤더를 전송 할 것이다. 사용자를 방해하지 않고(다른 값들은 다 유효하므로), 새 nonce로 재시도만 하라는 뜻이다.

DigestAuthenticationEntryPoint의 적절한 nonceValiditySeconds 값은 애플리케이션마다 다르다. 극도로 보안에 치중한 애플리케이션일 경우, 인증 헤더를 가로채면 만료일이 도래하기 전 까지 다른 사용자 행세를 할 수 있다는 것을 알고 있어야 한다. 이 점이 적절한 세팅값을 선택할때 고려해야 할 중요한 요소이다. 하지만 그런 애플리케이션은 애초에 TLS/HTTP를 사용했겠지.

복잡성 때문에 Digest Authentication에는 종종 user agent 이슈들이 있다. 예를들면, IE는 동일 세션에서 연속적인 요청에 대해 opaque 토큰을 표현하는데 실패한다. 그래서 Spring Security 플터들은 모든 상태 정보를 nonce 토큰으로 캡슐화 한다. 테스트 결과에 따르면, Spring Security 구현은 파폭과 IE에서 nonce timeout 등을 잘 처리 했다.

Configuration

구현을 위해선 필터 체인에 DigestAuthenticationFilter을 정의해야 한다. application context는 이 필터를 collaborator들과 함께 정의 해야 한다.

<bean id="digestFilter" class=
    "org.springframework.security.web.authentication.www.DigestAuthenticationFilter">
<property name="userDetailsService" ref="jdbcDaoImpl"/>
<property name="authenticationEntryPoint" ref="digestEntryPoint"/>
<property name="userCache" ref="userCache"/>
</bean>

<bean id="digestEntryPoint" class=
    "org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint">
<property name="realmName" value="Contacts Realm via Digest Authentication"/>
<property name="key" value="acegi"/>
<property name="nonceValiditySeconds" value="10"/>
</bean>

UserDetailService가 필요하다. 왜냐하면 DigestAuthenticationFilter는 유저의 순수한 텍스트 비밀번호에 대해 직접 접근을 해야 하기 때문이다. DAO에서 인코딩된 암호를 사용한다면 Digest Authentication는 동작하지 않을 것이다. DAO collaborator는 UserCache와 더불어서, 대개 DaoAuthenticationProvider를 통해 직접적으로 공유 된다. authenticationEntryPoint 속성은 반드시 DigestAuthenticationEntryPoint이여야 한다. 그래야 암호 연산을 위한 realmName과 키값을 획득할 수 있다.

BasicAuthenticationFilter 처럼, 인증이 성공하면 인증 요청 토큰은 SecurityContextHolder에 지정된다. 앞에서 언급한것과 동일하게 필터 체인이 인터럽트 되는것은 오로지 인증 실패 후 AuthenticationEntryPoint가 호출 될 때 뿐이다.

RFC 문서를 보면 보안 강화를 위한 추가적인 기능들에 대한 내용이 더 있다. 예를들면, 매 요청마다 nonce는 변화한다. 그치만, Spring Security는 구현체의 복잡성을 줄이기 위해 디자인 되었고(user agent들의 하위 호한성이 지원되지 않는다는 점도 한몫 했다), 서버 내에 상태를 저장해야 하는(server-side state) 문제를 피하려고 했다. 여튼 RFC의 최소 표준은 지켰다.

10.5. Remember-Me Authentication

10.5.1. Overview

세션이 바뀌어도 principal을 기억하는 기능. 쿠키를 브라우저에 보내서, 미래 세션에서 이 쿠키를 감지하면 자동으로 로그인이 일어나게끔 하면 된다. Spring Security는 이 연산들에 대한 필수적인 hook 들을 제공하고, 구체적인 2가지의 remember-me 구현체를 제공한다. 하나는 쿠키 기반의 토큰의 보안성을 보호하기 위해 해싱을 사용하는 방식이고, 하나는 생성된 토큰을 데이터베이스에 저장하는 방식이다.

두 구현 모두 UserDetailsService를 필요로 한다. 만약 LDAP provider처럼 UserDetailsService를 사용하지 않는 인정 방식을 사용한다면, application context에 UserDetailsService bean이 없기 때문에 동작이 안될것이다.

10.5.2. Simple Hash-Based Token Approach

해싱을 사용한다. 인터랙티브 인증 성공 하에 브라우저로 쿠키가 전송되며, 쿠키 내용은 다음과 같다.

base64(username + ":" + expirationTime + ":" +  
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))

username: As identifiable to the UserDetailsService  
password: That matches the one in the retrieved UserDetails  
expirationTime: The date and time when the remember-me token expires, expressed in milliseconds  
key: A private key to prevent modification of the remember-me token  

remember-me 토큰은 일정 기간동안만 유효하며, username, password, key 또한 변하지 않았을때만 유효하다. 탈취당한 remember-me 토큰은 만료 기간이 도래하기 전까지는 어느 user agent 에서도 사용될 수 있기 때문에 잠재적인 보안 이슈가 있다. 토큰이 탈취당했다는것을 알았다면, 암호를 바꿈으로써 모든 remember-me 토큰을 그 즉시 무효화 할 수 있다.

보다 중요한 보안이 필요할 경우 다음 섹션에 설명된 방식을 써야 한다. 아니면 그냥 remember-me 서비스를 쓰지 마라.

namespace 설정이라면 아래와 같이 remember-me 인증을 확성화 할 수 있다:

<http>
...
<remember-me key="myAppKey"/>
</http>

UserDetailsService가 자동으로 선택될 것이다. application context에 두개 이상이 있다면, user-service-ref 속성을 통해 해당 bean의 이름을 지정할 수 있다.

10.5.3. Persistent Token Approach

이 방식은 아래 문서를 기반으로 하고 있다.
http://jaspan.com/improved_persistent_login_cookie_best_practice with some minor modifications [16]. 이 방식을 namespace 설정에서 사용하려면, datasource 레퍼런스를 넣어주면 된다:

<http>
...
<remember-me data-source-ref="someDataSource"/>
</http>

데이터베이스에는 아래와 같은 스키마가 정의돼 있어야 한다.

create table persistent_logins (username varchar(64) not null,
                                series varchar(64) primary key,
                                token varchar(64) not null,
                                last_used timestamp not null)

10.5.4. Remember-Me Interfaces and Implementations

Remember-me 는 AbstractAuthenticationProcessingFilter 슈퍼클래스의 hook들을 구현한UsernamePasswordAuthenticationFilter와 함께 쓰인다. 또한 BasicAuthenticationFilter와도 쓰인다. 이러한 hook 들은 RememberMeServices의 구현체를 적절한 시점에 호출하게 된다. 인터페이스는 요렇게 생겼다:

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

void loginSuccess(HttpServletRequest request, HttpServletResponse response,  
Authentication successfulAuthentication);  

AbstractAuthenticationProcessingFilterloginFail()loginSuccess()만 호출하긴 하지만, 각 메서드가 어떤일을 하는지는 구체적으로 Javadoc을 읽어보자. autoLogin()메서드는 SecurityContextHolderAuthentication 값이 없을때마다 RememberMeAuthenticationFilter에 의해서만 호출된다. 이 인터페이스 덕분에 다양한 구현체를 만들 수 있다. Spring Security에서 제공하는 2가지를 살펴보자.

TokenBasedRememberMeServices

간단한 해시기반 구현체다. TokenBasedRememberMeServicesRememberMeAuthenticationToken 을 생성하고, 이것은 RememberMeAuthenticationProvider에 의해 처리된다. authentication provider와 TokenBasedRememberMeServiceskey를 함께 공유한다. 또한, TokenBasedRememberMeServices는 비교 목적으로 username과 password를 획득하기 위해 UserDetailsService를 필요로 하며, 올바른 GrantedAuthority를 포함하도록 하여 RememberMeAuthenticationToken을 생성한다. 로그아웃시 쿠키를 무효화기 위한 작업들이 필요하므로 TokenBasedRememberMeServices는 Spring Security의 LogoutHandler 인터페이스 또한 구현하고 있으며, LogoutFilter와 함께 쓰여 쿠키를 자동으로 삭제한다.

remember-me 서비스를 활성화 하기 위한 bean들은 아래와 같다:

<bean id="rememberMeFilter" class=
"org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices"/>
<property name="authenticationManager" ref="theAuthenticationManager" />
</bean>

<bean id="rememberMeServices" class=
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="myUserDetailsService"/>
<property name="key" value="springRocks"/>
</bean>

<bean id="rememberMeAuthenticationProvider" class=
"org.springframework.security.authentication.RememberMeAuthenticationProvider">
<property name="key" value="springRocks"/>
</bean>

UsernamePasswordAuthenticationFilter.setRememberMeServices()를 통해 RememberMeServices를 등록하는 것과 더불어, AuthenticationManager.setProviders() 목록에 RememberMeAuthenticationProvider를 추가 하는 것, FilterChailProxy(대개 UsernamePasswordAuthenticationFilter 바로 다음)에 RememberMeAuthenticationFilter를 추가하는 것을 잊지 말아야 한다.

PersistentTokenBasedRememberMeServices

TokenBasedRememberMeServices와 동일하게 쓰일 수 있지만, 토큰 저장을 위해 PersistentTokenRepository이 설정 되어야 한다. 2가지 표준 구현체가 있다.

  • InMemoryTokenRepositoryImpl: 테스트 용도로만 사용
  • JdbcTokenRepositoryImpl: database에 저장

database schema는 위에 Persistent Token Approach에 기술되어 있다.

10.6. Cross Site Request Forgery (CSRF)

10.6.1. CSRF Attacks

CSRF 공격이란 무엇인가?

예를들어 계좌이체 폼 요청이 아래와 같이 생겼다고 하자.

POST /transfer HTTP/1.1  
Host: bank.example.com  
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly  
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876  

이제, 로그아웃하지 않은 상태에서 아래와 같은 사기 사이트에 접속했다고 하자.

<form action="https://bank.example.com/transfer" method="post">
<input type="hidden"
    name="amount"
    value="100.00"/>
<input type="hidden"
    name="routingNumber"
    value="evilsRoutingNumber"/>
<input type="hidden"
    name="account"
    value="evilsAccountNumber"/>
<input type="submit"
    value="Win Money!"/>
</form>

당첨됐다고 신나서 버튼을 클릭하게 되면, 100달러가 애먼사람한테 이체될 것이다. 왜냐하면 해커가 은행 쿠키를 열어볼 순 없지만, 은행 쿠키는 모든 요청마다 함께 전송되기 때문이다.

만약 모든게 javascript로 되어 있어 심지어 클릭 할 필요도 없게 된다면?

10.6.2. Synchronizer Token Pattern

은행 사이트에서의 요청이나 사기 사이트에서의 요청이나 HTTP 요청은 동일하기 때문에 이런 일이 발생한다. 이것은 은행 입장에서 이 요청들을 구분할수 없다는 것을 의미한다. CSRF 공격에 대처하기 위해서는, request에다가 사기 사이트에서 제공할 수 없는 무언가를 추가해야 한다.

해결책 중 하나는 Synchronizer Token Pattern을 사용하는 것이다. 이 방법에서는 각 요청에 세션 쿠키 뿐만 아니라 HTTP 매개변수로써 무작위로 생성된 토큰을 필요로 하도록 한다. 요청이 전송되면 서버는 매개변수의 예상값을 찾아서 요청의 실제값과 비교한다. 만약 값이 일치하지 않으면 요청은 실패한다.

어떤 상태를 업데이트하는 HTTP 요청들에 대해서만 토큰을 필수로 포함하도록 할 수도 있다. 이것은 동일 출처 정책(same origin policy)이 악성 사이트가 응답(response)을 읽을 수 없도록 하기 때문에 안정하게 수행된다. 또한 토큰이 유출되지 않도록 HTTP GET에 토큰을 포함시키지 않는다.

그럼 이제 상기에서 논의 했던 예제 상황이 어떻게 변경이 될 수 있는지 확인하자. 임의로 생성된 토큰이 _csrf라는 HTTP 매개변수로 전달된다고 가정하자. 이체 요청은 아래와 같을 것이다.

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=<secure-random>

_csrf 매개변수에 랜덤값을 추가했다는 것을 알 수 있다. 이제 악성 웹사이트는 _csrf 매개변수의 올바른 값을 추측할 수 없어 서버가 실제 토큰값과 예상 토큰값을 비교할 경우 전송은 실패할 것이다.

10.6.3. When to use CSRF protection

그렇다면 CSRF 방어는 언제 사용해야 할까? 일단 일반 사용자에 의해 브라우저가 작동되는 어떠한 요청에 의해서든 CSRF 방어를 적용하는 것을 권장한다. 다만, 브라우저가 아닌 클라이언트를 사용하는 서비스를 만드는 경우에는 CSRF 방어를 비활성화해야 할 경우도 있다.

CSRF protection and JSON

일반적으로 하는 질문은 "javascript에 의한 JSON 요청에도 CSRF 방어를 적용해야 하는가?"이다. 대답은 "그렇다"이다. 그러나 JSON 요청에 영향을 줄 수 있는 CSRF 악용이 있을 수 있으므로 매우 조심해야 한다. 예를 들어, 악성 사용자는 아래와 같이 JSON를 사용하여 CSRF를 생성할 수 있다.

<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
  <input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
  <input type="submit" value="Win Money!"/>
</form>

이것은 아래와 같은 JSON 형식을 서버로 보내게 된다.

{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}

어플리케이션이 Content-Type의 유효성을 검사하지 않으면 이 공격에 노출되게 된다. Content-Type 유효성을 검사하는 Spring MVC 어플리케이션은 설정에 따라 아래와 같이 URL 접미어를 ".json"으로 끝나게 만들어 노출시킬 수 있다.

CSRF and Stateless Browser Applications

만약 어플리케이션이 Stateless 하면 어떨까? 이것은 반드시 보호받는다는 의미가 아니다. 사실상 사용자가 주어진 요청에 대해 웹 브라우저에서 어떤 행동을 취할 필요가 없다면 CSRF 공격에 여전히 취약할 수 있다.

예를 들어, 어플리케이션이 JSESSIONID 대신 인증용으로 모든 상태를 포함하는 커스터마이징된 쿠키를 사용한다고 가정해보자. CSRF 공격이 발생하면 커스터마이징된 쿠키는 이전 예제에서 JSESSIONID가 전송된 것과 같은 방식으로 요청과 함께 전송될 것이다.

브라우저는 이전 예제에서 JSESSIONID 쿠키가 전송된 것과 같은 방식으로 어떤 요청이든 사용자명과 비밀번호를 자동으로 포함하기 때문에 기본 인증을 사용하는 사용자는 CSRF 공격에 취약하다.

10.6.4. Using Spring Security CSRF Protection

그렇다면 CSRF 공격으로부터 사이트를 보호하기 위해 Spring Security를 사용하려면 어떻게 해야 할까? Spring Security의 CSRF 방어를 사용하는 단계는 아래와 같다.

  • 적절한 HTTP 메서드 사용
  • CSRF 방어 설정
  • CSRF 토큰 포함하기

Use proper HTTP verbs

CSRF 공격을 방어하기 위한 첫 번째 단계는 웹 사이트에서 적절한 HTTP 메서드를 사용하도록 하는 것이다. 특히, Spring Security CSRF 방어를 사용하려면 먼저 어플리케이션 상태를 수정하는 HTTP 메서드(PATCH, POST, PUT, DELETE)를 사용하고 있는지 확인하여야 한다.

이 부분은 Spring Security에서만 요구하는 것이 아니라 CSRF 공격 예방을 위해 일반적으로 요구하는 부분이다. 그 이유는 HTTP GET에 개인정보를 포함하면 정보가 유출될 수 있기 때문이다. 중요한 정보는 GET 대신 POST 메서드를 사용하는 것에 대한 가이드는 RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI's를 참고하면 좋다.

Configure CSRF Protection

다음 단계는 어플리케이션 내에 Spring Security의 CSRF 방어 기능을 적용하는 것이다. 일부 프레임워크들은 사용자의 세션을 무효화하는 방식으로 유효하지 않은 CSRF 토큰을 처리하지만, 이것은 자체적인 문제를 야기할 수 있다. 그와 다르게 기본적으로 Spring Security의 CSRF 방어는 HTTP 403 접근 거부가 되도록 만든다. 이는 InvalidCsrfTokenException을 다르게 처리하도록 AcessDeniedhandler를 설정하여 커스터마이징 할 수도 있다.

Spring Security 4.0부터 CSRF 방어는 XML 설정으로 설정이 가능하다. 만약 CSRF 방어를 사용하지 않고 싶다면, 아래와 같이 XML 설정을 사용하면 된다.

<http>
    <!-- ... -->
    <csrf disabled="true"/>
</http>

CSRF 방어는 또한 Java 설정으로도 설정이 가능하다. 만약 CSRF 방어를 사용하지 않고 싶다면, 아래와 같이 Java설정을 사용하면 된다. CSRF 방어 설정 방법에서의 추가적인 커스터마이징은 csrf() Javadoc을 참고하길 바란다.

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
      http
      .csrf().disable();
  }
}

Include the CSRF Token

Form Submissions

마지막 단계는 모든 PATCH, POST, PUT 및 DELETE 메소드에 CSRF 토큰을 포함시키는 것이다. 포함시키는 방법 중 하나는 요청속성 _csrf을 사용하는 것이다. 아래 예제는 이에 대한 jsp 예제이다.

<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}" method="post">
  <input type="submit" value="Log out" />
  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>

보다 쉬운 방법은 Spring Security JSP 태그 라이브러리의 csrfInput 태그를 사용하는 것이다.

Spring MVC form:form 태그 또는 Thymeleaf 2.1+를 사용하고 @EnableWebSecurity를 사용하는 경우 CsrfToken은 자동으로 포함된다. (CsrfRequestDataValueProcessor 사용)

Ajax and JSON Requests

JSON을 사용하는 경우 HTTP 매개변수 내에 CSRF 토큰을 포함하여 전송할 수 없다. 그 대신 HTTP 헤더 내에 토큰을 포함하여 전송할 수 있다. 전형적인 패턴은 메타 태그 내에 CSRF 토큰을 포함하는 것이다. 아래는 관련 JSP 예제이다.

<html>
  <head>
      <meta name="_csrf" content="${_csrf.token}"/>
      <!-- default header name is X-CSRF-TOKEN -->
      <meta name="_csrf_header" content="${_csrf.headerName}"/>
      <!-- ... -->
  </head>
  <!-- ... -->

메타태그를 수동으로 생성하는 대신 Spring Security JSP 태그 라이브러리에서 제공하는 보다 간단한 csrfMetaTags 태그를 사용할 수 있다.

그러고 나서 모든 Ajax 요청 내에 토큰을 포함시킬 수 있다. jQuery를 사용한다면 아래와 같이 작성 할 수 있다.

$(function () {
  var token = $("meta[name='_csrf']").attr("content");
  var header = $("meta[name='_csrf_header']").attr("content");
  $(document).ajaxSend(function(e, xhr, options) {
      xhr.setRequestHeader(header, token);
  });
});
CookieCsrfTokenRepository

사용자가 CsrfToken을 쿠키에 유지하려는 경우가 있을 수 있다. 기본적으로 CookieCsrfTokenRepository는 XSRF-TOKEN라는 쿠키를 만들고 X-XSRF-TOKEN라는 헤더정보 또는 _csrf HTTP 매개변수로부터 이 쿠키값을 읽어올 수 있다. 이러한 기본사항들은 AngularJS에서 나온 개념을 사용한 것이다.

아래는 XML을 사용하여 CookieCsrfTokenRepository를 설정하는 예제이다.

<http>
    <!-- ... -->
    <csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
          class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
          p:cookieHttpOnly="false"/>

이 샘플에서는 명시적으로 cookieHttpOnly=false를 설정한다. 이는 JavaScript(즉, AngularJS)가 쿠키를 읽을 수 있게 하려면 꼭 필요한 부분이다. JavaScript(즉, AngularJS)로 직접 쿠키를 읽을 필요가 없다면 보안을 향상시키기 위해 cookieHttpOnly=false를 생략하는 것을 추천한다.

아래는 Java 설정을 사용하여 CookieCsrfTokenRepository를 설정하는 예제이다.

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

이 샘플에서는 명시적으로 cookieHttpOnly=false를 설정한다. 이는 JavaScript(즉, AngularJS)가 쿠키를 읽을 수 있게 하려면 꼭 필요한 부분이다. JavaScript(즉, AngularJS)로 직접 쿠키를 읽을 필요가 없다면 보안을 향상시키기 위해 (CookieCsrfTokenRepository.withHttpOnlyFalse() 대신 new CookieCsrfTokenRepository()를 사용하여) cookieHttpOnly=false를 생략하는 것을 추천한다.

10.6.5. CSRF Caveats

CSRF를 구현할 때 몇 가지 주의사항이 있다.

Timeouts

첫 번째 이슈는 CSRF 토큰이 HttpSession에 저장되므로 HttpSession이 만료되는 즉시 설정된 AccessDeniedHandler가 InvalidCsrfTokenException을 수신할 것이라는 것이다. 기본적인 AccessDeniedHandler를 사용하는 경우 브라우저는 HTTP 403을 전달 받을 것이고 오류메시지가 표시될 것이다.

CsrfToken가 기본적으로 쿠키에 저장되지 않는 이유를 물을 때가 있다. 쿠키에 저장하지 않는 이유는 다른 도메인에서 헤더를 설정할 수 있는 취약점이 있기 때문이다(즉, 쿠키를, 토큰을 수정할 수 있는 것이다). 이는 X-Requested-With 헤더가 있으면 Ruby on Rails가 더 이상 CSRF 확인을 생략하지 않는 이유와 같다. 취약점에 대한 상세한 내용은 webappsec.org를 참고하면 된다. 또 다른 이유로는 상태(즉, timeout)를 제거하면 토큰이 손상될 경우 토큰을 강제로 만료시킬 수 없다는 단점이 있다.

timeout이 발생한 사용자들에게 보다 나은 사용자 경험을 제공하는 간단한 방법은 세션이 만료될 예정임을 알리는 JavaScript를 사용하는 것이다. 사용자는 버튼을 클릭하여 세션을 계속 진행하고 리프레쉬할 수 있다.

다른 방법으로는 AccessDeniedHandler를 커스터마이징하여 InvalidCsrfTokenException를 원하는 방식으로 처리하는 것이 있다. AccessDeniedHandler를 커스터마이징하는 방법으로는 xml 또는 Java 설정이 있다.

    @Configuration
    public class AccessDeniedHandlerRefConfig extends BaseWebConfig {
        protected void configure(HttpSecurity http) {
            CustomAccessDeniedHandler accessDeniedHandler = new CustomAccessDeniedHandler()
            http.
                exceptionHandling()
                    .accessDeniedHandler(accessDeniedHandler)
        }

마지막으로 만료되지 않는 CookieCsrfTokenRepository를 사용하도록 어플리케이션에 설정하는 방법이 있다. 앞서 언급했던 바와 같이 이는 세션을 사용하는 것만큼 안전하지 않지만 이러한 설정만으로도 충분한 경우가 많다.

Logging In

로그인 요청을 위조하는 것을 방지하기 위해 CSRF 공격에 대해서도 로그인 폼을 보호해야 한다. CsrfToken은 HttpSession에 저장되기 때문에 CsrfToken 토큰 속성에 접근하는 즉시 HttpSession가 생성된다는 것을 의미한다. RESTful/stateless 아키텍처에서는 이런 부분이 별로 좋진 않지만, 현실은 실질적인 보안을 구현하는데 상태 정보가 필요하다. 상태 정보가 없으면 토큰이 손상되어도 아무것도 할 수 없다. 실용적인 측면에서, CSRF 토큰은 크기가 상당히 작아서 아키텍처에 거의 영향을 미치지 아야 한다.

로그인 폼을 보호하는 일반적인 방법은 폼 전송 전에 유효한 CSRF 토큰을 얻기 위해 JavaScript 함수를 사용하는 것이다. 이렇게 하면 앞서 설명한 세션 timeout에 대해 생각할 필요가 없다. 폼 전송 직전에 세션이 만들어지기 때문이다(CookieCsrfTokenRepository가 설정되어 있지 않았다고 간주). 사용자는 로그인 페이지에 머무를 수 있고 원하는 때에 사용자명과 비밀번호를 입력하여 전송할 수 있다. 이렇게 하기 위해서 Spring Security가 제공하는 CsrfTokenArgumentResolver를 활용하고 아래와 같은 endpoint를 노출시킬수 있다.

@RestController
public class CsrfController {

    @RequestMapping("/csrf")
    public CsrfToken csrf(CsrfToken token) {
        return token;
    }
}

(CORS 설정을 끄지 않도록 주의)

Logging Out

CSRF를 추가하면 LogoutFilter가 HTTP POST만 사용하게끔 업데이트 된다. 이렇게 하면 로그아웃 시 CSRF 토큰이 필요하고 악성 사용자가 강제로 다른 사용자를 로그아웃시킬 수 없게 된다.

접근방식 중 하나는 로그아웃을 위한 폼을 사용하는 것이다. 링크가 실제로 필요한 경우에는 JavaScript를 사용해서 POST로 동작하는 링크를 만들 수 있다(즉, 숨겨진 폼이라고 생각하면 될 것 같다). JavaScript가 비활성화된 브라우저의 경우 선택적으로 링크를 통해 POST로 동작하는 로그아웃 확인 페이지로 사용자를 이동시킬 수 있다.

정말로 HTTP GET을 로그아웃과 함께 사용하고 싶다면 그렇게 할 수는 있지만 일반적으로 권장하지 않는다. 예를 들어, 아래 예제는 URL /lougout이 어떤 HTTP 메소드로 요청되든 로그아웃을 수행하는 Java 설정이다.

@EnableWebSecurity  
public class WebSecurityConfig extends  
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
    }
}

Multipart (file upload)

방법이 두가지 있는데 각각 트레이드 오프가 있음

CSRF 없이도 일단 파일 업로드가 가능한지 체크 먼저 할 것.

Placing MultipartFilter before Spring Security

첫 번째 옵션은 Spring Security 필터 앞에 MultipartFilter를 배치하는것이다. Spring Security 필터 앞에 MultipartFilter를 지정하면 MultipartFilter를 호출할 수 있는 권한이 없어진다. 이는 누군가 서버에 임시파일을 둘 수 없음을 의미한다. 오직 권한이 부여된 사용자만 파일을 전송하여 어플리케이션이 처리하게 할 수 있다. 일반적으로 이것은 임시 파일 업로드가 서버에 무시할 수 없는 영향을 주기 때문에 권장되는 방법이다.

Java 설정을 사용하여 Spring Security 필터 앞에 MultipartFilter가 지정되도록 하기 위해서는 아래와 같이 beforeSpringSecurityFilterChain을 오버라이드하면 된다.

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
        insertFilters(servletContext, new MultipartFilter());
    }
}

XML 설정을 사용하여 Spring Security 필터 앞에 MultipartFilter가 지정되도록 하기 위해서는 아래와 같이 web.xml에서 springSecurityFilterChain 이전에 MultipartFilter의 을 위치시켜야 한다.

<filter>
    <filter-name>MultipartFilter</filter-name>
    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>MultipartFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
Include CSRF token in action

두 번째 옵션은 Spring Security 필터 뒤에 MultipartFilter를 위치시키고 폼의 action 속성에 CSRF를 쿼리 매개변수로 포함하는 것이다. 아래는 jsp에서의 사용 예제이다.

<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">

이 접근방식의 단점은 쿼리 매개변수가 유출될 수 있다는 것이다. 유출되지 않도록 본문 또는 헤더 내에 중요한 데이터를 배치하는 것이 가장 좋은 방법이다. 추가적인 정보는 RFC 2616 15.1.3 Encoding Sensitive Information in URI's를 찾아보면 된다.

HiddenHttpMethodFilter

HiddenHttpMethodFilter는 Spring Security 필터보다 먼저 배치해야 한다. 일반적으로 이것은 사실이지만 CSRF 공격으로부터 보호할 때 추가적인 의미를 가질 수 있다.

HiddenHttpMethodFilter는 POST에서 HTTP 메소드만 재정의하므로 실제로 실제 문제를 일으킬 가능성은 낮다. 그러나 Spring Security의 필터보다 먼저 배치하는 것이 여전히 최선의 관행이다.

10.6.6. Overriding Defaults

Spring Security의 목표는 보안을 위협하는 공격으로부터 사용자들을 보호하기 위한 기본값들을 제공하는 것이다. 그렇다고 해서 모든 기본값들을 그냥 그대로 써야할수밖에 없다는 것은 아니다.

예를 들어, CsrfTokenRepository를 커스터마이징하여 CsrfToken을 저장하는 방식을 재정의할 수 있다.

또한 RequestMatcher를 커스터마이징하여 어떤 요청이 CSRF 공격으로부터 보호될 것인지 정할 수 있다. 간단히 말해서 Spring Security의 CSRF 방어가 원하는대로 정확하게 작동하지 않으면 동작을 커스터마이징할 수 있다. xml 설정을 사용할 때 이러한 커스터마이징에 대한 자세한 내용을 알고 싶다면 41.1.18. 항목을 참고하고, Java 설정을 사용할 때는 CsrfConfigurer javadoc을 참고하면 된다.

10.7. CORS

Spring Framework는 CORS에 대한 1차 지원을 제공한다. CORS는 Spring Security보다 이전에 처리 되어야 한다. 왜냐하면 사전 request 에는 JSESSIONID같은 쿠키들을 포함하고 있지 않을 것이기 때문이다. 아무 쿠키가 없는 상태에서 Spring Security을 만나면, request는 유저가 인증되지 않았다고 가정하고 거부 할 것이다.

CORS가 먼저 처리되는것을 보장하는 제일 쉬운 방법은 CorsFilter를 사용하는 것이다. 아래와 같이 CorsConfigurationSource를 제공함으로써 CorsFilter와 Spring Security를 인티그레이트 할 수 있다.

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // by default uses a Bean by the name of corsConfigurationSource
            .cors().and()
            ...
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

xml버전

<http>
    <cors configuration-source-ref="corsSource"/>
    ...
</http>
<b:bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
    ...
</b:bean>

만약 Spring MVC의 CORS 지원을 사용하고 있다면 CorsConfigurationSource 설정을 생략할 수 있고, Spring Security는 Spring MVC이 제공하는 CORS 설정을 사용하게 될 것이다.

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // if Spring MVC is on classpath and no CorsConfigurationSource is provided,
            // Spring Security will use CORS configuration provided to Spring MVC
            .cors().and()
            ...
    }
}

XML

<http>
    <!-- Default to Spring MVC's CORS configuration -->
    <cors />
    ...
</http>

10.8. Security HTTP Response Headers

이제 다양한 security 헤더를 응답(response)에 추가하는 Spring Security 기능에 대해서 설명할 것이다.

10.8.1. Default Security Headers

Spring Security는 사용자가 쉽게 어플리케이션 보안을 위한 기본 security 헤더를 삽입할 수 있게 한다. Spring Security의 기본설정은 아래 헤더를 포함하는 것이다.

Cache-Control: no-cache, no-store, max-age=0, must-revalidate  
Pragma: no-cache  
Expires: 0  
X-Content-Type-Options: nosniff  
Strict-Transport-Security: max-age=31536000 ; includeSubDomains  
X-Frame-Options: DENY  
X-XSS-Protection: 1; mode=block  

Strict-Transport-Security 는 HTTPS 요청에만 추가된다.

각 헤더에 대한 자세한 내용은 다른 섹션(17장)을 참고하자.

  • Cache Control
  • Content Type Options
  • HTTP Strict Transport Security
  • X-Frame-Options
  • X-XSS-Protection

이 헤더들 각각은 모범적인 실례이지만, 모든 클라이언트가 이 헤더를 다 사용하는 것은 아니므로 추가적인 테스팅이 장려된다.

특정 헤더들을 커스터마이징 할 수도 있다. 예를들어, HTTP 응답이 아래와 같기를 원한다고 가정하자.

Cache-Control: no-cache, no-store, max-age=0, must-revalidate  
Pragma: no-cache  
Expires: 0  
X-Content-Type-Options: nosniff  
X-Frame-Options: SAMEORIGIN  
X-XSS-Protection: 1; mode=block  

특히, 모든 기본 헤더가 아래와 같이 커스터마이징되기를 원한다고 하자.

  • X-Frame-Options은 같은 도메인으로부터 전달되어 온 어떤 요청이든 허용
  • HTTP Strict Transport Security (HSTS)는 response에 추가하지 않음

아래처럼 하면 된다:

@EnableWebSecurity  
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
      http
          // ...
          .headers()
              .frameOptions().sameOrigin()
              .httpStrictTransportSecurity().disable();
  }
}

XML 설정일 경우엔 이렇게:

<headers>
    <frame-options policy="SAMEORIGIN" />
    <hsts disable="true"/>
</headers>

기본값들을 사용하지 않고 어떤 것들이 사용되는지를 명시적으로 제어하고 싶다면, 이런 기본값들을 비활성화 할 수 있다. 예제는 다음과 같다 :

만약 자바 설정을 사용중이고 캐시 컨트롤만 추가하고 싶다면 아래와 같이 하면 된다:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
      http
      // ...
      .headers()
          // do not use any default headers unless explicitly listed
          .defaultsDisabled()
          .cacheControl();
  }
}

XML 으로는

<headers defaults-disabled="true">
    <cache-control/>
</headers>

필요하다면, 아래 Java 설정을 사용해서 모든 HTTP Security 응답헤더를 비활성화할 수도 있다.

@EnableWebSecurity  
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
      http  
      // ...  
      .headers().disable();  
  } 
}  

XML 으로는

<http>
    <!-- ... -->
    <headers disabled="true" />
</http>

Cache Control

이전에 Spring Security는 당신의 웹 애플리케이션에 대한 캐시 컨트롤 제공을 필요로 했었다. 그 당시엔 합리적으로 보였지만, 안전한 커넥션을 포함한 브라우저 캐시들이 관여되어 있다. 말인즉슨 유저는 인증 페이지를 보거나, 로그아웃을 하거나, 악의적인 사용자가 브라우저 히스토리를 통해 캐싱된 페이지를 사용할수 있다는 것을 의미한다. 이러한 것들을 완화시키기 위해 Spring Security는 캐시 컨트롤 지원을 추가하여 response에 아래와 같은 헤더들을 삽입할 것이다.

Cache-Control: no-cache, no-store, max-age=0, must-revalidate  
Pragma: no-cache  
Expires: 0  

단지 자식요소 없는 <headers>요소를 추가함으로써 캐시 컨트롤 및 꽤나 많은 보호 기능들이 추가 된다.

그러나 오직 캐시 컨트롤만을 추가하길 원한다면, Spring Security의 XML 네임스페이스에 <cache-control> 요소와 headers@defaults-disabled 속성을 통해 이러한 기능을 활성화 시킬 수 있다.

<http>
    <!-- ... -->

    <headers defaults-disable="true">
        <cache-control />
    </headers>
</http>

비슷하게, 자바 설정으로 캐시 컨트롤만을 활성화 하려면 아래와 같이 한다.

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .defaultsDisabled()
        .cacheControl();
}
}

만약 특정 응답에 대해서만 캐시를 원한다면, 당신의 애플리케이션에서 선택적으로 HttpServletResponse.setHeader(String,String)를 호출해 Spring Security에 의해 설정된 헤더를 덮어쓸 수 있다. CSS, JavaScript, 이미지가 적절히 캐시되도록 하려고 할 때 꽤나 유용하다.

Spring Web MVC를 사용 중이라면, 이러한 것들은 당신의 설정에서 완료 된다. 예를들어, 아래와 같인 설정은 모든 리소스에 대해 캐시 헤더가 적용되는것을 보장해 준다.

@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
            .addResourceHandler("/resources/**")
            .addResourceLocations("/resources/")
            .setCachePeriod(31556926);
    }

    // ...
}

Content Type Options

IE를 포함한 브라우저들은 역사적으로, content sniffing을 통해 content type을 추론해 왔고 이를 통해 사용자 경험을 향상시켜 왔다. 예를 들어, content type이 명시되지 않았더라도 JavaScript 파일을 만나면 이를 추론해 실행 시킬 수 있었다.

문제는 content sniffing이 XSS 공격에 쓰일 수 있다는 것이다. 아래와 같이 함으로써 이를 비활성화 시킬 수 있다.

X-Content-Type-Options: nosniff  

캐시 컨트롤 항목처럼, 자식 요소 없는 <headers> 요소 추가로 nosniff 지시자는 기본값으로 포함된다. 그러나 만약 추가되는 헤더들에 대한 제어권을 더 원한다면 <content-type-options> 요소에 headers@defaults-disabled 요소를 아래와 같이 추가하면 된다.

<http>
    <!-- ... -->

    <headers defaults-disabled="true">
        <content-type-options />
    </headers>
</http>

X-Content-Type-Options 헤더는 자바 설정시 자동으로 추가된다. 헤더들에 대한 더 많은 제어권을 가지고 싶다면, 명시적으로 아래와 같이 content type 옵션을 명시할 수 있다.

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .defaultsDisabled()
        .contentTypeOptions();
}
}

HTTP Strict Transport Security (HSTS)

은행 사이트를 타이핑 할 때 mybank.example.com을 입력하는가 아니면 https://mybank.example.com 을 입력하는가? 만약 https 프로토콜을 생략한다면, 잠재적으로 중간자공격에 노출 될 수 있다. 웹사이트가 https:~~ 으로 리다이렉트 하더라도, 악의적 유저가 최초 http 요청을 가로채 응답을 조작할 수 있다(https://mibank.example.com으로 리다이렉트 후 credential을 훔친다던지).

많은 유저들이 https 프로토콜을 생략하기 때문에 HTTP Strict Transport Security(HSTS)가 생겨났다. 한 번 mybank.example.com 가 HSTS 호스트에 추가되면, 브라우저는 미리 mybank.example.com으로의 요청을 https://mybank.example.com으로 해석한다. 이 방법으로 중간자 공격 발생 가능성이 현저히 줄어든다.

RFC6797 표준에 따르면, HSTS 헤더는 오직 HTTPS 응답에만 추가된다. 브라우저가 헤더를 인식하도록 하기 위해선, 브라우저는 먼저 커넥션 생성을 위해 사용되는 서명된 SSL 인증서인 CA를 신뢰 해야 한다(그냥 SSL가 아닌).

사이트가 HSTS 호스트로 마킹되기 위한 방법중 하나는 브라우저에 미리 적재된 호스트를 갖는 것이다. 다른 하나는 응답에 "Strict-Transport-Security" 헤더를 추가 하는 것이다. 예를 들어 아래 방식은 브라우저에게 도메인을 1년(=약 31536000초)동안 HSTS 호스트로 다루도록 지시하게 된다.

Strict-Transport-Security: max-age=31536000 ; includeSubDomains  

includeSubDomains 지시자 옵션은 Spring Security에게 서브도메인들 역시 HSTS 도메인(ex: secure.mybank.example.com)으로 다뤄지도록 지시하게 된다.

다른 헤더들과 함께, Spring Security는 HSTS를 기본으로 추가한다. 항목을 통해 아래와 같이 HSTS 헤더들을 커스터마이즈 할 수 있다.

<headers>
    <hsts
        include-subdomains="true"
        max-age-seconds="31536000" />
</headers>

유사하게, 자바 설정으로 HSTS 헤더를 활성화 할수도 있다.

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .httpStrictTransportSecurity()
            .includeSubdomains(true)
            .maxAgeSeconds(31536000);
}
}

HTTP Public Key Pinning (HPKP)

HTTP Public Key Pinning (HPKP)는 웹 클라이언트들에게 특정 웹서버와 구체적인 암호화 public key를 연결하여 중간자 공격(Man in the Middle attack, MITM)을 방지하도록 말해주는 보안 피쳐이다.

TLS 세션에 사용되는 서버의 공개키에 대한 인증을 담보하기 위해여, 공개키는 X.509 인증서로 래핑된다. TLS 세션에서 사용되는 서버의 공개 키의 신뢰성을 보장하기 위해이 공개 키는 일반적으로 인증 기관 (CA)에서 서명 한 X.509 인증서로 래핑된다. 브라우저와 같은 웹 클라이언트는 이러한 많은 CA를 신뢰하므로 임의의 도메인 이름에 대한 인증서를 만들 수 있다. 공격자가 단일 CA를 손상시킬 수 있으면 다양한 TLS 연결에서 MITM 공격을 수행 할 수 있다. HPKP는 클라이언트에게 특정 웹 서버에 속하는 공개 키를 알려 주어 HTTPS 프로토콜에 대한 이러한 위협을 피할 수 있다. HPKP는 TOFU (Trust on First Use) 기술이다. 웹 서버가 특수한 HTTP 헤더를 통해 클라이언트에게 어떤 공개 키가 속하는지를 처음 알려 주면 클라이언트는이 정보를 주어진 기간 동안 저장한다. 클라이언트가 서버를 다시 방문하면 이미 fingerprint가 HPKP를 통해 알려진 공개 키를 포함하는 인증서를 필요로 한다. 만약 서버가 알려지지 않은 공개 키를 전달하면 클라이언트는 사용자에게 경고를 표시해야 한다.

user-agent는 SSL 인증서 체인을 통해 pin의 유효성을 검사해야 하기 때문에, HPKP 헤더는 HTTPS 응답에만 추가된다.

당신의 사이트에 이 기능을 활성화 하는 것은 당신의 사이트가 HTTPS 환경에서 Public-Key-Pins HTTP 헤더를 반환하는 것 만큼이나 쉽다. 예를 들어, 아래 방법은 user-agent에게 report-uri를 통해 2개 핀의 유효성 실패를 리포트 하도록 알려줄 것이다.

Public-Key-Pins-Report-Only: max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=" ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; report-uri="https://example.net/pkp-report" ; includeSubDomains

핀 유효성 실패 리포트는 웹 애플리케이션이 가진 API 또는 공개적인 HPKP 리포팅 서비스(REPORT-URI)의해 획득 될 수 있는 표준 JSON 구조이다.

includeSubDomains 지시자 옵션은 브라우저에게 주어진 핀에 대해 서브 도메인도 검사하라고 지시한다.

다른 헤더들과는 달리, Spring Security는 HPKP를 기본 옵션으로 추가하지 않는다. 당신은 아래와 같이 요소와 함께 HPKP 헤더를 커스터마이즈 할 수 있다.

<http>
    <!-- ... -->

    <headers>
        <hpkp
            include-subdomains="true"
            report-uri="https://example.net/pkp-report">
            <pins>
                    <pin algorithm="sha256">d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=</pin>
                    <pin algorithm="sha256">E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=</pin>
            </pins>
        </hpkp>
    </headers>
</http>

유사하게, 아래와 같은 자바 설정을 통해 HPKP 헤더를 활성화 할 수 있다.

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
                http
                // ...
                .headers()
                        .httpPublicKeyPinning()
                                .includeSubdomains(true)
                                .reportUri("https://example.net/pkp-report")
                                .addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
        }
}

X-Frame-Options

당신의 웹사이트가 frame으로 추가될수 있도록 하는 것은 보안 이슈가 될 수 있다. 예를 들어, 영리한 CSS 스타일링을 통해 사용자로 하여금 의도되지 않은 클릭을 유도할 수 있다. 예를 들어, 은행 사이트에 로그인한 유저가 다른 유저에게 엑세스 권한을 위임할 수 있는 버튼을 클릭할 수 있다. 일련의 공격들은 Clickjacking으로 알려져 있다.

clickjacking을 처리하는 또다른 현대적인 접근은 Content Security Policy (CSP)을 사용하는 것이다.

clickjacking 공격을 완화시키는 몇가지 다른 방법들이 있다. 예를들어, 레거시 브라우저들을 clickjacking 공격으로부터 보호하려면 frame breaking code를 사용할 수 있다. 완벽하진 않지만, 레거시 브라우저에 대해선 frame breaking code가 최선의 방법이다.

더 현대적인 접근으로 clickjacking을 처리하는 방법은 X-Frame-Options 헤더를 사용하는 것이다.

  X-Frame-Options: DENY  

X-Frame-Options response header는 브라우저로 하여금 frame 안에 해당 사이트가 렌더링 되지 않도록 지시한다. 기본 옵션으로, Spring Security는 iframe 안에 렌더링 되는 것을 막아준다.

frame-options 요소를 통해 X-Frame-Options을 커스터마이징 할 수 있다. 예를 들어, 아래와 같이 동일 도메인 하에서만 iframe을 허용하게 Spring Security에게 지시를 내릴 수 있다:

<http>
    <!-- ... -->

    <headers>
        <frame-options
        policy="SAMEORIGIN" />
    </headers>
</http>

자바설정:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .frameOptions()
            .sameOrigin();
}
}

X-XSS-Protection

일부 브라우저들은 reflected XSS 공격을 필터링해주는 기능이 내장되어 있다. 결코 완전하진 않지만 XSS 보호를 지원한다.

필터링은 기본적으로 활성화 되어 있어서, 브라우저에게 XSS 공격이 감지 될 때 어떤 작업을 해야할지를 지시하도록 활성화하는 헤더를 보장한다. 예를 들어, 필터는 최소한으로 컨텐츠를 수정하여 모든 것을 렌더링 하기 위해 노력한다. 때때로, 이러한 유형의 대체는 그 자체로 XSS 취약점이 될 수 있다. 컨텐츠를 수정하는 대신 내용을 차단하는 것이 가장 좋다. 이를 위해 다음 헤더를 추가 할 수 있다.

X-XSS-Protection: 1; mode=block  

이 헤더는 기본값으로 포함되어 있다. 커스터마이징은 이렇게..

<http>
    <!-- ... -->

    <headers>
        <xss-protection block="false"/>
    </headers>
</http>

자바버전 :

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .xssProtection()
            .block(false);
}
}

Content Security Policy (CSP)

콘텐츠 보안 정책 Content Security Policy(CSP)는 웹 애플리케이션이 XSS (Cross-Site Scripting)와 같은 컨텐츠 주입 취약성을 완화하기 위해 활용할 수 있는 메커니즘이다. CSP는 웹 응용 프로그램 작성자가 웹 응용 프로그램이 리소스를로드 할 것으로 예상되는 소스를 선언하고 궁극적으로 클라이언트 (사용자 에이전트)에게 알리는 기능을 제공하는 선언적 정책이다.

콘텐츠 보안 정책은 모든 콘텐츠 삽입 취약점을 해결하기 위한 것은 아니다. 대신 CSP를 활용하여 콘텐츠 주입 공격으로 인한 피해를 줄일 수 있다. 첫 번째 방어선으로 웹 애플리케이션 작성자는 입력 내용을 확인하고 출력을 인코드 해야 한다.

웹 애플리케이션은 응답에 다음 헤더 중 하나를 포함시켜 CSP 사용을 채택할 수 있다.

  • Content-Security-Policy
  • Content-Security-Policy-Report-Only

이러한 각 헤더는 클라이언트에 보안 정책 을 전달하는 메커니즘으로 사용된다. 보안 정책에는 특정 자원 표시에 대한 제한 사항을 정의하는 각각 의 보안 정책 지시자들(예 : script-src 및 object-src)이 포함된다.

예를 들어, 웹 응용 프로그램은 응답에 다음 헤더를 포함하여 신뢰할 수있는 특정 소스에서 스크립트를 로드 할 것이라고 선언 할 수 있다.

Content-Security-Policy: script-src https://trustedscripts.example.com

script-src 지시문에 선언 된 것 이외의 다른 소스에서 스크립트를 로드하려는 시도는 user-agent에 의해 차단된다. 또한 report-uri 지시문이 보안 정책에 선언되면 사용자 에이전트가 선언 된 URL에 위반을 보고한다.

예를 들어 웹 응용 프로그램이 선언 된 보안 정책을 위반하는 경우 다음 응답 헤더는 사용자 에이전트가 정책의 report-uri 지시문에 지정된 URL로 위반 보고서를 보내도록 지시한다.

Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/

위반 리포트는 웹 애플리케이션이 가진 API 또는 공개적인 CSP 위반 리포팅 서비스(REPORT-URI)의해 획득 될 수 있는 표준 JSON 구조이다.

Content-Security-Policy-Report-Only 헤더는 웹 애플리케이션 작성자 또는 관리자에게 보안 정책을 강요하기보다는 모니터링 할수 있는 기능을 제공하는 기능을 제공한다. 이 헤더는 일반적으로 사이트의 보안 정책을 실험하거나 개발할 때 사용된다. 정책이 유효하다고 간주되면 Content-Security-Policy 헤더 필드를 대신 사용하여 시행하도록 강제할 수 있다.

아래 응답 헤더는 스크립트가 두가지 가능한 소스 중 하나에서 로드 될 수 있음을 선언한다.

Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/

evil.com에서 스크립트를 로드할려고 시도하여 정책을 위반하게 되면, user agent는 report-uri 지시자에 의해 명시된 URL에 위반 리포트를 보내지만, 그럼에도 여전히 위반된 자원이 계속 로드 될 수 있도록 허용한다.

Configuring Content Security Policy

컨텐츠 보안 정책 구성

Spring Security는 기본적으로 컨텐츠 보안 정책을 추가하지 않는다는것을 꼭 알아야 한다. 웹 애플리케이션 작성자는 반드시 보안 정책(들)을 선언하여 제한된 자원에 대한 모니터링을 강제하고(거나) 모니터링 해야 한다.

예를 들어, 다음과 같은 보안 정책이 주어졌다고 하자.

script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/  

아래와 같이 요소가 있는 XML 구성을 사용하여 CSP 헤더를 사용할 수 있다.

<http>
    <!-- ... -->

    <headers>
        <content-security-policy
            policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" />
    </headers>
</http>

CSP 'report-only' header를 사용하려면 다음과 같이 요소를 구성하자:

<http>
    <!-- ... -->

    <headers>
        <content-security-policy
            policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/"
            report-only="true" />
    </headers>
</http>

마찬가지로, 아래와 같이 Java 구성을 사용해 CSP 헤더를 사용할 수 있다.

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/");
}
}

CSP 'report-only' 헤더를 사용하려면 다음 Java 구성을 제공해라:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
        .reportOnly();
}
}
Additional Resources

웹 응용 프로그램에 콘텐츠 보안 정책을 적용하는 것은 사소한 일이 아니다. 다음 리소스는 사이트에 효과적인 보안 정책을 개발하는 데 도움이 될 수 있다.

  • An Introduction to Content Security Policy
  • CSP Guide - Mozilla Developer Network
  • W3C Candidate Recommendation

Referrer Policy

레퍼러 정책은 웹 애플리케이션이 사용자가 마지막에 있었던 페이지 정보를 포함하는 레퍼러 필드를 관리하기 위해 활용할 수 있는 메커니즘이다.

Spring Security의 접근방식은 다양한 정책을 제공하는 Referrer Policy 헤더를 사용하는 것이다.

Referrer-Policy: same-origin  

Referrer-Policy 응답 헤더는 브라우저에게 목적지에게 유저가 직전에 있었던 출발지를 알수 있도록 지시한다.

Configuring Referrer Policy

Spring Security 기본적으로 Referrer Policy 헤더를 추가하지 않는다.

아래와 같이 XML 설정에 헤더를 추가해 Referrer-Policy 헤더를 활성화 시킬 수 있다:

<http>
    <!-- ... -->

    <headers>
        <referrer-policy policy="same-origin" />
    </headers>
</http>

자바 설정:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .referrerPolicy(ReferrerPolicy.SAME_ORIGIN);
}
}

Feature Policy

Feature Policy는 웹 개발자로 하여금 브라우저에게 선택적으로 특정 API들과 웹 피처의 동작에 대한 활성화/비활성화/수정을 할 수 있게 해주는 메커니즘 이다.

Feature-Policy: geolocation 'self'  

Feature Policy를 통해서, 개발자는 당신의 사이트에서 사용되는 특정 피쳐들에 대한 일련의 정책들을 브라우저에게 강제할 수 있다. 이러한 정책들은 사이트가 어떤 API에 엑세스 할 수 있는지 제한하거나, 특정 피쳐에 대한 브라우저의 기본 동작을 수정할 수 있다.

Configuring Feature Policy

Spring Security는 기본적으로 Feature Policy 헤더를 추가하지 않는다.

XML으로 설정하기:

<http>
    <!-- ... -->

    <headers>
        <feature-policy policy-directives="geolocation 'self'" />
    </headers>
</http>

자바로 설정하기:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .featurePolicy("geolocation 'self'");
}
}

10.8.2. Custom Headers

Spring Security는 보다 일반적인 보안 헤더들을 당신의 애플리케이션에 편리하게 추가할 수 있게 해주는 메커니즘들을 가지고 있다. 그러나, 커스텀 헤더들을 추가할수 있도록 해주는 hook들을 제공해 주기도 한다.

Static Headers

아래와 같이 기본적으로 지원되지 않는 보안 헤더들을 추가하고 싶을 때가 있을 수 있다.

X-Custom-Security-Header: header-value  

XML설정 사용시 아래와 같이 추가하면 된다.

<http>
    <!-- ... -->

    <headers>
        <header name="X-Custom-Security-Header" value="header-value"/>
    </headers>
</http>

자바는 이렇게:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .addHeaderWriter(new StaticHeadersWriter("X-Custom-Security-Header","header-value"));
}
}
Headers Writer

아니면 ref 속성을 통해 직접 writer를 지정할수도 있다.

<http>
    <!-- ... -->

    <headers>
        <header ref="frameOptionsWriter"/>
    </headers>
</http>
<!-- Requires the c-namespace.
See https://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-c-namespace
-->
<beans:bean id="frameOptionsWriter"
    class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"
    c:frameOptionsMode="SAMEORIGIN"/>

자바로는 이렇게 :

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN));
}
}
DelegatingRequestMatcherHeaderWriter

특정 요청에만 헤더를 작성하고 싶을때, 예를 들어 로그인 페이지만 frame에 들어가지 못하도록 하고 싶은 경우 DelegatingRequestMatcherHeaderWriter를 쓰면 된다.

XML 설정;

<http>
    <!-- ... -->

    <headers>
        <frame-options disabled="true"/>
        <header ref="headerWriter"/>
    </headers>
</http>

<beans:bean id="headerWriter"
    class="org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter">
    <beans:constructor-arg>
        <bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher"
            c:pattern="/login"/>
    </beans:constructor-arg>
    <beans:constructor-arg>
        <beans:bean
            class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"/>
    </beans:constructor-arg>
</beans:bean>

자바설정:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    RequestMatcher matcher = new AntPathRequestMatcher("/login");
    DelegatingRequestMatcherHeaderWriter headerWriter =
        new DelegatingRequestMatcherHeaderWriter(matcher,new XFrameOptionsHeaderWriter());
    http
    // ...
    .headers()
        .frameOptions().disabled()
        .addHeaderWriter(headerWriter);
}
}

10.9. Session Management

HTTP 세션 관련 기능은 Filter가 위임하는 SessionManagementFilter 및 SessionAuthenticationStrategy 인터페이스의 조합으로 처리된다. 일반적인 사용방법에는 세션 고정 공격 방지, 세션 timeout 방지, 인증된 사용자가 동시에 열 수 있는 세션 수 제한 등이 포함된다.

10.9.1. SessionManagementFilter

SessionManagementFilter는 현재 요청(request)에서 사용자가 인증되었는지 알기 위해 사전 인증 또는 Remember-Me 인증과 같은 비상호적인 인증 메커니즘을 통해 SecurityContextHolder의 현재 내용과 대조하여 SecurityContextRepository의 내용을 확인한다.

SecurityContextRepository에 security context가 포함되면 SessionManagementFilter는 아무 일도 하지 않는다. SecurityContextRepository에 security context가 포함되지 않고 thread-local SecurityContext가 (익명이 아닌) Authentication 객체를 포함하고 있으면 SessionManagementFilter는 바로 앞선 필터에서 인증한 사용자로 간주한다. 그리고 나서 설정된 SessionAuthenticationStrategy를 호출한다. (-_- 음?)

사용자가 현재 인증되지 않은 경우, 필터는 유효하지 않은 세션 ID가 요청되었는지(예를 들어, timeout으로 인한) 확인하고 설정된 InvalidSessionStrategy가 있다면 이를 호출한다. 가장 일반적인 동작은 고정 URL로 리다이렉션하는 것이다. 이는 표준 구현체 SimpleRedirectInvalidSessionStrategy안에 캡슐화되어 있다. 또한 앞서 설명한 것과 같이 네임스페이스를 통해 유효하지 않은 세션 URL을 설정할 수도 있다.

10.9.2. SessionAuthenticationStrategy

SessionAuthenticationStrategy는 SessionManagementFilter와 AbstractAuthenticationProcessingFilter 둘 다에 의해 쓰여서, 만약 예를들어 커스터마이즈된 form-login 클래스를 사용하고 있다면, 두 군데 모두 이것을 inject 해야 할 것이다. 이러한 경우에는 일반적으로 아래와 같은 설정이 될 것이다:

<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
    <beans:property name="sessionAuthenticationStrategy" ref="sas" />
    ...
</beans:bean>

<beans:bean id="sas" class=
"org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />

기본값 사용시 스프링의 session-scoped 빈을 포함하여 HttpSessionBindingListener를 구현한 빈을 세션에 저장할 경우 SessionFixationProtectionStrategy가 문제를 야기 할 수 있다. 자세한 정보는 Javadoc을 참조하자.

10.9.3. Concurrency Control

Spring Security는 보안 주체가 지정된 횟수 이상 동일한 응용 프로그램에 동시에 인증하는 것을 방지 할 수 있다. 많은 ISV가 라이센싱을 위해 이 기능을 사용하지만, 네트워크 관리자들은 사람들이 로그인 이름을 공유하지 않도록 하기 위해 이 기능을 사용한다. 예를 들어, "Batman" 사용자가 두 개의 다른 세션에서 웹 응용 프로그램에 로그온하지 못하게 할 수 있다. 이전 로그인을 만료하거나 다시 로그인하려고 할 때 오류를 보고하여 두 번째 로그인을 막을 수 있다. 두 번째 방법을 사용하는 경우 명시적으로 로그아웃 하지 않은 (예 : 브라우저를 방금 닫은) 사용자는 원래 세션이 만료 될 때까지 다시 로그인 할 수 없다.

동시성 제어는 네임스페이스상에서 지원되므로 이전 네임스페이스 챕터에서 제일 간단한 설정을 참조해라. 때때로 커스터마이징이 필요할수 있다.

SessionAuthenticationStrategy의 특별 버전인 ConcurrentSessionControlAuthenticationStrategy가 사용된다.

이전에 동시 인증은 ProviderManager에 의해서 체크 되었고, ConcurrentSessionController에 의해 거부 되었었다. 후자는 허용된 세션 갯수 이상 시도하는 경우를 검사한다. 그러나, 이 접근법은 HTTP 세션이 사전에 만들어져 있어야 하므로 바람직하지 않다. Spring Security 3에서는, 유저는 처음 AuthenticationManager에 의해 인증 된 이후 세션이 생기고 또다른 세션을 열 수 있는지를 체크 한다.

동시 세션 지원을 사용하려면 web.xml에 다음을 추가해야 한다.

<listener>
    <listener-class>
    org.springframework.security.web.session.HttpSessionEventPublisher
    </listener-class>
</listener>

또한, FilterChainProxy에 ConcurrentSessionFilter를 추가 해야 한다. ConcurrentSessionFilter는 두개의 생성자 파라미터인 sessionRegistry(SessionRegistryImpl 인스턴스), sessionInformationExpiredStrategy(세션 만료시 적용되는 전략)를 필요로 한다. namespace를 사용하여 FilterChainProxy와 다른 기본 bean들을 사용하는 설정은 다음과 같다:

<http>
<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />

<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="redirectSessionInformationExpiredStrategy"
class="org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy">
<beans:constructor-arg name="invalidSessionUrl" value="/session-expired.htm" />
</beans:bean>

<beans:bean id="concurrencyFilter"
class="org.springframework.security.web.session.ConcurrentSessionFilter">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<beans:constructor-arg name="sessionInformationExpiredStrategy" ref="redirectSessionInformationExpiredStrategy" />
</beans:bean>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>

<beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<beans:constructor-arg>
    <beans:list>
    <beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
        <beans:constructor-arg ref="sessionRegistry"/>
        <beans:property name="maximumSessions" value="1" />
        <beans:property name="exceptionIfMaximumExceeded" value="true" />
    </beans:bean>
    <beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
    </beans:bean>
    <beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
        <beans:constructor-arg ref="sessionRegistry"/>
    </beans:bean>
    </beans:list>
</beans:constructor-arg>
</beans:bean>

<beans:bean id="sessionRegistry"
    class="org.springframework.security.core.session.SessionRegistryImpl" />

리스너를 web.xml에 추가하면 HttpSession이 시작될 때나 종료될 때 마다 Spring의 ApplicationContext에 ApplicationEvent가 발행된다. 이것은 크리티컬하다. 왜냐면 세션이 끝날 때 SessionRegistryImpl이 통지되도록 허용하기 때문이다. 이것 없이는, 유저가 다른 세션에서 로그아웃 하거나 타임아웃이 발생했더라도 세션 허용치를 초과했을 경우 다시 로그인 할 수가 없게 되기 때문이다.

Querying the SessionRegistry for currently authenticated users and their sessions

(현재 인증 된 사용자 및 해당 세션에 대한 SessionRegistry 쿼리하기)

네임스페이스든 평범한 빈으로든 캐시 컨트롤을 설정하는것은 유용한 부가 효과를 가진다. 그것은 당신 애플리케이션 내에서 SessionRegistry를 직접 사용할수 있는 레퍼런스가 제공된다는 것이다. 한명의 유저가 가질 수 있는 세션 갯수를 제한하지 않더라도 이런 인프라스트럭처를 구성하는 것은 값어치가 있다. maximumSession 속성을 -1로 세팅함으로써 세션 갯수 제한을 없앨 수 있다. 네임스페이스를 사용하는 경우, 내부적으로 생성된 SessionRegistry에 대해 session-registry-alias 속성을 사용해 alias를 설정할수 있고, 다른 bean들에 inject 할 수 있다.

getAllPrincipals() 메서드는 현시간 인증된 유저 목록을 제공한다. SessionInformation 리스트를 리턴하는 getAllSessions(Object principal, boolean includeExpiredSessions) 메서드를 호출함으로써 유저의 세션들을 리스팅 할 수 있다. 또한, 유저의 세션을 expireNow() 호출로 만료시킬수도 있다. 만약 유저가 다시 애플리케이션으로 돌아오면 작업이 막힐 것이다. 이런 메서드들은 관리자 화면에서 유용하게 쓰일 것이다. 자세한 내용은 Javadoc에서..

10.10. Anonymous Authentication

10.10.1. Overview

"기본적으론 거부" 전략을 차용해서 특정한것만 허용하도록 하는것은 좋은 보안 전략이다. 많은 사이트들이 몇 URL을 제외하고(예: 첫화면, 로그인 페이지) 인증을 필요로 한다. 이러면 설정할때도 몇개 소수만 지정하면 되므로 쉽다. 달리 말하면, 기본적으로 ROLE_SOMETHING를 기본으로 이 규칙에 예외만 적용하도록 하는것이 필요하다

이것이 우리가 말하는 익명 인증의 뜻이다. 익명으로 인증했다는 것은 인증되지 않은 유저라는 뜻과 개념척 차이가 없다. Spring Security에서 getCallerPrincipal같은 서블릿 API를 호출하면 SecurityContextHolder에 익명 인증 객체가 있더라도 여전히 null을 반환한다.

SecurityContextHolder에는 항상 Authentication 객체가 반드시 들어있고 null이 아니라는 사실로 코딩하기 더 쉬움

10.10.2. Configuration

익명 인증 지원은 Spring Security 3.0의 HTTP 구성을 사용할 때 자동으로 제공되며 요소를 사용하여 사용자 정의 (또는 비활성화) 할 수 있다. 전통적인 Bean 구성을 사용하지 않는 한 여기에 설명 된 Bean을 구성 할 필요가 없다.

익명 인증 기능을 구현하기 위해 3개의 클래스가 함께 사용된다. AnonymousAuthenticationToken 는 Authentication의 구현체고, 익명 유저에게 적용되는 GrantedAuthority를 저장한다. 이에 상응하여, ProviderManager에 묶여 AnonymousAuthenticationToken을 수용할수 있도록 하는 AnonymousAuthenticationProvider가 있다. 마지막으로, 일반 인증 메커니즘 뒤에 묶여서 SecurityContextHolder에 Authentication이 없으면 AnonymousAuthenticationToken을 붙여주는 AnonymousAuthentication Filter가 있다.

필터 정의와 인증 provider는 아래와 같이 생겼다:

<bean id="anonymousAuthFilter"
    class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
<property name="key" value="foobar"/>
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
</bean>

<bean id="anonymousAuthenticationProvider"
    class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
<property name="key" value="foobar"/>
</bean>

key는 필터와 인증 provider 사이에 공유되어, 선행자에 의해 생성된 토큰들이 후행자에 수용될 수 있도록 해준다. userAttribute는 다음과 같은 형태로 표현된다: usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority] 이 문법은 InMemoryDaoImpl의 userMap 속성과 동일한 형태이다. 앞서 설명했듯이, 익명 인증의 장점은 모든 URI에 보안이 적용될수 있다는 것이다. 예제:

<bean id="filterSecurityInterceptor"
    class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
<property name="securityMetadata">
    <security:filter-security-metadata-source>
    <security:intercept-url pattern='/index.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/hello.htm' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/logoff.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/login.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/**' access='ROLE_USER'/>
    </security:filter-security-metadata-source>" +
</property>
</bean>

10.10.3. AuthenticationTrustResolver

익명 인증에 대한 이야기는 AuthenticationTrustResolver 인터페이스와, 그에 따른 AuthenticationTrustResolverImpl 구현체로 마무리 된다. 이 인터페이스는 isAnonymous(Authentication) 메서드를 제공하는데, 관심있는 클래스들로 하여금 인증의 특별한 상태 타입을 고려하도록 해준다.ExceptionTranslationFilterAccessDeniedException을 처리 할 때 이 인터페이스를 사용한다. 만약 AccessDeniedException이 던져졌고 인증이 익명타입일때, 403(forbidden) 응답을 던지는 대신, 필터는 AuthenticationEntryPoint를 시작하여 유저가 인증을 적절히 수행하도록 유도한다. 이것은 중요하다. 그렇지 않으면 유저는 인증 받을 수 있는 기회를 가질 수 없을 것이다.

인터셉터 설정에서 ROLE_ANONYMOUS 요소가 IS_AUATHENTICATED_ANONYMOUSLYFH 으로 대체 된 것을 보게 될 것이다. 이것은 액세스 컨트롤을 정의할 때 동일한 영할을 한다. 다음은 authorization 챕터에서 보게 될 AuthenticatedVoter 사용 예제다. 특정 설정 요소와 익명 사용자에 대한 엑세스 위임을 위해 AuthenticationTrustResolver를 사용한다. AuthenticatedVoter 접근법은 매우 강력하고, 익명/remember-me/fully-authenticatd 유저들을 구분지을 수 있도록 해준다. 만약 이런 기능을 사용하지 않는다면 ROLE_ANONYMOUS에만 집중하면 되고, 그것은 Spring Security의 표준 RoleVoter를 사용하게 될 것이다.

10.11. WebSocket Security

생략 나중에..

1. Preaface

https://docs.spring.io/spring-security/site/docs/5.1.6.RELEASE/reference/html5

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

버전 지정하기, Spring Boot 없이 설정하기, starter 없이 최소한으로 설정하기, LDAP∙OpenID 기능 사용하기, Gradle 기준으로 설정하기, 샘플 프로젝트(튜토리얼, 주소록, LDAP∙OpenID∙CAS∙JASS 샘플)은 레퍼런스 원문 참조

6. Servlet Applications

6.1 Configuration

Spring Security Configuration을 추가하면 springSecurityFilterChain 라는 이름의 Servlet Filter가 생성되며, 이것은 보안에 관한 모든 처리를 해준다. (URL 보호, username/password 검사, 로그인 폼 리다이렉팅 등)
아래는 가장 기본적인 Spring Security Java Configuration 이다.

@EnableWebSecurity
public class WebSecurityConfig implements WebMvcConfigurer {

    @Bean
    public UserDetailsService userDetailsService() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
        return manager;
    }
}

위와 같은 설정 만으로도

  • 모든 URL에 대해 인증 검사
  • 인증을 위한 로그인 폼 제공 (기본 계정: user/password)
  • 로그아웃 기능 제공
  • CSRF 공격 방어
  • 세션 위조 공격 방어
  • 보안 헤더 integration
  • HttpServletRequestgetRemoteUser(), getUserPrincipal(), isUserInRole(), login(), logout() 기능 활성화

가 적용된다.

6.2 HttpSecurity

WebSecurityConfigurerAdapter 의 configure를 구현해서 http 설정을 변경 할 수 있다.

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .httpBasic();
}

모든 요청에 대해 HTTP Basic authentication이 적용된다.

6.3 Form Login

.httpBasic() 대신 .formLogin() 을 지정하게 되면 아래와 같이 동작한다.

  • 요청 주소에 대해 사용자 인증을 거침
  • /login 으로 접속시 로그인 폼이 나옴 (UserDetailsService 설정을 했으므로 user/password가 기본 로그인 계정이 되고, 미설정시엔 스프링 구동시 로그로 찍힌 값을 password로 사용)
  • /logout 으로 접속시 로그아웃 확인 폼이 나옴

아래와 같이 하면 loginPage를 커스터마이징 할 수 있다.

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/login") 
            .permitAll();        
}
<c:url value="/login" var="loginUrl"/>
<form action="${loginUrl}" method="post">       
    <c:if test="${param.error != null}">        
        <p>
            Invalid username and password.
        </p>
    </c:if>
    <c:if test="${param.logout != null}">       
        <p>
            You have been logged out.
        </p>
    </c:if>
    <p>
        <label for="username">Username</label>
        <input type="text" id="username" name="username"/>  
    </p>
    <p>
        <label for="password">Password</label>
        <input type="password" id="password" name="password"/>  
    </p>
    <input type="hidden"                        
        name="${_csrf.parameterName}"
        value="${_csrf.token}"/>
    <button type="submit" class="btn">Log in</button>
</form>

6.4 Authorize

경로 및 사용자 Role 별로 access control을 할 수 있다. 여기서는 Role에 ROLE_ prefix를 붙일 필요가 없다.

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()                                                                
            .antMatchers("/resources/**", "/signup", "/about").permitAll()                  
            .antMatchers("/admin/**").hasRole("ADMIN")                                      
            .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")            
            .anyRequest().authenticated()                                                   
            .and()
        // ...
        .formLogin();
}

6.5 Handling Logouts

.formLogin()와 .httpBasic()을 같이 썼더니 로그아웃이 제대로 안되는 경우가 있었음. 이 때는 application.properties에 > spring.cache.type=NONE 을 지정해서 해결하였음

WebSecurityConfigurerAdapter을 사용하면 logout 기능이 자동으로 적용된다. /logout URL을 통해 아래와 같은 동작들이 수행된다. (로그아웃 URL은 CSRF 기능이 활성화 되어 있을 경우 POST로만 호출되어야 함. 기본: enabled)

  • HTTP Session 무효화
  • 세팅되었던 RememberMe 인증 삭제
  • SecurityContextHolder 삭제
  • /login?logout 으로 리다이렉트

아래와 같이 커스터마이징이 가능하다.

protected void configure(HttpSecurity http) throws Exception {
    http
        .logout()
            .logoutUrl("/my/logout")
            .logoutSuccessUrl("/my/index") // 로그아웃 후 이동할 url. 기본값: /login?logout
            .logoutSuccessHandler(logoutSuccessHandler) // 이 항목이 지정되면 logoutSuccessUrl 설정은 무시됨
            .invalidateHttpSession(true) // 기본값: true. 내부적으로 SecurityContextLogoutHandler을 설정함
            .addLogoutHandler(logoutHandler) // 디폴트로 마지막 LogoutHandler로 SecurityContextLogoutHandler가 추가됨
            .deleteCookies(cookieNamesToClear) // 명시적으로 CookieClearingLogoutHandler을 추가 한 것과 같은 동작
            .and()
        ...
}

6.5.1 LogoutHandler

  • PersistentTokenBasedRememberMeServices
  • TokenBasedRememberMeServices
  • CookieClearingLogoutHandler
  • CsrfLogoutHandler
  • SecurityContextLogoutHandler

LogoutHandler들을 추가하는 shortcut들이 존재한다(E.g. deleteCookies()CookieClearingLogoutHandler의 shortcut임)

※ 주의사항: exception을 던져선 안됨

6.5.2 LogoutSuccessHandler

성공적인 로그아웃 이후, 리디렉션이나 포워딩같은 처리를 위해 LogoutFilter에 의해 호출되는 핸들러.

  • SimpleUrlLogoutSuccessHandler (logoutSuccessUrl()이 shortcut임. 기본 위치는 /login?logout)
  • HttpStatusReturningLogoutSuccessHandler (REST API 시나리오에 쓰일수있다. 리다이렉팅 대신 단순한 HTTP status code를 리턴한다. 기본값은 200)

6.6 ~ 6.8 OAuth 2.0

Client, Login, Resource Server 지원. 자세한 내용은 원문 참조

6.9 Authentication

6.9.1 In-Memory Authentication

@Bean
public UserDetailsService userDetailsService() throws Exception {
    // ensure the passwords are encoded properly
    UserBuilder users = User.withDefaultPasswordEncoder();
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(users.username("user").password("password").roles("USER").build());
    manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build());
    return manager;
}

6.9.2 JDBC Authentication

@Autowired
private DataSource dataSource;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    // ensure the passwords are encoded properly
    UserBuilder users = User.withDefaultPasswordEncoder();
    auth
        .jdbcAuthentication()
            .dataSource(dataSource)
            .withDefaultSchema()
            .withUser(users.username("user").password("password").roles("USER"))
            .withUser(users.username("admin").password("password").roles("USER","ADMIN"));
}

6.9.3 LDAP Authentication

LDAP 지원. 자세한 내용은 원문 참조

6.9.4 AuthenticationProvider

아래와 같이 AuthenticationProvider를 bean으로 등록하면 커스터마이징도 가능하다.
SpringAuthenticationProviderAuthenticationProvider의 구현체라고 가정하면

AuthenticationManagerBuilder가 populate 되지 않았을 때만 동작함

@Bean
public SpringAuthenticationProvider springAuthenticationProvider() {
    return new SpringAuthenticationProvider();
}

6.9.5 UserDetailsService

커스텀 UserDetailsService를 bean으로 등록하여 커스터마이징 할 수도 있다.
이때도 SpringDataUserDetailsServiceUserDetailsService의 구현체라고 가정하면

AuthenticationManagerBuilder가 populate 되지 않았고,
AuthenticationProviderBean가 정의되지 않았을 때만 동작함

@Bean
public SpringDataUserDetailsService springDataUserDetailsService() {
    return new SpringDataUserDetailsService();
}

6.10 Multiple HttpSecurity

여러개의 설정을 동시 적용할수 있다

@EnableWebSecurity
public class MultiHttpSecurityConfig {
    @Bean                                                             
    public UserDetailsService userDetailsService() throws Exception {
        // ensure the passwords are encoded properly
        UserBuilder users = User.withDefaultPasswordEncoder();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(users.username("user").password("password").roles("USER").build());
        manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build());
        return manager;
    }

    @Configuration
    @Order(1) // 1순위로 적용
    public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .antMatcher("/api/**")                               
                .authorizeRequests()
                    .anyRequest().hasRole("ADMIN")
                    .and()
                .httpBasic();
        }
    }

    @Configuration // Order가 명시되지 않으면 가장 마지막 적용임. /api 경로 이외의 항목들에 적용됨
    public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                .formLogin();
        }
    }
}

6.11 Method Security

메소드별로 security를 지정할 수 있다.

  • JSR-250 어노테이션 지원
  • 스프링 어노테이션 @Secured 지원
  • expression-based 어노테이션 지원 (hasRole 등)
  • bean 선언을 decorate해서 security를 하나의 빈에 적용할수도 있음 (intercept-methods 요소를 사용)

6.11.1 EnableGlobalMethodSecurity

아래와 같이 활성화시키면 @Secured 어노테이션을 쓸 수 있다.

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
    // ...
}

AccessDecisionManager에 전달되어 실제 인가 여부가 결정된다

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

아래와 같이 하면 JSR-250가 활성화 된다.

@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
    // ...
}

그런데 이 방법으로는 Simple role-based 제한만 적용되기 때문에 Spring Security의 native 어노테이션의 힘을 활용할수가 없다. expression-based 문법을 활용하려면 이렇게

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
    // ...
}
public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

6.11.2 GlobalMethodSecurityConfiguration

@EnableGlobalMethodSecurity 어노테이션이 허용하는 것보다 복잡한 동작이 필요할 때가 있다. 그럴땐 @EnableGlobalMethodSecurity 어노테이션이 달린 GlobalMethodSecurityConfiguration 서브클래스를 정의하여 메서드를 override 하면 된다. (메서드 목록은 자바독 참조)

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        // ... create and return custom MethodSecurityExpressionHandler ...
        return expressionHandler;
    }
}

6.12 ~ 6.13

생성된 객체 인스턴스를 수정하거나 대체하기 위해서 ObjectPostProcessor를 사용할 수 있다.
configure(http) 에서 사용되는 custom dsl를 정의할 수도 있다.

자세한 내용은 원문 참조.

7. xml로 설정하기

Java Configuration 말고 xml로 설정하는 방법. 자세한 내용은 원문 참조.

8. Architecture and Implementation

8.1 Technical Overview

8.1.2 Core Components

SecurityContextHolder

ThreadLocal을 사용하여 context 정보를 저장한다. (요청 처리 끝나고 스프링이 삭제까지 알아서 잘 해 줌)
context 저장 전략은 시스템 프로퍼티 설정 또는 static 메서드 호출을 통해 변경이 가능하다.

  • SecurityContextHolder.GLOBAL : standalone application에서 사용
  • SecurityContextHolder.MODE_INHERITABLETHREADLOCAL : spawn된 thread가 동일한 context를 가짐
  • SecurityContextHolder.MODE_THREADLOCAL : 기본값. 대부분의 애플리케이션은 이 값을 사용한다

Authentication

현재 상호작용중인 principal의 세부정보

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
    String username = ((UserDetails)principal).getUsername();
} else {
    String username = principal.toString();
}

UserDetailsService

Principal = UserDetails임. getAuthentication()에서 반환됨.
이 값을 찾아주는것 아래 interface가 UserDetailsService

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

GrantedAuthority

=ROLE. 특정 객체에 대한 사용권한을 나타내는 것이 아니라 애플리케이션 차원의 사용 권한을 나타냄. 그렇게 하려면 도메인 객체 보안 기능을 사용해야 함

8.1.3 Authentication

Spring Security 인증 과정

  • 사용자가 입력한 username / password로 UsernamePasswordAuthenticationToken 객체를 생성한다
  • 이걸 AuthenticationMananger에서 검사/체크 하고, 이상이 없다면 나머지 값들을 채워 Authentication 인스턴스로 반환한다.
  • SecurityContextHolder.getContext().setAuthentication(...)을 통해 컨텍스트가 세팅 된다.

User operation 인가를 위해 AbstractSecurityInterceptor로 넘어가기 전에만 SecurityContextHolderAuthentication 정보가 있으면 된다. 말인 즉슨, Spring Security에 기반한 구현이 아니더라도 필터나 MVC controller를 커스터마이징 하여 인증 시스템을 작성할 수 있는 것이다.

AuthenticationManager의 구현을 참조하자.

8.1.4 Authentication in a Web Application

일반적인 웹 애플리케이션 인증 절차

  1. 홈페이지에 방문해서 링크를 누름
  2. 요청은 서버로 보내지고, 서버는 이것이 보호된 자원에 대한 요청인지를 결정함
  3. 미리 인증되어 있지 않다면 서버는 인증해야 한다고 알려줄 것임. HTTP 응답코드를 주던지 아니면 특정 웹 페이지로 넘김
  4. 인증 메커니즘에 따라 브라우저가 로그인을 할 수 있도록 리다이렉트시키거나, 기본 인증 대화상자/쿠키/X.509인증서 등을 통해 사용자 정보를 획득
  5. 브라우저는 서버로 인증정보를 다시 보낸다. 사용자가 폼에 작성한 내용을 HTTP POST로 보내거나 HTTP header에 인증 정보를 포함하여 보낸다.
  6. 서버는 전달받은 인증정보가 유효한지 검증한다. 유효하지 않다면 보통 다시 인증정보를 물어본다. (2번으로 돌아감)
  7. 원래 요청은 다시 서버로 전달 된다. 이제 요청한 자원에 대해 접근 권한이 있으면 요청에 대한 응답을 받을 것이고 권한이 없으면 HTTP 403(forbidden) 에러 코드를 받을 것이다.

위 인증 과정에 참여하는 여러 클래스들이 있다

대부분 AbstractSecurityInterceptor에서 발생하게 되는 Spring Security exception들은 ExceptionTranslationFilter에서 403 코드 반환이나 로그인 페이지(AuthenticationEntryPoint) 이동으로 처리 된다.

Authentication Mechanism

인증 메커니즘 종류

  • form-base login
  • Basic authentication 등

User agent로부터 수집된 authentication details는 Authentication 요청 객체로 빌드되고, AuthenticationManager에게 넘겨진다. 모든 정보가 채워진 Authentication 객체는 SecurityContextHolder에 담겨지고 원래 요청을 재시도 한다. (7단계) AuthenticationManager가 요청을 거절하면 2단계로 돌아간다.

Storing the SecurityContext between requests

일반적으로, Session ID를 통해 매 요청간의 Security context가 저장된다. 서버는 duration session 시간동안 principal 정보를 캐싱한다. spring security에서는 SecurityContextPersistenceFilterSecurityContext를 저장하는 책임을 갖는다. HttpSession attribute를 통해 하는 것이 기본 동작이다. 매 요청마다 SecurityContextHolder에 값을 복원하고, 요청이 완료되면 이 값을 파기해 준다. 보안 관련 목적으로 HttpSession에 직접 접근해서는 안된다. 대신 꼭 SecurityContextHolder를 사용해라.

많은 유형의 애플리케이션(ex: RESTful web)에서 HTTP session을 사용하지 않고 매 요청마다 재인증 작업을 하기는 하지만, SecurityContextPersistenceFilter가 필터 체인에 포함되어 SecurityContextHolder가 잘 삭제되도록 하는 것이 중요하다.

동일 세션에서 동시다발적인 요청이 유입될 경우 SecurityContext 인스턴스는 쓰레드들 사이에서 공유 된다. setAuthentication() 를 통해 값을 바꿀 경우 모든 쓰레드들에 같이 적용된다. 이것을 피하기 위해선 SecurityContextPersistenceFilter를 커스터마이징 해서 매번 새로운 인스턴스를 생성하도록 하거나, 별도 변수로 참조하도록 해라

8.1.5 Access-Control(Authorization)

AccessDecisionManagerdecide 메서드가 Authentication 객체를 파라미터로 받아 처리해줌

Security and AOP Advice

AOP에 익숙하다면 before, after, throws, around 등과 같은 다양한 유형의 advice 가 있다는 것을 알고 있을 것이다. around advice 는 쓸모가 많은데 advisor 가 메소드 호출을 실행 할지 말지, 응답을 변경 할지 말지, 예외를 발생시킬지 말지를 결정할 수 있기 때문이다. 스프링 시큐리티(Spring Security)는 웹 요청뿐만 아니라 메소드 호출에 대한 around advice 를 제공한다. Spring Security는 Spring 표준 AOP 지원을 사용하여 메소드 호출에 대한 around advice 를 구현했고, 표준 필터를 사용하여 웹 요청에 대한 around advice 를 구현하고 있다.

Spring Security가 메소드 요청과 웹 요청을 보호할 수 있다는 사실을 이해 해야한다. 대부분의 개발자는 서비스 계층에서 메소드 호출 보안에 관심이 있다. 왜냐하면 서비스 계층에 Java EE 애플리케이션의 비즈니스 로직이 대부분 있기 때문이다. 서비스 계층에서 메소드 호출보안만 고려한다면 Spring의 표준 AOP로 구현이 가능하다. 하지만 도메인 객체를 직접 보호해야한다면, AspectJ를 활용해야 할 수 있다.

AspectJ 또는 Spring AOP를 사용하여 메소드 권한 부여를 수행하거나 필터를 사용하여 웹 요청 권한 부여를 수행하도록 선택할 수 있다.

Secure Objects and the AbstractSecurityInterceptor

Secure Object는 보안 적용이 필요한 모든 것을 의미한다. 예를 들면 웹 호출 과 메소드 호출 이 있다.

Secure Object 유형들에는 각각 AbstractSecurityInterceptor의 서브 클래스인 자체 인터셉터 클래스가 있다. 중요한 것은 AbstractSecurityInterceptor가 호출되기 전까지 Principal이 인증되면 SecurityContextHolder에 인증된 Authentication이 포함된다는 것이다.

AbstractSecurityInterceptor는 보안 객체 요청을 처리하기위한 일관된 워크 플로우를 제공한다.

  1. 현재 요청과 연관된 configuration attributes 을 찾는다.
  2. Secure object, 현재 Authenticationconfiguration attributesAccessDecisionManager에 제출하여 권한 결정을 내림
  3. 필요하면 호출이 일어난 Authentication를 변경
  4. Secure object 호출 할 수 있도록 허가함(접근권한이 있을 때)
  5. 호출이 반환되면 AfterInvocationManager를 호출합니다 (설정된 경우). 호출에서 예외가 발생하면 AfterInvocationManager가 호출되지 않음.
What are Configuration Attributes?

ConfigAttribute 인터페이스 안에 있는 문자열들. ROLE_어쩌구 등. AbstractSecurityInterceptor에 의해 사용됨.

RunAsManager

AccessDecisionManager가 요청을 허용하기로 결정했다면, AbstractSecurityInterceptor는 일반적으로 요청 처리를 진행한다. 하지만 이것을 SecurityContext 내부에 있는 AuthenticationRunAsManager를 호출하는 AccessDecisionManager가 관리하는 Authentication 으로 대체하고자 할 수 있다. 이 기능은 어떤 서비스 레이어의 메소드가 원격 시스템을 호출하려고 하는데 다른 사용자로 바꿔서 호출해야 하는 경우에 매우 유용하다. Spring Security는 한 서버에서 다른 서버로 보안 신원을 자동으로 전파하기 때문에 (적절하게 구성된 RMI 또는 HttpInvoker 원격 프로토콜 클라이언트를 사용한다고 가정 할 때) 유용 할 수 있다.

AfterInvocationManager

AbstractSecurityInterceptorSecurity Object가 호출되고 리턴된 뒤 -메소드 호출 완료 또는 필터 체인 진행을 의미 할 수 있음- 는 호출을 처리 할 마지막 지점이 있다. 이 단계에서 AbstractSecurityInterceptor는 반환된 객체를 수정하는데 관심이 있다. 보안 객체 호출에 대한 승인이 나지 않았을 때 이런 상황을 만들 수 있다. AbstractSecurityInterceptorAfterInvocationManager에 컨트롤을 전달하여 필요할 경우 실제로 객체를 수정할 수 있다. 이 클래스는 객체를 완전히 바꾸거나, 예외를 던지거나, 변경하지 않을 수 있다. after-invocation은 호출이 성공한 경우에만 실행된다. 예외가 발생하면 추가 검사를 건너 뛴다.

Extending the Secure Object Model

요청을 가로채고 권한을 부여하는 완전히 새로운 방법을 고민하는 개발자만 보안 객체를 직접 사용해야한다. 예를 들어 메시징 시스템에 대한 호출을 보호하기 위해 새 보안 개체를 만들 수 있다. 보안이 필요하고 (AOP 관련 advice 시맨틱과 같이) 호출을 가로채는 방법을 제공하는 것은 보안 객체로 만들어 질 수 있다. 이미 언급했듯이, 대부분의 Spring 애플리케이션은 완전한 투명성으로 현재 지원되는 세 가지 보안 객체 유형 (AOP Alliance MethodInvocation, AspectJ JoinPoint 및 웹 요청 FilterInvocation)을 단순히 사용한다.

8.1.6 Localization

exception 메시지에 대한 국제화 지원함. spring-security-coreorg.springframework.security 패키지에 messages.properties 파일이 있으며, 몇가지 주요 언어에 대한 번역본이 같이 들어 있다. Spring의 MessageSourceAware 인터페이스를 구현한 당신의 ApplicationContext에 의해 참조 되어야 하며, 애플리케이션 시작시 message resolver 가 DI 돼야 한다.

그냥 이거만 하면 된다.

<bean id="messageSource"
    class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:org/springframework/security/messages"/>
</bean>

메시지를 커스터마이징 하고 싶을땐 messages.properties 파일을 카피해서 편집한다음 위 속성에 반영하여 넣으면 된다.

Spring Security는 Spring의 국제화 지원에 의존하기 때문에, Spring의 org.springframework.context.i18n.LocaleContextHolder에 locale 값이 잘 전달되어 오는지 확인해야 한다. Spring MVC의 DispatcherServlet이 이 일을 해준다. 하지만 Spring Security의 필터가 이것보다 더 일찍 호출 되기 때문에, LocaleContextHolder에 유효한 Locale이 세팅되어야 한다. Spring Security 필터 전에 직접 이 값을 넣거나, Spring의 RequestContextFilter를 사용할 수 있다. 자세한 내용은 Spring Framework의 문서를 참조해라.

8.2 Core Services

고수준의 오버뷰로, AuthenticationManagerUserDetailsService, AccessDecisionManager를 살펴보자

8.2.1 The AuthenticationManager, ProviderManager and AuthenticationProvider

AuthenticationManager는 단지 우리가 구현체를 선택할 수 있는 인터페이스 일 뿐이다. 다수의 인증서버 또는 상이한 인증서비스(ex: DB + LDAP)를 써야 할 때는 어떻게 해야 할까?

Spring Security에서의 기본 구현체는 ProviderManager이다. ProviderManager는 인증 요청을 직접 처리하지 않고 설정된 AuthenticationProvider 리스트에 위임한다. ProviderManager는 리스트에 있는 AuthenticationProvider가 인증을 수행 할 수 있는지를 차례로 돌면서 인증을 시도한다. 각각의 provider들은 exception을 던지든지, 모든 값이 채워진 Authentication 객체를 리턴한다. 인증 요청을 인가하는 일반적인 접근방식은 일치하는 UserDetails 를 로드하여 사용자가 입력한 password와 로드된 password를 비교하는 것이다. 이것은 DaoAuthenticationProvider에 의해 사용되는 방식이다. 로드된 UserDetails에는 GrantedAuthority 또한 포함되고, 이렇게 구성된 Authentication 객체는 SecurityContext에 저장된다.

네임스페이스(xml)로 설정을 구성했을 경우엔 ProviderManager 인스턴스가 생성되고 내부적으로 유지되므로 'authentication provider' element로 목록을 구성하지만, ProviderManager 인스턴스가 없을땐 아래에 상응하는 방식으로 bean을 구성 해 주어야 한다.

<bean id="authenticationManager"
        class="org.springframework.security.authentication.ProviderManager">
    <constructor-arg>
        <list>
            <ref local="daoAuthenticationProvider"/>
            <ref local="anonymousAuthenticationProvider"/>
            <ref local="ldapAuthenticationProvider"/>
        </list>
    </constructor-arg>
</bean>

각각의 provider들은 순차적으로 시도되고, null이 리턴되면 다음 provider로 skip 한다. 모든 구현체가 null을 반환하면 ProviderManangerProviderNotFoundException을 던진다. provider들에 대해 궁금하다면 ProviderMananger의 javadoc을 참조할것.

Erasing Credentials on Successful Authentication

기본적으로, ProviderMananger는 인증 성공 후 request의 Authentication 객체에서 모든 민감한 정보(credentials)를 삭제하는데, 만약 user 객체들을 캐시하고 있었다면 문제가 생긴다. 이를 해결하기 위해선 캐시 구현체에서 Authentication의 copy본을 만들어서 사용하도록 하거나, AuthenticationManager에서 Authentication 객체를 생성하도록 해야한다. 대안으로, ProviderManager에서 eraseCredentialsAfterAuthentication 속성을 끌수도 있다. 자세한 정보는 Javadoc에서..

DaoAuthenticationProvider

Spring Security에서 구현된 가장 간단한 AuthenticationProviderDaoAuthenticationProvider 이며 username, password, GrantedAuthority들을 읽기위해 UserDetailsService를 DAO로 사용한다. 인증을 위해서 user로부터 submit된 UsernamePasswordAuthenticationTokenUserDetailsService에서 읽은 값을 비교한다. provider 설정 방법은 아래와 같다.

<bean id="daoAuthenticationProvider"
    class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>

PasswordEncoder는 optional이다. UserDetailsService에서 반환된 UserDetails의 password를 encode/decode하는데 쓰인다.

8.2.2 UserDetailsService Implementations

앞에서 언급했듯이 대부분의 authentication provider들은, 실제로 username과 password으로 인증하지 않더라도(LDAP, X.5.9, CAS 등) UserDetailsUserDetailsService 인터페이스를 사용한다(GrantedAuthority를 사용하기 위해서).

In-Memory Authentication

입맛에 맞는 저장 엔진을 사용해서 UserDetailsService를 구현 할 수도 있지만, 그러한 복잡도가 필요 없거나 / 프로토타입 애플리케이션을 개발할때 사용하게 되는 구현체.

namespace로 손쉽게 설정하기

<user-service id="userDetailsService">
<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that
NoOpPasswordEncoder should be used. This is not safe for production, but makes reading
in samples easier. Normally passwords should be hashed using BCrypt -->
<user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" />
</user-service>

외부 property 파일로도 가능

<user-service id="userDetailsService" properties="users.properties"/>

파일 포맷은 아래와 같음

username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]

예를 들면

jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
bob=bobspassword,ROLE_USER,enabled

JdbcDaoImpl

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>

<bean id="userDetailsService"
    class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>

기본적으로, JdbcDaoImpl은 user와 authority를 1:1 매핑으로 간주한다. 이에대한 자세한 내용은 JavaDaoImpl의 Javadoc을 참조하자.
테이블 스키마는 appendix 섹션에 나온다

8.2.3 Password Encoding

PasswordEncoder는 password가 안전하게 저장되도록 단방향 transform을 해주는 인터페이스이다. 의도적으로 리소스 사용을 과하게 하여 해시 결과를 천천히 얻도록 하는것이 보안에 유리한데, 이 정도(work-factor, 1초 정도 걸리면 적당)를 조절할수 있도록 하는게 adaptive one-way function이고, bcrypt, PBKDF2, scrypt, Argon2 등이 있다.

short term credential(i.e. session, OAuth Token, etc)를 위해 long term credential (i.e. username / password) 을 교환하는 방법을 추천한다.

DelegatingPasswordEncoder

Spring Security 5.0 이전에는 디폴트 PasswordEncoderNoOpPasswordEncoder 였다(plain text로 저장). 이후 디폴트값을 BCryptPasswordEncoder로 바로 지정하지 않고, 레거시 호환성과 추후 업그레이드를 대비할수 있도록 DelegatingPasswordEncoder으로 사용하였다.

인스턴스는 아래와 같이 생성할수 있다

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

아니면 아래처럼 커스터마이징 할 수도 있다

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);
Password Storage Format

패스워드를 저장하는 일반적인 포맷은 이렇다:

{id}encodedPassword

ex)

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG    // BCryptPasswordEncoder
{noop}password   // NoOpPasswordEncoder
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc  // Pbkdf2PasswordEncoder
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=   // SCryptPasswordEncoder
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0  // StandardPasswordEncoder
Password Matching

PasswordEncoder에 잘못된 id나 null이 입력되면 IllegalArgumentException이 발생하는데, DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)를 통해 이 동작을 커스터마이징 할 수 있다.

plaintext로의 복원이 불가능하기 때문에 항상 최신 인코딩을 사용해라. 마이그레이션이 불가능하다.

PasswordEncoder 구현체들

password 검증시 1초 정도 걸리도록 튜닝해서 써야함

  • BCryptPasswordEncoder
  • Pbkdf2PasswordEncoder: FIPS certification이 필요할 경우 좋은 선택지가 됨
  • SCryptPasswordEncoder: 고성능 하드웨어 환경에서도 뚫리지 않기 위해 메모리 사용량을 크게 잡음으로써 느리게 만든 알고리즘
  • 기타 등등: deprecated 시켜놓았지만 하위 호환성을 위해서 삭제할 계획은 없음 (마이그레이션이 불가하므로)

8.2.4 Jackson Support

distributed session(Session replication, Spring Session ...) 환경에서 serializing 성능을 향상시키기 위해 Jackson 지원함

ObjectMapper mapper = new ObjectMapper();
ClassLoader loader = getClass().getClassLoader();
List<Module> modules = SecurityJackson2Modules.getModules(loader);
mapper.registerModules(modules);

// ... use ObjectMapper as normally ...
SecurityContext context = new SecurityContextImpl();
// ...
String json = mapper.writeValueAsString(context);

9. Testing

Dependency에 spring-security-test-5.1.6.RELEASE.jar 포함해야 함

9.1 Testing Method Security

... 나중에 필요할 때..

+ Recent posts