Files
vue-driven-cloud-storage/backend/middleware/csrf.js

69 lines
1.9 KiB
JavaScript

const crypto = require('crypto');
const { logger } = require('../utils/logger');
const CSRF_COOKIE_NAME = 'csrf_token';
const CSRF_COOKIE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
function generateCsrfToken() {
return crypto.randomBytes(32).toString('hex');
}
function setCsrfCookie(res, token, secureCookies) {
res.cookie(CSRF_COOKIE_NAME, token, {
httpOnly: false,
secure: secureCookies,
sameSite: secureCookies ? 'strict' : 'lax',
maxAge: CSRF_COOKIE_MAX_AGE_MS
});
}
function createCsrfCookieMiddleware({ secureCookies = false } = {}) {
return (req, res, next) => {
if (!req.cookies[CSRF_COOKIE_NAME]) {
setCsrfCookie(res, generateCsrfToken(), secureCookies);
}
next();
};
}
function tokensEqual(left, right) {
if (typeof left !== 'string' || typeof right !== 'string') return false;
const leftBuffer = Buffer.from(left);
const rightBuffer = Buffer.from(right);
return leftBuffer.length === rightBuffer.length && crypto.timingSafeEqual(leftBuffer, rightBuffer);
}
function csrfProtection(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
const hasCookieAuth = Boolean(req.cookies?.token || req.cookies?.refreshToken);
if (!hasCookieAuth) return next();
const cookieToken = req.cookies[CSRF_COOKIE_NAME];
const headerToken = req.headers['x-csrf-token'];
if (!tokensEqual(cookieToken, headerToken)) {
(req.log || logger).warn({
path: req.path,
hasCookieToken: Boolean(cookieToken),
hasHeaderToken: Boolean(headerToken)
}, 'CSRF validation failed');
return res.status(403).json({
success: false,
message: 'CSRF 验证失败,请刷新页面后重试',
requestId: req.id
});
}
return next();
}
module.exports = {
CSRF_COOKIE_NAME,
createCsrfCookieMiddleware,
csrfProtection,
generateCsrfToken,
setCsrfCookie,
tokensEqual
};