Spring Security Web 5.1.2 源码解析 -- CsrfFilter

概述

Spring Security Web使用该Filter解决Cross-Site Request Forgery (CSRF)攻击,使用的模式是Synchronizer token pattern (STP)

STP模式本意是每个请求都生成一个不同的,随机的,不可预测的token用于CSRF保护。这种严格的模式CSRF保护能力很强。但是每请求必验给服务端增加了额外的负担,另外它也要求浏览器必须保持正确的事件顺序,从而会带来一些可用性上的问题(比如用户打开了多个Tab的情况)。所以Spring Security中把这种限制放宽到了每个session使用一个csrf token,并且仅针对会对服务器进行状态更新的HTTP动作:PATCH, POST, PUT,DELETE等。

源代码解析

package org.springframework.security.web.csrf;

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 javax.servlet.http.HttpSession;

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.util.UrlUtils;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;


public final class CsrfFilter extends OncePerRequestFilter {
	/** * The default RequestMatcher that indicates if CSRF protection is required or * not. The default is to ignore GET, HEAD, TRACE, OPTIONS and process all other * requests. * 用于检测哪些请求需要csrf保护,这里的缺省配置是:GET, HEAD, TRACE, OPTIONS这种只读的 * HTTP动词都被忽略不做csrf保护,而其他PATCH, POST, PUT,DELETE等会修改服务器状态的HTTP * 动词会受到当前Filter的csrf保护。 */
	public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();

	private final Log logger = LogFactory.getLog(getClass());
	private final CsrfTokenRepository tokenRepository;
	private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
	// 用于CSRF保护验证逻辑失败进行处理
	private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

	// 构造函数,使用指定的csrf token存储库构造一个CsrfFilter实例
	// 缺省情况下,使用Spring Security 的 Springboot web 应用,选择使用的
	// csrfTokenRepository是一个做了惰性封装的HttpSessionCsrfTokenRepository实例。
	// 也就是说相应的 csrf token保存在http session中。 
	public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
		Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
		this.tokenRepository = csrfTokenRepository;
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
					throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);

		// 从csrf token存储库中获取针对当前请求的csrf token。
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		// 记录针对当前请求是否不存在csrf token
		final boolean missingToken = csrfToken == null;
		if (missingToken) {
			// 如果存储库中尚不存在针对当前请求的csrf token,生成一个,把它关联到
			// 当前请求保存到csrf token存储库中
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}

		// 将从存储库中获取得到的或者新建并保存到存储库的csrf token保存为请求的两个属性
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);
		
		if (!this.requireCsrfProtectionMatcher.matches(request)) {
			// 检测当前请求是否需要csrf保护,如果不需要,放行继续执行filter chain的其他逻辑
			filterChain.doFilter(request, response);
			return;
		}

		// 尝试从请求头部或者参数中获取浏览器端传递过来的实际的csrf token。
		// 缺省情况下,从头部取出时使用header name: X-CSRF-TOKEN
		// 从请求中获取参数时使用的参数名称是 : _csrf
		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
		if (!csrfToken.getToken().equals(actualToken)) {
			// csrf token存储库中取出的token和浏览器端传递过来的token不相等的情况有两种:
			// 1. 针对该请求在存储库中并不存在csrf token
			// 2. 针对该请求在存储库中的csrf token和请求参数实际携带的不一致
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Invalid CSRF token found for "
						+ UrlUtils.buildFullRequestUrl(request));
			}
			if (missingToken) {
				// 1. 针对该请求在存储库中并不存在csrf token , 处理方案:
				// 抛出异常 MissingCsrfTokenException
				this.accessDeniedHandler.handle(request, response,
						new MissingCsrfTokenException(actualToken));
			}
			else {
				// 2. 针对该请求在存储库中的csrf token和请求参数实际携带的不一致,处理方案:
				// 抛出异常 InvalidCsrfTokenException
				this.accessDeniedHandler.handle(request, response,
						new InvalidCsrfTokenException(csrfToken, actualToken));
			}
			return;
		}
		
		// 当前请求需要经该Filter的csrf验证逻辑并且通过了csrf验证,放行,继续执行filter chain
		// 其他部分逻辑
		filterChain.doFilter(request, response);
	}

	/** * Specifies a RequestMatcher that is used to determine if CSRF protection * should be applied. If the RequestMatcher returns true for a given request, * then CSRF protection is applied. * * 指定一个RequestMatcher用来检测一个请求是否需要应用csrf保护验证逻辑。 * * The default is to apply CSRF protection for any HTTP method other than GET, HEAD, * TRACE, OPTIONS. * 缺省行为是针对GET, HEAD,TRACE, OPTIONS这种只读性的HTTP请求不做csrf保护验证,验证其他 * 那些会更新服务器状态的HTTP请求,比如PATCH, POST, PUT,DELETE等。 * * * @param requireCsrfProtectionMatcher the 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 AccessDeniedHandler that should be used when CSRF protection * fails. * 指定一个AccessDeniedHandler用于CSRF保护验证逻辑失败进行处理。 * * The default is to use AccessDeniedHandlerImpl with no arguments. * 缺省行为是使用一个不但参数的AccessDeniedHandlerImpl实例。 * * @param accessDeniedHandler the AccessDeniedHandler to use */
	public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
		Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
		this.accessDeniedHandler = accessDeniedHandler;
	}
	 // 用于检测哪些HTTP请求需要应用csrf保护的RequestMatcher,
	 // 缺省行为是针对GET, HEAD,TRACE, OPTIONS这种只读性的HTTP请求不做csrf保护,
	 // 其他那些会更新服务器状态的HTTP请求,比如PATCH, POST, PUT,DELETE等需要csrf保护。
	private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
		private final HashSet<String> allowedMethods = new HashSet<>(
				Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
	
		@Override
		public boolean matches(HttpServletRequest request) {
			return !this.allowedMethods.contains(request.getMethod());
		}
	}
}

参考文章

Spring Security Web 5.1.2 源码解析 – 安全相关Filter清单
Cross-Site Request Forgery (CSRF)
Synchronizer token pattern (STP)
10.6.2 Synchronizer Token Pattern

    原文作者:Spring Cloud
    原文地址: https://blog.csdn.net/andy_zhang2007/article/details/84779444
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