스프링 시큐리티(Spring Security) CSRF 적용

FSP 0 1,296 04.01 12:16

스프링 시큐리티(Spring Security) CSRF 적용 

 

1. pom.xml

 

<properties>

  <security.version>4.2.7.RELEASE</security.version> 

 </properties>

 

........

 

    <!-- Security -->

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-core</artifactId>

<version>${security.version}</version>

</dependency>

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-web</artifactId>

<version>${security.version}</version>

</dependency>

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-config</artifactId>

<version>${security.version}</version>

</dependency>

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-taglibs</artifactId>

<version>${security.version}</version>

</dependency>

 

2. web.xml

 

<!-- Spring Security -->

<listener>

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

</listener>

<filter>

<filter-name>springSecurityFilterChain</filter-name>

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

</filter>

<filter-mapping>

<filter-name>springSecurityFilterChain</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<filter>

<filter-name>csrfFilter</filter-name>

<filter-class>com.flyasiana.ifp.csrf.CsrfTokenAdder</filter-class>

</filter>

<filter-mapping>

<filter-name>csrfFilter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

 

      <context-param>

<param-name>contextConfigLocation</param-name>

<param-value>classpath:config/spring/security-config.xml</param-value>

</context-param>

 

3. security-config.xml

 

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:sec="http://www.springframework.org/schema/security"

xmlns:context="http://www.springframework.org/schema/context"

xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd

http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

 

<!-- Spring Security 대상에서 제외 -->

<sec:http pattern="/resources/**" security="none" />

<sec:http pattern="/images/**" security="none" />

<sec:http pattern="/js/**" security="none" />

<sec:http pattern="/css/**" security="none" />

<sec:http pattern="/ckeditor/**" security="none" />

<sec:http pattern="/fonts/**" security="none" />

<sec:http pattern="/webjars/**" security="none" />

<sec:http pattern="/filedownload/**" security="none" />

<sec:http pattern="/fileupload/**" security="none" />

<sec:http pattern="/WEB-INF/**" security="none" />

<sec:http pattern="/favicon.ico" security="none" />

<bean id="ifpAccessDeniedHandler" class="com.flyasiana.ifp.csrf.IfpAccessDeniedHandler"/>    

<bean id="ifpCsrfRequestMatcher"  class="com.flyasiana.ifp.csrf.IfpCsrfRequestMatcher" />

 

<sec:http create-session="never" use-expressions="true">

<sec:intercept-url pattern="/**" access="permitAll" /> 

<sec:http-basic />

<sec:headers>

        <sec:frame-options policy="SAMEORIGIN"/>

        </sec:headers>    

        <sec:csrf request-matcher-ref="ifpCsrfRequestMatcher" />

        <sec:access-denied-handler ref="ifpAccessDeniedHandler" />

</sec:http>

<sec:authentication-manager/>

</beans>

 

 

4. CsrfTokenAdder.java

 

import java.io.ByteArrayOutputStream; 

import java.io.IOException;

import java.io.OutputStreamWriter;

import java.io.PrintWriter;

 

import javax.servlet.Filter;

import javax.servlet.FilterChain;

import javax.servlet.FilterConfig;

import javax.servlet.ServletException;

import javax.servlet.ServletOutputStream;

import javax.servlet.ServletRequest;

import javax.servlet.ServletResponse;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpServletResponseWrapper;

 

import org.apache.commons.lang.StringUtils;

import org.springframework.security.web.csrf.CsrfToken;

 

