SpringSecurity - 启动流程分析(八)- CsrfFilter 过滤器


活动地址:CSDN21天学习挑战赛

前言

SpringSecurity - 启动流程分析(五)- (七) 这几篇文章中,我们主要是对 UsernamePasswordAuthenticationFilter 这一个 Filter 做了源码分析,接下来我们来看一下 CsrfFilter

分析

HttpSecurity

HttpSecurity 中加载默认配置:

public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
	ApplicationContext context = getContext();
	return getOrApply(new CsrfConfigurer<>(context));
}

CsrfConfigurer

public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
		extends AbstractHttpConfigurer<CsrfConfigurer<H>, H> {

	// 1、初始化 CsrfTokenRepository
	private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());

	// 2、初始化 RequestMatcher
	private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;

	// 可以自定义忽略 csrf 验证的 RequestMatcher
	private List<RequestMatcher> ignoredCsrfProtectionMatchers = new ArrayList<>();

	...
	
	@SuppressWarnings("unchecked")
	@Override
	// 核心方法
	public void configure(H http) {
		// 这里初始化 CsrfFilter,并把 csrfTokenRepository 作为参数传递
		CsrfFilter filter = new CsrfFilter(this.csrfTokenRepository);
		
		// 设置 filter 的一些属性
		...
		http.addFilter(filter);
	}
}

1、CsrfTokenRepository

LazyCsrfTokenRepositoryHttpSessionCsrfTokenRepository 外包了一层,作用是:延迟保存新的 CsrfToken 直到上一次生成的 CsrfToken 被访问。

这里可能还不理解什么是 CsrfToken,先知道有这个概念就行,继续往下看

private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());

2、requireCsrfProtectionMatcher

这个属性里面是需要 CSRF 保护的请求匹配器 CsrfFilter.DEFAULT_CSRF_MATCHER

public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();

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) {
		// 注意这里是取反,就是匹配除了上面几种请求方式之外的其他请求方式,比如 POST、PUT 等
		return !this.allowedMethods.contains(request.getMethod());
	}

	@Override
	public String toString() {
		return "CsrfNotRequired " + this.allowedMethods;
	}

}

CsrfFilter

/**
 * <p>
 * Applies
 * <a href="https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)" >CSRF</a>
 * protection using a synchronizer token pattern. Developers are required to ensure that
 * {@link CsrfFilter} is invoked for any request that allows state to change. Typically
 * this just means that they should ensure their web application follows proper REST
 * semantics (i.e. do not change state with the HTTP methods GET, HEAD, TRACE, OPTIONS).
 * </p>
 */
public final class CsrfFilter extends OncePerRequestFilter {}

从源码注释中可以了解到关于 CSRF 相关的介绍,而且网上也有很多关于 CSRF 的介绍,这里就不赘述了,直接来分析一下其中的核心内容:

1、doFilterInternal()

首先看一下 doFilterInternal() 方法,这个是过滤器的入口方法:

@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
	request.setAttribute(HttpServletResponse.class.getName(), response);
	// 1、首先会加载一个 CsrfToken
	CsrfToken csrfToken = this.tokenRepository.loadToken(request);
	boolean missingToken = (csrfToken == null);
	if (missingToken) {
		// 2、如果 session 中没有的话,会生成一个。通过 1.2.2 的分析,知道这里是内部类 SaveOnAccessCsrfToken
		csrfToken = this.tokenRepository.generateToken(request);
		this.tokenRepository.saveToken(csrfToken, request, response);
	}
	request.setAttribute(CsrfToken.class.getName(), csrfToken);
	request.setAttribute(csrfToken.getParameterName(), csrfToken);
	// 如果符合 requireCsrfProtectionMatcher 里面定义的那些 GET、OPTION 等请求,直接放行
	if (!this.requireCsrfProtectionMatcher.matches(request)) {
		if (this.logger.isTraceEnabled()) {
			this.logger.trace("Did not protect against CSRF since request did not match "
					+ this.requireCsrfProtectionMatcher);
		}
		filterChain.doFilter(request, response);
		return;
	}
	// 获取请求头中的 _csrf 属性
	String actualToken = request.getHeader(csrfToken.getHeaderName());
	if (actualToken == null) {
		// 如果请求头中没有值,获取请求参数中的 X-CSRF-TOKEN 属性值
		actualToken = request.getParameter(csrfToken.getParameterName());
	}
	// 如果请求的信息中没有携带这些东西,就会抛出异常
	// 3、这里调用内部类 SaveOnAccessCsrfToken 的 getToken() 方法
	if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
		this.logger.debug(
				LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
		AccessDeniedException exception = (!missingToken) ? 
		// session 中有 *.CSRF_TOKEN 这个属性的值,但是请求没有携带 csrf 相关的值,会抛出 InvalidCsrfTokenException
		new InvalidCsrfTokenException(csrfToken, actualToken)
		// session 中缺少 *.CSRF_TOKEN 这个属性的值会抛出 MissingCsrfTokenException
				: new MissingCsrfTokenException(actualToken);
		// 处理异常
		this.accessDeniedHandler.handle(request, response, exception);
		return;
	}
	filterChain.doFilter(request, response);
}
1.1、CsrfToken
CsrfToken csrfToken = this.tokenRepository.loadToken(request);

