十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
上篇文章松哥和大家聊了什么是 CSRF 攻击,以及 CSRF 攻击要如何防御。主要和大家聊了 Spring Security 中处理该问题的几种办法。

今天松哥来和大家简单的看一下 Spring Security 中,CSRF 防御源码。
本文主要从两个方面来和大家讲解:
我们先来看一下 Spring Security 中的 csrf 参数是如何生成的。
首先,Spring Security 中提供了一个保存 csrf 参数的规范,就是 CsrfToken:
- public interface CsrfToken extends Serializable {
 - String getHeaderName();
 - String getParameterName();
 - String getToken();
 - }
 
这里三个方法都好理解,前两个是获取 _csrf 参数的 key,第三个是获取 _csrf 参数的 value。
CsrfToken 有两个实现类,如下:
默认情况下使用的是 DefaultCsrfToken,我们来稍微看下 DefaultCsrfToken:
- public final class DefaultCsrfToken implements CsrfToken {
 - private final String token;
 - private final String parameterName;
 - private final String headerName;
 - public DefaultCsrfToken(String headerName, String parameterName, String token) {
 - this.headerName = headerName;
 - this.parameterName = parameterName;
 - this.token = token;
 - }
 - public String getHeaderName() {
 - return this.headerName;
 - }
 - public String getParameterName() {
 - return this.parameterName;
 - }
 - public String getToken() {
 - return this.token;
 - }
 - }
 
这段实现很简单,几乎没有添加额外的方法,就是接口方法的实现。
CsrfToken 相当于就是 _csrf 参数的载体。那么参数是如何生成和保存的呢?这涉及到另外一个类:
- public interface CsrfTokenRepository {
 - CsrfToken generateToken(HttpServletRequest request);
 - void saveToken(CsrfToken token, HttpServletRequest request,
 - HttpServletResponse response);
 - CsrfToken loadToken(HttpServletRequest request);
 - }
 
这里三个方法:
CsrfTokenRepository 有四个实现类,在上篇文章中,我们用到了其中两个:HttpSessionCsrfTokenRepository 和 CookieCsrfTokenRepository,其中 HttpSessionCsrfTokenRepository 是默认的方案。
我们先来看下 HttpSessionCsrfTokenRepository 的实现:
- public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
 - private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
 - private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";
 - private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class
 - .getName().concat(".CSRF_TOKEN");
 - private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
 - private String headerName = DEFAULT_CSRF_HEADER_NAME;
 - private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
 - 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.setAttribute(this.sessionAttributeName, token);
 - }
 - }
 - public CsrfToken loadToken(HttpServletRequest request) {
 - HttpSession session = request.getSession(false);
 - if (session == null) {
 - return null;
 - }
 - return (CsrfToken) session.getAttribute(this.sessionAttributeName);
 - }
 - public CsrfToken generateToken(HttpServletRequest request) {
 - return new DefaultCsrfToken(this.headerName, this.parameterName,
 - createNewToken());
 - }
 - private String createNewToken() {
 - return UUID.randomUUID().toString();
 - }
 - }
 
这段源码其实也很好理解:
这是默认的方案,适用于前后端不分的开发,具体用法可以参考上篇文章。
如果想在前后端分离开发中使用,那就需要 CsrfTokenRepository 的另一个实现类 CookieCsrfTokenRepository ,代码如下:
- public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
 - static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
 - static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
 - static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
 - private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
 - private String headerName = DEFAULT_CSRF_HEADER_NAME;
 - private String cookieName = DEFAULT_CSRF_COOKIE_NAME;
 - private boolean cookieHttpOnly = true;
 - private String cookiePath;
 - private String cookieDomain;
 - public CookieCsrfTokenRepository() {
 - }
 - @Override
 - public CsrfToken generateToken(HttpServletRequest request) {
 - return new DefaultCsrfToken(this.headerName, this.parameterName,
 - createNewToken());
 - }
 - @Override
 - public void saveToken(CsrfToken token, HttpServletRequest request,
 - HttpServletResponse response) {
 - String tokenValue = token == null ? "" : token.getToken();
 - Cookie cookie = new Cookie(this.cookieName, tokenValue);
 - cookie.setSecure(request.isSecure());
 - if (this.cookiePath != null && !this.cookiePath.isEmpty()) {
 - cookie.setPath(this.cookiePath);
 - } else {
 - cookie.setPath(this.getRequestContext(request));
 - }
 - if (token == null) {
 - cookie.setMaxAge(0);
 - }
 - else {
 - cookie.setMaxAge(-1);
 - }
 - cookie.setHttpOnly(cookieHttpOnly);
 - if (this.cookieDomain != null && !this.cookieDomain.isEmpty()) {
 - cookie.setDomain(this.cookieDomain);
 - }
 - response.addCookie(cookie);
 - }
 - @Override
 - public CsrfToken loadToken(HttpServletRequest request) {
 - Cookie cookie = WebUtils.getCookie(request, this.cookieName);
 - if (cookie == null) {
 - return null;
 - }
 - String token = cookie.getValue();
 - if (!StringUtils.hasLength(token)) {
 - return null;
 - }
 - return new DefaultCsrfToken(this.headerName, this.parameterName, token);
 - }
 - public static CookieCsrfTokenRepository withHttpOnlyFalse() {
 - CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
 - result.setCookieHttpOnly(false);
 - return result;
 - }
 - private String createNewToken() {
 - return UUID.randomUUID().toString();
 - }
 - }
 
