SpringSecurity 核心过滤器——CsrfFilter

2023-09-19 12:11:26

前言

Spring Security除了认证授权外功能外,还提供了安全防护功能。本文我们来介绍下SpringSecurity中是如何阻止CSRF攻击的。

什么是CSRF攻击

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

解决方案

检查Referer字段

HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.bankchina.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.bankhacker.com之下,这时候服务器就能识别出恶意的访问。

这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。

CsrfToken

其实CSRF攻击是在用户登录且没有退出浏览器的情况下访问了第三方的站点而被攻击的,完全是携带了认证的cookie来实现的,我们只需要在服务端响应给客户端的页面中绑定随机的信息,然后提交请求后在服务端校验,如果携带的数据和之前的不一致就认为是CSRF攻击,拒绝这些请求即可。

SpringSecurity是如何防止CSRF攻击的

首先从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。

开启关闭CSRF防御

在SpringSecurity中默认是开启csrf防御的,我们可以通过一下配置来关闭csrf防御

http.csrf().disable();

或者在基于配置文件的使用中使用如下操作关闭

<security:csrf disabled="true"/>

SpringSecurity的实现

CSRF的原理

  1. 生成csrfToken保存到HttpSession或者Cookie中
  2. 请求到来时,程序会从请求中获取提交的csrfToken,同时会从HttpSession中获取之前存储的csrfToken进行比较,如果相同则认为是合法的请求,继续后面的操作,如果不相等则认为是CSRF工具,拒绝该请求

SpringSecurity中的代码是如何实现的,主要看的是 spring-security-web.jar中的org.springframework.security.web.csrf包下的源码。

image.png

CsrfToken

CsrfToken是一个非常简单的接口,定义了Token令牌,消息头和请求参数。

public interface CsrfToken extends Serializable {

	/**
	 * 获取我们放置在请求头中CSRF随机值的名称
	 */
	String getHeaderName();

	/**
	 * 获取请求体中的csrf随机值的参数名称
	 */
	String getParameterName();

	/**
	 * 返回具体的Token值
	 */
	String getToken();

}

CsrfToken的默认实现是DefaultCsrfToken。

image.png

CsrfTokenRepository

CsrfTokenRepository接口也非常简单,定义了Token的生成,存储和获取的相关API

public interface CsrfTokenRepository {

	/**
	 * 生成Token
	 */
	CsrfToken generateToken(HttpServletRequest request);

	/**
	 * 存储生成的Token
	 */
	void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);

	/**
	 * 返回Token
	 */
	CsrfToken loadToken(HttpServletRequest request);

}

CsrfTokenRepository的实现在SpringSecurity中有两个实现。

image.png

默认的实现是HttpSessionCsrfTokenRepository。是一个基于HttpSession保存csrfToken的实现。

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;

    // 保存Token到session中
	@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.setAttribute(this.sessionAttributeName, token);
		}
	}

// 从session中加载token
	@Override
	public CsrfToken loadToken(HttpServletRequest request) {
		HttpSession session = request.getSession(false);
		if (session == null) {
			return null;
		}
		return (CsrfToken) session.getAttribute(this.sessionAttributeName);
	}
  // 生成Token 
	@Override
	public CsrfToken generateToken(HttpServletRequest request) {
		return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken());
	}

	/**
	 * Sets the {@link HttpServletRequest} parameter name that the {@link CsrfToken} is
	 * expected to appear on
	 * @param parameterName the new parameter name to use
	 */
	public void setParameterName(String parameterName) {
		Assert.hasLength(parameterName, "parameterName cannot be null or empty");
		this.parameterName = parameterName;
	}

	/**
	 * Sets the header name that the {@link CsrfToken} is expected to appear on and the
	 * header that the response will contain the {@link CsrfToken}.
	 * @param headerName the new header name to use
	 */
	public void setHeaderName(String headerName) {
		Assert.hasLength(headerName, "headerName cannot be null or empty");
		this.headerName = headerName;
	}

	/**
	 * Sets the {@link HttpSession} attribute name that the {@link CsrfToken} is stored in
	 * @param sessionAttributeName the new attribute name to use
	 */
	public void setSessionAttributeName(String sessionAttributeName) {
		Assert.hasLength(sessionAttributeName, "sessionAttributename cannot be null or empty");
		this.sessionAttributeName = sessionAttributeName;
	}
    // 通过UUID来生成Token信息
	private String createNewToken() {
		return UUID.randomUUID().toString();
	}

}

CsrfFilter

CsrfFilter用于处理跨站请求伪造。检查表单提交的_csrf隐藏域的value与内存中保存的的是否一致,如果一致框架则认为当然登录页面是安全的,如果不一致,会报403forbidden错误。

具体处理请求的方法

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);
// 从session中加载 Token
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		boolean missingToken = (csrfToken == null);
// 如果是第一次访问就生成Token信息
		if (missingToken) {
			csrfToken = this.tokenRepository.generateToken(request);
// 把生成的Token信息存储在Session中
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);
// 匹配是否是需要做CSRF防御的相关请求
		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;
		}
// 获取请求携带在header中的Token信息
		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
// 从请求参数中获取Token信息
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
// 判断请求中的Token是否和Session中存储的Token相等
		if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
			this.logger.debug(
					LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
// Token不相等,说明是CSRF攻击,抛出访问拒绝的异常
			AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
					: new MissingCsrfTokenException(actualToken);
			this.accessDeniedHandler.handle(request, response, exception);
			return;
		}
