活动地址: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
以 LazyCsrfTokenRepository
在 HttpSessionCsrfTokenRepository
外包了一层,作用是:延迟保存新的 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
防御的校验流程
分析
- 如果是
POST
、PUT
之类的由DefaultRequiresCsrfMatcher
定义的需要CSRF
拦截的请求,就必须在请求头
或者请求参数
中携带对应session
中存的值