编码技巧——HTTP接口安全防范CSRF攻击

上一篇《编码技巧——HTTP接口安全防范CORS攻击》介绍了同源策略、跨域、CORS,本篇介绍下CRSF漏洞及常用服务端解决方案;

思考一个问题:如果你说 同源策略SOP 就是“禁止跨域请求”,这也不完全准确,因为本质上 SOP 并不是禁止跨域请求,而是在请求后拦截了请求的回应。因此——这就会引起CSRF漏洞,即被恶意网站模拟的、带上了cookies(虽然由于SOP策略读不到cookies信息)的、非用户预期的请求,已经打到了服务端的接口上!

注意下这里的几个概念:

SOP:同源策略,协议+域名+端口三者一致称之为同源请求;

跨域:违反了同源策略的资源访问请求,视为跨域,受到SOP策略的限制;

CORS:跨域资源共享(Cross-origin resource sharing),是一种浏览器机制,而非漏洞;可以对位于给定域之外的资源进行受控访问,它增加了同源策略 ( SOP ) 的灵活性。但是,如果CORS 策略配置和实施不当,它也会为基于跨域的攻击提供可能性,如CRSF攻击;

CRSF:跨站请求伪造,是一种网络安全攻击方式,尽管读不到Cookies,但是通过携带整个Cookies,将自己伪造成已登录的用户去请求;由于SOP的机制:1.可以通过 html tag 加载资源;2.SOP 不阻止接口请求而是拦截请求结果,而CSRF 恰恰占了这两个便宜。

所以 SOP 不能作为防范 CSRF 的方法!!!

介绍CSRF之前,先介绍下典型的集中网络安全攻击;

1. 有哪些典型的网络攻击?

CSRF(Cross-site request forgery)跨站请求伪造,获取Cookies,将自己伪造成已登录的用户去请求;

XSS,首先,这个词实际上是CSS(Cross Site Scripting),但它与CSS同名。所以名字是XSS;将JS注入到页面,在用户正常访问的时候触发JS,干扰用户的正常请求行为;

SQL注入攻击,将SQL语句插到SQL参数里面,例如删表语句、批量查询等之类;

DDoS 攻击,全称Distributed Denial of Service,中文意思为“分布式拒绝服务”,就是利用大量合法的分布式服务器对目标发送请求,从而导致正常合法用户无法获得服务。

2. 什么是CSRF?

CSRF(Cross Site Request Forgery,跨站域请求伪造)是一种网络的攻击方式,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用也就是人们所知道的钓鱼网站。

尽管听起来像跨站脚本(XSS),但它与XSS非常不同,并且攻击方式几乎相左。

XSS利用站点内的信任用户(在用户的请求中通过JS“插入”恶意请求),而CSRF则通过伪装成来自受信任用户的请求来利用受信任的网站。

与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。

可以这样来理解CSRF攻击:

攻击者盗用了你的身份,以你的名义发送恶意请求,例如攻击者拿到了你身份和已登录服务器的状态的Cookies,它在拿着这些Cookies请求服务器的时候,对服务器来说这个请求是完全合法的,

但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

先举个例子:其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,User C为Web A网站的合法用户;CSRF攻击攻击原理及过程如下:

1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;

2. 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;

3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B(实际上,关闭了浏览器并不意味着Cookies被清理或Cookies立即失效);

4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A,即带着A网站的Cookies去访问A网站;

5. 网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。 

3. 举个CSRF攻击的实例?

1. 受害者 Bob 在银行有一笔存款,通过对银行的网站发送请求 http://bank.example/withdraw?account=bob&amount=1000000&for=bob2 可以使 Bob 把 1000000 的存款转到 bob2 的账号下。

2. 通常情况下,该请求发送到网站后,服务器会先验证该请求是否来自一个合法的 session,并且该 session 的用户 Bob 已经成功登陆。

3. 黑客 Mallory 自己在该银行也有账户,他知道上文中的 URL 可以把钱进行转帐操作。Mallory 可以自己发送一个请求给银行:http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory。但是这个请求来自 Mallory 而非 Bob,他不能通过安全认证,因此该请求不会起作用。

4. 这时,Mallory 想到使用 CSRF 的攻击方式,他先自己做一个网站,在网站中放入如下代码: src=”http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory ”,并且通过广告/图片等诱使 Bob 来访问他的网站。

