一、CSRF攻击到底是什么鬼?

咱们先从一个生活场景说起。假设你正在网上银行转账,填好收款人和金额后点击确认,钱就转出去了。这时候如果有个坏蛋给你发了个钓鱼链接,你一不小心点了,结果发现账户里的钱莫名其妙转给了陌生人。这就是典型的CSRF(跨站请求伪造)攻击。

专业点说,CSRF就是攻击者利用用户已登录的身份,在用户不知情的情况下,以用户的名义发送恶意请求。因为浏览器会自动带上用户的cookie等认证信息,服务器无法区分这是用户自愿的操作还是被伪造的请求。

举个具体例子:

<!-- 恶意网站上的代码 -->
<img src="http://yourbank.com/transfer?to=hacker&amount=10000" width="0" height="0">

当你登录网银后访问这个恶意页面,浏览器会自动向银行发送转账请求,而你可能完全不知情。

二、防御CSRF的三大法宝

1. 验证Referer头

服务器可以检查HTTP请求头中的Referer字段,判断请求是否来自合法域名。

// Java示例(Spring框架)
@GetMapping("/transfer")
public String transfer(@RequestParam String to, @RequestParam int amount, 
                      HttpServletRequest request) {
    String referer = request.getHeader("Referer");
    if(referer == null || !referer.startsWith("https://yourbank.com")) {
        return "非法请求来源";
    }
    // 处理转账逻辑
    return "转账成功";
}

注意:这种方法依赖浏览器行为,可能被绕过,且某些情况下Referer会被过滤。

2. 使用CSRF Token

这是目前最可靠的防御方式。服务器生成一个随机token,要求客户端在提交请求时必须带上这个token。

// Java示例(Spring Security)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

// 前端表单中需要包含这个token
<form action="/transfer" method="post">
    <input type="hidden" name="_csrf" value="${_csrf.token}"/>
    <!-- 其他表单字段 -->
</form>

3. 双重Cookie验证

将token放在Cookie和请求参数中,服务器验证两者是否一致。

// Node.js示例(Express框架)
const csrf = require('csurf');
const cookieParser = require('cookie-parser');

app.use(cookieParser());
app.use(csrf({ cookie: true }));

// 前端需要这样发送请求
fetch('/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'CSRF-Token': getCookie('XSRF-TOKEN') // 从cookie中读取
    },
    body: JSON.stringify(payload)
});

三、实战:Spring Security中的CSRF防护

让我们深入看看Spring Security是如何实现CSRF防护的:

// 更完整的Spring Security配置
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll()
                .and()
            .csrf()
                .csrfTokenRepository(new HttpSessionCsrfTokenRepository())
                .ignoringAntMatchers("/api/public/**");
    }
}

// 控制器示例
@RestController
public class AccountController {
    
    @PostMapping("/transfer")
    public ResponseEntity<String> transferMoney(
            @RequestParam String toAccount,
            @RequestParam BigDecimal amount,
            @RequestHeader("X-CSRF-TOKEN") String csrfToken) {
        // Spring Security会自动验证CSRF token
        // 业务逻辑处理
        return ResponseEntity.ok("转账成功");
    }
}

关键点说明

  1. csrfTokenRepository 指定token存储方式(Session或Cookie)
  2. ignoringAntMatchers 可以排除某些API的CSRF检查
  3. 前端需要从_csrf属性获取token并包含在请求中

四、特殊场景下的CSRF防护

1. REST API的防护

对于无状态的REST API,传统的CSRF防护可能不太适用。这时候可以考虑:

// 使用自定义Header的防护方式
@Configuration
@EnableWebSecurity
public class RestSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .requireCsrfProtectionMatcher(
                    new RequestMatcher() {
                        private Pattern allowedMethods = 
                            Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
                        
                        @Override
                        public boolean matches(HttpServletRequest request) {
                            // 仅对非安全方法启用CSRF检查
                            return !allowedMethods.matcher(request.getMethod()).matches();
                        }
                    })
                .and()
            .addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
    }
}

// 自定义Filter将token放入响应头
public class CsrfTokenResponseHeaderBindingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
        if (token != null) {
            response.setHeader("X-CSRF-HEADER", token.getHeaderName());
            response.setHeader("X-CSRF-TOKEN", token.getToken());
        }
        filterChain.doFilter(request, response);
    }
}

2. 单页应用(SPA)的防护

现代前端框架如React/Vue通常使用JWT认证,这时可以采用以下方案:

// 前端axios配置示例(Vue项目)
import axios from 'axios';
import cookies from 'js-cookie';

const service = axios.create({
    baseURL: process.env.VUE_APP_BASE_API,
    timeout: 5000
});

// 请求拦截器
service.interceptors.request.use(
    config => {
        // 从cookie中获取CSRF token
        const csrfToken = cookies.get('XSRF-TOKEN');
        if (csrfToken) {
            config.headers['X-XSRF-TOKEN'] = csrfToken;
        }
        return config;
    },
    error => {
        return Promise.reject(error);
    }
);

五、CSRF防护的注意事项

  1. Token的存储:不要将CSRF token存储在localStorage中,这可能导致XSS攻击窃取token。推荐使用HttpOnly的Cookie。

  2. Token的时效性:应该为每个会话生成新的token,重要操作可以使用一次性token。

  3. CORS配置:正确配置CORS策略,避免恶意网站发起跨域请求。

  4. 敏感操作二次验证:对于转账、改密等敏感操作,应该要求用户进行二次验证(如短信验证码)。

  5. 框架选择:优先使用成熟框架(如Spring Security、Django CSRF中间件)提供的防护机制,不要自己造轮子。

六、总结

CSRF攻击虽然原理简单,但危害巨大。通过本文的介绍,我们了解了多种防御手段:

  1. 验证Referer头是最简单的方式,但不够可靠
  2. CSRF Token是目前最主流的解决方案
  3. 双重Cookie验证适合前后端分离的应用
  4. 不同技术栈(Java/Node.js等)都有成熟的实现方案

在实际开发中,我们应该根据应用场景选择合适的防护策略,同时注意与其他安全措施(如XSS防护)配合使用。记住,安全是一个整体,不能只依赖单一防护手段。

最后提醒大家,安全无小事,特别是在金融、电商等涉及敏感数据的系统中,一定要重视CSRF防护,定期进行安全审计和渗透测试,确保系统安全可靠。