和 HttpSessionCsrfTokenRepository 相比,这里 _csrf 数据保存的时候,都保存到 cookie 中去了,当然读取的时候,也是从 cookie 中读取,其他地方则和 HttpSessionCsrfTokenRepository 是一样的。
OK,这就是我们整个 _csrf 参数生成的过程。
总结一下,就是生成一个 CsrfToken,这个 Token,本质上就是一个 UUID 字符串,然后将这个 Token 保存到 HttpSession 中,或者保存到 Cookie 中,待请求到来时,从 HttpSession 或者 Cookie 中取出来做校验。
那接下来就是校验了。
校验主要是通过 CsrfFilter 过滤器来进行,我们来看下核心的 doFilterInternal 方法:
- protected void doFilterInternal(HttpServletRequest request,
 - HttpServletResponse response, FilterChain filterChain)
 - throws ServletException, IOException {
 - request.setAttribute(HttpServletResponse.class.getName(), response);
 - CsrfToken csrfToken = this.tokenRepository.loadToken(request);
 - final boolean missingToken = csrfToken == null;
 - if (missingToken) {
 - csrfToken = this.tokenRepository.generateToken(request);
 - this.tokenRepository.saveToken(csrfToken, request, response);
 - }
 - request.setAttribute(CsrfToken.class.getName(), csrfToken);
 - request.setAttribute(csrfToken.getParameterName(), csrfToken);
 - if (!this.requireCsrfProtectionMatcher.matches(request)) {
 - filterChain.doFilter(request, response);
 - return;
 - }
 - String actualToken = request.getHeader(csrfToken.getHeaderName());
 - if (actualToken == null) {
 - actualToken = request.getParameter(csrfToken.getParameterName());
 - }
 - if (!csrfToken.getToken().equals(actualToken)) {
 - if (this.logger.isDebugEnabled()) {
 - this.logger.debug("Invalid CSRF token found for "
 - + UrlUtils.buildFullRequestUrl(request));
 - }
 - if (missingToken) {
 - this.accessDeniedHandler.handle(request, response,
 - new MissingCsrfTokenException(actualToken));
 - }
 - else {
 - this.accessDeniedHandler.handle(request, response,
 - new InvalidCsrfTokenException(csrfToken, actualToken));
 - }
 - return;
 - }
 - filterChain.doFilter(request, response);
 - }
 
这个方法我来稍微解释下:
如此之后,就完成了整个校验工作了。
前面我们说了 CsrfTokenRepository 有四个实现类,除了我们介绍的两个之外,还有一个 LazyCsrfTokenRepository,这里松哥也和大家做一个简单介绍。
在前面的 CsrfFilter 中大家发现,对于常见的 GET 请求实际上是不需要 CSRF 攻击校验的,但是,每当 GET 请求到来时,下面这段代码都会执行:
- if (missingToken) {
 - csrfToken = this.tokenRepository.generateToken(request);
 - this.tokenRepository.saveToken(csrfToken, request, response);
 - }
 
生成 CsrfToken 并保存,但实际上却没什么用,因为 GET 请求不需要 CSRF 攻击校验。
所以,Spring Security 官方又推出了 LazyCsrfTokenRepository。
LazyCsrfTokenRepository 实际上不能算是一个真正的 CsrfTokenRepository,它是一个代理,可以用来增强 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的功能:
- public final class LazyCsrfTokenRepository implements CsrfTokenRepository {
 - @Override
 - public CsrfToken generateToken(HttpServletRequest request) {
 - return wrap(request, this.delegate.generateToken(request));
 - }
 - @Override
 - public void saveToken(CsrfToken token, HttpServletRequest request,
 - HttpServletResponse response) {
 - if (token == null) {
 - this.delegate.saveToken(token, request, response);
 - }
 - }
 - @Override
 - public CsrfToken loadToken(HttpServletRequest request) {
 - return this.delegate.loadToken(request);
 - }
 - private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {
 - HttpServletResponse response = getResponse(request);
 - return new SaveOnAccessCsrfToken(this.delegate, request, response, token);
 - }
 - private static final class SaveOnAccessCsrfToken implements CsrfToken {
 - private transient CsrfTokenRepository tokenRepository;
 - private transient HttpServletRequest request;
 - private transient HttpServletResponse response;
 - private final CsrfToken delegate;
 - SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository,
 - HttpServletRequest request, HttpServletResponse response,
 - CsrfToken delegate) {
 - this.tokenRepository = tokenRepository;
 - this.request = request;
 - this.response = response;
 - this.delegate = delegate;
 - }
 - @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;
 - }
 - }
 - }
 - }
 - }
 
这里,我说三点:
使用了 LazyCsrfTokenRepository 之后,只有在使用 csrfToken 时才会去存储它,这样就可以节省存储空间了。
LazyCsrfTokenRepository 的配置方式也很简单,在我们使用 Spring Security 时,如果对 csrf 不做任何配置,默认其实就是 LazyCsrfTokenRepository+HttpSessionCsrfTokenRepository 组合。
当然我们也可以自己配置,如下:
- @Override
 - protected void configure(HttpSecurity http) throws Exception {
 - http.authorizeRequests().anyRequest().authenticated()
 - .and()
 - .formLogin()
 - .loginPage("/login.html")
 - .successHandler((req,resp,authentication)->{
 - resp.getWriter().write("success");
 - })
 - .permitAll()
 - .and()
 - .csrf().csrfTokenRepository(new LazyCsrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
 - }
 
今天主要和小伙伴聊了一下 Spring Security 中 csrf 防御的原理。
整体来说,就是两个思路:
生成 csrfToken 保存在 HttpSession 或者 Cookie 中。
请求到来时,从请求中提取出来 csrfToken,和保存的 csrfToken 做比较,进而判断出当前请求是否合法。
本文转载自微信公众号「江南一点雨」,可以通过以下二维码关注。转载本文请联系江南一点雨公众号。