当 Bob 访问该网站时,上述 url 就会从 Bob 的浏览器发向银行(访问源是Mallory的网站),而这个请求会附带 Bob 浏览器中的 cookie 一起发向银行服务器。

大多数情况下,该请求会失败,因为他要求 Bob 的认证信息。但是,如果 Bob 当时恰巧刚访问他的银行后不久,他的浏览器与银行网站之间的 session 尚未过期,浏览器的 cookie 之中含有 Bob 的认证信息。

这时,悲剧发生了,这个 url 请求就会得到响应,钱将从 Bob 的账号转移到 Mallory 的账号,而 Bob 当时毫不知情。

等以后 Bob 发现账户钱少了,即使他去银行查询日志,他也只能发现确实有一个来自于他本人的合法请求转移了资金,没有任何被攻击的痕迹。而 Mallory 则可以拿到钱后逍遥法外。

4. CSRF防御的几个思路?

"SOP 可以通过 html tag 加载资源,而且 SOP 不阻止接口请求而是拦截请求结果,CSRF 恰恰占了这两个便宜"

通过上面的描述,SOP 被 CSRF 占了便宜,那 SOP 真的是一无是处吗?

不是!是否记得 SOP 限制了 cookie 的命名区域,虽然请求会自动带上 cookies,但是攻击者无论如何还是无法获取 cookie 的内容本身;

所以应对 CSRF 有这样的思路:同时把一个 token 写到 cookie 里,在发起请求时再通过 query、body 或者 header 带上这个 token。请求到达服务器,核对这个 token,如果正确,那一定是能看到 cookie 的本域发送的请求,CSRF 则做不到这一点;

(1)CSRF漏洞检测

检测CSRF漏洞是一项比较繁琐的工作,最简单的方法就是抓取一个正常请求的数据包,去掉Referer字段后再重新提交,如果该提交还有效,那么基本上可以确定存在CSRF漏洞。

随着对CSRF漏洞研究的不断深入,不断涌现出一些专门针对CSRF漏洞进行检测的工具,如CSRFTester,CSRF Request Builder等。

以CSRFTester工具为例,CSRF漏洞检测工具的测试原理如下:

使用CSRFTester进行测试时,首先需要抓取我们在浏览器中访问过的所有链接以及所有的表单等信息,然后通过在CSRFTester中修改相应的表单等信息,重新提交,这相当于一次伪造客户端请求。

如果修改后的测试请求成功被网站服务器接受,则说明存在CSRF漏洞,当然此款工具也可以被用来进行CSRF攻击。

(2)网上最常见的几个解决方法

目前防御 CSRF 攻击主要有三种策略:1.验证 HTTP Referer 字段;2.在请求地址中添加 token 并验证;3.在 HTTP 头中自定义属性并验证。

I. 验证 HTTP Referer 字段

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。

在通常情况下,访问一个安全受限页面的请求来自于同一个网站,比如需要访问 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件。

这时,该转帐请求的 Referer 值就会是转账按钮所在的页面的 URL,通常是以 bank.example 域名开头的地址

而如果黑客要对银行网站实施 CSRF 攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的 Referer 是指向黑客自己的网站

因此,要防御 CSRF 攻击,银行网站只需要对于每一个转账请求验证其 Referer 值,如果是以 bank.example 开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。

这种方法的显而易见的好处就是简单易行,网站的普通开发人员不需要操心 CSRF 的漏洞,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。

特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。如图所示:

存在问题?

然而,这种方法并非万无一失。Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞

使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。

事实上,对于某些浏览器,比如 IE6 或 FF2,目前已经有一些方法可以篡改 Referer 值。如果 bank.example 网站支持 IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。

即便是使用最新的浏览器,黑客无法篡改 Referer 值,这种方法仍然有问题。

因为 Referer 值会记录下用户的访问来源,有些用户认为这样会侵犯到他们自己的隐私权,特别是有些组织担心 Referer 值会把组织内网中的某些信息泄露到外网中。

因此,用户自己可以设置浏览器使其在发送请求时不再提供 Referer。当他们正常访问银行网站时,网站会因为请求没有 Referer 值而认为是 CSRF 攻击,拒绝合法用户的访问。

II. 在请求地址中添加 token 并验证

CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道有哪些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证

