《Java实战开发》利用spring-security解决CSRF问题,通过重写CsrfFilter 过滤掉指定方法

最近项目渗透测试检测出一些安全问题其中一项为csrf攻击隐患,然后开始修复

csrf简介

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。——百度百科

详细介绍:http://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html

一、maven pom.xml 引入jar包

        
			org.springframework.security
			spring-security-web
			5.0.6.RELEASE
		
		
			org.springframework.security
			spring-security-config
			5.0.6.RELEASE
		

 

二、web.xml配置filter

mgmtCsrfFilter是我重写的csrfFilter名称


	
		mgmtCsrfFilter
		org.springframework.web.filter.DelegatingFilterProxy
	
	
		mgmtCsrfFilter
		/*
	

 三、spring-application.xml配置


		
			
		
	
	

四、重写csrffilter

重写后的csrfFilter为MgmtCsrfFilter代码如下:

package com.haha.sps.mgmt.core.filter;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;

import javax.servlet.FilterChain;
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.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * Rewrite  org.springframework.security.web.csrf.CsrfFilter 
 * exclude user/doLogion method filter
 *
 * @author Pente
 */
public final class MgmtCsrfFilter extends OncePerRequestFilter {
	/**
	 * The default {@link RequestMatcher} that indicates if CSRF protection is required or
	 * not. The default is to ignore GET, HEAD, TRACE, OPTIONS and process all other
	 * requests.
	 */
	public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfSecurityRequestMatcher();

	private final Log logger = LogFactory.getLog(getClass());
	private final CsrfTokenRepository tokenRepository;
	private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
	private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

	public MgmtCsrfFilter(CsrfTokenRepository csrfTokenRepository) {
		Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
		this.tokenRepository = csrfTokenRepository;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.springframework.web.filter.OncePerRequestFilter#doFilterInternal(javax.servlet
	 * .http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
	 * javax.servlet.FilterChain)
	 */
	@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
					throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);

		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		final boolean missingToken = csrfToken == null;
		if (missingToken) {
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);

		if (!this.requireCsrfProtectionMatcher.matches(request)) {
			filterChain.doFilter(request, response);
			return;
		}

		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
		if (!csrfToken.getToken().equals(actualToken)) {
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Invalid CSRF token found for "
						+ UrlUtils.buildFullRequestUrl(request));
			}
			if (missingToken) {
				this.accessDeniedHandler.handle(request, response,
						new MissingCsrfTokenException(actualToken));
			}
			else {
				this.accessDeniedHandler.handle(request, response,
						new InvalidCsrfTokenException(csrfToken, actualToken));
			}
			return;
		}

		filterChain.doFilter(request, response);
	}

	/**
	 * Specifies a {@link RequestMatcher} that is used to determine if CSRF protection
	 * should be applied. If the {@link RequestMatcher} returns true for a given request,
	 * then CSRF protection is applied.
	 *
	 * 

* The default is to apply CSRF protection for any HTTP method other than GET, HEAD, * TRACE, OPTIONS. *

* * @param requireCsrfProtectionMatcher the {@link RequestMatcher} used to determine if * CSRF protection should be applied. */ public void setRequireCsrfProtectionMatcher( RequestMatcher requireCsrfProtectionMatcher) { Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null"); this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher; } /** * Specifies a {@link AccessDeniedHandler} that should be used when CSRF protection * fails. * *

* The default is to use AccessDeniedHandlerImpl with no arguments. *

* * @param accessDeniedHandler the {@link AccessDeniedHandler} to use */ public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null"); this.accessDeniedHandler = accessDeniedHandler; } /* * exclude "user/doLogion" "GET", "HEAD", "TRACE", "OPTIONS" */ private static final class CsrfSecurityRequestMatcher implements RequestMatcher { private RegexRequestMatcher unprotectedMatcher = new RegexRequestMatcher("^/user/doLogin", null); private final HashSet allowedMethods = new HashSet( Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); /* * (non-Javadoc) * @see * org.springframework.security.web.util.matcher.RequestMatcher#matches(javax. * servlet.http.HttpServletRequest) */ @Override public boolean matches(HttpServletRequest request) { if(this.allowedMethods.contains(request.getMethod())){ return false; } return !unprotectedMatcher.matches(request); } } }

 实现自定义过滤掉登录方法,主要是重写csrfFilter里的DefaultRequiresCsrfMatcher替换成自己写的CsrfSecurityRequestMatcher ,如下:

/*
	 * exclude "user/doLogion"  "GET", "HEAD", "TRACE", "OPTIONS"
	 */
	private static final class CsrfSecurityRequestMatcher implements RequestMatcher {
	    private RegexRequestMatcher unprotectedMatcher = new RegexRequestMatcher("^/user/doLogin", null);

		private final HashSet allowedMethods = new HashSet(
				Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));

		/*
		 * (non-Javadoc)
		 * @see
		 * org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.
		 * servlet.http.HttpServletRequest)
		 */
		@Override
		public boolean matches(HttpServletRequest request) {
			  if(this.allowedMethods.contains(request.getMethod())){
		            return false;
		        }
		        return !unprotectedMatcher.matches(request);
		}
		
	}

五、页面配置

在post请求或ajax请求中会遇到问题,在公用jsp文件中加上,例如每个页面都会引用的头文件top.jsp



 

$.ajaxSetup的意思就是给我们所有的请求都加上这个header和token,或者放到form表单中。注意,_csrf这个要与spring security的配置文件中的配置相匹配,默认为_csrf

六、源码解析

看一下CsrfFilter中的doFilterInternal源码解析就知道我们为什么这么修改了

@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
					throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);
		// 先从tokenRepository中加载token
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		final boolean missingToken = csrfToken == null;
		  // 如果为空,则tokenRepository生成新的token,并保存到tokenRepository中
		if (missingToken) {
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
        // 将token写入request的attribute中,方便页面上使用
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);
		//这个macher就是我们在Spring配置文件中自定义的过滤器,也就是GET,HEAD, TRACE, OPTIONS和我们的rest都不处理

        // 这个macher就是我们在Spring配置文件中自定义的过滤器,
		//如果不需要csrf验证的请求,则直接下传请求(requireCsrfProtectionMatcher是默认的对象,对符合^(GET|HEAD|TRACE|OPTIONS)$的请求和我们自定义的请求不验证)
		if (!this.requireCsrfProtectionMatcher.matches(request)) {
			filterChain.doFilter(request, response);
			return;
		}
        // 从用户请求中获取token信息
		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
        // 验证,如果相同,则下传请求,如果不同,则抛出异常
		if (!csrfToken.getToken().equals(actualToken)) {
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Invalid CSRF token found for "
						+ UrlUtils.buildFullRequestUrl(request));
			}
			if (missingToken) {
				this.accessDeniedHandler.handle(request, response,
						new MissingCsrfTokenException(actualToken));
			}
			else {
				this.accessDeniedHandler.handle(request, response,
						new InvalidCsrfTokenException(csrfToken, actualToken));
			}
			return;
		}

		filterChain.doFilter(request, response);
	}

 

你可能感兴趣的:(Java开发实战)