1. 项目概述为什么APISIX的CSRF防护值得深挖最近在排查一个线上API网关的安全配置时我又把Apache APISIX的CSRF防护插件翻出来仔细研究了一遍。这个插件默认启用的“双提交Cookie”机制听起来简单但真要把它配置得既安全又对业务无感里头的门道可不少。我见过不少团队直接照搬默认配置结果要么是防护形同虚设被安全扫描工具揪出漏洞要么就是过于严格把正常的跨域请求也给拦了导致前端页面功能异常开发运维互相扯皮。CSRF跨站请求伪造这玩意儿对于任何有状态交互的Web应用来说都是个老生常谈却又必须严肃对待的威胁。攻击者诱导用户在已登录的浏览器中向目标网站发送一个恶意请求因为浏览器会自动带上用户的认证Cookie服务器很难区分这是用户的真实意愿还是被伪造的。而APISIX作为流量入口在这里统一做防护相当于给后端的多个服务加了一道公共安检门比在每个应用里各自实现要省心且一致得多。“双提交Cookie”是当前业界防御CSRF的主流实践之一它的核心思想是“同源检测”。APISIX的插件实现了这一机制但它的配置项如cookie_name、expires、key等每一个都直接关系到防护的有效性和兼容性。这篇文章我就结合多次实战踩坑和源码梳理的经验带你彻底搞懂APISIX CSRF插件的工作原理并给出针对不同场景如前后端分离、多域名、API优先应用的配置策略和避坑指南。无论你是运维工程师、架构师还是关注安全的开发者都能从中找到直接可用的干货。2. 核心原理拆解双提交Cookie机制是如何工作的要配置好防护首先得明白它到底在防什么以及是怎么防的。双提交CookieDouble Submit Cookie机制本质上是一种利用浏览器同源策略的“令牌验证”法。2.1 CSRF攻击的经典场景与防御思路想象一个场景用户登录了银行网站bank.com浏览器保存了会话Cookie。此时用户不小心访问了一个恶意网站evil.com。这个恶意网站的页面上隐藏了一个表单其action指向bank.com/transfer并预设了转账参数。由于浏览器会自动在请求中携带bank.com的Cookie这个恶意请求就会被服务器当作是用户的合法操作执行。防御CSRF的核心思路就是让这个自动携带的Cookie“失效”。服务器需要一种方法来验证这个请求确实是来自它自己的前端页面而不是来自其他域。常见的方法有同步令牌模式服务器在用户会话中生成一个随机令牌Token并在渲染页面时将其嵌入表单如隐藏域或Meta标签。前端在提交请求时必须额外带上这个令牌通常放在请求头或请求体中服务器进行比对验证。双提交Cookie模式服务器同样生成一个随机令牌但将其同时放在两个地方一个是通过Set-Cookie响应头设置一个Cookie通常名为XSRF-TOKEN另一个是要求前端在后续请求中以非Cookie的方式通常是HTTP头如X-XSRF-TOKEN将这个令牌的值再提交一次。APISIX的CSRF插件采用的就是第二种模式。为什么选它对于API网关这种无状态、高性能的中间件来说双提交Cookie模式有几个天然优势它不需要服务器端存储会话状态无状态验证逻辑简单比对两个值是否相等对网关性能影响极小。2.2 APISIX插件的运作流程与状态图让我们一步步拆解APISIX CSRF插件的工作流程。这个过程涉及到客户端浏览器、APISIX网关和后端应用三方的协作。令牌生成与下发当用户首次访问受保护的应用前端时例如GET请求首页请求会经过APISIX。如果该路由启用了CSRF插件APISIX会检查请求中是否已经包含了有效的CSRF令牌Cookie。如果没有它会使用配置的key用于签名的密钥和随机数生成一个加密的令牌。然后APISIX通过Set-Cookie响应头将这个令牌设置到用户的浏览器中。这里有个关键点这个Cookie的Path和Domain属性需要精心设置以确保它在后续相关请求中能被浏览器正确发送。前端获取与携带令牌前端应用通常是JavaScript需要从Cookie中读取这个令牌值。由于JavaScript可以通过document.cookie读取同源或适当设置了HttpOnlyfalse的非HttpOnly Cookie的Cookie因此它能获取到这个值。然后前端需要在下一次向受保护端点发起“状态变更”请求如POST、PUT、DELETE时将这个令牌值添加到一个自定义的HTTP请求头中比如X-XSRF-TOKEN。网关验证当这个携带了自定义头的“状态变更”请求再次到达APISIX时CSRF插件会启动验证。它做两件事从请求的Cookie中找到之前下发的那个CSRF令牌Cookie的值。从请求的自定义头如X-XSRF-TOKEN中获取前端提交的令牌值。 插件会比较这两个值。如果它们完全相等则验证通过请求被放行至后端应用。如果任一值缺失或不匹配APISIX会直接返回403 Forbidden错误请求不会到达后端。豁免请求插件通常允许配置一些豁免条件。例如通过excluded_method可以设置不验证GET、HEAD、OPTIONS等“安全方法”根据HTTP规范这些方法不应有副作用。还可以通过excluded_uri来设置一些无需CSRF防护的API端点白名单。这个流程的核心安全假设是攻击者所在的evil.com域无法读取或设置bank.com域的Cookie同源策略保护也无法让浏览器在跨域请求中自动携带自定义的X-XSRF-TOKEN头CORS策略限制。因此即使攻击者能伪造请求他也无法提供正确的、与Cookie匹配的令牌值。注意这种防护依赖于浏览器对Cookie和CORS策略的遵守。如果后端应用错误地配置了CORS允许来自任意源的请求并携带自定义头那么此防护将失效。因此CSRF防护必须与严格的CORS策略配合使用。3. APISIX CSRF插件配置全解析与避坑指南理解了原理我们来看在APISIX里具体怎么配。插件的配置直接写在路由Route、服务Service或全局插件中。一个完整的配置示例如下{ plugins: { csrf: { key: your-secret-key-here-32bytes-long, expires: 3600, cookie_name: XSRF-TOKEN, cookie_domain: .example.com, cookie_path: /, cookie_same_site: Lax, cookie_http_only: false, header_name: X-XSRF-TOKEN, excluded_methods: [GET, HEAD, OPTIONS], excluded_uri: [/api/public/*, /healthz] } } }每一个参数都至关重要配置不当就会埋下隐患或导致功能故障。3.1 核心安全参数key与签名机制key参数是整套机制的基石。它用于对生成的CSRF令牌进行签名防止令牌被篡改。APISIX生成的令牌并非一个简单的随机字符串而是一个“随机数签名”的组合体。为什么需要签名假设令牌只是一个随机数如UUID攻击者虽然无法从evil.com读取你的Cookie但他可以诱骗你的浏览器向bank.com发起一个请求这个请求会自动携带你的CSRF Cookie。如果后端只是简单比对Cookie值和头部的值是否相等那么攻击者只需要在自己的恶意请求中也设置一个相同的头值即可。通过签名APISIX能验证这个令牌是否是自己当初签发的而不仅仅是值相等。如何设置key这个密钥必须足够长且随机建议使用32字节256位或以上的密码学安全随机字符串。绝对不要使用默认值或简单的单词。你可以用openssl rand -base64 32命令来生成一个。并且这个key应该被当作敏感信息管理最好通过环境变量或APISIX的Secret插件注入而不是硬编码在配置文件中。expires过期时间令牌的有效期单位是秒。设置太短会增加前端频繁获取令牌的负担设置太长则增加了令牌泄露后被利用的时间窗口。根据业务的安全等级权衡对于普通Web应用3600秒1小时到86400秒1天是常见范围。注意这个过期时间是由APISIX在验证时解码令牌后检查的与Cookie的Max-Age是两回事但通常建议让两者保持一致或接近。3.2 Cookie相关参数确保令牌可达这部分配置决定了CSRF令牌Cookie如何被设置和发送是兼容性问题的高发区。cookie_name默认是XSRF-TOKEN。可以自定义但要确保前端能对应上。cookie_domain与cookie_path这是最大的坑点之一。场景1前后端同域。如果前端www.example.com和后端APIwww.example.com/api在同一个主域下设置cookie_domain为.example.comcookie_path为/这样前端页面和API请求都能共享这个Cookie。场景2前后端跨子域。前端在app.example.comAPI网关在api.example.com。你需要设置cookie_domain为.example.com注意前面的点这样Cookie才能在所有example.com的子域下被发送。同时确保APISIX实例本身可以通过api.example.com这个域名访问因为Cookie的Domain需要匹配请求的域名。场景3完全跨域CORS。前端在www.client-app.comAPI在api.service.com。这是最复杂的情况。浏览器默认不会在跨域请求中携带Cookie。你需要在APISIX的CSRF插件中依然设置cookie_domain为.service.com。前端发起请求时必须设置withCredentials: trueFetch API或Axios。APISIX或后端服务的CORS配置必须明确允许www.client-app.com这个源并且响应头中需要包含Access-Control-Allow-Credentials: true和Access-Control-Allow-Origin: www.client-app.com不能是通配符*。前端需要先从api.service.com发起一个GET请求可豁免CSRF检查来获取并存储Cookie然后在后续的POST请求中手动从Cookie读取令牌并添加到X-XSRF-TOKEN头中。这个过程对前端代码有侵入性。cookie_same_site这个属性是现代浏览器防御CSRF的重要补充。它控制Cookie在跨站请求中是否被发送。Lax默认在跨站的子请求如图片、iframe中不发送但在用户从外部站点导航到目标站点如点击链接时发送。这是一个平衡安全与兼容性的选择对于大多数Web应用推荐使用Lax。Strict任何跨站请求都不发送Cookie安全性最高但可能导致用户体验问题例如从邮件链接点击进入网站需要重新登录。None允许跨站发送Cookie但必须同时设置Securetrue即仅HTTPS。这主要用于前述的跨域CORS场景。在非HTTPS环境下设置为None是无效的。cookie_http_only必须设置为false。因为双提交Cookie机制要求前端JavaScript能够读取Cookie的值以便将其放入自定义请求头。如果设置为trueJavaScript将无法读取整个机制就无法工作。3.3 请求头与豁免策略header_name前端需要将令牌放入的请求头名称默认是X-XSRF-TOKEN。前后端需要对此达成一致。一些前端框架如Angular会自动识别名为XSRF-TOKEN的Cookie并在请求时自动添加X-XSRF-TOKEN头这与APISIX的默认配置是兼容的。excluded_methods根据HTTP幂等性原则通常豁免GET、HEAD、OPTIONS这些“安全方法”。但这里需要根据你的API设计仔细考量。如果你的某个GET接口会触发重要操作如GET /api/logout那么它可能不应该被豁免。一个重要的原则是任何会改变服务器状态的操作无论HTTP方法是什么都应受到CSRF保护。对于RESTful API通常保护POST、PUT、PATCH、DELETE。excluded_uri用于设置白名单。例如公开的API接口/api/public/*、健康检查端点/healthz、Webhook回调第三方服务调用无法携带你的CSRF令牌等。配置时尽量使用精确匹配或前缀匹配避免过于宽泛的通配符带来安全风险。4. 实战部署从零搭建一个受保护的API网关光说不练假把式。我们以一个典型的前后端分离项目为例实战演练如何配置APISIX的CSRF插件。假设我们的前端部署在https://app.mycompany.com后端API网关APISIX入口在https://api.mycompany.com。4.1 环境准备与APISIX部署首先你需要一个运行中的APISIX实例。可以通过Docker快速启动# 拉取最新APISIX镜像 docker pull apache/apisix:latest # 启动APISIX并挂载配置文件目录 docker run -d \ --name apisix \ -p 9080:9080 -p 9443:9443 -p 2379:2379 \ -v /your-local-conf-dir/:/usr/local/apisix/conf \ apache/apisix接下来通过APISIX的Admin API或Dashboard创建一条路由将/api/*的流量代理到你的后端服务假设后端服务运行在http://backend-service:8080。curl http://127.0.0.1:9180/apisix/admin/routes/1 -H X-API-KEY: your-admin-key -X PUT -d { uri: /api/*, upstream: { type: roundrobin, nodes: { backend-service:8080: 1 } }, plugins: { // 我们先不启用CSRF插件确保基础代理正常工作 } }测试路由是否生效curl http://127.0.0.1:9080/api/hello应该能返回后端服务的响应。4.2 配置并启用CSRF插件现在我们来给这条路由加上CSRF防护。根据我们的跨子域场景app.mycompany.com和api.mycompany.com进行配置。curl http://127.0.0.1:9180/apisix/admin/routes/1 -H X-API-KEY: your-admin-key -X PUT -d { uri: /api/*, upstream: { type: roundrobin, nodes: { backend-service:8080: 1 } }, plugins: { csrf: { key: eF7zN2p8YtRwCqA9KvJxLsDgHmZc5T0B, // 替换为你用 openssl rand -base64 32 生成的密钥 expires: 7200, cookie_name: XSRF-TOKEN, cookie_domain: .mycompany.com, // 关键设置为父级域使子域共享 cookie_path: /, cookie_same_site: Lax, // 同站策略平衡安全与兼容性 cookie_http_only: false, // 必须为false让JS可读 header_name: X-XSRF-TOKEN, excluded_methods: [GET, HEAD, OPTIONS], excluded_uri: [/api/public/*, /api/health] } } }同时必须配置CORS插件以允许前端域进行跨域请求并携带凭证。将CORS插件与CSRF插件一起启用cors: { allow_origins: https://app.mycompany.com, // 明确指定前端源不能用 * allow_methods: GET,POST,PUT,DELETE,PATCH,OPTIONS,HEAD, allow_headers: X-XSRF-TOKEN,Content-Type,Authorization, // 必须包含 X-XSRF-TOKEN allow_credentials: true // 关键允许携带Cookie等凭证 }4.3 前端代码适配示例前端需要完成两件事1. 获取CSRF令牌Cookie2. 在发起“状态变更”请求时将其添加到请求头。以下是一个使用原生fetchAPI的示例// 假设这是你的API基础URL const API_BASE_URL https://api.mycompany.com/api; // 1. 定义一个函数来获取Cookie中的CSRF令牌 function getCsrfToken() { const name XSRF-TOKEN; const decodedCookie decodeURIComponent(document.cookie); const ca decodedCookie.split(;); for(let i 0; i ca.length; i) { let c ca[i]; while (c.charAt(0) ) { c c.substring(1); } if (c.indexOf(name) 0) { return c.substring(name.length, c.length); } } return ; } // 2. 封装一个安全的fetch函数 async function safeFetch(endpoint, options {}) { const url ${API_BASE_URL}${endpoint}; const method options.method || GET; const isSafeMethod [GET, HEAD, OPTIONS].includes(method.toUpperCase()); const headers { Content-Type: application/json, ...options.headers, }; // 对于非安全方法添加CSRF令牌头 if (!isSafeMethod) { const csrfToken getCsrfToken(); if (csrfToken) { headers[X-XSRF-TOKEN] csrfToken; } else { console.warn(CSRF token not found. The request might be rejected.); } } // 跨域请求必须携带凭证 const fetchOptions { ...options, headers, credentials: include, // 关键确保Cookie被发送 }; return fetch(url, fetchOptions); } // 3. 使用示例 // 首次访问获取一个CSRF令牌Cookie通过一个豁免的GET请求 safeFetch(/public/info).then(r r.json()).then(console.log); // 执行一个受保护的操作 safeFetch(/user/profile, { method: POST, body: JSON.stringify({ name: New Name }), }).then(response { if (!response.ok) { // 如果收到403可能是CSRF令牌无效或过期 if (response.status 403) { // 可以尝试刷新页面或重新获取令牌 console.error(CSRF validation failed.); } throw new Error(Request failed); } return response.json(); }).then(data console.log(Success:, data));对于使用Axios的Vue/React项目可以在请求拦截器中统一处理import axios from axios; const instance axios.create({ baseURL: https://api.mycompany.com/api, withCredentials: true, // 关键允许跨域携带Cookie }); // 请求拦截器 instance.interceptors.request.use( (config) { const method config.method?.toUpperCase(); const isSafeMethod [GET, HEAD, OPTIONS].includes(method); if (!isSafeMethod) { const csrfToken getCsrfToken(); // 复用上面的getCsrfToken函数 if (csrfToken) { config.headers[X-XSRF-TOKEN] csrfToken; } } return config; }, (error) Promise.reject(error) );5. 深度排查常见问题与解决方案实录在实际部署中你几乎一定会遇到下面这些问题。我把它们和解决方案整理成了表格方便你快速对照排查。问题现象可能原因排查步骤与解决方案前端请求收到403 Forbidden1. CSRF令牌缺失或不匹配。2. Cookie未正确发送。3. 请求方法未被豁免但未携带令牌头。1.检查浏览器开发者工具-Network标签查看请求是否携带了X-XSRF-TOKEN头值是否正确-Application/Storage标签查看https://api.mycompany.com域名下是否存在名为XSRF-TOKEN的Cookie其Path和Domain是否正确2.检查APISIX日志启用debug级别日志查看CSRF插件的验证过程通常会打印令牌比对结果。3.确认豁免配置检查请求的URI和方法是否在excluded_uri或excluded_methods中。跨域请求失败CORS报错CORS配置不正确尤其是携带凭证的跨域请求要求更严格。1. 确保APISIX的CORS插件中allow_credentials: true。2. 确保allow_origins是具体的源如https://app.mycompany.com不能是通配符*。3. 确保allow_headers包含了X-XSRF-TOKEN。4. 前端代码中fetch或axios请求必须设置credentials: include或withCredentials: true。Cookie在子域间不共享cookie_domain设置错误。1. 如果前端在app.mycompany.comAPI在api.mycompany.comcookie_domain必须设置为.mycompany.com注意开头的点。2. 确认APISIX自身是通过api.mycompany.com这个域名访问的因为Cookie的Domain需要与设置它的服务器域名匹配或为其父域。JavaScript无法读取CookieCookie被设置为HttpOnlytrue。在CSRF插件配置中必须将cookie_http_only设置为false。双提交Cookie机制依赖于JS读取Cookie值。HTTPS环境下Cookie不生效cookie_same_site设置为None但未设置Secure。在HTTPS环境下如果cookie_same_site为None则Cookie必须同时具有Secure属性。APISIX CSRF插件默认在HTTPS请求中会自动为Cookie添加Secure标志。检查响应头中的Set-Cookie是否包含Secure。令牌频繁过期用户体验差expires时间设置过短。适当增加expires值例如从3600调整为72002小时或864001天。同时前端可以考虑在令牌即将过期时如通过响应状态码403主动发起一个豁免的GET请求来刷新令牌。攻击绕过防护1. CORS配置过于宽松允许任意源和任意头。2. 存在其他XSS漏洞导致攻击者能窃取令牌。1.收紧CORS策略这是双提交Cookie机制生效的前提。确保allow_origins是白名单列表。2.实施全面的安全措施CSRF防护不能替代其他安全实践。确保应用没有XSS漏洞因为XSS可以直接窃取Cookie和令牌使CSRF防护失效。同时使用Content-Security-Policy等头来增强防护。一个高级排查技巧使用APISIX的proxy-rewrite插件进行调试。如果你怀疑是请求头或Cookie的问题可以临时添加这个插件将相关的头信息打印到响应体或日志中方便查看APISIX实际接收到了什么。proxy-rewrite: { headers: { X-Debug-CSRF-Cookie: $cookie_XSRF-TOKEN, // 将Cookie值放到一个自定义响应头 X-Debug-CSRF-Header: $http_x_xsrf_token // 将请求头值放到响应头 } }然后在前端查看响应头就能一目了然地看到网关收到的信息是否如你所愿。最后我个人在多次部署中的体会是APISIX的CSRF插件是一个“配置即正确”的组件一旦调通非常稳定。最大的挑战往往不在APISIX本身而在于前后端对跨域、Cookie策略的理解和统一配置上。建议在项目初期就由架构师或资深开发者统一制定这些安全规范并编写好前端SDK或拦截器避免每个开发团队重复踩坑。对于令牌的管理可以考虑将其与用户会话绑定在用户登出时使令牌失效这需要稍微定制化插件逻辑但对于金融等高安全场景是值得的。