/**

 * CsrfTokenAdder.java

 * - CSRF Ptotection을 위한 Text/Html "응답"에 히든태그 Csrf Token 삽입

 * 

 * ################ Spring Security CSRF Protection 설정 ##################

 * [CsrfTokenAdder.java, 서블릿필터] ###"응답에 태그 삽입"###

 * - Spring Security4 이상은 CSRF Enabled가 기본설정

 * - HttpServletRequest(일반  JSP요청), XMLHttpRequest(AJAX) 요청이든 응답에 Csrf 히든테그 삽입

 * - CSRF Protection을 위한 Text/Html 응답에 히든태그 Xsrf Token 삽입

 * - AJAX JSON응답(콤보 채우기등)은 히든태그 Csrf Token 삽입할 필요없다.

 * 

 * [security-config.xaml]

 * - 애당초 Spring Security 적용이 필요없는 부분에 대한 정의(/resources, /images 등)

 * - RequestMatcher에 대한 정의(스프링 시큐리티에서 요청에 대해 CSRF를 적용할건지 아닐건지 판단)

 * - AccessDeniedHandler에 대한 정의(403 에러발생시 대응)

 * 

 * [IfpCsrfRequestMatcher.java] ###"요청에 따라 CSRF 적용여부 결정"###

 * - 스프링 시큐리티에서 CSRF를 적용할건지 아닐건지 판단

 * - AJAX CALL, 첫화면, 최측메뉴로딩, 팝업로등 요청은 CSRF 적용하지 않음

 * - (TEXT/HTML을 응답으로 보낸는 부분은 CsrfTokenAdder 필터에서 Csrf 히든태그 삽입함)

 * - 403 Forbidden 에러가 발생하는 부분이 있다면 이곳에서 추가할 것

 * 

 * [IfpAccessDeniedHandler.java]

 * - 403 Forbidden 에러 발생하는 경우 처리하는 핸들러

 * ######################################################################

 */

 

public class CsrfTokenAdder implements Filter {

 

@Override

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

throws IOException, ServletException {

String url = ((HttpServletRequest)request).getRequestURL().toString();

String responseText = null;

HtmlResponseWrapper  newResponse = null;

CsrfToken token = null;

// 필터대상 에서 제외할 목록

final String[] EXCLUDE_URL_LIST = {"/webjars", "/favicon.ico","/fileupload", "/filedownload", "/css", "/js", "/images", "/fonts", "/ckeditor", "/WEB-INF" };

String paramName = null;

String strToken = null; 

String tokenStr = null;

String csrfTokenInput = null;

String replacedText = null;

boolean excludeState = false;

//-------------------------------------

// 필터 제외 URI SKIP

//-------------------------------------

for( String target : EXCLUDE_URL_LIST )

{

if( url.indexOf( target ) > -1 )

{

excludeState = true;

break;

}

}

 

if( excludeState )

{

chain.doFilter( request, response );

return;

}

//-------------------------------------

//-------------------------------------------------------------

// AJAX APPLICATION/JSON RESPONSE, NON TEXT/HTML

// 화면 콤보 초기화 등에서 AJAX CALL 하는 경우

// 이 경우는 CSRF 태그를 삽입할 필요없다.

//-------------------------------------------------------------

        if ( ((HttpServletResponse)response).getContentType() != null 

        && "XMLHttpRequest".equals(((HttpServletRequest) request).getHeader("X-Requested-With"))

        && ((HttpServletResponse)response).getContentType().contains("application/json")) {  

response.getWriter().write(responseText);

        } 

        //-------------------------------------------------------------

        // 응답이 text/html인 경우 Csrf 히든 태그를 응답 </form> 앞에 삽입한다. 

        // 일반 JSP로 보내는 응답, AJAX TEXT/HTML 응답인 팝업 화면로딩 등

        //-------------------------------------------------------------

        else if ( ((HttpServletResponse)response).getContentType() != null         

        && ((HttpServletResponse)response).getContentType().contains("text/html")) {   

       

        newResponse = new HtmlResponseWrapper ((HttpServletResponse) response);

    chain.doFilter(request, newResponse);

   

    responseText = newResponse.getCaptureAsString();  

        token = (CsrfToken) request.getAttribute("_csrf");

            paramName = token.getParameterName();

            strToken = token.getToken();

  

if (token != null) {

tokenStr = String.format("<input type=\"hidden\" name=\"%s\" id=\"%s\" value=\"%s\" />",

paramName, paramName, strToken);

}

// 일반 JSP로 응답을 보내는 경우(HttpServletRequest, TEXT/HTML응답)

if (!StringUtils.contains(responseText, "_csrf.parameterName")

&& StringUtils.contains(responseText, "<form>")) {

csrfTokenInput = tokenStr + "</form>";

     replacedText = StringUtils.replace(responseText, "</form>", csrfTokenInput);

// 팝업창 로딩(XMLHttpRequest요청,AJAX TEXT/HTML응답)

else {

csrfTokenInput = "<form>" + tokenStr + "</form></body>";

replacedText = StringUtils.replace(responseText, "</body>", csrfTokenInput);

}

        

response.getWriter().write(replacedText);

}

else {

chain.doFilter(request, response);

}

}

 

@Override

public void init(FilterConfig arg0) throws ServletException {

}

 

@Override

public void destroy() {

}

}

 

 

//----------------------------------------------------------

// 응답에 Csrf 히든 테그를 삽입하기 위한 래퍼응답

//----------------------------------------------------------

class HtmlResponseWrapper extends HttpServletResponseWrapper {

 

