安全认证
Web 安全是一个不可忽视的话题。Express 应用面临着各种安全威胁,包括 XSS、CSRF、注入攻击等。本章将系统介绍 Express 应用的安全防护措施和认证授权实现。
为什么安全很重要?
Web 应用安全漏洞可能导致严重后果:
- 数据泄露:用户信息、商业数据被窃取
- 服务中断:攻击者可能导致服务不可用
- 经济损失:直接的经济损失和品牌信誉损害
- 法律风险:违反数据保护法规可能面临处罚
安全应该是应用设计的第一考量,而非事后补救。
安全中间件
Helmet:设置安全响应头
Helmet 是 Express 安全的必备中间件,它通过设置各种 HTTP 响应头来保护应用免受常见 Web 漏洞的影响。
npm install helmet
const helmet = require('helmet');
const express = require('express');
const app = express();
// 使用默认配置
app.use(helmet());
Helmet 默认设置的响应头:
| 响应头 | 作用 |
|---|---|
Content-Security-Policy | 防止 XSS 攻击,控制资源加载来源 |
Cross-Origin-Opener-Policy | 进程隔离,防止跨源攻击 |
Cross-Origin-Resource-Policy | 防止跨源资源加载 |
Referrer-Policy | 控制 Referer 头信息 |
Strict-Transport-Security | 强制使用 HTTPS |
X-Content-Type-Options | 防止 MIME 类型嗅探 |
X-Frame-Options | 防止点击劫持 |
X-XSS-Protection | XSS 过滤(现代浏览器已弃用,Helmet 禁用) |
自定义配置:
app.use(helmet({
// 禁用某个中间件
contentSecurityPolicy: false,
// 自定义 Content-Security-Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "trusted-cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "api.example.com"],
fontSrc: ["'self'", "fonts.gstatic.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: []
}
},
// 自定义 HSTS
hsts: {
maxAge: 31536000, // 1 年
includeSubDomains: true, // 包含子域名
preload: true // 加入浏览器预加载列表
}
}));
Content-Security-Policy 详解
CSP(内容安全策略)是防止 XSS 攻击的最有效手段之一。它通过白名单机制控制浏览器可以加载哪些资源。
// 基本配置
app.use(helmet.contentSecurityPolicy({
directives: {
// 默认:只允许同源资源
defaultSrc: ["'self'"],
// 脚本:允许同源和指定 CDN
scriptSrc: ["'self'", "https://cdn.example.com"],
// 样式:允许内联样式(谨慎使用)
styleSrc: ["'self'", "'unsafe-inline'"],
// 图片:允许 data URI 和 HTTPS
imgSrc: ["'self'", "data:", "https:"],
// 字体
fontSrc: ["'self'", "https://fonts.gstatic.com"],
// AJAX/WebSocket 连接
connectSrc: ["'self'", "https://api.example.com"],
// 禁止加载插件
objectSrc: ["'none'"],
// 禁止内联脚本和 eval
scriptSrc: ["'self'"], // 不包含 'unsafe-inline' 和 'unsafe-eval'
// 报告违规行为
reportUri: '/csp-report'
}
}));
// CSP 违规报告处理
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.error('CSP Violation:', req.body);
res.status(204).send();
});
CORS:跨域资源共享
当 API 需要被前端应用跨域访问时,需要配置 CORS。
npm install cors
const cors = require('cors');
// 允许所有来源(仅开发环境)
app.use(cors());
// 生产环境配置
app.use(cors({
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count'],
credentials: true, // 允许携带 Cookie
maxAge: 86400 // 预检请求缓存 24 小时
}));
// 动态配置
app.use(cors({
origin: (origin, callback) => {
// 允许的域名白名单
const whitelist = ['https://example.com', 'https://app.example.com'];
// 开发环境允许无 origin(如 Postman)
if (!origin || whitelist.includes(origin)) {
callback(null, true);
} else {
callback(new Error('不允许的来源'));
}
}
}));
预检请求:对于非简单请求(如带自定义头的请求),浏览器会先发送 OPTIONS 预检请求。cors 中间件会自动处理。
请求限流:防止暴力攻击
限制请求频率可以有效防止暴力破解、DDoS 攻击。
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// 通用限流:15 分钟内最多 100 次请求
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 时间窗口:15 分钟
max: 100, // 最大请求数
message: { error: '请求过于频繁,请稍后再试' },
standardHeaders: true, // 返回 RateLimit 头
legacyHeaders: false
});
app.use('/api', generalLimiter);
// 登录限流:更严格的限制
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 5, // 最多 5 次尝试
message: { error: '登录尝试过多,请 15 分钟后再试' },
skipSuccessfulRequests: true // 成功的请求不计数
});
app.post('/login', loginLimiter, loginHandler);
// 注册限流:防止批量注册
const registerLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 小时
max: 3, // 最多 3 次注册
message: { error: '注册次数过多,请稍后再试' }
});
app.post('/register', registerLimiter, registerHandler);
认证方式
Session 认证
传统的 Session 认证方式,适合服务端渲染的应用。
npm install express-session connect-mongo
const session = require('express-session');
const MongoStore = require('connect-mongo');
// Session 配置
app.use(session({
name: 'sessionId', // 不要使用默认名称
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
ttl: 14 * 24 * 60 * 60 // 14 天
}),
cookie: {
secure: process.env.NODE_ENV === 'production', // 仅 HTTPS
httpOnly: true, // 防止 XSS
maxAge: 14 * 24 * 60 * 60 * 1000, // 14 天
sameSite: 'strict' // CSRF 防护
}
}));
// 登录
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ error: '邮箱或密码错误' });
}
// 存储用户信息到 Session
req.session.userId = user._id;
req.session.userRole = user.role;
res.json({ message: '登录成功', user: { id: user._id, name: user.name } });
});
// 登出
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: '登出失败' });
}
res.clearCookie('sessionId');
res.json({ message: '已登出' });
});
});
// 认证中间件
const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ error: '请先登录' });
}
next();
};
// 使用认证中间件
app.get('/profile', requireAuth, async (req, res) => {
const user = await User.findById(req.session.userId);
res.json(user);
});
JWT 认证
JWT(JSON Web Token)是无状态的认证方式,适合 API 服务和微服务架构。
npm install jsonwebtoken bcryptjs
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
// 生成 Token
const generateToken = (user) => {
return jwt.sign(
{ id: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
};
// 生成刷新 Token
const generateRefreshToken = (user) => {
return jwt.sign(
{ id: user._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '30d' }
);
};
// 注册
app.post('/register', async (req, res) => {
const { name, email, password } = req.body;
// 检查用户是否存在
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(409).json({ error: '邮箱已被注册' });
}
// 创建用户(密码会通过 Mongoose 中间件自动加密)
const user = await User.create({ name, email, password });
const token = generateToken(user);
const refreshToken = generateRefreshToken(user);
res.status(201).json({
user: { id: user._id, name: user.name, email: user.email },
token,
refreshToken
});
});
// 登录
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({ error: '邮箱或密码错误' });
}
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({ error: '邮箱或密码错误' });
}
const token = generateToken(user);
const refreshToken = generateRefreshToken(user);
res.json({
user: { id: user._id, name: user.name, email: user.email },
token,
refreshToken
});
});
// 刷新 Token
app.post('/refresh-token', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: '缺少刷新令牌' });
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({ error: '用户不存在' });
}
const newToken = generateToken(user);
res.json({ token: newToken });
} catch (err) {
res.status(401).json({ error: '刷新令牌无效或已过期' });
}
});
// JWT 认证中间件
const authMiddleware = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '请先登录' });
}
const token = authHeader.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({ error: '用户不存在' });
}
req.user = user;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: '令牌已过期', code: 'TOKEN_EXPIRED' });
}
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: '令牌无效' });
}
next(err);
}
};
// 使用认证中间件
app.get('/profile', authMiddleware, (req, res) => {
res.json(req.user);
});
Passport.js:统一的认证框架
Passport.js 是 Node.js 最流行的认证中间件,支持多种认证策略。
npm install passport passport-local passport-jwt
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
// 本地登录策略
passport.use('local', new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, async (email, password, done) => {
try {
const user = await User.findOne({ email }).select('+password');
if (!user) {
return done(null, false, { message: '邮箱或密码错误' });
}
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return done(null, false, { message: '邮箱或密码错误' });
}
return done(null, user);
} catch (err) {
return done(err);
}
}));
// JWT 策略
passport.use('jwt', new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET
}, async (payload, done) => {
try {
const user = await User.findById(payload.id);
if (user) {
return done(null, user);
}
return done(null, false);
} catch (err) {
return done(err, false);
}
}));
// 初始化 Passport
app.use(passport.initialize());
// 登录路由
app.post('/login',
passport.authenticate('local', { session: false }),
(req, res) => {
const token = generateToken(req.user);
res.json({ user: req.user, token });
}
);
// 受保护路由
app.get('/profile',
passport.authenticate('jwt', { session: false }),
(req, res) => {
res.json(req.user);
}
);
权限控制
基于角色的访问控制(RBAC)
// 角色权限中间件
const checkRole = (...allowedRoles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '请先登录' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
};
// 使用
app.delete('/users/:id',
authMiddleware,
checkRole('admin'),
deleteUser
);
app.put('/posts/:id',
authMiddleware,
checkRole('admin', 'editor'),
updatePost
);
资源所有权检查
// 检查资源所有权
const checkOwnership = (getModel) => {
return async (req, res, next) => {
try {
const Model = typeof getModel === 'function' ? getModel() : getModel;
const resource = await Model.findById(req.params.id);
if (!resource) {
return res.status(404).json({ error: '资源不存在' });
}
// 检查所有权(资源有 author 或 user 字段)
const ownerId = resource.author?.toString() || resource.user?.toString();
if (ownerId !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({ error: '无权访问此资源' });
}
req.resource = resource;
next();
} catch (err) {
next(err);
}
};
};
// 使用
app.put('/posts/:id',
authMiddleware,
checkOwnership(() => require('../models/Post')),
updatePost
);
app.delete('/posts/:id',
authMiddleware,
checkOwnership(() => require('../models/Post')),
deletePost
);
细粒度权限控制
// 权限定义
const permissions = {
admin: {
users: ['create', 'read', 'update', 'delete'],
posts: ['create', 'read', 'update', 'delete', 'publish'],
comments: ['read', 'delete']
},
editor: {
posts: ['create', 'read', 'update', 'publish'],
comments: ['read', 'delete']
},
author: {
posts: ['create', 'read', 'update'],
comments: ['read']
},
user: {
posts: ['read'],
comments: ['create', 'read']
}
};
// 权限检查中间件
const checkPermission = (resource, action) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '请先登录' });
}
const rolePermissions = permissions[req.user.role];
const resourcePermissions = rolePermissions?.[resource] || [];
if (!resourcePermissions.includes(action)) {
return res.status(403).json({
error: `没有权限执行此操作: ${action} ${resource}`
});
}
next();
};
};
// 使用
app.post('/posts',
authMiddleware,
checkPermission('posts', 'create'),
createPost
);
app.put('/posts/:id/publish',
authMiddleware,
checkPermission('posts', 'publish'),
publishPost
);
常见安全漏洞防护
XSS(跨站脚本攻击)
XSS 攻击通过注入恶意脚本窃取用户信息或执行恶意操作。
防护措施:
- 转义用户输入
const escapeHtml = (str) => {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
- 使用 Helmet 的 CSP
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // 禁止内联脚本
styleSrc: ["'self'", "'unsafe-inline'"]
}
}));
- 设置 Cookie httpOnly
res.cookie('token', token, {
httpOnly: true, // JavaScript 无法访问
secure: true,
sameSite: 'strict'
});
CSRF(跨站请求伪造)
CSRF 攻击利用用户已认证的身份,诱导用户在不知情的情况下执行恶意请求。
防护措施:
npm install csrf
const csrf = require('csrf');
const tokens = new csrf();
// 生成 CSRF Token
app.get('/csrf-token', (req, res) => {
const secret = tokens.secretSync();
const token = tokens.create(secret);
// 将 secret 存储在 session 中
req.session.csrfSecret = secret;
res.json({ csrfToken: token });
});
// 验证 CSRF Token
const validateCSRF = (req, res, next) => {
const token = req.body._csrf || req.headers['x-csrf-token'];
const secret = req.session.csrfSecret;
if (!token || !secret || !tokens.verify(secret, token)) {
return res.status(403).json({ error: '无效的 CSRF Token' });
}
next();
};
// 使用 CSRF 保护
app.post('/transfer', validateCSRF, transferMoney);
或者使用 csurf 中间件:
npm install csurf
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
// CSRF Token 已验证
res.json({ success: true });
});
SQL/NoSQL 注入
注入攻击通过构造恶意输入,篡改数据库查询。
防护措施:
- 使用 ORM/ODM(Mongoose、Sequelize、Prisma)
// 安全:Mongoose 自动转义
const user = await User.findOne({ email: userInput });
// 危险:直接拼接查询
const query = `SELECT * FROM users WHERE email = '${userInput}'`;
- 验证和转义输入
const { body, validationResult } = require('express-validator');
app.post('/users', [
body('email').isEmail().normalizeEmail(),
body('name').trim().escape(),
body('age').isInt({ min: 0, max: 150 })
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 安全处理...
});
- MongoDB 查询注入防护
// 危险:用户可能传入 { $ne: null }
const user = await User.findOne({ email: req.body.email });
// 安全:验证输入类型
const email = String(req.body.email);
const user = await User.findOne({ email });
开放重定向
开放重定向漏洞可能被用于钓鱼攻击。
// 危险:未验证的重定向
app.get('/redirect', (req, res) => {
res.redirect(req.query.url);
});
// 安全:白名单验证
const allowedDomains = ['example.com', 'app.example.com'];
app.get('/redirect', (req, res) => {
const url = req.query.url;
try {
const parsedUrl = new URL(url);
if (!allowedDomains.includes(parsedUrl.host)) {
return res.status(400).json({ error: '不允许的重定向' });
}
res.redirect(url);
} catch (err) {
res.status(400).json({ error: '无效的 URL' });
}
});
安全最佳实践清单
1. 环境与依赖
- 使用最新稳定版本的 Express 和 Node.js
- 定期运行
npm audit检查依赖漏洞 - 使用
npm audit fix自动修复已知漏洞 - 锁定依赖版本(使用 package-lock.json)
2. 传输安全
- 生产环境强制使用 HTTPS
- 设置 HSTS 头
- 使用安全的 TLS 配置
3. 认证与授权
- 使用强密码哈希(bcrypt)
- 实现登录限流
- Token 设置合理过期时间
- 敏感操作需要二次验证
4. Cookie 安全
- 设置
httpOnly: true - 生产环境设置
secure: true - 设置
sameSite: 'strict'或'lax' - 不要使用默认 Cookie 名称
5. 输入验证
- 验证所有用户输入
- 使用参数化查询
- 限制请求体大小
- 验证文件上传类型和大小
6. 响应安全
- 不暴露服务器信息
- 生产环境不返回错误堆栈
- 设置安全响应头
7. 日志与监控
- 记录认证失败事件
- 监控异常请求模式
- 定期审计访问日志
小结
本章详细介绍了 Express 应用的安全措施:
- 安全中间件:Helmet 设置安全响应头、CORS 跨域配置、请求限流
- 认证方式:Session 认证、JWT 认证、Passport.js 框架
- 权限控制:RBAC 角色、资源所有权、细粒度权限
- 漏洞防护:XSS、CSRF、注入攻击、开放重定向
- 最佳实践:从环境到日志的全面安全清单
安全是一个持续的过程,需要定期审查和更新防护措施。
练习
- 为一个 Express API 实现 JWT 认证,包含注册、登录、刷新 Token 功能
- 使用 Helmet 配置 CSP,只允许加载同源资源和指定 CDN 的脚本
- 实现基于角色的权限控制,区分管理员、编辑、普通用户的权限
- 添加登录限流,同一 IP 15 分钟内最多尝试 5 次
- 实现 CSRF 保护,为表单提交添加 Token 验证