从上面对 CsrfConfigurer 的分析中,我们知道这个 tokenRepository 就是 new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository()),所以这里的 loadToken() 方法,是调用的 HttpSessionCsrfTokenRepository 中的重写方法:

private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;

private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName()
			.concat(".CSRF_TOKEN");

@Override
public CsrfToken loadToken(HttpServletRequest request) {
	HttpSession session = request.getSession(false);
	if (session == null) {
		return null;
	}
	// 从 session 中获取指定属性的值
	return (CsrfToken) session.getAttribute(this.sessionAttributeName);
}
1.2、LazyCsrfTokenRepository
@Override
public CsrfToken generateToken(HttpServletRequest request) {
	// 1、这里的 delegate 是 HttpSessionCsrfTokenRepository
	return wrap(request, this.delegate.generateToken(request));
	// 通过 1.2.2 的分析,也就是说这个方法返回的是包了一层 DefaultCsrfToken 的 SaveOnAccessCsrfToken
}

private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {
	HttpServletResponse response = getResponse(request);
	// 2、这个 token 是生成的 DefaultCsrfToken
	return new SaveOnAccessCsrfToken(this.delegate, request, response, token);
}
1.2.1、查看 HttpSessionCsrfTokenRepository 中的 generateToken() 方法:
private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;

private String headerName = DEFAULT_CSRF_HEADER_NAME;

private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";

private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";

@Override
public CsrfToken generateToken(HttpServletRequest request) {
	// 创建了一个 CsrfToken
	return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken());
}

private String createNewToken() {
	return UUID.randomUUID().toString();
}
1.2.2、查看 LazyCsrfTokenRepository 的内部类 SaveOnAccessCsrfToken 中的构造方法:
private static final class SaveOnAccessCsrfToken implements CsrfToken {

	private transient CsrfTokenRepository tokenRepository;

	private transient HttpServletRequest request;

	private transient HttpServletResponse response;

	private final CsrfToken delegate;

	// 入参 CsrfTokenRepository 是 HttpSessionCsrfTokenRepository
	// 入参 CsrfToken 是 DefaultCsrfToken
	SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository, HttpServletRequest request,
			HttpServletResponse response, CsrfToken delegate) {
		this.tokenRepository = tokenRepository;
		this.request = request;
		this.response = response;
		this.delegate = delegate;
	}

	@Override
	public String getHeaderName() {
		return this.delegate.getHeaderName();
	}

	@Override
	public String getParameterName() {
		return this.delegate.getParameterName();
	}

	@Override
	public String getToken() {
		saveTokenIfNecessary();
		return this.delegate.getToken();
	}

	private void saveTokenIfNecessary() {
		if (this.tokenRepository == null) {
			return;
		}
		synchronized (this) {
			if (this.tokenRepository != null) {
				this.tokenRepository.saveToken(this.delegate, this.request, this.response);
				this.tokenRepository = null;
				this.request = null;
				this.response = null;
			}
		}
	}
	...
}
1.3、调用 SaveOnAccessCsrfToken 中的 getToken() 方法:
@Override
public String getToken() {
	saveTokenIfNecessary();
	// 2、返回 DefaultCsrfToken 中的 getToken() 方法的返回值
	return this.delegate.getToken();
}

private void saveTokenIfNecessary() {
	if (this.tokenRepository == null) {
		return;
	}
	synchronized (this) {
		if (this.tokenRepository != null) {
			// tokenRepository 是 HttpSessionCsrfTokenRepository
			// delagate 是 DefaultCsrfToken
			// 1、核心流程,调用的是 HttpSessionCsrfTokenRepository 中的 saveToken 方法
			this.tokenRepository.saveToken(this.delegate, this.request, this.response);
			this.tokenRepository = null;
			this.request = null;
			this.response = null;
		}
	}
}
1.3.1、调用 HttpSessionCsrfTokenRepository 中的 saveToken() 方法:
private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;

private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName()
			.concat(".CSRF_TOKEN");

@Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
	if (token == null) {
		HttpSession session = request.getSession(false);
		if (session != null) {
			session.removeAttribute(this.sessionAttributeName);
		}
	}
	else {
		HttpSession session = request.getSession();
		// 会保存到 session 中,用于之后请求对比 CsrfToken 中的值
		session.setAttribute(this.sessionAttributeName, token);
	}
}

总结

以上就是 SpringSecurity 中关于 CSRF 防御的 校验流程 分析

  • 如果是 POSTPUT 之类的由 DefaultRequiresCsrfMatcher 定义的需要 CSRF 拦截的请求,就必须在 请求头 或者 请求参数 中携带对应 session 中存的值
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值