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
:SecurityContext
가SecurityContextHolder
에 세팅되어 함 (HttpSession 사용)ConcurrentSessionFilter
:SecurityContextHolder
를 참조하여SessionRegistry
에 변경사항을 반영함- Authentication processing mechanism들.
UsernamePasswordAuthenticationFilter
,CasAuthenticationFilter
,BasicAuthenticationFilter
등.SecurityContextHolder
이 올바르게 수정되어 유효한Authentication
request token을 포함할수 있도록. SecurityContextHolderAwareRequestFilter
: servlet container에 Spring Security awareHttpServletRequestWrapper
를 넣어주기 위해서.JaasApiIntegrationFilter
:JaasAuthenticationToken
이SecurityContextHolder
에 있을 경우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
는 무시된다. 불행히도 서블릿 스펙은 servletPath
와 pathInfo
에 어떤 URI가 정확히 들어가는지에 대한 정의가 없다. 예를 들어, RFC2396에 의하면 URL의 각 path segment에는 파라미터가 포함 될 수 있다. 해당 스펙에는 이런값들이 servletPath와 pathInfo에 포함되어야 하는지가 명확하지 않고, 서블릿 컨테이너들마다 동작이 다르다. path parameter를 strip하지 않는 컨테이너들에 애플리케이션이 디플로이 되면 공격자는 패턴매칭을 이용해 공격할 수 있다. 다른 URL 변형 또한 가능하다. 예를 들어 경로 변경 시퀸스(/../ 등과 같이)나 다중 포워드 슬래시(//)가 패턴 매칭을 실패시킬수 있다. 컨테이너마다 이런 항목에 대한 처리를 해주는것도 안해주는 것도 있다. 이것을 보완하기 위해 FilterChainProxy
는 HttpFirewall
전략을 사용해 request를 래핑한다. 기본적으로 정규화되지 않은 요청은 자동으로 거절되며, 다중 슬래시는 삭제된다. 그렇기 때문에 FilterChainProxy
가 security filter chain을 관리하기 위해 사용되는 것이 필수적이다.
servletPath
와 pathInfo
는 컨테이너에 의해서 디코딩 되므로, 당신의 애플리케이션은 세미컬론을 포함한 유효 경로를 포함해서는 안된다. 이런 파트들은 매칭 목적을 위해 제거 될 것이다.
위에서 언급했듯이 기본 전략은 Ant-style path이며, 아마 대부분의 유저들에게 최선의 선택일 것이다. 이 전략은 AntPathRequestMatcher
에 구현되어 있으며 스프링의 AntPathMatcher
를 사용해 servletPath
와 pathInfo
매칭을 수행할 것이며 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;
}
StrictHttpFirewall
는 Cross 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 리소스 보안을 처리할 책임이 있다. 이것은 AuthenticationManager
와 AccessDecisionManager
에 대한 레퍼런스를 필요로 한다. 각기 다린 HTTP URL request에 대한 Configuration attributes(ROLE_어쩌구 등.. 8.1.5에서 언급)도 같이 지정 된다.
FilterSecurityInterceptor
는 Configuration attributes와 함께 두가지 방법으로 설정 될 수 있다. 하나는 위에서 본것처럼 <filter-security-metadata-source>
요소를 이용한 방식이다. 이것은 namespace 챕터의 http 요소와 유사하지만, <intercept-url>
자식 요소들은 오직 pattern
와 access
만을 사용한다. 컴마를 통해 각기 다른 configuration attribute가 HTTP URL에 적용된다. 두번째 방식은 나만의 SecurityMetadataSource
를 작성하는 것이다. 하지만 이것은 이 문서의 영역을 벗어난다. 어떤 접근법을 사용하든 관계없이, SecurityMetadataSource
는 각 secure HTTP URL에 적용되는 configuration attribute를 List<ConfigAttribute>
로 반환해야 하는 책임을 갖는다.
FilterSecurityInterceptor.setSecurityMetadataSource()
는 FilterInvocationSecurityMetadataSource
인스턴스를 필요로 한다. 이것은 SecurityMetadataSource
의 서브클래스를 나타내는 마커 인터페이스이다. 이것은 단순히 SecurityMetadataSource
가 FilterInvocation
을 이해하고 있다는 것을 나타낸다. 단순함을 위해 앞으론 FilterInvocationSecurityMetadataSource
을 SecurityMetadataSource
라고 언급 하겠다(대부분의 유저에겐 상관이 없을것이다)
네임스페이스 문법에 의해 생성된 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)
RequestCache
는 HttpServletRequest
인스턴스를 저장하고 복원시키는데 필요한 기능들을 캡슐화 했다. 기본적으로 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
를 호출해서 인증 요청들을 처리한다. 성공/실패 목적지는 각각AuthenticationSuccessHandler
와 AuthenticationFailureHandler
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 기반의 인증일 경우, 일반적으로 이 Authentication
은 UsernamePasswordAuthenticationToken
인스턴스이다. 추가적인 유저 정보를 얻을 때 유용하다. 예를 들어, 커스텀 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);
AbstractAuthenticationProcessingFilter
가 loginFail()
과 loginSuccess()
만 호출하긴 하지만, 각 메서드가 어떤일을 하는지는 구체적으로 Javadoc을 읽어보자. autoLogin()
메서드는 SecurityContextHolder
에 Authentication
값이 없을때마다 RememberMeAuthenticationFilter
에 의해서만 호출된다. 이 인터페이스 덕분에 다양한 구현체를 만들 수 있다. Spring Security에서 제공하는 2가지를 살펴보자.
TokenBasedRememberMeServices
간단한 해시기반 구현체다. TokenBasedRememberMeServices
는 RememberMeAuthenticationToken
을 생성하고, 이것은 RememberMeAuthenticationProvider
에 의해 처리된다. authentication provider와 TokenBasedRememberMeServices
는 key
를 함께 공유한다. 또한, 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)
메서드를 제공하는데, 관심있는 클래스들로 하여금 인증의 특별한 상태 타입을 고려하도록 해준다.ExceptionTranslationFilter
는 AccessDeniedException
을 처리 할 때 이 인터페이스를 사용한다. 만약 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
생략 나중에..
'레퍼런스 번역 > Spring Security' 카테고리의 다른 글
Spring Security (5.1.9.RELEASE) Section 12 Additional Topics (0) | 2019.08.20 |
---|---|
Spring Security (5.1.9.RELEASE) Section 11 Authorization (0) | 2019.08.20 |
Spring Security (5.1.6.RELEASE) Section 1 ~ 9 Preface ~ Config, Testing (0) | 2019.08.11 |