跳到主要内容

安全认证

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-ProtectionXSS 过滤(现代浏览器已弃用,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 攻击通过注入恶意脚本窃取用户信息或执行恶意操作。

防护措施

  1. 转义用户输入
const escapeHtml = (str) => {
return str
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
};
  1. 使用 Helmet 的 CSP
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // 禁止内联脚本
styleSrc: ["'self'", "'unsafe-inline'"]
}
}));
  1. 设置 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 注入

注入攻击通过构造恶意输入,篡改数据库查询。

防护措施

  1. 使用 ORM/ODM(Mongoose、Sequelize、Prisma)
// 安全:Mongoose 自动转义
const user = await User.findOne({ email: userInput });

// 危险:直接拼接查询
const query = `SELECT * FROM users WHERE email = '${userInput}'`;
  1. 验证和转义输入
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() });
}
// 安全处理...
});
  1. 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 设置合理过期时间
  • 敏感操作需要二次验证
  • 设置 httpOnly: true
  • 生产环境设置 secure: true
  • 设置 sameSite: 'strict''lax'
  • 不要使用默认 Cookie 名称

5. 输入验证

  • 验证所有用户输入
  • 使用参数化查询
  • 限制请求体大小
  • 验证文件上传类型和大小

6. 响应安全

  • 不暴露服务器信息
  • 生产环境不返回错误堆栈
  • 设置安全响应头

7. 日志与监控

  • 记录认证失败事件
  • 监控异常请求模式
  • 定期审计访问日志

小结

本章详细介绍了 Express 应用的安全措施:

  1. 安全中间件:Helmet 设置安全响应头、CORS 跨域配置、请求限流
  2. 认证方式:Session 认证、JWT 认证、Passport.js 框架
  3. 权限控制:RBAC 角色、资源所有权、细粒度权限
  4. 漏洞防护:XSS、CSRF、注入攻击、开放重定向
  5. 最佳实践:从环境到日志的全面安全清单

安全是一个持续的过程,需要定期审查和更新防护措施。

练习

  1. 为一个 Express API 实现 JWT 认证,包含注册、登录、刷新 Token 功能
  2. 使用 Helmet 配置 CSP,只允许加载同源资源和指定 CDN 的脚本
  3. 实现基于角色的权限控制,区分管理员、编辑、普通用户的权限
  4. 添加登录限流,同一 IP 15 分钟内最多尝试 5 次
  5. 实现 CSRF 保护,为表单提交添加 Token 验证

参考资料