1. 为什么我们需要自动携带CSRF Token
在Web应用开发中,CSRF(Cross-Site Request Forgery)攻击是一种常见的安全威胁。攻击者利用用户已登录的身份,在用户不知情的情况下执行非预期的操作。想象一下这样的场景:你登录了银行网站后,不小心点击了恶意链接,结果你的账户被自动转账——这就是典型的CSRF攻击。
CSRF Token的防御机制要求每个状态变更请求(POST、PUT、DELETE等)必须携带一个服务器颁发的随机Token。这个Token通常存储在:
- 表单的隐藏字段中
- 或者通过Cookie传递
但手动在每个请求中添加Token既繁琐又容易遗漏。这就是为什么我们需要在axios中实现自动携带CSRF Token的功能——它能让我们的应用更安全,同时减少开发者的重复劳动。
2. 理解CSRF Token的工作机制
2.1 服务器端的Token生成与验证
服务器通常会在用户会话开始时生成一个唯一的CSRF Token,并通过以下方式之一提供给客户端:
- 在渲染页面时嵌入到表单的隐藏字段中
- 通过Set-Cookie头设置到客户端的Cookie中
对于常见的实现方式,以Spring Security为例:
// Spring Security的默认CSRF保护配置 http.csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) );这种配置会让服务器:
- 生成名为XSRF-TOKEN的Cookie
- 期望客户端在后续请求的X-XSRF-TOKEN头中带回这个值
2.2 客户端的Token存储与携带
客户端需要完成以下工作流程:
- 从Cookie或DOM中提取CSRF Token
- 在后续的修改请求中自动附加这个Token
- 确保Token在页面刷新时能正确更新
3. 实现axios请求拦截器自动携带Token
3.1 基础拦截器实现
以下是完整的axios拦截器实现代码:
import axios from 'axios'; // 创建axios实例 const http = axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL, withCredentials: true // 允许携带cookie }); // 请求拦截器 http.interceptors.request.use(config => { // 只对写操作添加CSRF Token if (['post', 'put', 'delete', 'patch'].includes(config.method.toLowerCase())) { // 从cookie中获取CSRF Token const csrfToken = getCookie('XSRF-TOKEN'); if (csrfToken) { // 添加到请求头 config.headers['X-XSRF-TOKEN'] = csrfToken; } } return config; }, error => { return Promise.reject(error); }); // 从cookie中获取指定值的辅助函数 function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); } export default http;3.2 关键实现细节解析
withCredentials配置:
- 必须设置为true才能让浏览器在跨域请求中携带Cookie
- 对应服务器Access-Control-Allow-Credentials头也要设为true
请求方法过滤:
- 我们只对可能修改数据的请求方法添加CSRF Token
- GET、HEAD等安全方法通常不需要保护
Cookie处理:
- 直接从document.cookie中解析需要的Token值
- 注意处理Cookie不存在的情况
4. 高级配置与优化
4.1 双Token模式增强安全性
更安全的实现可以采用双Token模式:
- HttpOnly Cookie:存储主Token,JavaScript无法访问
- 可读Cookie:存储供JavaScript读取的副Token
服务器端配置示例(Spring Security):
http.csrf(csrf -> csrf .csrfTokenRepository(new CookieCsrfTokenRepository() { @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { // 主Token - HttpOnly String cookieValue = token == null ? "" : token.getToken(); Cookie cookie = new Cookie("CSRF-TOKEN", cookieValue); cookie.setHttpOnly(true); // 副Token - 可读 Cookie readableCookie = new Cookie("XSRF-TOKEN", cookieValue); response.addCookie(cookie); response.addCookie(readableCookie); } }) );客户端相应调整:
// 修改getCookie调用为读取XSRF-TOKEN而非CSRF-TOKEN const csrfToken = getCookie('XSRF-TOKEN');4.2 Token刷新机制
为防止Token过期,可以实现自动刷新机制:
http.interceptors.response.use(response => { return response; }, async error => { if (error.response.status === 403 && error.response.data.message === 'Invalid CSRF Token') { // Token过期,获取新Token后重试请求 await refreshCsrfToken(); return http(error.config); } return Promise.reject(error); }); async function refreshCsrfToken() { // 专门请求获取新Token await http.get('/auth/csrf'); // 服务器会在响应中设置新Cookie }5. 常见问题与解决方案
5.1 跨域问题排查清单
当CSRF Token未正确携带时,按以下步骤排查:
检查浏览器开发者工具的Application > Cookies:
- 确认XSRF-TOKEN Cookie存在且值正确
- 确认Cookie的Domain和Path匹配你的API地址
检查请求的Request Headers:
- 确认X-XSRF-TOKEN头存在且值正确
- 确认withCredentials: true已设置
检查服务器CORS配置:
@Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 必须 config.addAllowedOrigin("https://yourdomain.com"); // 不能是* config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); }
5.2 测试环境特殊处理
在测试环境中,你可能需要:
禁用CSRF保护进行测试:
@Profile("test") @Configuration public class NoCsrfSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); } }或者在测试代码中手动添加Token:
// Jest测试示例 describe('API测试', () => { beforeAll(() => { // 设置测试用的CSRF Token document.cookie = 'XSRF-TOKEN=test-token'; }); it('应该自动携带CSRF Token', async () => { const res = await http.post('/api/data', {}); expect(res.status).toBe(200); }); });
6. 安全增强建议
6.1 同源策略检查
可以在拦截器中增加额外的安全层:
http.interceptors.request.use(config => { // 检查请求目标是否同源 const requestUrl = new URL(config.url, config.baseURL); const currentUrl = new URL(window.location.href); if (requestUrl.origin !== currentUrl.origin) { console.warn(`跨源请求到 ${requestUrl.origin},请确认是否合法`); // 可以在这里添加额外的安全验证 } // ...原有CSRF逻辑 return config; });6.2 敏感操作二次验证
对于关键操作(如转账、密码修改),建议:
- 服务器要求额外的验证(如当前密码)
- 或者在前端实现验证码流程
async function transferMoney(amount, account) { // 先获取验证码 const { captchaId } = await http.get('/captcha'); const captcha = prompt('请输入验证码'); // 然后执行转账 return http.post('/transfer', { amount, account, captchaId, captcha }); }7. 实际项目中的经验总结
在大型项目中应用CSRF保护时,我们积累了一些宝贵经验:
Token命名一致性:
- 确保前后端使用相同的Token名称(如XSRF-TOKEN)
- 在微服务架构中,所有服务应该统一Token名称
SPA应用的特别处理:
- 在单页应用中,初始加载后可能不会刷新页面
- 需要定期(如每小时)主动获取新Token
性能考虑:
- Cookie的Path设置要合理,避免发送不必要的Cookie
- 对于静态资源请求,服务器应省略Set-Cookie头
日志监控:
http.interceptors.response.use(response => { if (response.headers['x-csrf-refreshed']) { log('CSRF Token已自动刷新'); } return response; });框架集成建议:
- 在Vue/React中,可以将axios实例挂载到全局属性
- 提供useHttp这样的组合式API方便组件使用
// Vue3示例 app.config.globalProperties.$http = http; // React示例 const HttpContext = createContext(http); function App() { return ( <HttpContext.Provider value={http}> {/* 组件树 */} </HttpContext.Provider> ); }