防CSRF案例

wen java案例 2

防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风险)

参考资源

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