// 说明是正常的访问,放过
		filterChain.doFilter(request, response);
	}

分布式Session处理

上面介绍的CsrfToken校验,生成的Token信息是存储在HttpSession中的,那么在分布式环境下,跨进程的场景下我们要如何实现Session共享呢?这时我们可以通过SpringSession来实现,但是这里有个前提就是分布式的项目必须都得是在一个一级域名下的多个二级域名是可以实现的。

配置SpringSession

配置SpringSession可以参考Spring的官网:https://docs.spring.io/spring-session/docs/2.5.6/reference/html5/ 因为在分布式Session我们需要把Session数据独立的存储在Redis服务中,所以还需要启动Redis服务。

添加相关依赖:

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

然后添加对应的配置

spring.redis.host=192.168.10.10
spring.redis.port=6379
spring.session.store-type=redis
spring.session.redis.namespace=spring:session

添加配置文件,设置Cookie中的domain为一级域名

@Configuration
public class MySessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("test.com");
        cookieSerializer.setCookieName("csrfSession");
        return cookieSerializer;
    }
}
更多推荐

Wish下单支付教程,测评养号需要满足哪些技术要点?

Wish是2011年成立的一家高科技独角兽公司,有90%的卖家来自中国,也是北美和欧洲最大的移动电商平台。它使用一种优化算法大规模获取数据,并快速了解如何为每个客户提供最相关的商品,让消费者在移动端便捷购物的同时享受购物的乐趣,被评为硅谷最佳创新平台和欧美最受欢迎的购物类APP。1.打开wish首页,注册账号。2.进入

SSL证书如何做到保障网站安全?

当网站显示不安全时,用户会在头脑中产生该网站是否合法的疑问,如果是购物网站或者购物商城,那意味着可能会损失大部分的用户。而SSL证书能有效保障网站的安全性,轻松解决网站不被用户信任的问题。那么,SSL证书究竟是如何保障网站安全的呢?本文将为大家解答这一问题。SSL证书怎么保障网站安全?加密通信SSL证书是在Web服务器

【JAVASE】图书管理系统

⭐作者:小胡_不糊涂🌱作者主页:小胡_不糊涂的个人主页📀收录专栏:浅谈Java💖持续更文,关注博主少走弯路,谢谢大家支持💖图书管理系统1.设计思路图2.创建book包2.1Book类2.2BookList类3.创建operation包3.1FindOperation类-查找图书3.2AddOperation类-

电子图书馆

bookget支持一下电子图书馆藏书:中国地区数字图书馆:国家图书馆http://read.nlc.cn/thematDataSearch/toGujiIndex北京故宫博物院-故宫名画记https://minghuaji.dpm.org.cn/广州大典http://gzdd.gzlib.gov.cn/Hrcanton

时间序列的重采样和pandas的resample方法介绍

重采样是时间序列分析中处理时序数据的一项基本技术。它是关于将时间序列数据从一个频率转换到另一个频率,它可以更改数据的时间间隔,通过上采样增加粒度,或通过下采样减少粒度。在本文中,我们将深入研究Pandas中重新采样的关键问题。为什么重采样很重要?时间序列数据到达时通常带有可能与所需的分析间隔不匹配的时间戳。例如以不规则

DETR纯代码分享(八)position_encoding.py(models)

一、导入一些Python库和模块importmathimporttorchfromtorchimportnnfromutil.miscimportNestedTensor上面的代码段主要是Python代码,用于导入一些Python库和模块,以下是对每行代码的详细解释:importmath:这一行代码导入了Python的

【STM32笔记】HAL库I2C通信配置、读写操作及通用函数定义

【STM32笔记】HAL库I2C通信配置、读写操作及通用函数定义文章目录I2C协议I2C配置I2C操作判断I2C是否响应I2C读写附录:Cortex-M架构的SysTick系统定时器精准延时和MCU位带操作SysTick系统定时器精准延时延时函数阻塞延时非阻塞延时位带操作位带代码位带宏定义总线函数一、位带操作理论及实践

解决Selenium中无法点击元素,Selemium使用JS代码 driver.execute_script点击元素

@FindBy(how=How.XPATH,using="//*[text()='A1.Approved']")privateWebElementApproved;driver.execute_script("arguments[0].click();",Approved)这句话的意思是使用JavaScript在浏览器

【C++】AVL树

个人主页:🍝在肯德基吃麻辣烫我的gitee:C++仓库个人专栏:C++专栏文章目录前言一、什么是AVL树?设计AVL树的原因二、AVL树的性质三、二叉树节点的定义四、AVL树的插入旋转1)右单旋2)左单旋3)左右双旋4)右左双旋AVL树插入完整代码验证一棵树为AVL树AVL树的性能分析总结前言本文章将会模拟实现一棵A

iMazing 2 .17.9最新官方中文版免费下载安装激活

iMazing2.17.9最新版是一款帮助用户管理IOS手机的应用程序,iMazing2最新版能力远超iTunes提供的终极的iOS设备管理器。IMazing与你的iOS设备(iPhone、iPad或iPod)相连,使用起来非常的方便。作为苹果指定的iOS设备同步工具。mazing什么意思iMazing2.17.9是一

springcloud相关面试题

目录springcloud相关面试题SpringCloud几个核心组件服务注册与发现组件——Eureka网关组件——Gateway路由:过滤:服务调用组件——Feign(默认包含Ribbon、Hystrix,基于Ribbon实现负载均衡)Ribbon和Feign调用服务的区别Feign、Ribbon、Hystrix三者

热文推荐