说说什么是 CSRF 攻击?如何防止?

2025-12-12 0 305

说说什么是 CSRF 攻击?如何防止?

前阵子帮老项目做安全加固,测试小哥扔过来一个截图 —— 用个假网页居然调通了咱系统的 “修改用户角色” 接口。查了半天发现,居然是早年没做 CSRF 防护留下的坑。做 Java 开发八年,从 SSH 写传统管理系统,到 Spring Cloud 搞微服务,跟 CSRF 斗智斗勇的次数不少,今天就从实战角度,把这事儿聊透 —— 不整虚的理论,只说项目里能用的干货。

一、先破误区:CSRF 不是 “偷数据”,是 “冒你干活”

很多人一听到 “网络攻击” 就觉得是 “偷密码、扒数据”,但 CSRF(跨站请求伪造)不一样 —— 它是拿着你的 “已登录身份”,冒充你发恶意请求。打个比方:你刚登录电商后台,还没退出,点开个 “领优惠券” 的链接,结果后台悄悄执行了 “取消订单”—— 这就是 CSRF 干的。

咱拿 Java 后端最常见的 “退款接口” 举例,早年我写的接口是这样的:

  • 请求方式:POST
  • 路径:/api/order/refund
  • 参数:orderId=12345
  • 校验逻辑:只查 Session 里有没有登录信息,有就执行退款。

结果测试小哥用这招破了防护:

  1. 做个静态网页,里面藏个自动提交的表单:
<form action=\"https://咱的电商域名/api/order/refund\" method=\"POST\">
  <input type=\"hidden\" name=\"orderId\" value=\"67890\"> 
</form>
<script>document.forms[0].submit();</script>
  1. 用管理员账号登录咱的系统,再打开这个静态网页;
  2. 浏览器一看请求是发往 “咱的电商域名”,自动带上了 Session 对应的 JSESSIONID Cookie;
  3. 后端校验 Session 有效,直接执行了退款 —— 全程管理员没点任何 “确认” 按钮。

这里必须划两个重点,很多同行都踩过:

  1. POST 请求一样会被 CSRF 攻击!别以为只有 GET 危险,隐藏表单、JS 自动提交都能发起 POST 请求;
  2. CSRF 能成,全靠俩 “漏洞”:浏览器会自动给目标域名带 Cookie,以及后端只认 “登录状态”,不认 “请求是不是用户主动发的”。

二、Java 项目里,这 4 个场景最容易中招

八年下来,从单体项目到微服务,我总结了 CSRF 的高频 “踩坑点”,看看你项目里有没有:

  1. 传统 JSP/Thymeleaf 表单没防护:早年做管理系统,表单直接写form action=\"/https://images.downcodes.comxxx\",没加任何验证。攻击者仿写个一模一样的表单,骗用户点一下,请求就发出去了;
  2. 前后端分离忽略 AJAX 请求:有些同学觉得用了 Vue/React 就安全了,后端没做防护。攻击者用 JS 发跨域 AJAX(靠 CORS 配置漏洞绕开限制),一样能带上 Cookie 发起请求;
  3. 单独依赖 Referer 校验:有些项目靠 “校验请求来源(Referer)” 防 CSRF,但 Referer 能被篡改(比如用浏览器插件、Flash)。我之前碰到过生产环境被这么绕过的,差点出大事;
  4. 只防 “高危接口”,漏了 “弱敏感接口” :只在 “删数据、转钱” 接口加防护,却忘了 “修改收货地址”“绑定手机号” 这类接口。攻击者一样能通过这些接口篡改用户信息,造成损失。

三、Java 实战:4 个防护方案,覆盖所有场景

结合项目经验,这 4 个方案最实用,从单体到分布式、前后端分离都能用,咱一个个说:

方案 1:首选 ——CSRF Token 验证(通用性最强)

核心思路:给每个合法请求加个 “随机验证码”,后端验完再干活。Spring 项目里这么落地:

步骤 1:生成 Token

用户登录成功后,用UUID生成 Token,单体项目存 Session,分布式项目存 Redis(Key 用用户 ID 或 SessionID):