注意这一句很关键,即攻击者只是能拿到登录状态/用户身份的Cookies,而Cookies里面放了哪些字段(K/V)他是不知道的!

还有一点,就是攻击的请求一般是写死的,他不能随意变请求参数,并且用户的请求参数它是拿不到的(同源策略),只能拿到Cookies,还是个“黑盒的”Cookies;

要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。不放在Cookies里,那放在哪?——放在请求参数中,带到服务端来!而且由于“同源策略”,攻击者拿不到请求参数;

可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于 session 之中,然后在每次请求时把 token 从 session 中拿出,与请求中的 token 进行比对,但这种方法的难点在于如何把 token 以参数的形式加入请求

对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上 <input type=”hidden” name=”csrftoken” value=”tokenvalue”/>,这样就把 token 以参数的形式加入请求了。如图所示:

存在问题?

缺点:

1. 这个Token不一定是安全的;尽管同源策略会让攻击者拿不到请求参数,但是有的网站,如论坛,允许发布自己个人网站的地址,就把Token给带出去了;

2. 对于每次请求,服务端做Token验证的话,需要在服务端存一份Token,这个存储明显是对资源的浪费,无论是作为配置还是放到Session;

改进的解决方法:

1. 既然不安全,就加密;

2. 既然不想存,那就传两份Cookies,就像身份验证Cookies一样,不放信息,存储的是加密方案,即把加密前后的两份Cookies分别以Cookies和请求参数传过来,服务端校验能否从未加密的Token得到加密的Token;

这里就有了个思路,即在请求中放Cookies中Token的密文,作为请求参数传到服务端,而服务端校验Cookies中的Token与传参中的密文,这样服务端也不需要存Token了;

这里需要留意:这里是否需要考率——这个加密算法最好不要是那种公开的、常见的加密算法?

III. 在 HTTP 头中自定义属性并验证

这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。

这样解决了上种方法在请求中加入 token 的不便,同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会透过 Referer 泄露到其他网站中去。

IV. 加验证码验证

但这种方式涉及到页面交互,在通常情况下,验证码能很好遏制CSRF攻击。

存在问题?

但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段,不能作为主要解决方案。

参考:

CSRF攻击原理以及防御方法(写的很好) - 无情川客 - 博客园

csrf攻击原理和防御 - 云+社区 - 腾讯云

前端网络安全必修 1 同源策略和CSRF - 掘金

5. 服务端常用方案及代码示例

本次介绍SpringSecurity框架下实现CSRF跨站攻击防御的方法,具体代码如下:

先注册CsrfFilter,设置匹配规则(对那些url和请求方法拦截)和拒绝后处理方式(如跳转指定的controller 如SecurityController .../api/crsf):

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.regex.Pattern;
/**
 * spring安全框架下的csrfFilter配置类
 */
@Configuration
public class WebSecurityConfig  {

    @Bean
    public FilterRegistrationBean csrfFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        CsrfSecurityRequestMatcher matcher = new CsrfSecurityRequestMatcher();
        CsrfAccessDeniedHandler handler = new CsrfAccessDeniedHandler();
        CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository();
        cookieCsrfTokenRepository.setCookieHttpOnly(false);
        CsrfFilter csrfFilter = new CsrfFilter(cookieCsrfTokenRepository);
        csrfFilter.setRequireCsrfProtectionMatcher(matcher);
        csrfFilter.setAccessDeniedHandler(handler);
        registration.setFilter(csrfFilter);
        registration.addUrlPatterns("/*");
        return registration;
    }

}

class CsrfSecurityRequestMatcher implements RequestMatcher {

    private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
    private RegexRequestMatcher unprotectedMatcher = new RegexRequestMatcher("^/api/myDoain/.*", null);

    @Override
    public boolean matches(HttpServletRequest request) {
        if(allowedMethods.matcher(request.getMethod()).matches()){
            return false;
        }
        return unprotectedMatcher.matches(request);
    }

}

class CsrfAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        request.getRequestDispatcher("/api/security/csrf").forward(request, response);
        return;
    }
}

SecurityController:

/**
 * crsfFilter拦截跳转的controller
 */
@RestController
@RequestMapping("/api/security")
public class SecurityController {

