Spring Boot整合Spring Security(三)csrf

本文详细解析了SpringSecurity中CSRF防护机制的工作原理,并提供了如何在Thymeleaf模板及纯API场景中获取CSRF Token的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

阅前提示

此文章基于Spring Security 6.0

一、csrf 与 xss

关于csrf与xss的介绍就去网上找一下吧,这篇写的不错。在这里就不多做介绍了
关于csrf与xss与验证码的介绍
在之前的自定义登录界面篇章中,关闭了Spring Security的csrf配置,在这个篇章中就介绍下csrf的使用。
关于csrf的内容本来是不想写的。因为现在的应用开发大多都实现了前后端分离模式,并禁用了cookie与session,使用token进行身份认证。而关于csrf的攻击,基本都是基于cookie与session的。但是,对于一些维护传统项目,使用了模板引擎甚至jsp的程序员没错,就是我来说,确实有必要记录一下这方面的配置。
Spring Security 默认开启csrf,不需要额外配置

二、CsrfFilter浅析

public final class CsrfFilter extends OncePerRequestFilter {
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
		this.requestHandler.handle(request, response, deferredCsrfToken::get);
		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;
		}
		CsrfToken csrfToken = deferredCsrfToken.get();
		String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);
		if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
			boolean missingToken = deferredCsrfToken.isGenerated();
			this.logger.debug(
					LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
			AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
					: new MissingCsrfTokenException(actualToken);
			this.accessDeniedHandler.handle(request, response, exception);
			return;
		}
		filterChain.doFilter(request, response);
	}
	
	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());
		}

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

	}
}

来分析一下代码,当请求过来时,创建一个DeferredCsrfToken对象。来看一下创建过程

final class RepositoryDeferredCsrfToken implements DeferredCsrfToken {

	private final CsrfTokenRepository csrfTokenRepository;

	private final HttpServletRequest request;

	private final HttpServletResponse response;

	private CsrfToken csrfToken;

	private boolean missingToken;

	RepositoryDeferredCsrfToken(CsrfTokenRepository csrfTokenRepository, HttpServletRequest request,
			HttpServletResponse response) {
		this.csrfTokenRepository = csrfTokenRepository;
		this.request = request;
		this.response = response;
	}

	@Override
	public CsrfToken get() {
		init();
		return this.csrfToken;
	}

	@Override
	public boolean isGenerated() {
		init();
		return this.missingToken;
	}

	private void init() {
		if (this.csrfToken != null) {
			return;
		}

		this.csrfToken = this.csrfTokenRepository.loadToken(this.request);
		this.missingToken = (this.csrfToken == null);
		if (this.missingToken) {
			this.csrfToken = this.csrfTokenRepository.generateToken(this.request);
			this.csrfTokenRepository.saveToken(this.csrfToken, this.request, this.response);
		}
	}
}

其实这里就是选择调用哪个csrfTokenRepository(cookie or session)。然后调用get()方法获取到csrfToken对象。绕来绕去,还得看init()方法。这里的csrfTokenRepository.loadToken(this.request)其实就是根据session或者cookie获取csrfToken,若没有获取到csrfToken对象(this.csrfToken == null),那么创建一个csrfToken对象。
接着返回CsrfFilter中的代码,调用了一个handler

	public void handle(HttpServletRequest request, HttpServletResponse response,
			Supplier<CsrfToken> deferredCsrfToken) {
		Assert.notNull(request, "request cannot be null");
		Assert.notNull(response, "response cannot be null");
		Assert.notNull(deferredCsrfToken, "deferredCsrfToken cannot be null");

		request.setAttribute(HttpServletResponse.class.getName(), response);
		CsrfToken csrfToken = new SupplierCsrfToken(deferredCsrfToken);
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		String csrfAttrName = (this.csrfRequestAttributeName != null) ? this.csrfRequestAttributeName
				: csrfToken.getParameterName();
		request.setAttribute(csrfAttrName, csrfToken);
	}

将csrfToken放入到request中
继续往下走,如果请求的方法是"GET",“HEAD”,“TRACE”,“OPTIONS”,那么直接放行详情看DefaultRequiresCsrfMatcher.matches()
如果不是上面那几个方法的请求,那么将比对请求中的_csrf参数或者X-CSRF-TOKEN请求头与session中的csrfToken,若相同则通过,若不同,则拦截请求

三、获取csrfToken

因为csrfToken是写入request中的,所以我们需要用到模板引擎来获取csrfToken

使用模板引擎thymeleaf 你说你想用jsp?老子反手给你一拳

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>3.0.1</version>
        </dependency>

在需要发送post等请求的页面中,加入以下代码

<input type="hidden" name="_csrf" th:value="${_csrf.getToken()}"/>

那么再来看一下现在我们自定义的登录界面的样子

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<form action="/login" method="post">
  <table>
    <tr>
      <td>用户名</td>
      <td><input type="text" name="username"/> </td>
    </tr>
    <tr>
      <td>密码</td>
      <td><input type="password" name="password"/></td>
    </tr>
    <tr>
      <td>csrf_token</td>
      <td><input th:name="${_csrf.parameterName}" th:value="${_csrf.getToken()}"/></td>
    </tr>
    <tr>
      <td colspan="2"><input type="submit" value="提交"/></td>
    </tr>
  </table>
</form>
</body>
</html>

四、不使用模板引擎获取csrfToken

@RestController
public class CsrfController {

	@RequestMapping("/csrf")
	public CsrfToken csrf(CsrfToken token) {
		return token;
	}
}

上面的代码,就是直接从官网文档里抄来的。关于这个接口,官方文档就写了It is important to keep the CsrfToken a secret from other domains. This means that, if you use Cross Origin Sharing (CORS), you should NOT expose the CsrfToken to any external domains.
至于怎么做,这里就不展开讲了。总之,本篇章就讲怎么使用Spring Security的csrf使用。
然后是前端的一些操作,这里用JQuery的Ajax演示,想来能用到这个的也不会去用axios

<script type="text/javascript" src="/js/jquery.js"></script>
<script>
  $(function(){
    $.ajax({
      url:"/csrf",
      type:"GET",
      dataType:"json",
      success:callback
    })
  })
  function callback(response) {
    let csrfInput = document.getElementById('csrf');
    csrfInput.value = response.token;
  }
</script>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值