일단 사용하기에 앞서 Spring Security 사용방법과 CSRF 에 대해 알아야 할것이다.
이곳 링크를 통해 확인해 보기 바란다.
링크...
Spring Security CSRF 적용을 위해서는 사용중인 스프링과 시큐리티 버전을 확인해보기 바란다.
스프링 시큐리티는 3.2.0 버전 부터 사용 할 수 있고, 지금 여기서는 JAVA Configration 이 아닌
XML 설정과 이후 처리 방법에 대해 알아 보겠다.
Security-config.xml
<http auto-config="false" use-expressions="true" disable-url-rewriting="true" >
<csrf />
<intercept-url pattern="/auth/**" access="permitAll" />
<intercept-url pattern="/login/**" access="permitAll" />
<intercept-url pattern="/assets/**" access="permitAll" />
<intercept-url pattern="/errorpages/**" access="permitAll" />
<intercept-url pattern="/guide/**" access="permitAll" />
<intercept-url pattern="/**" access="hasAnyRole('SOFOS')" />
<form-login login-page="/auth/auth_form.do"
login-processing-url="/login"
default-target-url="/home/homepage.do"
always-use-default-target="true"
authentication-failure-handler-ref="AuthLoginFailureHandler"
authentication-success-handler-ref="AuthLoginSuccessHandler"
password-parameter="authCode"
username-parameter="userCode" />
<logout invalidate-session="true" logout-success-url="/auth/auth_form.do" logout-url="/auth/logout.do" />
<!--
////////////////////////////////////////////////////
// SESSION MANAGER
////////////////////////////////////////////////////
-->
<session-management >
<concurrency-control max-sessions="1" expired-url="/auth/auth_form.do?resultMessage=DUPLICATION_LOGIN" />
</session-management>
<access-denied-handler ref="AccessDeniedHandler"/>
</http>
<authentication-manager>
<authentication-provider ref="AuthenticationProvider" />
</authentication-manager>
<beans:bean id="AccessDeniedHandler" class="com.skan.potal.web.auth.handler.AccessDeniedHandler"/>
<beans:bean id="AuthenticationProvider" class="com.skan.potal.web.auth.provider.AuthenticationProvider"/>
<beans:bean id="AuthLoginFailureHandler" class="com.skan.potal.web.auth.handler.AuthLoginFailureHandler">
</beans:bean>
위와 같이 사용중인 곳에 <csrf /> 를 적어 선언함으로 사용 가능 하다.
자이제 Tag를 넣었으니 잘 동작하겠지? 하지만 현실은 그렇치 않다.
사용중인 form 전송마다 다음과 같은 테크를 넣어 줌으로 csrf Token 인증을 수행한다.
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
(Spring <form:form > Tag 를 사용하면 자동으로 값을 넣어 주기때문에 폼테그 사용시 위 내용을 선언하지 않아도 사용 가능 하다.)
${csrf_ ....} 을 선언하지 않으면 403 접근 권한 오류가 나며 시큐리티에서 사용중인 forbidden Page로 이동 되게 된다.
하지만 우리는 모든 페이지 마다 이런 요청을 모두 넣어줄수 없으며, 이 요청을 처리하기 위해 위 테그를 삽입 한다 하여도, redirect 요청 혹은 forward 요청시 값이 누락될수 있다.
이 대안으로 사용 할수 있는 방법은 filter 이다.
모든 요청에 대한 html 값을 읽어 form 테그를 찾아 마지막에 끝나는 지점에 대해.
위 테그를 자동(강제) 삽입하는 것이다.
OOOTokenFilter.java
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.stereotype.Component;
@Component
public class FormTokenVerificationFilter implements Filter
{
////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////
private static final String[] EXCLUDE_URL_LIST = { "/logout", "/assets", "/errorpages", "/WEB-INF", "/report/download" };
////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////
private static final String PREFIX_NAME = "_";
private static final String SESSION_TOKEN_NAME = "SESSION_TOKEN_NAME";
private static final String SESSION_ACTIVE_TOKEN = "SESSION_ACTIVE_TOKEN";
private static final String REDIRECT_URL = "/potal/auth/auth_form.do?resultMessage=INVALID_TOKEN";
////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////
private Logger logger = LoggerFactory.getLogger( this.getClass() );
@Autowired
private AuditLogDao AuditDao;
@Autowired
private CsrfTokenRepository csrfTokenRepository;
@Override
public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException{
if( request instanceof HttpServletRequest ){
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpSession httpSession = httpRequest.getSession();
boolean excludeState = false;
String reqUrl = httpRequest.getRequestURL().toString();
for( String target : EXCLUDE_URL_LIST )
{
if( reqUrl.indexOf( target ) > -1 )
{
excludeState = true;
break;
}
}
if( excludeState )
{
chain.doFilter( request, response );
return;
}
ServletResponse newResponse = new FormTokenResponseWrapper( httpResponse );
chain.doFilter( request, newResponse );
if( newResponse instanceof FormTokenResponseWrapper ) {
String tokenName = PREFIX_NAME + RandomCodeUtils.generate();
String tokenValue = RandomCodeUtils.generate();
String encryptedTokenValue = CryptoUtils.encrypt( tokenValue );
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
// Spring Security will allow the Token to be included in this header name
//response. setHeader("X-CSRF-HEADER", token.getHeaderName());
// Spring Security will allow the token to be included in this parameter name
//response.setHeader("X-CSRF-PARAM", token.getParameterName());
// this is the value of the token to be included as either a header or an HTTP parameter
//response.setHeader("X-CSRF-TOKEN", token.getToken());
//final CsrfToken token = csrfTokenRepository.loadToken(httpRequest);
String tokenStr = "";
if(token != null) {
tokenStr = String.format("<input type=\"hidden\" name=\"%s\" value=\"%s\" />", token.getParameterName(), token.getToken());
}
//////////////////////////////////////////////////////////////////////////
//
//////////////////////////////////////////////////////////////////////////
String responseText = newResponse.toString();
if( responseText != null && httpSession != null )
{
httpSession.removeAttribute( SESSION_ACTIVE_TOKEN );
httpSession.removeAttribute( SESSION_TOKEN_NAME );
//////////////////////////////////////////////////////////////////////////
//
//////////////////////////////////////////////////////////////////////////
httpSession.setAttribute( SESSION_TOKEN_NAME, tokenName );
httpSession.setAttribute( SESSION_ACTIVE_TOKEN, tokenValue );
logger.debug( "===============================");
logger.debug( "TOKEN-NAME : {} ", httpRequest.getRequestURI() );
logger.debug( "TOKEN-NAME : {} ", tokenName );
logger.debug( "TOKEN-VALUE : {} ", tokenValue );
String formTokenInput = String.format("<input type=\"hidden\" name=\"%s\" value=\"%s\" />"
+ tokenStr +"</form>", tokenName, encryptedTokenValue );
responseText = StringUtils.replace( responseText, "</form>", formTokenInput );
response.getWriter().write( responseText );
}
}
}
else {
chain.doFilter( request, response );
}
}
@Override
public void init( FilterConfig arg0 ) throws ServletException {
}
@Override
public void destroy() {
}
}
formTokenInput 변수에 사용한것 처럼 모든 응답요청에 대하여 CSRF Tag 를 삽입 하도록 한다.
CSRF 테그 삽입시 CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
형태로 값을 가져 올 수 있다.
CSRF 토큰이 전달되는 모습을 fiddler 등 http 요청을 감시하는 툴을 통해 확인 할 수 있다.
아큐넥티스로 확인해도 취약점이 안나오는것을 확인 할 수 있다.
하지만, muiltipart-form 의경우 403 페이지로 권한이 없다고 페이지가 이동되게 되는데
muiltipart-form 의 경우는
<form Tag 에 다음과 같이 추가하여 준다.
<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
마지막으로 잘못된 토큰에 의해 오류 발생시 스프링에서 제공 중인 forbidden 페이지를 사용하지 않고, 싶다면
AccessDeniedHandler 이녀석을 상속 받아 다음과 같이 구현하도록 한다.
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
public class AccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
response.sendRedirect("errorPageURL");
}
}
Forbidden 시에 원하는 페이지로 이동 되는것을 확인 할 수 있다.
끝~~
http://info.michael-simons.eu/2014/01/29/csrf-protection-with-spring-security-revisited/
'Spring > Spring Security' 카테고리의 다른 글
Spring Security 동적 권한 할당 (0) | 2016.10.19 |
---|---|
Spring security Session name 은 어느곳에? (0) | 2016.09.12 |
Spring Security logout Handler custom (0) | 2014.11.14 |
Spring Security Session 제어 (0) | 2014.11.06 |