浅析SpringSecurity如何防御CSRF攻击

  今天无意间看到原来 SpringSecurity 自带了 CSRF 防御处理,所以记录下,不得不说 SpringSecurity 功能还是挺强大的,蛮多业务场景都提供了支持。

  CSRF 就是跨域请求伪造,英文全称是 Cross Site Request Forgery。这是一种非常常见的 Web 攻击方式,其实是很好防御的,但是由于经常被很多开发者忽略,进而导致很多网站实际上都存在 CSRF 攻击的安全隐患。

一、CSRF 原理

  想要防御 CSRF 攻击,那我们得先搞清楚什么是 CSRF 攻击:

二、CSRF 实践

  我创建一个名为 csrf-1 的 Spring Boot 项目,这个项目相当于我们上面所说的网上银行网站,创建项目时引入 Web 和 Spring Security 依赖。创建成功后提供2个接口

@RestController
public class HelloController {
    @PostMapping("/transfer")
    public void transferMoney(String name, Integer money) {
        System.out.println("name = " + name);
        System.out.println("money = " + money);
    }
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

  假设 transfer 是一个转账接口。

  最后我们还需要配置一下 Spring Security,因为 Spring Security 中默认是可以自动防御 CSRF 攻击的,所以我们要把这个关闭掉

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf()
                .disable();
    }
}

  配置完成后,我们启动 csrf-1 项目。

  接下来,我们再创建一个 csrf-2 项目,这个项目相当于是一个危险网站,为了方便,这里创建时我们只需要引入 web 依赖即可。

  然后我们在 resources/static 目录下创建一个 hello.html ,内容如下

<body>
<form action="http://localhost:8080/transfer" method="post">
    <input type="hidden" value="javaboy" name="name">
    <input type="hidden" value="10000" name="money">
    <input type="submit" value="点击查看美女图片">
</form>
</body>

  这里有一个超链接,超链接的文本是点击查看美女图片,当你点击了超链接之后,会自动请求 http://localhost:8080/transfer 接口,同时隐藏域还携带了两个参数。

  配置完成后,启动 csrf-2 项目。接下来,用户首先访问 csrf-1 项目中的接口,在访问的时候需要登录,用户就执行了登录操作,访问完整后,用户并没有执行登出操作,然后用户访问 csrf-2 中的页面,看到了超链接,好奇这美女到底长啥样,一点击,结果钱就被人转走了。

三、CSRF 防御

  先说下防御思路:CSRF 防御,一个核心思路就是在前端请求中,添加一个随机数。

  因为在 CSRF 攻击中,黑客网站其实是不知道用户的 Cookie 具体是什么的,他是让用户自己发送请求到网上银行这个网站的,因为这个过程会自动携带上 Cookie 中的信息。

  所以我们的防御思路是这样:用户在访问网上银行时,除了携带 Cookie 中的信息之外,还需要携带一个随机数,如果用户没有携带这个随机数,则网上银行网站会拒绝该请求。黑客网站诱导用户点击超链接时,会自动携带上 Cookie 中的信息,但是却不会自动携带随机数,这样就成功的避免掉 CSRF 攻击了。

  Spring Security 中对此提供了很好的支持,我们一起来看下:Spring Security 中默认实际上就提供了 csrf 防御,但是需要开发者做的事情比较多。我们主要看下前后端分离的项目。

  如果是前后端分离项目,Spring Security 也提供了解决方案。这次不是将 _csrf 放在 Model 中返回前端了,而是放在 Cookie 中返回前端,配置方式如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

  有小伙伴可能会说放在 Cookie 中不是又被黑客网站盗用了吗?其实不会的,大家注意如下两个问题:

(1)黑客网站根本不知道你的 Cookie 里边存的啥,他也不需要知道,因为 CSRF 攻击是浏览器自动携带上 Cookie 中的数据的。

(2)我们将服务端生成的随机数放在 Cookie 中,前端需要从 Cookie 中自己提取出来 _csrf 参数,然后拼接成参数传递给后端,单纯的将 Cookie 中的数据传到服务端是没用的。

  理解透了上面两点,你就会发现 _csrf 放在 Cookie 中是没有问题的,但是大家注意,配置的时候我们通过 withHttpOnlyFalse 方法获取了 CookieCsrfTokenRepository 的实例,该方法会设置 Cookie 中的 HttpOnly 属性为 false,也就是允许前端通过 js 操作 Cookie(否则你就没有办法获取到 _csrf)。

  配置完成后,重启项目,此时我们就发现返回的 Cookie 中多了一项:

  接下来,我们通过自定义登录页面,来看看前端要如何操作:首先我们在 resources/static 目录下新建一个 html 页面叫做 login.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/jquery.min.js"></script>
    <script src="js/jquery.cookie.js"></script>
</head>
<body>
<div>
    <input type="text" id="username">
    <input type="password" id="password">
    <input type="button" value="登录" id="loginBtn">
</div>
<script>
    $("#loginBtn").click(function () {
        let _csrf = $.cookie('XSRF-TOKEN');
        $.post('/login.html',{username:$("#username").val(),password:$("#password").val(),_csrf:_csrf},function (data) {
            alert(data);
        })
    })
</script>
</body>
</html>

  其实也就是从 cookie 里拿出 _csrf,然后作为参数传递

  服务端我们也稍作修改,如下

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**");
    }

    @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(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

  其实也很简单:就是一方面给 js 放行,允许操作 cookie;另一方面配置一下登录页面,以及登录成功的回调。这里简单起见,登录成功的回调我就给一个字符串就可以了。

  所有事情做完之后,我们访问 login.html 页面,输入用户名密码进行登录,就可以看到,我们的 _csrf 配置已经生效了。

  个人总结:其实就是利用了防止 CSRF攻击里的双重 Cookie 的方法,还是不错的,有兴趣的可以看我之前博客。本文详细内容见这篇文章:《【SpringSecurity系列(十八)】SpringBoot 如何防御 CSRF 攻击?》https://mp.weixin.qq.com/s/gOecyB84nt8Q5UKLIJrJiQ

posted @ 2022-04-27 22:28  古兰精  阅读(1100)  评论(0编辑  收藏  举报