    @RequestMapping(value = "/csrf", method = RequestMethod.POST)
    @ResponseBody
    public DefaultResponseDTO<Void> csrf() {
        return DefaultResponseDTO.fail(ResultCodeEnum.SECURITY_ERROR);
    }
}

或者全局跳转的异常页面:

package comAA.controller.error;

import com.AA.common.model.DefaultResponseDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 接口异常控制器
 */
@Controller
@Slf4j
public class BasicResponseErrorController implements ErrorController {
    @Value("${error.path:/error}")
    private String errorPath;

    private final ErrorAttributes errorAttributes;

    @Autowired
    public BasicResponseErrorController(ErrorAttributes errorAttributes) {
        Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
        this.errorAttributes = errorAttributes;
    }

    @Override
    public String getErrorPath() {
        return this.errorPath;
    }

    @RequestMapping(value = "${error.path:/error}")
    @ResponseBody
    public ResponseEntity error(HttpServletRequest request) {
        RequestAttributes requestAttributes = new ServletRequestAttributes(request);
        Throwable exception = this.errorAttributes.getError(requestAttributes);
        HttpStatus status = getStatus(request);
        if (exception != null) {
            log.warn("error occur, http status is [httpStatus={}]. e:{}", status, exception);
        } else {
            log.warn("error occur, http status is [httpStatus={}].", status);
        }

        DefaultResponseDTO response = DefaultResponseDTO.builder()
                .code(status.value())
                .msg(status.getReasonPhrase()).build();
        return new ResponseEntity<>(response, status);

    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request
                .getAttribute("javax.servlet.error.status_code");
        if (statusCode != null) {
            try {
                return HttpStatus.valueOf(statusCode);
            } catch (Exception ex) {
            }
        }
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

 思路和步骤如下

(1)服务端

使用CookieCsrfTokenRepository生成CSRF Token放入cookie,并设置cookie的HttpOnly=false,允许js读取该cookie。

使用ignoringAntMatchers开放一些不需要进行CSRF防护的访问路径,比如:登录授权。

至此,我们生成了CSRF token保存在了cookies中,浏览器向服务端发送的HTTP请求,都要将CSRF token带上,服务端校验通过才能正确的响应。这个校验的过程并不需要我们自己写代码实现,Spring Security会自动处理。但是我们需要关注前端代码,如何正确的携带CSRF token。

(2)前端

在thymeleaf模板中可以使用如下方式,在发送HTTP请求的时候携带CSRF Token。如果是前后端分离的应用,或者其他模板引擎,酌情从cookies中获取CSRF Toekn。

  1. 在Header中携带CSRF token;
  2. 直接作为参数提交;
  3. form表单的隐藏字段;

抓包看下是否如预期一样:

(1)首次请求,前端的cookies和header无xsrf-token,而服务端返回时 setCookies xsrf-token

 (2)后续请求,前端用js从cookies取出xsrf-token,设置到请求的cookies和header中

 (3)如果未带上xsrf-token,用postman模拟请求,试下

 结果:

 

CsrfFilter源码分析:

package org.springframework.security.web.csrf;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;


public final class CsrfFilter extends OncePerRequestFilter {
	/**
	 * The default RequestMatcher that indicates if CSRF protection is required or
	 * not. The default is to ignore GET, HEAD, TRACE, OPTIONS and process all other
	 * requests.
	 * 用于检测哪些请求需要csrf保护,这里的缺省配置是:GET, HEAD, TRACE, OPTIONS这种只读的
	 * HTTP动词都被忽略不做csrf保护,而其他PATCH, POST, PUT,DELETE等会修改服务器状态的HTTP
	 * 动词会受到当前Filter的csrf保护。
	 */
	public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();

	private final Log logger = LogFactory.getLog(getClass());
	private final CsrfTokenRepository tokenRepository;
	private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
	// 用于CSRF保护验证逻辑失败进行处理
	private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

	// 构造函数,使用指定的csrf token存储库构造一个CsrfFilter实例
	// 缺省情况下,使用Spring Security 的 Springboot web 应用,选择使用的
	// csrfTokenRepository是一个做了惰性封装的HttpSessionCsrfTokenRepository实例。
	// 也就是说相应的 csrf token保存在http session中。	
	public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
		Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
		this.tokenRepository = csrfTokenRepository;
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
					throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);

		// 从csrf token存储库中获取针对当前请求的csrf token。
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		// 记录针对当前请求是否不存在csrf token
		final boolean missingToken = csrfToken == null;
		if (missingToken) {
			// 如果存储库中尚不存在针对当前请求的csrf token,生成一个,把它关联到
			// 当前请求保存到csrf token存储库中
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}

		// 将从存储库中获取得到的或者新建并保存到存储库的csrf token保存为请求的两个属性
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);
		
