Spring Boot整合Spring Security(三)csrf

阅前提示一、csrf 与 xss二、CsrfFilter浅析三、获取csrfToken四、不使用模板引擎获取csrfToken

阅前提示

此文章基于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 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 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?老子反手给你一拳

org.springframework.boot

spring-boot-starter-thymeleaf

3.0.1

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

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

Login

用户名
密码
csrf_token

四、不使用模板引擎获取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

查看原文