SpringSecurity 核心过滤器——CsrfFilter

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

@[TOC]

前言

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;
    }
}
相关文章
|
9月前
|
安全 Java API
深入解析 Spring Security 配置中的 CSRF 启用与 requestMatchers 报错问题
本文深入解析了Spring Security配置中CSRF启用与`requestMatchers`报错的常见问题。针对CSRF,指出默认已启用,无需调用`enable()`,只需移除`disable()`即可恢复。对于`requestMatchers`多路径匹配报错,分析了Spring Security 6.x中方法签名的变化,并提供了三种解决方案:分次调用、自定义匹配器及降级使用`antMatchers()`。最后提醒开发者关注版本兼容性,确保升级平稳过渡。
1130 2
|
10月前
|
NoSQL Java Redis
springboot怎么使用Redisson
通过以上步骤,已经详细介绍了如何在Spring Boot项目中使用Redisson,包括添加依赖、配置Redisson、创建配置类以及使用Redisson实现分布式锁和分布式集合。Redisson提供了丰富的分布式数据结构和工具,可以帮助开发者更高效地实现分布式系统。通过合理使用这些工具,可以显著提高系统的性能和可靠性。
3644 34
|
9月前
|
存储 安全 Java
Spring Security 入门与详解
Spring Security 是 Spring 框架中的核心安全模块,提供认证、授权及防护功能。本文详解其核心概念,包括认证(Authentication)、授权(Authorization)和过滤器链(Security Filter Chain)。同时,通过代码示例介绍基本配置,如 PasswordEncoder、UserDetailsService 和自定义登录页面等。最后总结常见问题与解决方法,助你快速掌握 Spring Security 的使用与优化。
2467 0
|
11月前
|
安全 Linux
CentOS下载ISO镜像的方法
访问CentOS官方网站(https://www.centos.org/download/),在“Downloads”页面找到ISO镜像下载链接,选择所需版本和架构(如x86_64)开始下载。CentOS分为Linux版和Stream版,前者每两年发行一次并提供10年安全维护,后者为滚动更新。旧版本可在Vault(https://vault.centos.org/)下载。建议选择DVD格式镜像,包含完整系统和常用软件。
14272 14
CentOS下载ISO镜像的方法
|
10月前
|
搜索推荐 开发者 UED
【开发者必看—运动篇】数据赋能运动App留存率再创新高
如何在拉新后促活并成功留存?如何减少新用户流失?
【开发者必看—运动篇】数据赋能运动App留存率再创新高
|
安全 前端开发 JavaScript
什么是 CSRF 攻击?如何启用 CSRF 保护来抵御该攻击?
什么是 CSRF 攻击?如何启用 CSRF 保护来抵御该攻击?
1912 5
|
存储 安全 Java
SpringSecurity对CSRF的支持实践
SpringSecurity对CSRF的支持实践
305 2
|
消息中间件 JSON Java
Spring Boot、Spring Cloud与Spring Cloud Alibaba版本对应关系
Spring Boot、Spring Cloud与Spring Cloud Alibaba版本对应关系
31924 0
|
存储 安全 Java
发现 XSS 漏洞?别急!SpringBoot这招轻松搞定!
在SpringBoot中,发现XSS(跨站脚本)漏洞时,可以通过一系列措施来轻松搞定这些安全问题。XSS攻击允许攻击者在受害者的浏览器中注入恶意脚本,这些脚本可以窃取用户的敏感信息、劫持用户会话或进行其他恶意操作。以下是一些在SpringBoot中修复XSS漏洞的有效方法
2510 7