防CSRF案例

wen java案例 283

防CSRF攻击案例详解

什么是CSRF攻击?

CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种攻击方式,攻击者诱导用户访问恶意网站,利用用户已登录的身份信息,向目标网站发送伪造的请求,执行非用户意愿的操作。

防CSRF案例

攻击流程示例

用户登录银行网站,获得有效Cookie
2. 用户未退出银行网站,又访问了恶意网站
3. 恶意网站向银行网站发送转账请求(携带用户Cookie)
4. 银行网站验证用户身份通过,执行转账操作

常见CSRF防御方案对比

防御方案 安全性 用户体验 实现难度 适用场景
同步器令牌 高 完美 中等 所有场景
双重Cookie 高 完美 简单 经典方案
SameSite Cookie 中高 完美 极低 浏览器支持
自定义请求头 高 完美 低 API接口
二次验证 很高 较差 中等 敏感操作

案例1:同步器令牌(最常用方案)

前端代码

<!-- 表单中包含CSRF Token -->
<form action="/api/transfer" method="POST" id="transferForm">
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
    <input type="text" name="amount" placeholder="转账金额">
    <button type="submit">确认转账</button>
</form>
// AJAX请求时携带CSRF Token
async function transferMoney(amount) {
    const response = await fetch('/api/transfer', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': getCsrfToken()
        },
        body: JSON.stringify({
            amount: amount,
            _csrf: document.querySelector('input[name="csrf_token"]').value
        })
    });
    return response.json();
}

后端代码(Node.js/Express示例)

// 使用csurf中间件
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
// 生成Token并返回给前端
app.get('/form', csrfProtection, (req, res) => {
    res.render('transfer', { 
        csrfToken: req.csrfToken() 
    });
});
// 验证Token的POST请求
app.post('/api/transfer', csrfProtection, (req, res) => {
    // 到这里说明Token验证通过
    const { amount } = req.body;
    // 执行转账逻辑...
    res.json({ success: true });
});

案例2:双重Cookie验证(不占用服务端状态)

工作原理

用户登录时,服务端在Cookie中设置随机Token
2. 前端JS读取Cookie中的Token值
3. 发送请求时,在请求头或请求体中携带该Token
4. 服务端比对Cookie中的Token和请求中携带的Token

实现代码

// 登录成功后设置随机Token
router.post('/login', (req, res) => {
    const sessionToken = crypto.randomBytes(32).toString('hex');
    // 1. 设置Cookie
    res.cookie('csrfToken', sessionToken, {
        httpOnly: false,  // 必须允许JS读取
        secure: true,     // HTTPS
        sameSite: 'strict'
    });
    res.json({ success: true });
});
// 全局中间件验证CSRF
app.use('/api/*', (req, res, next) => {
    // 2. 从Cookie获取Token
    const cookieToken = req.cookies.csrfToken;
    // 3. 从请求头或请求体获取Token
    const requestToken = req.headers['x-csrf-token'] || req.body.csrfToken;
    // 4. 比对
    if (cookieToken && requestToken && cookieToken === requestToken) {
        next();
    } else {
        res.status(403).json({ error: 'CSRF验证失败' });
    }
});
// 前端JS需要在请求前注入Token
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': getCookie('csrfToken')
    }
});

案例3:SameSite Cookie(最简单的防御)

// 设置SameSite属性
res.cookie('session', sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'  // 或 'lax'(推荐平衡方案)
});
/*
 * strict: 完全禁止第三方Cookie,所有跨站请求都不会携带
 * lax:    允许部分安全请求(GET/HEAD/OPTIONS)携带
 * none:   完全不做限制(需配合Secure)
 */

注意: 同Site判定标准(2023年后浏览器改为"同源"判定):

  • 只关心scheme://domain
  • 忽略端口号
  • 不同子域被视为跨站

案例4:自定义请求头验证