		if (!this.requireCsrfProtectionMatcher.matches(request)) {
			// 检测当前请求是否需要csrf保护,如果不需要,放行继续执行filter chain的其他逻辑
			filterChain.doFilter(request, response);
			return;
		}

		// 尝试从请求头部或者参数中获取浏览器端传递过来的实际的csrf token。
		// 缺省情况下,从头部取出时使用header name: X-CSRF-TOKEN
		// 从请求中获取参数时使用的参数名称是 : _csrf
		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
		if (!csrfToken.getToken().equals(actualToken)) {
			// csrf token存储库中取出的token和浏览器端传递过来的token不相等的情况有两种:
			// 1. 针对该请求在存储库中并不存在csrf token
			// 2. 针对该请求在存储库中的csrf token和请求参数实际携带的不一致
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Invalid CSRF token found for "
						+ UrlUtils.buildFullRequestUrl(request));
			}
			if (missingToken) {
				// 1. 针对该请求在存储库中并不存在csrf token , 处理方案:
				// 抛出异常 MissingCsrfTokenException
				this.accessDeniedHandler.handle(request, response,
						new MissingCsrfTokenException(actualToken));
			}
			else {
				// 2. 针对该请求在存储库中的csrf token和请求参数实际携带的不一致,处理方案:
				// 抛出异常 InvalidCsrfTokenException
				this.accessDeniedHandler.handle(request, response,
						new InvalidCsrfTokenException(csrfToken, actualToken));
			}
			return;
		}
		
		// 当前请求需要经该Filter的csrf验证逻辑并且通过了csrf验证,放行,继续执行filter chain
		// 其他部分逻辑
		filterChain.doFilter(request, response);
	}

	/**
	 * Specifies a RequestMatcher that is used to determine if CSRF protection
	 * should be applied. If the RequestMatcher returns true for a given request,
	 * then CSRF protection is applied.
	 *
	 * 指定一个RequestMatcher用来检测一个请求是否需要应用csrf保护验证逻辑。
	 * 
	 * The default is to apply CSRF protection for any HTTP method other than GET, HEAD,
	 * TRACE, OPTIONS.
	 * 缺省行为是针对GET, HEAD,TRACE, OPTIONS这种只读性的HTTP请求不做csrf保护验证,验证其他
	 * 那些会更新服务器状态的HTTP请求,比如PATCH, POST, PUT,DELETE等。
	 * 
	 *
	 * @param requireCsrfProtectionMatcher the RequestMatcher used to determine if
	 * CSRF protection should be applied.
	 */
	public void setRequireCsrfProtectionMatcher(
			RequestMatcher requireCsrfProtectionMatcher) {
		Assert.notNull(requireCsrfProtectionMatcher,
				"requireCsrfProtectionMatcher cannot be null");
		this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
	}

	/**
	 * Specifies a AccessDeniedHandler that should be used when CSRF protection
	 * fails.
	 * 指定一个AccessDeniedHandler用于CSRF保护验证逻辑失败进行处理。
	 *
	 * The default is to use AccessDeniedHandlerImpl with no arguments.
	 * 缺省行为是使用一个不但参数的AccessDeniedHandlerImpl实例。
	 *
	 * @param accessDeniedHandler the AccessDeniedHandler to use
	 */
	public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
		Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
		this.accessDeniedHandler = accessDeniedHandler;
	}
	 // 用于检测哪些HTTP请求需要应用csrf保护的RequestMatcher,
	 // 缺省行为是针对GET, HEAD,TRACE, OPTIONS这种只读性的HTTP请求不做csrf保护,
	 // 其他那些会更新服务器状态的HTTP请求,比如PATCH, POST, PUT,DELETE等需要csrf保护。
	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());
		}
	}
}

参考:

SpringSecurity框架下实现CSRF跨站攻击防御的方法

SpringSecurity过滤器CsrfFilter源码解析

SpringSecurity源码之CsrfFilter

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值