    private final ByteArrayOutputStream capture;

    private ServletOutputStream output;

    private PrintWriter writer;

 

    public HtmlResponseWrapper(HttpServletResponse response) {

        super(response);

        capture = new ByteArrayOutputStream(response.getBufferSize());

    }

 

    @Override

    public ServletOutputStream getOutputStream() {

        if (writer != null) {

            throw new IllegalStateException(

                    "getWriter() has already been called on this response.");

        }

 

        if (output == null) {

            output = new ServletOutputStream() {

                @Override

                public void write(int b) throws IOException {

                    capture.write(b);

                }

 

                @Override

                public void flush() throws IOException {

                    capture.flush();

                }

 

                @Override

                public void close() throws IOException {

                    capture.close();

                }

            };

        }

 

        return output;

    }

 

    @Override

    public PrintWriter getWriter() throws IOException {

        if (output != null) {

            throw new IllegalStateException(

                    "getOutputStream() has already been called on this response.");

        }

 

        if (writer == null) {

            writer = new PrintWriter(new OutputStreamWriter(capture,

                    getCharacterEncoding()));

        }

 

        return writer;

    }

 

    @Override

    public void flushBuffer() throws IOException {

        super.flushBuffer();

 

        if (writer != null) {

            writer.flush();

        } else if (output != null) {

            output.flush();

        }

    }

 

    public byte[] getCaptureAsBytes() throws IOException {

        if (writer != null) {

            writer.close();

        } else if (output != null) {

            output.close();

        }

 

        return capture.toByteArray();

    }

 

    public String getCaptureAsString() throws IOException {

        return new String(getCaptureAsBytes(), getCharacterEncoding());

    }

 

}

 

 

 

5. IfpAccessDeniedHandler.java

 

import java.io.IOException;

 

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.springframework.security.access.AccessDeniedException;

import org.springframework.security.web.access.AccessDeniedHandler;

import org.springframework.security.web.csrf.InvalidCsrfTokenException;

import org.springframework.security.web.csrf.MissingCsrfTokenException;

 

public class IfpAccessDeniedHandler implements AccessDeniedHandler {

    @Override