// 后端验证自定义Header
app.use('/api/*', (req, res, next) => {
    const customHeader = req.headers['x-requested-with'];
    if (customHeader !== 'XMLHttpRequest') {
        return res.status(403).json({ error: '非法请求来源' });
    }
    // 还可以配合Origin验证
    const origin = req.headers.origin;
    const allowedOrigins = ['https://bank.example.com'];
    if (origin && !allowedOrigins.includes(origin)) {
        return res.status(403).json({ error: '非允许来源' });
    }
    next();
});
// 使用场景
fetch('/api/data', {
    headers: {
        'X-Requested-With': 'XMLHttpRequest'
    }
});

案例5:二次身份验证(敏感操作)

<!-- 高敏感操作需要重新输入密码 -->
<form action="/api/delete-account" method="POST">
    <input type="password" name="password" placeholder="输入密码确认">
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
    <button type="submit">永久删除账户</button>
</form>
// 后端验证
router.post('/api/delete-account', csrfProtection, async (req, res) => {
    const { password } = req.body;
    // 校验密码
    const user = await User.findById(req.session.userId);
    const valid = await bcrypt.compare(password, user.passwordHash);
    if (!valid) {
        return res.status(403).json({ error: '密码错误,操作取消' });
    }
    // 执行删除操作
    await User.deleteOne({ _id: user._id });
});

实际项目方案组合示例

完整的安全防护层

class CSRFProtection {
    constructor() {
        this.allowedOrigins = ['https://bank.example.com'];
        this.tokenStore = new Map();
    }
    // 中间件主入口
    middleware = async (req, res, next) => {
        // 1. 检查请求来源
        const origin = req.headers.origin;
        if (origin && !this.allowedOrigins.includes(origin)) {
            return res.status(403).json({ error: '拒绝来源' });
        }
        // 2. 检查是否是非简单请求
        if (this.isUnsafeMethod(req.method)) {
            // 3. 双重验证:Header + 自定义Header
            const tokenFromHeader = req.headers['x-csrf-token'];
            const tokenFromCookie = req.cookies['csrf-token'];
            const tokenFromSession = req.session.csrfToken;
            // 三重校验
            if (!tokenFromHeader || tokenFromHeader !== tokenFromCookie 
                || tokenFromCookie !== tokenFromSession) {
                return res.status(403).json({ error: 'Token验证失败' });
            }
        }
        next();
    }
    isUnsafeMethod(method) {
        return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
    }
    // 生成新Token
    generateToken(sessionId) {
        const token = crypto.randomBytes(32).toString('hex');
        this.tokenStore.set(sessionId, token);
        return token;
    }
}

前端配合方案

// 封装请求,统一注入CSRF Token
class HTTPService {
    constructor() {
        this.baseURL = '/api';
    }
    async request(path, options = {}) {
        return fetch(`${this.baseURL}${path}`, {
            ...options,
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': this.getCSRFToken(),
                'X-Requested-With': 'XMLHttpRequest',
                ...options.headers
            },
            credentials: 'same-origin'
        });
    }
    getCSRFToken() {
        const token = document.cookie.match('(^|;)\\s*csrf-token\\s*=\\s*([^;]+)');
        return token ? token.pop() : '';
    }
}

总结与最佳实践

三层防御搭配使用

  1. 第一层(基础防护):SameSite Cookie + 自定义请求头
  2. 第二层(核心防护):同步器令牌(服务端Session存储)
  3. 第三层(增强防护):敏感操作二次验证

注意事项

  • ✅ Token必须安全随机,使用密码学安全伪随机数
  • ✅ Token有效期限制,建议30分钟~2小时
  • ✅ 所有非安全方法都必须验证(POST/PUT/DELETE等)
  • ✅ 使用HTTPS传输,防止中间人获取Token
  • ❌ 不要使用简单随机数生成Token
  • ❌ 不要把Token放在URL中(会记录在日志和Referer中)
  • ❌ 不要只依赖JSONP(JSONP本身存在CSRF风险)

参考资源

抱歉,评论功能暂时关闭!

友情链接攻略窝 · 礼包窝 · 配置窝 · 换算窝 · 算法窝 · 查文窝 · 手册窝 · 问答窝 · 全文搜 · 手册库
相关站点礼包窝 · 查文窝