欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Spring Security专栏(关于如何实现 CSRF 保护)

最编程 2024-08-12 11:06:43
...

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

写在前面

本节我们将进入一些高级的学习当中。为什么说高级 主要是这些东西能保证你服务的安全性,也非常具有实战意义。

本节我们一起来学习下CSRF 保护

这里多唠叨一句,欢迎大家查看我的专栏,目前正在进行的Security专栏和队列并发专栏,设计模式专题(已完结)

什么是 CSRF?

我们先来看 CSRF。CSRF 的全称是 Cross-Site Request Forgery,翻译成中文就是跨站请求伪造。 那么,究竟什么是跨站请求伪造,面对这个问题我们又该如何应对呢?

从安全的角度来讲,你可以将 CSRF 理解为一种攻击手段,即攻击者盗用了你的身份,然后以你的名义向第三方网站发送恶意请求。我们可以使用如下所示的流程图来描述 CSRF:

image.png

具体流程如下:

  • 用户浏览并登录信任的网站 A,通过用户认证后,会在浏览器中生成针对 A 网站的 Cookie;

  • 用户在没有退出网站 A 的情况下访问网站 B,然后网站 B 向网站 A 发起一个请求;

  • 用户浏览器根据网站 B 的请求,携带 Cookie 访问网站 A;

  • 由于浏览器会自动带上用户的 Cookie,所以网站 A 接收到请求之后会根据用户具备的权限进行访问控制,这样相当于用户本身在访问网站 A,从而网站 B 就达到了模拟用户访问网站 A 的操作过程。

显然,从应用程序开发的角度来讲,CSRF 就是系统的一个安全漏洞,这种安全漏洞也在 Web 开发中广泛存在。

基于 CSRF 的工作流程,进行 CSRF 保护的基本思想就是为系统中的每一个连接请求加上一个随机值,我们称之为 csrf_token。这样,当用户向网站 A 发送请求时,网站 A 在生成的 Cookie 中就会设置一个 csrf_token 值。而在浏览器发送请求时,提交的表单数据中也有一个隐藏的 csrf_token 值,这样网站 A 接收到请求后,一方面从 Cookie 中提取出 csrf_token,另一方面也从表单提交的数据中获取隐藏的 csrf_token,将两者进行比对,如果不一致就代表这就是一个伪造的请求。

接下来我们具体看下保护应用的方法

使用 CsrfFilter 保护应用

在 Spring Security 中,专门提供了一个 CsrfFilter 来实现对 CSRF 的保护。CsrfFilter 拦截请求,并允许使用 GET、HEAD、TRACE 和 OPTIONS 等 HTTP 方法的请求。而针对 PUT、POST、DELETE 等可能会修改数据的其他请求,CsrfFilter 则希望接收包含 csrf_token 的消息头。如果这个消息头不存在或包含不正确的 csrf_token 值,应用程序将拒绝该请求并将响应的状态设置为 403。

看到这里,你可能会问,这个 csrf_token 到底长什么样子呢?其实它本质上就是一个字符串。在 Spring Security 中,专门定义了一个 CsrfToken 接口来约定它的格式:

public interface CsrfToken extends Serializable {
 
    //获取消息头名称
    String getHeaderName();
 
    //获取应该包含 Token 的参数名称
    String getParameterName();
	 
	//获取具体的 Token 值
    String getToken();
}

而在 CsrfFilter 类中,我们也找到了如下所示的针对 CsrfToken 的处理过程:

@Override
protected void doFilterInternal(HttpServletRequest request,
             HttpServletResponse response, FilterChain filterChain)
                     throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
 
        //从 CsrfTokenRepository 中获取 CsrfToken
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        final boolean missingToken = csrfToken == null;
 
        //如果找不到 CsrfToken 就生成一个并保存到 CsrfTokenRepository 中
        if (missingToken) {
             csrfToken = this.tokenRepository.generateToken(request);
             this.tokenRepository.saveToken(csrfToken, request, response);
        }
 
        //在请求中添加 CsrfToken
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
 
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
             filterChain.doFilter(request, response);
             return;
        }
 
        //从请求中获取 CsrfToken
        String actualToken = request.getHeader(csrfToken.getHeaderName());
        if (actualToken == null) {
             actualToken = request.getParameter(csrfToken.getParameterName());
        }
 
        //如果请求所携带的 CsrfToken 与从 Repository 中获取的不同,则抛出异常
        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);
}

整个过滤器执行流程还是比较清晰的,基本就是围绕 CsrfToken 的校验工作。

