CSRF
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种网络攻击方式,它允许攻击者利用用户已经认证的身份,在不知情的情况下,以该用户的名义执行恶意操作。这些操作可能包括提交表单、更改用户设置或进行金钱交易等。CSRF攻击适用于基于cookie的认证系统,因为浏览器会自动附带当前域下的cookie信息。

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