    public void handle(HttpServletRequest request, HttpServletResponse response,

            AccessDeniedException accessDeniedException) throws IOException, ServletException {

   

       final Log logger = LogFactory.getLog( IfpAccessDeniedHandler.class );

 

       String ajaxHeader = ((HttpServletRequest) request).getHeader("X-Requested-With");

       String url = request.getRequestURL().toString();

   String queryString = request.getQueryString();

   String contentType = response.getContentType();

   

   // response.sendRedirect(request.getContextPath() + "/common/page_error.jsp");

   logger.info("############################[Security AccessDenied]##############################");

   logger.info("##### [X-Requested-With ::::::::: [" + ajaxHeader + "] #####");

   logger.info("##### [ContentType      ::::::::: [" + contentType + "] #####");

   logger.info("##### " + url + "?" + queryString + " #####");

   logger.info("#################################################################################");

           

   if(accessDeniedException instanceof InvalidCsrfTokenException) {

           response.setStatus(HttpServletResponse.SC_FORBIDDEN);

       }

       // 서버가 재시작된 경우 csrfToken이 없어지게 되므로, 아래와 같은 Exception이 발생함

       if(accessDeniedException instanceof MissingCsrfTokenException) {

           response.setStatus(HttpServletResponse.SC_FORBIDDEN);

       }

    }

}

 

 

6. IfpCsrfRequestMatcher.java

 

 

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.springframework.security.web.util.matcher.RequestMatcher;

 

/**

 * IfpCsrfRequestMatcher.java

 * - 요청에 대해 스프링 시큐리티에서 CSRF를 적용할건지 아닐건지 판단

 * 

 * ################ Spring Security CSRF Protection 설정 ##################

 * [CsrfTokenAdder.java, 서블릿필터] ###"응답에 태그 삽입"###

 * - Spring Security4 이상은 CSRF Enabled가 기본설정

 * - HttpServletRequest(일반  JSP요청), XMLHttpRequest(AJAX) 요청이든 응답에 Csrf 히든테그 삽입

 * - CSRF Protection을 위한 Text/Html 응답에 히든태그 Xsrf Token 삽입

 * - AJAX JSON응답(콤보 채우기등)은 히든태그 Csrf Token 삽입할 필요없다.

 * 

 * [security-config.xaml]

 * - 애당초 Spring Security 적용이 필요없는 부분에 대한 정의(/resources, /images 등)

 * - RequestMatcher에 대한 정의(스프링 시큐리티에서 요청에 대해 CSRF를 적용할건지 아닐건지 판단)

 * - AccessDeniedHandler에 대한 정의(403 에러발생시 대응)

 * 

 * [IfpCsrfRequestMatcher.java] ###"요청에 따라 CSRF 적용여부 결정"###

 * - 스프링 시큐리티에서 CSRF를 적용할건지 아닐건지 판단

 * - AJAX CALL, 첫화면, 최측메뉴로딩, 팝업로등 요청은 CSRF 적용하지 않음

 * - (TEXT/HTML을 응답으로 보낸는 부분은 CsrfTokenAdder 필터에서 Csrf 히든태그 삽입함)

 * - 403 Forbidden 에러가 발생하는 부분이 있다면 이곳에서 추가할 것

 * 

 * [IfpAccessDeniedHandler.java]

 * - 403 Forbidden 에러 발생하는 경우 처리하는 핸들러

 * ######################################################################

 * @since 2019.03.28 

 */

public class IfpCsrfRequestMatcher implements RequestMatcher {

final Log logger = LogFactory.getLog( IfpAccessDeniedHandler.class );

    @Override

    public boolean matches(HttpServletRequest request) {

   

    String strUrl = request.getRequestURL().toString();

    String strUri = request.getRequestURI();

    String queryString = request.getQueryString() == null ? "" : request.getQueryString();

    String contentType = request.getContentType() == null ? "" : request.getContentType();

 

    //---------------------------------------------------------

    // CSRF 필터링, 필요시 추가 하세요. 여기에 추가안하시면 403 오류 발생!

    //---------------------------------------------------------

    // AJAX CALL

        if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With")))                                                   

                                                   return false;

        // 첫화면

        else if ("/".equals(strUri))                   return false;        

        // 좌측 메뉴로딩

        else if ("/ad/menu.do".equals(strUri))         return false;

        // 로그아웃

        else if ("/ad/login.do".equals(strUri))        return false;

        // 팝업

        else if ("/ad/commpopup.do".equals(strUri))    return false;

        else {

        logger.info("###################################### request.getRequestURL() :: " + strUrl);

        logger.info("###################################### request.getRequestURI() :: " + strUri);

        logger.info("###################################### request.getQueryString() :: " + queryString);

        logger.info("###################################### request.getContentType() :: " + contentType);       

        }

        return true;

    }

}

 

 

Comments