我们注意到这里引入了一个 CsrfTokenRepository,这个 Repository 组件实现了对 CsrfToken 的存储管理,其中就包含前面提到的专门针对 Cookie 的 CookieCsrfTokenRepository。

从 CookieCsrfTokenRepository 中,首先我们能看到一组常量定义,包括针对 CSRF 的 Cookie 名称、参数名称以及消息头名称,如下所示:

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";

CookieCsrfTokenRepository 的 saveToken() 方法也比较简单,就是基于 Cookie 对象进行了 CsrfToken 的设置工作,如下所示:

@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);
}

在 Spring Security 中,CsrfTokenRepository 接口具有一批实现类,除了 CookieCsrfTokenRepository,还有 HttpSessionCsrfTokenRepository 等,这里不再一一展开。

了解了 CsrfFilter 的基本实现流程,下面我们继续讨论如何使用它来实现 CSRF 保护。从 Spring Security 4.0 开始,默认启用 CSRF 保护,以防止 CSRF 攻击应用程序。Spring Security CSRF 会针对 POST、PUT 和 DELETE 方法进行防护。

因此,对于开发人员而言,实际上你并不需要做什么额外工作就能使用这个功能了。当然,如果你不想使用这个功能.

也可以通过如下配置方法进行关闭:

//这份代码应该在自己的工程当中经常见到
http.csrf().disable();

如何定制化 CSRF 保护

明白了其中原理 我们怎么进行在上面套一层 进行定制化呢

根据前面的讨论,如果你想获取 HTTP 请求中的 CsrfToken,只需要使用如下所示的代码:

CsrfToken token = (CsrfToken)request.getAttribute("_csrf");

如果你不想使用 Spring Security 内置的存储方式,而是想基于自身需求把 CsrfToken 存储起来,要做的事情就是实现 CsrfTokenRepository 接口。

这里我们尝试把 CsrfToken 保存到关系型数据库中,所以可以通过扩展 Spring Data 中的 JpaRepository 来定义一个 JpaTokenRepository,如下所示:

//存储信息
public interface JpaTokenRepository extends JpaRepository<Token, Integer> {
    Optional<Token> findTokenByIdentifier(String identifier);
}

JpaTokenRepository 很简单,只有一个根据 identifier 获取 Token 的查询方法,而新增接口则是 JpaRepository 默认提供的,我们可以直接使用。

然后,我们基于 JpaTokenRepository 来构建一个 DatabaseCsrfTokenRepository,如下所示:

public class DatabaseCsrfTokenRepository
        implements CsrfTokenRepository {
 
    @Autowired
    private JpaTokenRepository jpaTokenRepository;
 
    @Override
    public CsrfToken generateToken(HttpServletRequest httpServletRequest) {
        String uuid = UUID.randomUUID().toString();
        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", uuid);
    }
 
    @Override
    public void saveToken(CsrfToken csrfToken, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        String identifier = httpServletRequest.getHeader("X-IDENTIFIER");
        Optional<Token> existingToken = jpaTokenRepository.findTokenByIdentifier(identifier);
 
        if (existingToken.isPresent()) {
            Token token = existingToken.get();
            token.setToken(csrfToken.getToken());
        } else {
            Token token = new Token();
            token.setToken(csrfToken.getToken());
            token.setIdentifier(identifier);
            jpaTokenRepository.save(token);
        }
    }
 
    @Override
    public CsrfToken loadToken(HttpServletRequest httpServletRequest) {
        String identifier = httpServletRequest.getHeader("X-IDENTIFIER");
        Optional<Token> existingToken = jpaTokenRepository.findTokenByIdentifier(identifier);
 
        if (existingToken.isPresent()) {
            Token token = existingToken.get();
            return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token.getToken());
        }
 
        return null;
    }
}

DatabaseCsrfTokenRepository 类的代码基本都是自解释的,这里借助了 HTTP 请求中的“X-IDENTIFIER”请求头来确定请求的唯一标识,从而将这一唯一标识与特定的 CsrfToken 关联起来。然后我们使用 JpaTokenRepository 完成了针对关系型数据库的持久化工作。

最后,想要上述代码生效,我们需要通过配置方法完成对 CSRF 的设置,如下所示,这里直接通过 csrfTokenRepository 方法集成了自定义的 DatabaseCsrfTokenRepository:

@Override
protected void configure(HttpSecurity http) throws Exception {
        http.csrf(c -> {
            c.csrfTokenRepository(databaseCsrfTokenRepository());
        });
        //省略不写
        …
}

作为总结,我们可以用如下所示的示意图来梳理整个定制化 CSRF 所包含的各个组件以及它们之间的关联关系:

image.png

总结

好,今天就到这里,下期我们继续学习跨域

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作