CSRF就是跨域请求伪造,英文全称是 Cross Site Request Forgery。

这是一种非常常见的 Web 攻击方式,其实是很好防御的,但是由于经常被很多开发者忽略,进而导致很多网站实际上都存在 CSRF 攻击的安全隐患。

示意图

image.png

模拟接口

创建项目csrf-1

  1. @RestController
  2. public class HelloController {
  3. @PostMapping("/transfer")
  4. public void transferMoney(String name, Integer money) {
  5. System.out.println("name = " + name);
  6. System.out.println("money = " + money);
  7. }
  8. @GetMapping("/hello")
  9. public String hello() {
  10. return "hello";
  11. }
  12. }

配置

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. http.authorizeRequests().anyRequest().authenticated()
  6. .and()
  7. .formLogin()
  8. .and()
  9. .csrf()
  10. .disable();
  11. }
  12. }

配置8080端口启动一个实例相当于银行网站

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

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

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <form action="http://localhost:8080/transfer" method="post">
  9. <input type="hidden" value="javaboy" name="name">
  10. <input type="hidden" value="10000" name="money">
  11. <input type="submit" value="点击查看美女图片">
  12. </form>
  13. </body>
  14. </html>

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

启动csrf-2项目
首先访问csrf-1项目,进行登录访问接口,但是不注销,接着继续访问csrf-2网站
看到了超链接,好奇这美女到底长啥样,一点击,结果钱就被人转走了。

防御

CSRF 防御,一个核心思路就是在前端请求中,添加一个随机数。
因为在 CSRF 攻击中,黑客网站其实是不知道用户的 Cookie 具体是什么的,
他是让用户自己发送请求到网上银行这个网站的,因为这个过程会自动携带上 Cookie 中的信息。
所以我们的防御思路是这样:

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

spring security默认方案

这个测试接口是一个 POST 请求,因为默认情况下,GET、HEAD、TRACE 以及 OPTIONS 是不需要验证 CSRF 攻击的

  @PostMapping("/csrf")
    @ResponseBody
    public String csrfTest() {
        return "csrf";
    } 
    @GetMapping("/csrf")
    public String csrf() {
        return "csrf";
    }

添加一个thymeleaf模板【csrf.html】

<body>
<form action="/csrf" method="post">
    <input type="hidden" th:value="${_csrf.token}" th:name="${_csrf.parameterName}">
    <input type="submit" value="hello">
</form>
</body>

注意,在发送 POST 请求的时候,还额外携带了一个隐藏域,隐藏域的 key 是 ${_csrf.parameterName},value 则是 ${_csrf.token}。
添加完成后,启动项目,我们访问 hello 页面,在访问时候,需要先登录,登录成功之后,我们可以看到登录请求中也多了一个参数,
多了 _csrf 参数。

这里我们用了 Spring Security 的默认登录页面,如果大家使用自定义登录页面,可以参考上面 hello.html 的写法,通过一个隐藏域传递 _csrf 参数。
访问到 hello 页面之后,再去点击按钮,就可以访问到 hello 接口了

前后端分离配置

前后端分离中,不再将_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)。

前端页面操作

<!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>
  1. 首先引入 jquery 和 jquery.cookie ,方便我们一会操作 Cookie。
  2. 定义三个 input,前两个是用户名和密码,第三个是登录按钮。
  3. 点击登录按钮之后,我们先从 Cookie 中提取出 XSRF-TOKEN,这也就是我们要上传的 csrf 参数。
  4. 通过一个 POST 请求执行登录操作,注意携带上 _csrf 参数。

大家看到,csrf 攻击主要是借助了浏览器默认发送 Cookie 的这一机制,所以如果你的前端是 App、小程序之类的应用,不涉及浏览器应用的话,其实可以忽略这个问题,如果你的前端包含浏览器应用的话,这个问题就要认真考虑了