乐闻世界logo
搜索文章和话题

CSRF

CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种网络攻击方式,它允许攻击者利用用户已经认证的身份,在不知情的情况下,以该用户的名义执行恶意操作。这些操作可能包括提交表单、更改用户设置或进行金钱交易等。CSRF攻击适用于基于cookie的认证系统,因为浏览器会自动附带当前域下的cookie信息。
CSRF
CSRF 防护的性能影响有哪些,如何进行优化?CSRF 防护的性能影响和优化是生产环境中需要重点考虑的问题,特别是在高流量和高并发的场景下。 ## CSRF 防护的性能影响 ### 1. Token 生成开销 ```javascript // Token 生成的性能测试 const crypto = require('crypto'); function benchmarkTokenGeneration(iterations = 10000) { const start = Date.now(); for (let i = 0; i < iterations; i++) { crypto.randomBytes(32).toString('hex'); } const duration = Date.now() - start; const avgTime = duration / iterations; return { totalDuration: duration, iterations, avgTimePerToken: avgTime, tokensPerSecond: 1000 / avgTime }; } // 测试结果示例 const result = benchmarkTokenGeneration(); console.log(result); // { // totalDuration: 234, // iterations: 10000, // avgTimePerToken: 0.0234, // tokensPerSecond: 42735 // } ``` ### 2. Token 验证开销 ```javascript // Token 验证的性能测试 async function benchmarkTokenValidation(iterations = 10000) { const tokens = []; // 预生成 Token for (let i = 0; i < iterations; i++) { tokens.push(crypto.randomBytes(32).toString('hex')); } const start = Date.now(); // 模拟验证过程 for (const token of tokens) { // 假设的验证逻辑 const isValid = token.length === 64 && /^[a-f0-9]+$/.test(token); } const duration = Date.now() - start; const avgTime = duration / iterations; return { totalDuration: duration, iterations, avgTimePerValidation: avgTime, validationsPerSecond: 1000 / avgTime }; } ``` ### 3. 数据库查询开销 ```javascript // 数据库查询性能测试 async function benchmarkDatabaseQueries(iterations = 1000) { const queries = []; for (let i = 0; i < iterations; i++) { const userId = `user_${i}`; const token = crypto.randomBytes(32).toString('hex'); queries.push( db.query('SELECT * FROM csrf_tokens WHERE user_id = ? AND token = ?', [userId, token]) ); } const start = Date.now(); await Promise.all(queries); const duration = Date.now() - start; return { totalDuration: duration, iterations, avgTimePerQuery: duration / iterations, queriesPerSecond: 1000 / (duration / iterations) }; } ``` ## 性能优化策略 ### 1. Token 缓存优化 ```javascript // 使用 Redis 缓存 Token const redis = require('redis'); const client = redis.createClient(); class CachedTokenService { constructor() { this.localCache = new Map(); this.cacheTTL = 300000; // 5 分钟 this.maxCacheSize = 10000; } async getToken(userId) { // 首先检查本地缓存 const cached = this.localCache.get(userId); if (cached && Date.now() - cached.timestamp < this.cacheTTL) { return cached.token; } // 从 Redis 获取 const redisToken = await client.get(`csrf:${userId}`); if (redisToken) { this.updateLocalCache(userId, redisToken); return redisToken; } // 生成新 Token const newToken = crypto.randomBytes(32).toString('hex'); await client.setex(`csrf:${userId}`, 3600, newToken); this.updateLocalCache(userId, newToken); return newToken; } updateLocalCache(userId, token) { // LRU 缓存策略 if (this.localCache.size >= this.maxCacheSize) { const oldestKey = this.localCache.keys().next().value; this.localCache.delete(oldestKey); } this.localCache.set(userId, { token, timestamp: Date.now() }); } } ``` ### 2. 批量 Token 验证 ```javascript // 批量验证 Token 以减少数据库查询 class BatchTokenValidator { constructor(db) { this.db = db; this.batchSize = 100; } async validateBatch(requests) { const results = []; for (let i = 0; i < requests.length; i += this.batchSize) { const batch = requests.slice(i, i + this.batchSize); const batchResults = await this.validateSingleBatch(batch); results.push(...batchResults); } return results; } async validateSingleBatch(requests) { // 构造批量查询 const userIds = [...new Set(requests.map(r => r.userId))]; const tokens = requests.map(r => r.token); // 单次数据库查询 const validTokens = await this.db.query( 'SELECT user_id, token FROM csrf_tokens WHERE user_id IN (?) AND token IN (?)', [userIds, tokens] ); // 构造查找表 const validTokenSet = new Set( validTokens.map(t => `${t.user_id}:${t.token}`) ); // 验证每个请求 return requests.map(req => ({ userId: req.userId, token: req.token, isValid: validTokenSet.has(`${req.userId}:${req.token}`) })); } } ``` ### 3. 异步 Token 刷新 ```javascript // 异步刷新 Token 以避免阻塞请求 class AsyncTokenRefresher { constructor(tokenService) { this.tokenService = tokenService; this.refreshQueue = new Map(); this.refreshInProgress = new Set(); } async getToken(userId) { // 检查是否有正在进行的刷新 if (this.refreshInProgress.has(userId)) { return await this.waitForRefresh(userId); } // 获取当前 Token const currentToken = await this.tokenService.getToken(userId); // 检查是否需要刷新 if (this.shouldRefreshToken(currentToken)) { this.scheduleRefresh(userId); } return currentToken; } shouldRefreshToken(token) { // 检查 Token 是否即将过期(例如剩余时间 < 10 分钟) const tokenData = this.parseToken(token); const timeToExpiry = tokenData.expiresAt - Date.now(); return timeToExpiry < 600000; } scheduleRefresh(userId) { if (this.refreshInProgress.has(userId)) { return; } this.refreshInProgress.add(userId); // 异步刷新 setImmediate(async () => { try { const newToken = await this.tokenService.generateToken(userId); this.resolveRefreshQueue(userId, newToken); } catch (error) { this.rejectRefreshQueue(userId, error); } finally { this.refreshInProgress.delete(userId); } }); } waitForRefresh(userId) { return new Promise((resolve, reject) => { if (!this.refreshQueue.has(userId)) { this.refreshQueue.set(userId, []); } this.refreshQueue.get(userId).push({ resolve, reject }); }); } resolveRefreshQueue(userId, token) { const queue = this.refreshQueue.get(userId) || []; queue.forEach(item => item.resolve(token)); this.refreshQueue.delete(userId); } rejectRefreshQueue(userId, error) { const queue = this.refreshQueue.get(userId) || []; queue.forEach(item => item.reject(error)); this.refreshQueue.delete(userId); } } ``` ## 负载测试和监控 ### 1. 负载测试 ```javascript // 使用 Artillery 进行负载测试 // load-test.yml config: target: "http://localhost:3000" phases: - duration: 60 arrivalRate: 100 name: "Warm up" - duration: 120 arrivalRate: 500 name: "Ramp up" - duration: 300 arrivalRate: 1000 name: "Sustained load" scenarios: - name: "CSRF Token Generation" flow: - get: url: "/api/csrf-token" - name: "CSRF Token Validation" flow: - post: url: "/api/submit" headers: X-CSRF-Token: "{{ $randomString() }}" ``` ```bash # 运行负载测试 artillery run load-test.yml ``` ### 2. 性能监控 ```javascript // 性能监控中间件 const prometheus = require('prom-client'); // 创建指标 const csrfTokenGenerationDuration = new prometheus.Histogram({ name: 'csrf_token_generation_duration_seconds', help: 'Duration of CSRF token generation', labelNames: ['status'] }); const csrfTokenValidationDuration = new prometheus.Histogram({ name: 'csrf_token_validation_duration_seconds', help: 'Duration of CSRF token validation', labelNames: ['status'] }); const csrfCacheHitRate = new prometheus.Gauge({ name: 'csrf_cache_hit_rate', help: 'CSRF token cache hit rate' }); // 监控中间件 function csrfMetricsMiddleware(req, res, next) { const start = Date.now(); res.on('finish', () => { const duration = (Date.now() - start) / 1000; if (req.path === '/api/csrf-token') { csrfTokenGenerationDuration .labels({ status: res.statusCode }) .observe(duration); } else if (req.method !== 'GET') { csrfTokenValidationDuration .labels({ status: res.statusCode }) .observe(duration); } }); next(); } // 暴露指标端点 app.get('/metrics', (req, res) => { res.set('Content-Type', prometheus.register.contentType); res.end(prometheus.register.metrics()); }); ``` ### 3. 性能分析 ```javascript // 性能分析工具 class CSRFPerformanceAnalyzer { constructor() { this.metrics = { tokenGeneration: [], tokenValidation: [], cacheHits: 0, cacheMisses: 0 }; } recordTokenGeneration(duration) { this.metrics.tokenGeneration.push({ duration, timestamp: Date.now() }); } recordTokenValidation(duration, fromCache) { this.metrics.tokenValidation.push({ duration, fromCache, timestamp: Date.now() }); if (fromCache) { this.metrics.cacheHits++; } else { this.metrics.cacheMisses++; } } analyze() { const avgGenerationTime = this.calculateAverage( this.metrics.tokenGeneration.map(m => m.duration) ); const avgValidationTime = this.calculateAverage( this.metrics.tokenValidation.map(m => m.duration) ); const cacheHitRate = this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses); return { avgTokenGenerationTime: avgGenerationTime, avgTokenValidationTime: avgValidationTime, cacheHitRate, totalTokenGenerations: this.metrics.tokenGeneration.length, totalTokenValidations: this.metrics.tokenValidation.length }; } calculateAverage(values) { if (values.length === 0) return 0; return values.reduce((sum, val) => sum + val, 0) / values.length; } } ``` ## 优化建议 ### 1. 选择合适的 Token 存储方案 ```javascript // 不同存储方案的性能对比 const storageOptions = { redis: { readLatency: '1-5ms', writeLatency: '1-5ms', scalability: 'High', complexity: 'Medium', bestFor: 'Distributed systems, high traffic' }, database: { readLatency: '10-50ms', writeLatency: '10-50ms', scalability: 'Medium', complexity: 'Low', bestFor: 'Simple applications, low traffic' }, memory: { readLatency: '<1ms', writeLatency: '<1ms', scalability: 'Low', complexity: 'Low', bestFor: 'Single instance, low traffic' } }; ``` ### 2. 实施缓存策略 ```javascript // 多级缓存策略 class MultiLevelCache { constructor() { this.l1Cache = new Map(); // 内存缓存 this.l2Cache = null; // Redis 缓存 this.l3Cache = null; // 数据库 } async get(key) { // L1: 内存缓存 if (this.l1Cache.has(key)) { return this.l1Cache.get(key); } // L2: Redis 缓存 if (this.l2Cache) { const value = await this.l2Cache.get(key); if (value) { this.l1Cache.set(key, value); return value; } } // L3: 数据库 if (this.l3Cache) { const value = await this.l3Cache.get(key); if (value) { this.l1Cache.set(key, value); if (this.l2Cache) { await this.l2Cache.set(key, value); } return value; } } return null; } } ``` ### 3. 优化 Token 长度和复杂度 ```javascript // Token 长度和复杂度的权衡 const tokenConfigurations = { minimal: { length: 16, charset: '0123456789abcdef', entropy: 64, // bits collisionProbability: 'Very low', performance: 'Best' }, balanced: { length: 32, charset: '0123456789abcdef', entropy: 128, // bits collisionProbability: 'Extremely low', performance: 'Good' }, secure: { length: 64, charset: '0123456789abcdef', entropy: 256, // bits collisionProbability: 'Negligible', performance: 'Acceptable' } }; // 根据需求选择配置 function selectTokenConfig(requirements) { if (requirements.performance === 'critical') { return tokenConfigurations.minimal; } else if (requirements.security === 'critical') { return tokenConfigurations.secure; } else { return tokenConfigurations.balanced; } } ``` CSRF 防护的性能优化需要在安全性和性能之间找到平衡点,通过合理的架构设计和优化策略,可以在保证安全的同时提供良好的性能表现。
服务端 · 2月19日 21:06
REST API 中如何防护 CSRF 攻击,有哪些特殊考虑?REST API 中的 CSRF 防护与传统 Web 应用有所不同,因为 REST API 通常使用 JSON 格式进行数据交换,并且可能被各种客户端(Web、移动应用、第三方服务)调用。 ## REST API 中 CSRF 的特殊性 ### 1. 客户端多样性 - Web 应用:浏览器环境,自动发送 Cookie - 移动应用:原生环境,需要手动管理认证 - 第三方服务:API 调用,可能使用不同的认证方式 ### 2. 请求格式 - 传统 Web:表单提交,Content-Type: application/x-www-form-urlencoded - REST API:JSON 数据,Content-Type: application/json ### 3. 认证方式 - Cookie 认证:容易受到 CSRF 攻击 - Token 认证:JWT、OAuth 等,相对安全 - 混合认证:Cookie + Token ## REST API CSRF 防护策略 ### 1. 使用 Token 认证(推荐) ```javascript // JWT Token 认证 function authenticateJWT(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) { return res.status(401).send('No token provided'); } try { const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; next(); } catch (error) { return res.status(401).send('Invalid token'); } } // 保护路由 app.post('/api/transfer', authenticateJWT, (req, res) => { // 处理转账请求 }); ``` **优势**: - Token 存储在客户端(localStorage 或内存) - 不会自动发送,天然防护 CSRF - 适合移动应用和第三方集成 ### 2. 自定义请求头验证 ```javascript // 生成 CSRF Token function generateCSRFToken() { return crypto.randomBytes(32).toString('hex'); } // 设置 Token app.get('/api/csrf-token', (req, res) => { const token = generateCSRFToken(); req.session.csrfToken = token; res.json({ csrfToken: token }); }); // 验证自定义头 function validateCSRFHeader(req, res, next) { const token = req.headers['x-csrf-token']; if (!token || token !== req.session.csrfToken) { return res.status(403).send('Invalid CSRF token'); } next(); } // 保护路由 app.post('/api/transfer', validateCSRFHeader, (req, res) => { // 处理请求 }); ``` **前端实现**: ```javascript // 获取 Token async function getCSRFToken() { const response = await fetch('/api/csrf-token'); const data = await response.json(); return data.csrfToken; } // 发送请求 async function makeRequest() { const token = await getCSRFToken(); await fetch('/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token }, body: JSON.stringify({ to: 'user123', amount: 100 }) }); } ``` ### 3. SameSite Cookie 配置 ```javascript app.use(session({ secret: 'secret', cookie: { httpOnly: true, secure: true, sameSite: 'strict' // 或 'lax' } })); ``` ### 4. Origin 头验证 ```javascript function validateOrigin(req, res, next) { const origin = req.headers.origin; const allowedOrigins = ['https://example.com', 'https://app.example.com']; if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') { return next(); } if (!origin || !allowedOrigins.includes(origin)) { return res.status(403).send('Invalid origin'); } next(); } ``` ## 不同场景的防护方案 ### 场景 1:纯 Web 应用(浏览器) ```javascript // 使用 Cookie + CSRF Token app.use(session({ secret: 'secret', cookie: { httpOnly: true, secure: true, sameSite: 'lax' } })); app.use(csrf({ cookie: true })); app.post('/api/transfer', (req, res) => { // req.csrfToken() 可用于前端 }); ``` ### 场景 2:移动应用 + Web 应用 ```javascript // 混合认证策略 function authenticate(req, res, next) { // 优先使用 JWT Token const authHeader = req.headers.authorization; if (authHeader) { return authenticateJWT(req, res, next); } // 回退到 Cookie 认证 if (req.session.userId) { req.user = { id: req.session.userId }; return next(); } return res.status(401).send('Authentication required'); } // CSRF 防护仅对 Cookie 认证生效 function csrfProtection(req, res, next) { if (req.headers.authorization) { // JWT 认证,跳过 CSRF 验证 return next(); } // Cookie 认证,需要 CSRF 验证 if (req.body._csrf !== req.session.csrfToken) { return res.status(403).send('Invalid CSRF token'); } next(); } ``` ### 场景 3:第三方 API 集成 ```javascript // API Key 认证 function authenticateAPIKey(req, res, next) { const apiKey = req.headers['x-api-key']; if (!apiKey) { return res.status(401).send('API key required'); } // 验证 API Key const user = await validateAPIKey(apiKey); if (!user) { return res.status(401).send('Invalid API key'); } req.user = user; next(); } // API Key 认证不需要 CSRF 防护 app.post('/api/transfer', authenticateAPIKey, (req, res) => { // 处理请求 }); ``` ## CORS 配置与 CSRF ```javascript const corsOptions = { origin: function (origin, callback) { const allowedOrigins = ['https://example.com', 'https://app.example.com']; // 允许没有 origin 的请求(如移动应用) if (!origin) return callback(null, true); if (allowedOrigins.indexOf(origin) !== -1) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, // 允许发送 Cookie methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'] }; app.use(cors(corsOptions)); ``` ## 最佳实践总结 1. **优先使用 Token 认证**:JWT、OAuth 等天然防护 CSRF 2. **Cookie 认证必须防护**:SameSite + CSRF Token 3. **自定义请求头**:比表单字段更安全 4. **Origin 头验证**:补充防护措施 5. **CORS 正确配置**:限制允许的来源 6. **分层防护**:多种措施组合使用 REST API 的 CSRF 防护需要根据具体的使用场景和客户端类型来选择合适的策略,没有通用的解决方案。
服务端 · 2月19日 19:55
SameSite Cookie 属性如何防止 CSRF 攻击?SameSite Cookie 属性是防御 CSRF 攻击的重要机制,它控制 Cookie 在跨站请求中的发送行为。 ## SameSite 属性概述 SameSite 是 Cookie 的一个属性,用于指示浏览器是否应该在跨站请求中发送该 Cookie。它有三个可选值:`Strict`、`Lax` 和 `None`。 ## 属性值详解 ### 1. SameSite=Strict **行为**: - 只在同站请求中发送 Cookie - 跨站请求(包括导航)都不会发送 Cookie **适用场景**: - 银行、支付等高安全性应用 - 敏感操作(如转账、修改密码) - 不需要跨站功能的应用 **示例**: ```javascript // 设置 SameSite=Strict document.cookie = 'sessionid=abc123; SameSite=Strict; Secure; HttpOnly'; ``` **优点**: - 提供最强的 CSRF 防护 - 完全阻止跨站请求携带 Cookie **缺点**: - 用户从外部链接进入时需要重新登录 - 可能影响用户体验 ### 2. SameSite=Lax(推荐) **行为**: - 允许某些跨站请求发送 Cookie - 阻止大多数 CSRF 攻击 **允许的跨站请求**: - 顶级导航(GET 请求) - 链接跳转(`<a>` 标签) - 表单 GET 请求 **阻止的跨站请求**: - POST 请求(表单提交) - AJAX 请求 - `<iframe>`、`<img>`、`<script>` 等资源加载 **示例**: ```javascript // 设置 SameSite=Lax document.cookie = 'sessionid=abc123; SameSite=Lax; Secure; HttpOnly'; ``` **适用场景**: - 大多数 Web 应用 - 需要外部链接跳转的应用 - 平衡安全性和用户体验 **优点**: - 提供良好的 CSRF 防护 - 用户体验较好 - 现代浏览器默认值 **缺点**: - 某些跨站 POST 请求可能受影响 - 需要确保应用兼容性 ### 3. SameSite=None **行为**: - 允许所有跨站请求发送 Cookie - 必须配合 `Secure` 属性使用 **示例**: ```javascript // 设置 SameSite=None document.cookie = 'sessionid=abc123; SameSite=None; Secure; HttpOnly'; ``` **适用场景**: - 需要跨站功能的应用 - 第三方登录(如 OAuth) - 嵌入式内容 **优点**: - 不影响现有跨站功能 - 兼容旧应用 **缺点**: - 无法防御 CSRF 攻击 - 需要其他防护措施 ## 浏览器支持情况 ### 主流浏览器支持 - **Chrome**:51+ 版本支持,80+ 版本默认 Lax - **Firefox**:60+ 版本支持 - **Safari**:12+ 版本支持 - **Edge**:79+ 版本支持 - **Opera**:39+ 版本支持 ### 兼容性处理 ```javascript // 检测浏览器是否支持 SameSite function setCookie(name, value, days) { let expires = ''; if (days) { const date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); expires = '; expires=' + date.toUTCString(); } // 现代浏览器 let sameSite = '; SameSite=Lax'; // 旧版浏览器不支持 SameSite const cookieString = name + '=' + value + expires + sameSite + '; path=/; Secure; HttpOnly'; document.cookie = cookieString; } ``` ## 同站与跨站的定义 ### 同站(Same-Site) - 相同的顶级域名(eTLD+1) - 例如: - `https://example.com` 和 `https://www.example.com` 是同站 - `https://app.example.com` 和 `https://api.example.com` 是同站 ### 跨站(Cross-Site) - 不同的顶级域名 - 例如: - `https://example.com` 和 `https://evil.com` 是跨站 - `https://example.com` 和 `https://example.net` 是跨站 ## 实际应用示例 ### 1. 银行应用(Strict) ```javascript // 高安全性要求 document.cookie = 'sessionid=abc123; SameSite=Strict; Secure; HttpOnly; Max-Age=3600'; ``` ### 2. 电商网站(Lax) ```javascript // 平衡安全性和用户体验 document.cookie = 'sessionid=abc123; SameSite=Lax; Secure; HttpOnly; Max-Age=86400'; ``` ### 3. 第三方登录(None) ```javascript // 需要跨站功能 document.cookie = 'oauth_token=xyz789; SameSite=None; Secure; HttpOnly; Max-Age=3600'; ``` ## 框架集成 ### Express.js ```javascript app.use(session({ secret: 'your-secret', cookie: { secure: true, httpOnly: true, sameSite: 'lax' } })); ``` ### Spring Boot ```java @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and() .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } } ``` ### Django ```python # settings.py SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax' CSRF_COOKIE_SECURE = True CSRF_COOKIE_HTTPONLY = True CSRF_COOKIE_SAMESITE = 'Lax' ``` ## 常见问题 ### 1. SameSite=None 不生效 **原因**:缺少 `Secure` 属性 **解决**:必须同时设置 `Secure` 属性 ```javascript // 错误 document.cookie = 'sessionid=abc123; SameSite=None'; // 正确 document.cookie = 'sessionid=abc123; SameSite=None; Secure'; ``` ### 2. 跨站 POST 请求失败 **原因**:SameSite=Lax 阻止了跨站 POST 请求 **解决**: - 使用 SameSite=None(需要其他 CSRF 防护) - 改用同站请求 - 使用 CSRF Token ### 3. 第三方登录失败 **原因**:SameSite 属性阻止了跨站 Cookie **解决**: - 为特定 Cookie 设置 SameSite=None - 使用 OAuth 2.0 的授权码模式 - 使用 PostMessage 通信 ## 最佳实践 ### 1. 默认使用 SameSite=Lax ```javascript // 大多数应用的最佳选择 document.cookie = 'sessionid=abc123; SameSite=Lax; Secure; HttpOnly'; ``` ### 2. 敏感操作使用 SameSite=Strict ```javascript // 高安全性要求 document.cookie = 'admin_session=xyz789; SameSite=Strict; Secure; HttpOnly'; ``` ### 3. 避免 SameSite=None ```javascript // 尽量避免,除非必要 // 如果必须使用,配合其他防护措施 document.cookie = 'third_party_token=abc123; SameSite=None; Secure; HttpOnly'; ``` ### 4. 配合其他防护措施 - CSRF Token - Origin/Referer 验证 - 自定义请求头 ### 5. 测试兼容性 - 在不同浏览器中测试 - 测试跨站场景 - 测试第三方集成 ## 总结 SameSite Cookie 属性是防御 CSRF 攻击的有效手段。推荐使用 `SameSite=Lax` 作为默认配置,它在提供良好 CSRF 防护的同时保持良好的用户体验。对于高安全性要求的应用,可以使用 `SameSite=Strict`。尽量避免使用 `SameSite=None`,除非确实需要跨站功能,并配合其他防护措施。
服务端 · 2月19日 19:35
SameSite Cookie 属性如何防护 CSRF 攻击,有哪些使用场景?SameSite Cookie 属性是现代浏览器提供的一种有效防护 CSRF 攻击的机制,它通过控制 Cookie 在跨站请求中的发送行为来增强安全性。 ## SameSite 属性的三种值 ### 1. Strict(严格模式) - Cookie 只在同站请求中发送 - 跨站请求不会携带 Cookie - 提供最强的 CSRF 防护 - 可能影响用户体验(如从外部链接点击进入网站时不会携带 Cookie) ### 2. Lax(宽松模式,推荐) - 允许某些安全的跨站请求携带 Cookie - 允许的情况: - GET 请求 - 顶级导航(如点击链接) - 预加载请求 - 不允许的情况: - POST、PUT、DELETE 等修改性请求 - iframe、image、script 等资源请求 - 平衡了安全性和用户体验 ### 3. None(不限制) - 允许所有跨站请求携带 Cookie - 必须配合 Secure 属性使用 - 不提供 CSRF 防护 - 仅在特定场景下使用(如第三方登录) ## 实现方式 ### 设置 SameSite Cookie ```javascript // Node.js Express 示例 res.cookie('sessionId', 'abc123', { httpOnly: true, secure: true, sameSite: 'lax' // 或 'strict', 'none' }); // PHP 示例 setcookie('sessionId', 'abc123', [ 'httponly' => true, 'secure' => true, 'samesite' => 'Lax' ]); ``` ## SameSite 属性的兼容性 - **现代浏览器**:Chrome 51+、Firefox 60+、Safari 12+、Edge 79+ - **旧版浏览器**:不支持 SameSite 属性,需要其他防护措施 - **移动浏览器**:iOS Safari 12.2+、Android Chrome 51+ ## 最佳实践 1. **默认使用 Lax 模式**: - 提供良好的 CSRF 防护 - 保持正常的用户体验 - 适用于大多数应用场景 2. **敏感操作使用 Strict 模式**: - 涉及资金交易、权限变更等敏感操作 - 可以在特定路由或页面设置更严格的策略 3. **配合其他防护措施**: - CSRF Token - Referer 头验证 - 自定义 HTTP 头 4. **渐进增强策略**: - 检测浏览器是否支持 SameSite - 不支持时回退到其他防护机制 ## 注意事项 1. **Secure 属性要求**: - SameSite=None 必须配合 Secure 属性 - 需要使用 HTTPS 协议 2. **子域名行为**: - SameSite 将子域名视为同站 - a.example.com 和 b.example.com 是同站关系 3. **测试验证**: - 在不同浏览器中测试行为 - 验证跨站请求的正确处理 SameSite Cookie 属性是防护 CSRF 攻击的重要工具,但应该作为多层防护策略的一部分,而不是唯一的防护措施。
前端 · 2月19日 19:19
什么是双重提交 Cookie 防护 CSRF 的原理和实现方式?双重提交 Cookie(Double Submit Cookie)是一种 CSRF 防护技术,它通过在 Cookie 和请求参数中同时存储相同的 Token 来验证请求的合法性。 ## 双重提交 Cookie 的基本原理 1. **Token 生成**:服务器生成一个随机的 CSRF Token 2. **双重存储**:Token 同时存储在 Cookie 和请求参数中 3. **验证逻辑**:服务器验证 Cookie 中的 Token 和请求参数中的 Token 是否匹配 ## 实现步骤 ### 1. 生成 Token ```javascript function generateCSRFToken() { return crypto.randomBytes(32).toString('hex'); } // 中间件:生成并设置 Token function csrfTokenMiddleware(req, res, next) { const token = generateCSRFToken(); res.cookie('csrfToken', token, { httpOnly: false, // JavaScript 需要读取 secure: true, sameSite: 'strict' }); res.locals.csrfToken = token; next(); } ``` ### 2. 在表单中包含 Token ```html <form action="/api/submit" method="POST"> <input type="hidden" name="csrfToken" value="<%= csrfToken %>"> <!-- 其他表单字段 --> <button type="submit">提交</button> </form> <!-- 或者通过 JavaScript 设置 --> <script> const form = document.querySelector('form'); const csrfToken = document.querySelector('meta[name="csrf-token"]').content; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'csrfToken'; input.value = csrfToken; form.appendChild(input); </script> ``` ### 3. 验证 Token ```javascript function validateDoubleSubmitCookie(req) { const cookieToken = req.cookies.csrfToken; const paramToken = req.body.csrfToken || req.query.csrfToken; if (!cookieToken || !paramToken) { return false; } // 使用恒定时间比较防止时序攻击 return crypto.timingSafeEqual( Buffer.from(cookieToken), Buffer.from(paramToken) ); } // 验证中间件 function csrfProtection(req, res, next) { if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') { return next(); } if (!validateDoubleSubmitCookie(req)) { return res.status(403).send('CSRF token validation failed'); } next(); } ``` ## 工作原理 ### 为什么双重提交有效? 1. **同源策略**:恶意网站无法读取目标网站的 Cookie 2. **跨站请求限制**:恶意网站无法在请求参数中包含正确的 Token 3. **匹配验证**:只有同源请求才能同时访问 Cookie 和设置请求参数 ### 攻击场景分析 ```html <!-- 恶意网站尝试发起 CSRF 攻击 --> <form action="https://example.com/api/transfer" method="POST"> <input type="hidden" name="to" value="attacker"> <input type="hidden" name="amount" value="1000"> <!-- 无法获取正确的 csrfToken --> </form> <script> document.querySelector('form').submit(); </script> ``` - 恶意网站可以发起请求 - 浏览器会自动发送 Cookie 中的 Token - 但恶意网站无法在请求参数中包含正确的 Token - 服务器验证失败,拒绝请求 ## 优势 1. **无需服务器状态**:不需要在服务器端存储 Token 2. **易于实现**:实现相对简单 3. **可扩展性**:适合分布式系统 4. **性能好**:不需要查询数据库或 Session ## 局限性 1. **Cookie 安全性**: - 如果 Cookie 被窃取(XSS),防护失效 - 需要配合 HttpOnly 使用(但 JavaScript 无法读取) 2. **子域名风险**: - 如果子域名存在 XSS 漏洞,可能影响主域名 - 需要谨慎设置 Cookie 的域属性 3. **Token 泄露**: - 如果 Token 在 URL 中暴露,可能被记录在日志中 - 应该使用 POST 请求传递 Token ## 最佳实践 ### 1. 结合其他防护措施 ```javascript app.use(helmet()); // XSS 防护 app.use(cookieSession({ secret: 'secret', cookie: { httpOnly: true, secure: true, sameSite: 'strict' } })); app.use(csrfTokenMiddleware); app.use(csrfProtection); ``` ### 2. Token 刷新策略 ```javascript // 每次请求后刷新 Token function refreshTokenMiddleware(req, res, next) { if (req.method !== 'GET' && req.method !== 'HEAD') { const newToken = generateCSRFToken(); res.cookie('csrfToken', newToken, { httpOnly: false, secure: true, sameSite: 'strict' }); res.locals.csrfToken = newToken; } next(); } ``` ### 3. 安全配置 ```javascript // Cookie 配置 res.cookie('csrfToken', token, { httpOnly: false, // 允许 JavaScript 读取 secure: true, // 仅 HTTPS sameSite: 'strict', // 最严格的同站策略 maxAge: 3600000, // 1小时过期 domain: '.example.com' // 谨慎设置域 }); ``` ## 与 CSRF Token 的对比 | 特性 | 双重提交 Cookie | 传统 CSRF Token | |------|----------------|----------------| | 服务器状态 | 无需 | 需要 Session | | 实现复杂度 | 简单 | 中等 | | 分布式支持 | 优秀 | 需要共享 Session | | 安全性 | 良好 | 优秀 | | 性能 | 优秀 | 良好 | 双重提交 Cookie 是一种有效的 CSRF 防护技术,特别适合分布式系统和需要高性能的场景。但应该与其他安全措施配合使用,提供全面的安全保护。
服务端 · 2月19日 19:15
在前端框架(React、Vue、Angular)中如何实现 CSRF 防护?前端框架(如 React、Vue、Angular)中的 CSRF 防护需要考虑框架特性和最佳实践,以确保在单页应用(SPA)中提供有效的安全保护。 ## React 中的 CSRF 防护 ### 1. 使用 CSRF Token ```jsx // 获取 CSRF Token import { useEffect, useState } from 'react'; function CSRFProtectedForm() { const [csrfToken, setCsrfToken] = useState(''); const [formData, setFormData] = useState({ name: '', email: '' }); useEffect(() => { // 从服务器获取 CSRF Token fetch('/api/csrf-token') .then(res => res.json()) .then(data => setCsrfToken(data.csrfToken)); }, []); const handleSubmit = async (e) => { e.preventDefault(); try { const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken // 在请求头中发送 Token }, body: JSON.stringify(formData) }); if (response.ok) { alert('提交成功!'); } else { alert('提交失败'); } } catch (error) { console.error('Error:', error); } }; return ( <form onSubmit={handleSubmit}> <input type="text" value={formData.name} onChange={(e) => setFormData({...formData, name: e.target.value})} placeholder="姓名" /> <input type="email" value={formData.email} onChange={(e) => setFormData({...formData, email: e.target.value})} placeholder="邮箱" /> <button type="submit">提交</button> </form> ); } ``` ### 2. 使用 Axios 拦截器 ```jsx import axios from 'axios'; // 创建 Axios 实例 const api = axios.create({ baseURL: '/api', withCredentials: true // 允许发送 Cookie }); // 请求拦截器:自动添加 CSRF Token api.interceptors.request.use(async (config) => { // 对于需要 CSRF 保护的请求方法 if (['post', 'put', 'patch', 'delete'].includes(config.method)) { try { const response = await axios.get('/api/csrf-token'); config.headers['X-CSRF-Token'] = response.data.csrfToken; } catch (error) { console.error('Failed to get CSRF token:', error); } } return config; }); // 使用示例 function SubmitForm() { const handleSubmit = async () => { try { await api.post('/submit', { data: 'example' }); alert('提交成功!'); } catch (error) { alert('提交失败'); } }; return <button onClick={handleSubmit}>提交</button>; } ``` ## Vue 中的 CSRF 防护 ### 1. 使用 Vue Router 和 Axios ```vue <template> <form @submit.prevent="handleSubmit"> <input v-model="formData.name" placeholder="姓名" /> <input v-model="formData.email" placeholder="邮箱" /> <button type="submit">提交</button> </form> </template> <script> import axios from 'axios'; export default { data() { return { csrfToken: '', formData: { name: '', email: '' } }; }, async created() { // 获取 CSRF Token const response = await axios.get('/api/csrf-token'); this.csrfToken = response.data.csrfToken; }, methods: { async handleSubmit() { try { const response = await axios.post('/api/submit', this.formData, { headers: { 'X-CSRF-Token': this.csrfToken } }); if (response.data.success) { alert('提交成功!'); } } catch (error) { alert('提交失败'); } } } }; </script> ``` ### 2. 使用 Vuex 管理 CSRF Token ```javascript // store/csrf.js import axios from 'axios'; export default { namespaced: true, state: { token: null }, mutations: { SET_TOKEN(state, token) { state.token = token; } }, actions: { async fetchToken({ commit }) { try { const response = await axios.get('/api/csrf-token'); commit('SET_TOKEN', response.data.csrfToken); } catch (error) { console.error('Failed to fetch CSRF token:', error); } } } }; ``` ## Angular 中的 CSRF 防护 ### 1. 使用 HttpClient 拦截器 ```typescript // csrf.interceptor.ts import { Injectable } from '@angular/core'; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; import { Observable, from } from 'rxjs'; import { switchMap } from 'rxjs/operators'; @Injectable() export class CsrfInterceptor implements HttpInterceptor { constructor(private http: HttpClient) {} intercept( req: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { // 对于需要 CSRF 保护的请求方法 if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) { return from(this.getCsrfToken()).pipe( switchMap(token => { const csrfReq = req.clone({ setHeaders: { 'X-CSRF-Token': token } }); return next.handle(csrfReq); }) ); } return next.handle(req); } private async getCsrfToken(): Promise<string> { const response = await this.http.get<{ csrfToken: string }>('/api/csrf-token').toPromise(); return response.csrfToken; } } // app.module.ts import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { CsrfInterceptor } from './csrf.interceptor'; @NgModule({ imports: [HttpClientModule], providers: [ { provide: HTTP_INTERCEPTORS, useClass: CsrfInterceptor, multi: true } ] }) export class AppModule {} ``` ## 通用最佳实践 ### 1. Token 刷新策略 ```javascript // 通用 Token 刷新逻辑 class CSRFTokenManager { constructor() { this.token = null; this.tokenExpiry = null; } async getToken() { // 检查 Token 是否过期 if (!this.token || Date.now() > this.tokenExpiry) { await this.refreshToken(); } return this.token; } async refreshToken() { const response = await fetch('/api/csrf-token'); const data = await response.json(); this.token = data.csrfToken; this.tokenExpiry = Date.now() + 3600000; // 1 小时后过期 } clearToken() { this.token = null; this.tokenExpiry = null; } } ``` ### 2. 错误处理和重试 ```javascript // 带有重试机制的请求 async function makeRequestWithRetry(url, data, maxRetries = 3) { let retries = 0; while (retries < maxRetries) { try { const token = await csrfTokenManager.getToken(); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token }, body: JSON.stringify(data) }); if (response.status === 403) { // Token 可能已过期,刷新并重试 csrfTokenManager.clearToken(); retries++; continue; } return await response.json(); } catch (error) { retries++; if (retries >= maxRetries) { throw error; } } } } ``` ### 3. 安全配置 ```javascript // Cookie 配置(服务器端) res.cookie('sessionId', sessionId, { httpOnly: true, // 防止 XSS 窃取 secure: true, // 仅 HTTPS sameSite: 'strict', // 最严格的 CSRF 防护 maxAge: 3600000 // 1 小时过期 }); ``` ## 框架特定的注意事项 ### React - 使用 Context API 共享 CSRF Token - 考虑使用 React Query 或 SWR 管理请求 - 在组件卸载时清理 Token ### Vue - 使用 Vuex 或 Pinia 管理 Token 状态 - 利用 Vue 的生命周期钩子获取 Token - 考虑使用 VueUse 的 useFetch ### Angular - 使用 HTTP 拦截器自动处理 Token - 利用依赖注入管理 Token 服务 - 使用 RxJS 处理异步操作 前端框架中的 CSRF 防护需要结合框架特性和最佳实践,确保在提供良好用户体验的同时,不牺牲安全性。
服务端 · 2月19日 19:13
移动应用中如何实施 CSRF 防护,有哪些特殊考虑?移动应用中的 CSRF 防护与 Web 应用有所不同,因为移动应用通常不使用浏览器自动发送 Cookie 的机制,但仍然需要考虑各种安全风险。 ## 移动应用 CSRF 的特殊性 ### 1. 认证方式差异 **Web 应用**: - 使用 Cookie 存储 Session ID - 浏览器自动发送 Cookie - 容易受到 CSRF 攻击 **移动应用**: - 使用 Token(JWT、OAuth) - 手动管理认证信息 - 相对不易受到传统 CSRF 攻击 - 但存在其他安全风险 ### 2. 网络环境差异 ```javascript // 移动应用面临的网络挑战 const mobileChallenges = { networkUnreliable: '网络不稳定可能导致重放攻击', deviceCompromise: '设备被 Root 或越狱', appTampering: '应用被篡改或重新打包', insecureStorage: '不安全的本地存储' }; ``` ## 移动应用 CSRF 防护策略 ### 1. 使用 Token 认证(推荐) #### iOS 实现 ```swift // Swift - Token 认证管理 class TokenManager { static let shared = TokenManager() private let keychain = Keychain() func saveToken(_ token: String) { // 使用 Keychain 安全存储 Token keychain["authToken"] = token } func getToken() -> String? { return keychain["authToken"] } func clearToken() { keychain["authToken"] = nil } } // 网络请求管理器 class NetworkManager { static let shared = NetworkManager() private let session = URLSession.shared func request<T: Decodable>( _ endpoint: String, method: String = "GET", body: Data? = nil, completion: @escaping (Result<T, Error>) -> Void ) { guard let url = URL(string: endpoint) else { completion(.failure(NetworkError.invalidURL)) return } var request = URLRequest(url: url) request.httpMethod = method request.httpBody = body // 添加认证 Token if let token = TokenManager.shared.getToken() { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } session.dataTask(with: request) { data, response, error in if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(NetworkError.noData)) return } do { let result = try JSONDecoder().decode(T.self, from: data) completion(.success(result)) } catch { completion(.failure(error)) } }.resume() } } ``` #### Android 实现 ```java // Java - Token 认证管理 public class TokenManager { private static TokenManager instance; private SharedPreferences preferences; private TokenManager(Context context) { preferences = context.getSharedPreferences("auth", Context.MODE_PRIVATE); } public static synchronized TokenManager getInstance(Context context) { if (instance == null) { instance = new TokenManager(context); } return instance; } public void saveToken(String token) { preferences.edit().putString("authToken", token).apply(); } public String getToken() { return preferences.getString("authToken", null); } public void clearToken() { preferences.edit().remove("authToken").apply(); } } // 网络请求拦截器 public class AuthInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); // 添加认证 Token String token = TokenManager.getInstance(context).getToken(); if (token != null) { Request authenticatedRequest = originalRequest.newBuilder() .header("Authorization", "Bearer " + token) .build(); return chain.proceed(authenticatedRequest); } return chain.proceed(originalRequest); } } ``` ### 2. 设备指纹识别 ```javascript // 服务器端 - 设备指纹验证 class DeviceFingerprintService { async generateFingerprint(deviceInfo) { const fingerprintData = { deviceId: deviceInfo.deviceId, os: deviceInfo.os, osVersion: deviceInfo.osVersion, appVersion: deviceInfo.appVersion, screenResolution: deviceInfo.screenResolution, timestamp: Date.now() }; // 生成设备指纹 const fingerprint = crypto.createHash('sha256') .update(JSON.stringify(fingerprintData)) .digest('hex'); return fingerprint; } async validateFingerprint(userId, fingerprint) { const storedFingerprint = await this.getStoredFingerprint(userId); if (!storedFingerprint) { // 首次使用,存储指纹 await this.storeFingerprint(userId, fingerprint); return true; } // 验证指纹是否匹配 return storedFingerprint === fingerprint; } } ``` ### 3. 请求签名 ```swift // Swift - 请求签名 class RequestSigner { static func signRequest(_ request: URLRequest, secretKey: String) -> URLRequest { var signedRequest = request // 生成时间戳和随机数 let timestamp = String(Int(Date().timeIntervalSince1970)) let nonce = UUID().uuidString // 构造签名字符串 let method = request.httpMethod ?? "GET" let url = request.url?.absoluteString ?? "" let body = request.httpBody?.base64EncodedString() ?? "" let signString = "\(method)\n\(url)\n\(timestamp)\n\(nonce)\n\(body)" // 生成 HMAC-SHA256 签名 let signature = signString.hmacSHA256(key: secretKey) // 添加签名头 signedRequest.setValue(timestamp, forHTTPHeaderField: "X-Timestamp") signedRequest.setValue(nonce, forHTTPHeaderField: "X-Nonce") signedRequest.setValue(signature, forHTTPHeaderField: "X-Signature") return signedRequest } } // HMAC-SHA256 扩展 extension String { func hmacSHA256(key: String) -> String { let keyData = key.data(using: .utf8)! let messageData = self.data(using: .utf8)! var digestData = Data(count: Int(CC_SHA256_DIGEST_LENGTH)) keyData.withUnsafeBytes { keyBytes in messageData.withUnsafeBytes { messageBytes in CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyBytes.baseAddress, keyData.count, messageBytes.baseAddress, messageData.count, &digestData) } } return digestData.base64EncodedString() } } ``` ```java // Java - 请求签名 public class RequestSigner { public static Request signRequest(Request request, String secretKey) { // 生成时间戳和随机数 String timestamp = String.valueOf(System.currentTimeMillis() / 1000); String nonce = UUID.randomUUID().toString(); // 构造签名字符串 String method = request.method(); String url = request.url().toString(); String body = bodyToString(request.body()); String signString = method + "\n" + url + "\n" + timestamp + "\n" + nonce + "\n" + body; // 生成 HMAC-SHA256 签名 String signature = hmacSHA256(signString, secretKey); // 添加签名头 return request.newBuilder() .addHeader("X-Timestamp", timestamp) .addHeader("X-Nonce", nonce) .addHeader("X-Signature", signature) .build(); } private static String hmacSHA256(String data, String key) { try { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "HmacSHA256"); mac.init(secretKeySpec); byte[] hash = mac.doFinal(data.getBytes()); return Base64.encodeToString(hash, Base64.NO_WRAP); } catch (Exception e) { throw new RuntimeException("Failed to generate signature", e); } } } ``` ### 4. 双因素认证 ```swift // Swift - 双因素认证 class TwoFactorAuthManager { static func verifyOTP(userId: String, otp: String, completion: @escaping (Bool) -> Void) { // 发送 OTP 到服务器验证 let endpoint = "https://api.example.com/verify-otp" var request = URLRequest(url: URL(string: endpoint)!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body = [ "userId": userId, "otp": otp ] request.httpBody = try? JSONSerialization.data(withJSONObject: body) URLSession.shared.dataTask(with: request) { data, response, error in if let httpResponse = response as? HTTPURLResponse { completion(httpResponse.statusCode == 200) } else { completion(false) } }.resume() } } ``` ## 服务器端验证 ### 1. Token 验证中间件 ```javascript // Express.js - Token 验证中间件 const jwt = require('jsonwebtoken'); function authenticateMobile(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'No token provided' }); } const token = authHeader.substring(7); try { const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; next(); } catch (error) { return res.status(401).json({ error: 'Invalid token' }); } } // 请求签名验证中间件 function verifySignature(req, res, next) { const timestamp = req.headers['x-timestamp']; const nonce = req.headers['x-nonce']; const signature = req.headers['x-signature']; if (!timestamp || !nonce || !signature) { return res.status(401).json({ error: 'Missing signature headers' }); } // 验证时间戳(防止重放攻击) const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) { // 5 分钟 return res.status(401).json({ error: 'Request expired' }); } // 验证 nonce(防止重放攻击) if (isNonceUsed(nonce)) { return res.status(401).json({ error: 'Nonce already used' }); } // 验证签名 const expectedSignature = generateSignature(req, timestamp, nonce); if (signature !== expectedSignature) { return res.status(401).json({ error: 'Invalid signature' }); } // 标记 nonce 为已使用 markNonceAsUsed(nonce); next(); } ``` ### 2. 设备指纹验证 ```javascript // 设备指纹验证中间件 function verifyDeviceFingerprint(req, res, next) { const userId = req.user.id; const fingerprint = req.headers['x-device-fingerprint']; if (!fingerprint) { return res.status(401).json({ error: 'Device fingerprint required' }); } deviceFingerprintService.validateFingerprint(userId, fingerprint) .then(isValid => { if (!isValid) { return res.status(401).json({ error: 'Invalid device fingerprint' }); } next(); }) .catch(error => { res.status(500).json({ error: 'Fingerprint validation failed' }); }); } ``` ## 最佳实践 ### 1. 安全存储 ```swift // iOS - 使用 Keychain import Security class KeychainHelper { static func save(key: String, data: Data) -> Bool { let query = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data ] as [String: Any] SecItemDelete(query as CFDictionary) return SecItemAdd(query as CFDictionary, nil) == errSecSuccess } static func load(key: String) -> Data? { let query = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] as [String: Any] var dataTypeRef: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) if status == errSecSuccess { return dataTypeRef as? Data } return nil } } ``` ```java // Android - 使用 EncryptedSharedPreferences public class SecureStorage { private static EncryptedSharedPreferences preferences; public static void init(Context context) { try { MasterKey masterKey = new MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); preferences = (EncryptedSharedPreferences) EncryptedSharedPreferences.create( context, "secure_prefs", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); } catch (Exception e) { throw new RuntimeException("Failed to initialize secure storage", e); } } public static void saveString(String key, String value) { preferences.edit().putString(key, value).apply(); } public static String getString(String key) { return preferences.getString(key, null); } } ``` ### 2. 证书绑定 ```swift // iOS - SSL 证书绑定 class CertificatePinning { static func validateCertificate(for serverTrust: SecTrust) -> Bool { // 获取服务器证书 guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else { return false } // 获取证书数据 let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data // 加载本地证书 guard let localCertificatePath = Bundle.main.path(forResource: "certificate", ofType: "cer"), let localCertificateData = try? Data(contentsOf: URL(fileURLWithPath: localCertificatePath)) else { return false } // 比较证书 return serverCertificateData == localCertificateData } } ``` 移动应用的 CSRF 防护需要结合平台特性和安全最佳实践,确保在各种网络环境和设备状态下都能提供有效的安全保护。
服务端 · 2月19日 17:57
在 Spring Boot 中如何实现 CSRF 防护?在 Spring Boot 中实现 CSRF 防护有多种方式,Spring Security 提供了内置的 CSRF 保护机制。 ## Spring Security CSRF 保护概述 Spring Security 默认启用 CSRF 保护,它通过以下方式工作: - 生成 CSRF Token - 将 Token 存储在服务器会话中 - 在表单中自动添加 Token - 验证请求中的 Token ## 配置方式 ### 1. 使用默认配置(推荐) Spring Security 默认启用 CSRF 保护,无需额外配置。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll(); } } ``` ### 2. 自定义 CSRF Token 存储 默认使用 HttpSessionCsrfTokenRepository,可以自定义存储方式。 #### 使用 Cookie 存储 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .authorizeRequests() .anyRequest().authenticated(); } } ``` #### 自定义 Token Repository ```java public class CustomCsrfTokenRepository implements CsrfTokenRepository { @Override public CsrfToken generateToken(HttpServletRequest request) { String token = UUID.randomUUID().toString(); return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { // 自定义存储逻辑 request.getSession().setAttribute("_csrf", token); } @Override public CsrfToken loadToken(HttpServletRequest request) { // 自定义加载逻辑 return (CsrfToken) request.getSession().getAttribute("_csrf"); } } @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(new CustomCsrfTokenRepository()) .and() .authorizeRequests() .anyRequest().authenticated(); } } ``` ### 3. 禁用 CSRF 保护(不推荐) 某些情况下可能需要禁用 CSRF 保护(如 REST API)。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .disable() .and() .authorizeRequests() .anyRequest().authenticated(); } } ``` ### 4. 部分禁用 CSRF 保护 只为特定路径禁用 CSRF 保护。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .ignoringAntMatchers("/api/**", "/public/**") .and() .authorizeRequests() .antMatchers("/api/**").permitAll() .anyRequest().authenticated(); } } ``` ## 前端集成 ### 1. Thymeleaf 模板 Spring Security 自动在 Thymeleaf 模板中添加 CSRF Token。 ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Login</title> </head> <body> <form th:action="@{/login}" method="post"> <!-- CSRF Token 自动添加 --> <input type="text" name="username" placeholder="Username"> <input type="password" name="password" placeholder="Password"> <button type="submit">Login</button> </form> </body> </html> ``` ### 2. JSP 模板 ```jsp <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <form action="/login" method="post"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <input type="text" name="username" placeholder="Username"> <input type="password" name="password" placeholder="Password"> <button type="submit">Login</button> </form> ``` ### 3. AJAX 请求 ```javascript // 获取 CSRF Token function getCsrfToken() { const metaTag = document.querySelector('meta[name="_csrf"]'); const headerName = document.querySelector('meta[name="_csrf_header"]'); return { token: metaTag ? metaTag.getAttribute('content') : '', headerName: headerName ? headerName.getAttribute('content') : 'X-CSRF-TOKEN' }; } // 发送 AJAX 请求 function sendAjaxRequest(url, data) { const { token, headerName } = getCsrfToken(); return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', [headerName]: token }, body: JSON.stringify(data) }); } // 使用示例 sendAjaxRequest('/api/data', { name: 'John' }) .then(response => response.json()) .then(data => console.log(data)); ``` ### 4. 在 HTML 中添加 Meta 标签 ```html <!DOCTYPE html> <html> <head> <meta name="_csrf" th:content="${_csrf.token}"/> <meta name="_csrf_header" th:content="${_csrf.headerName}"/> <title>My App</title> </head> <body> <!-- 页面内容 --> </body> </html> ``` ## 高级配置 ### 1. 自定义 CSRF Token 生成器 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(csrfTokenRepository()) .and() .authorizeRequests() .anyRequest().authenticated(); } @Bean public CsrfTokenRepository csrfTokenRepository() { HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository(); repository.setHeaderName("X-CSRF-TOKEN"); repository.setParameterName("_csrf"); return repository; } } ``` ### 2. 自定义 CSRF Token 验证器 ```java public class CustomCsrfTokenValidator implements CsrfTokenValidator { @Override public boolean validateToken(HttpServletRequest request, CsrfToken token) { // 自定义验证逻辑 String requestToken = request.getHeader(token.getHeaderName()); return token.getToken().equals(requestToken); } } @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenValidator(new CustomCsrfTokenValidator()) .and() .authorizeRequests() .anyRequest().authenticated(); } } ``` ### 3. 配置 SameSite Cookie ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setCookieName("SESSION"); serializer.setCookiePath("/"); serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); serializer.setSameSite("Lax"); serializer.setUseHttpOnlyCookie(true); serializer.setUseSecureCookie(true); return serializer; } } ``` ## 测试 CSRF 保护 ### 1. 单元测试 ```java @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class CsrfProtectionTest { @Autowired private MockMvc mockMvc; @Test public void testCsrfProtection() throws Exception { mockMvc.perform(post("/transfer") .contentType(MediaType.APPLICATION_JSON) .content("{\"amount\":100}")) .andExpect(status().isForbidden()); } @Test public void testWithValidCsrfToken() throws Exception { MvcResult result = mockMvc.perform(get("/csrf")) .andReturn(); String csrfToken = result.getResponse().getContentAsString(); mockMvc.perform(post("/transfer") .contentType(MediaType.APPLICATION_JSON) .header("X-CSRF-TOKEN", csrfToken) .content("{\"amount\":100}")) .andExpect(status().isOk()); } } ``` ### 2. 集成测试 ```java @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc public class CsrfIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Test public void testCsrfWithRestTemplate() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); // 没有 CSRF Token HttpEntity<String> request = new HttpEntity<>("{\"amount\":100}", headers); ResponseEntity<String> response = restTemplate.postForEntity("/transfer", request, String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); } } ``` ## 常见问题 ### 1. AJAX 请求 403 错误 **原因**:缺少 CSRF Token **解决**:在请求头中添加 CSRF Token ```javascript fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': getCsrfToken() }, body: JSON.stringify(data) }); ``` ### 2. 多标签页 Token 失效 **原因**:会话过期或 Token 不匹配 **解决**:确保所有标签页使用同一个会话 ### 3. 文件上传失败 **原因**:文件上传无法使用表单 Token **解决**:使用请求头或预签名 URL ## 最佳实践 1. **使用默认配置**:Spring Security 默认配置已经足够安全 2. **使用 Cookie 存储 Token**:便于前端获取 3. **为 AJAX 请求添加 Token**:确保所有请求都包含 Token 4. **定期更新 Token**:降低 Token 泄露风险 5. **配合其他防护措施**:如 SameSite Cookie、Origin 验证 6. **测试 CSRF 保护**:确保防护机制正常工作 ## 总结 Spring Boot 通过 Spring Security 提供了完善的 CSRF 保护机制。默认配置已经足够安全,可以根据需要自定义 Token 存储方式和验证逻辑。前端需要确保所有请求都包含有效的 CSRF Token,特别是 AJAX 请求。配合其他防护措施可以构建更强大的安全防护体系。
服务端 · 2月19日 17:52
如何通过验证 Referer 头来防护 CSRF 攻击,有哪些局限性?验证 Referer 头是防护 CSRF 攻击的一种传统方法,通过检查 HTTP 请求的 Referer 头来验证请求来源的合法性。 ## Referer 头的基本原理 Referer 头是 HTTP 请求头的一部分,它包含了发起请求的页面 URL。服务器可以通过检查 Referer 头来判断请求是否来自受信任的来源。 ## 验证 Referer 头的实现 ### 基本验证逻辑 ```javascript function validateReferer(req, trustedDomains) { const referer = req.headers.referer; if (!referer) { return false; // 拒绝没有 Referer 的请求 } try { const refererUrl = new URL(referer); const refererDomain = refererUrl.hostname; return trustedDomains.some(domain => refererDomain === domain || refererDomain.endsWith('.' + domain) ); } catch (error) { return false; // 无效的 URL } } // 使用示例 app.post('/api/transfer', (req, res) => { const trustedDomains = ['example.com', 'www.example.com']; if (!validateReferer(req, trustedDomains)) { return res.status(403).send('Invalid referer'); } // 处理转账请求 }); ``` ## 验证策略 ### 1. 严格匹配 - Referer 必须完全匹配受信任的域名 - 提供最强的安全性 - 可能影响某些合法请求 ### 2. 域名匹配 - 允许子域名 - 更灵活的验证 - 需要确保所有子域名都是受信任的 ### 3. 允许空 Referer - 某些情况下 Referer 可能为空(如直接输入 URL) - 需要结合其他验证方式 - 降低安全性 ## Referer 头的局限性 ### 1. 隐私设置 - 某些浏览器或隐私插件可能阻止发送 Referer - 用户可以禁用 Referer 头 - 导致合法请求被拒绝 ### 2. 可伪造性 - Referer 头可以被攻击者伪造 - 不是绝对安全的防护方式 - 需要配合其他防护措施 ### 3. HTTPS 降级 - 从 HTTPS 页面发起 HTTP 请求时,Referer 可能被移除 - 混合内容场景下的处理复杂 ### 4. 浏览器兼容性 - 不同浏览器对 Referer 的处理可能不同 - 某些移动浏览器的行为不一致 ## 最佳实践 ### 1. 多层防护 ```javascript function csrfProtection(req, res, next) { // 验证 Referer if (!validateReferer(req, trustedDomains)) { return res.status(403).send('Invalid referer'); } // 验证 CSRF Token if (req.body._csrf !== req.session.csrfToken) { return res.status(403).send('Invalid CSRF token'); } next(); } ``` ### 2. 配置 Referer-Policy ```html <meta name="referrer" content="strict-origin-when-cross-origin"> ``` ### 3. 白名单管理 - 维护受信任的域名列表 - 定期更新和审查 - 支持动态配置 ### 4. 日志记录 ```javascript if (!validateReferer(req, trustedDomains)) { logger.warn('Invalid referer attempt', { referer: req.headers.referer, ip: req.ip, path: req.path }); return res.status(403).send('Invalid referer'); } ``` ## 适用场景 ### 适合使用 Referer 验证的场景 - 内部管理系统 - API 接口保护 - 作为辅助防护措施 ### 不适合单独使用的场景 - 公共网站 - 对安全性要求极高的系统 - 需要支持多种访问方式的场景 ## 现代替代方案 随着 SameSite Cookie 和 CSRF Token 的普及,Referer 验证通常作为辅助防护手段使用,而不是主要的防护方式。 验证 Referer 头虽然有一定的局限性,但在正确的使用场景下,配合其他防护措施,仍然可以提供有效的 CSRF 防护。
服务端 · 2月19日 17:49
如何防御 CSRF 攻击?防御 CSRF 攻击需要多层防护策略,以下是几种有效的防御方法: ## 1. CSRF Token(推荐) ### 工作原理 - 服务器生成随机 Token,存储在会话中 - Token 添加到表单隐藏字段或请求头中 - 服务器验证请求中的 Token 是否与会话中的匹配 ### 实现示例 ```javascript // 前端 <form action="/transfer" method="POST"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="text" name="amount"> <button type="submit">Transfer</button> </form> // 或使用 AJAX fetch('/transfer', { method: 'POST', headers: { 'X-CSRF-Token': csrf_token, 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 100 }) }); ``` ### 注意事项 - Token 必须足够随机(至少 128 位) - Token 应该有时效性 - 每个 Token 只能使用一次(可选) - 敏感操作必须验证 Token ## 2. SameSite Cookie 属性 ### 工作原理 SameSite 属性控制 Cookie 在跨站请求中的发送行为。 ### 属性值 ```javascript // Strict:只在同站请求中发送 Cookie Set-Cookie: sessionid=abc123; SameSite=Strict // Lax:允许某些跨站请求(如导航) Set-Cookie: sessionid=abc123; SameSite=Lax // None:允许跨站请求(需要 Secure 属性) Set-Cookie: sessionid=abc123; SameSite=None; Secure ``` ### 推荐配置 - 敏感操作使用 `SameSite=Strict` - 一般应用使用 `SameSite=Lax` - 避免 `SameSite=None` 除非必要 ## 3. 验证 Referer/Origin 头 ### 工作原理 检查请求的来源是否合法。 ### 实现示例 ```javascript // 服务器端验证 const allowedOrigins = ['https://example.com', 'https://www.example.com']; function validateOrigin(req) { const origin = req.headers.origin || req.headers.referer; if (!origin) return false; const originUrl = new URL(origin); return allowedOrigins.includes(originUrl.origin); } ``` ### 注意事项 - Referer 可能被浏览器禁用或修改 - Origin 只在跨站请求中存在 - 不能作为唯一防御手段 ## 4. 双重提交 Cookie ### 工作原理 - Token 同时存储在 Cookie 和请求参数中 - 服务器验证两者是否匹配 ### 实现示例 ```javascript // 设置 Cookie res.cookie('csrf_token', token, { httpOnly: true, sameSite: 'strict' }); // 表单中包含 Token <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> // 服务器验证 if (req.cookies.csrf_token !== req.body.csrf_token) { throw new Error('CSRF token mismatch'); } ``` ### 注意事项 - 需要配合其他防护措施 - 不适用于所有场景 ## 5. 自定义请求头 ### 工作原理 - 使用自定义 Header(如 `X-Requested-With`) - 简单跨站请求无法添加自定义 Header ### 实现示例 ```javascript fetch('/api/data', { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); ``` ### 注意事项 - 只适用于 AJAX 请求 - 需要服务器验证 Header ## 6. 验证码 ### 工作原理 - 敏感操作要求用户输入验证码 - 验证码需要用户主动输入 ### 适用场景 - 金额较大的转账 - 删除重要数据 - 修改关键设置 ## 7. 重新认证 ### 工作原理 - 敏感操作要求重新输入密码 - 确保用户意图 ### 实现示例 ```javascript // 执行敏感操作前 function performSensitiveAction(callback) { showPasswordPrompt((password) => { verifyPassword(password).then(valid => { if (valid) callback(); }); }); } ``` ## 综合防御策略 ### 最佳实践 1. **使用 CSRF Token**:主要防御手段 2. **设置 SameSite=Lax**:增强防护 3. **验证 Origin/Referer**:额外验证 4. **敏感操作双重确认**:重要操作 5. **定期更新 Token**:降低风险 6. **监控异常请求**:及时发现问题 ### 框架支持 - **Spring Security**:自动生成和验证 CSRF Token - **Django**:内置 CSRF 保护中间件 - **Express**:使用 `csurf` 中间件 - **Laravel**:自动添加 CSRF Token ## 总结 没有单一的防御方法可以完全防止 CSRF 攻击。建议使用多层防护策略,以 CSRF Token 为主,配合 SameSite Cookie、Origin 验证等措施,为敏感操作添加额外的确认机制。
服务端 · 2月19日 17:48