CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。

你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账…造成的问题包括:个人隐私泄露以及财产安全。

CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI…而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。

CSRF的原理:先登录受信任网站A,并在本地生成Cookie,然后在不登出A的情况下,访问危险网站B。
看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。

是的,确实如此,但你不能保证以下情况不会发生:
1.你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。
2.你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了…)

Spring Security Web使用CsrfFilter解决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());
		}
	}
}