// 登录成功后调用
public void generateCsrfToken(HttpServletRequest request) {
    String csrfToken = UUID.randomUUID().toString();
    // 单体项目存Session
    request.getSession().setAttribute(\"csrfToken\", csrfToken);
    // 分布式项目存Redis(示例)
    // redisTemplate.opsForValue().set(\"csrf_token_\" + request.getSession().getId(), csrfToken, 30, TimeUnit.MINUTES);
}
步骤 2:前端携带 Token
  • 传统表单:加隐藏域,用 Thymeleaf 取值:
<form action=\"/api/order/refund\" method=\"POST\">
  <input type=\"hidden\" name=\"_csrf\" th:value=\"${session.csrfToken}\">
  <input type=\"text\" name=\"orderId\" value=\"12345\">
  <button type=\"submit\">申请退款

  • 前后端分离:前端先调用/api/getCsrfToken拿 Token,存 localStorage,再在 AJAX Header 里带:
// 拿Token
axios.get(\"/api/getCsrfToken\").then(res => {
  localStorage.setItem(\"csrfToken\", res.data.token);
});
// 发请求
axios.post(\"/api/order/refund\", { orderId: 12345 }, {
  headers: { \"X-CSRF-Token\": localStorage.getItem(\"csrfToken\") }
});
步骤 3:后端拦截校验

写个拦截器,拦截所有非 GET 请求,校验 Token:

@Component
public class CsrfInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 排除GET/OPTIONS请求(OPTIONS是跨域预检请求)
        String method = request.getMethod();
        if (\"GET\".equalsIgnoreCase(method) || \"OPTIONS\".equalsIgnoreCase(method)) {
            return true;
        }

        // 1. 拿存储的Token(单体从Session,分布式从Redis)
        String storedToken = (String) request.getSession().getAttribute(\"csrfToken\");
        // 分布式取法:String storedToken = redisTemplate.opsForValue().get(\"csrf_token_\" + request.getSession().getId());
        if (storedToken == null) {
            response.setStatus(403);
            response.getWriter().write(\"CSRF Token不存在,请重新登录\");
            return false;
        }

        // 2. 拿请求里的Token(表单取参数,AJAX取Header)
        String requestToken = request.getParameter(\"_csrf\");
        if (requestToken == null) {
            requestToken = request.getHeader(\"X-CSRF-Token\");
        }
        if (requestToken == null) {
            response.setStatus(403);
            response.getWriter().write(\"请携带CSRF Token\");
            return false;
        }

        // 3. 校验Token
        if (!storedToken.equals(requestToken)) {
            response.setStatus(403);
            response.getWriter().write(\"CSRF Token校验失败\");
            return false;
        }

        // 可选:验证成功后刷新Token,防止重复使用
        request.getSession().setAttribute(\"csrfToken\", UUID.randomUUID().toString());
        return true;
    }
}
步骤 4:注册拦截器

在 Spring Boot 里配置拦截规则,只拦截需要防护的接口:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private CsrfInterceptor csrfInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(csrfInterceptor)
                .addPathPatterns(\"/api/**\") // 要防护的接口
                .excludePathPatterns(\"/api/login\", \"/api/getCsrfToken\"); // 排除登录、拿Token的接口
    }
}

方案 2:轻量方案 ——SameSite Cookie(适合现代浏览器)

近几年最推荐的 “懒人方案”,利用 Cookie 的 SameSite 属性限制携带规则,不用写太多代码:

原理

设置SameSite=StrictSameSite=Lax后,浏览器会拒绝在 “跨站请求” 中携带 Cookie(比如从攻击者的网页发请求,就不会带你的系统 Cookie)。

Spring Boot 配置
@Configuration
public class CookieConfig {
    @Bean
    public ServletContextInitializer servletContextInitializer() {
        return servletContext -> {
            SessionCookieConfig sessionCookieConfig = servletContext.getSessionCookieConfig();
            // Lax比Strict更灵活:同站请求、GET的跨站请求(比如从百度跳过来的GET)能携带Cookie
            sessionCookieConfig.setSameSite(\"Lax\");
            // 配合HttpOnly,防止JS窃取Cookie(双重保障)
            sessionCookieConfig.setHttpOnly(true);
            // 生产环境一定要开Secure,只在HTTPS下传输Cookie
            sessionCookieConfig.setSecure(true);
        };
    }
}
注意

兼容 Chrome 51+、Firefox 60+,如果要兼容老浏览器,得和 Token 方案配合用。

方案 3:前后端分离专属 —— 双重 Cookie 验证

适合无状态服务(不用 Session),核心思路:Token 存在 Cookie 里,但前端要把 Cookie 里的 Token 放到 Header 里,后端校验 “Cookie Token” 和 “Header Token” 一致:

步骤 1:登录后存 Token 到 Cookie
public void setCsrfCookie(HttpServletResponse response) {
    String csrfToken = UUID.randomUUID().toString();
    Cookie cookie = new Cookie(\"csrfToken\", csrfToken);
    cookie.setPath(\"/\");
    cookie.setHttpOnly(false); // 允许前端JS读取
    cookie.setSecure(true); // HTTPS下传输
    cookie.setMaxAge(30 * 60); // 30分钟有效期
    response.addCookie(cookie);
}
步骤 2:前端读取 Cookie,带在 Header 里
// 读取Cookie的工具函数
function getCookie(name) {
    let arr = document.cookie.split(\"; \");
    for (let i = 0; i < arr.length; i++) {
        let [key, value] = arr[i].split(\"=\");
        if (key === name) return value;
    }
    return \"\";
}

// 发请求
axios.post(\"/api/order/refund\", { orderId: 12345 }, {
  headers: { \"X-CSRF-Token\": getCookie(\"csrfToken\") }
});
步骤 3:后端校验

拦截器里加一段逻辑:

// 从Cookie拿Token
String cookieToken = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
    for (Cookie cookie : cookies) {
        if (\"csrfToken\".equals(cookie.getName())) {
            cookieToken = cookie.getValue();
            break;
        }
    }
}

// 从Header拿Token
String headerToken = request.getHeader(\"X-CSRF-Token\");

// 校验一致
if (cookieToken == null || headerToken == null || !cookieToken.equals(headerToken)) {
    response.setStatus(403);
    response.getWriter().write(\"CSRF校验失败\");
    return false;
}

方案 4:辅助防护 ——Referer/Origin 校验

不能单独用(Referer 可篡改),但能增加攻击难度,作为补充:

// 校验Referer/Origin
String referer = request.getHeader(\"Referer\");
String origin = request.getHeader(\"Origin\");
String allowedDomain = \"https://你的域名\";

// Origin优先级比Referer高,优先校验Origin
if (origin != null) {
    if (!allowedDomain.equals(origin)) {
        response.setStatus(403);
        return false;
    }
} else if (referer != null) {
    if (!referer.startsWith(allowedDomain)) {
        response.setStatus(403);
        return false;
    }
}
// 注意:如果是直接在浏览器输入地址访问(没有Referer/Origin),要特殊处理

四、八年踩坑总结:这 3 个错千万别犯

  1. 把 CSRF Token 存在 Cookie 里:等于给攻击者送 “弹药”—— 因为 Cookie 会自动携带,攻击者不用费劲就能拿到 Token,等于没防;
  2. 分布式项目里 Token 存 Session:多节点部署时,Session 不共享,A 节点生成的 Token,B 节点拿不到,会导致正常请求也被拦截;
  3. 只防 POST 不防 GET:虽然 GET 一般用于查询,但有些项目用 GET 做 “注销账号”“取消订单”(比如/api/order/cancel?orderId=123),攻击者用就能发起请求,一样要防。

最后说句实在的:CSRF 防护不是 “加个 Token 就完事”,得结合项目场景选方案 —— 单体项目用 Token+Session,前后端分离用双重 Cookie,现代浏览器加 SameSite Cookie,再配合 Referer 校验做补充。只有形成 “组合拳”,才能真正防住。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 编程相关 说说什么是 CSRF 攻击?如何防止? https://www.zuozi.net/35665.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务