首页 开发教程 说说什么是 CSRF 攻击?如何防止?

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

开发教程 2025年12月4日
906 浏览

说说什么是 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 校验做补充。只有形成 “组合拳”,才能真正防住。

发表评论
暂无评论

还没有评论呢,快来抢沙发~

客服

点击联系客服 点击联系客服

在线时间:09:00-18:00

关注微信公众号

关注微信公众号
客服电话

400-888-8888

客服邮箱 122325244@qq.com

手机

扫描二维码

手机访问本站

扫描二维码
搜索