安全认证
本章节介绍 Express.js 应用中的安全措施和认证授权实现。
安全中间件
Helmet
设置各种 HTTP 头来保护应用:
npm install helmet
const helmet = require('helmet');
app.use(helmet());
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "trusted-cdn.com"],
},
},
}));
CORS
跨域资源共享配置:
npm install cors
const cors = require('cors');
app.use(cors());
app.use(cors({
origin: ['https://example.com', 'https://api.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
限流
防止暴力攻击:
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { error: '请求过于频繁,请稍后再试' }
});
app.use('/api', limiter);
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: '登录尝试过多,请稍后再试' }
});
app.post('/login', authLimiter, loginHandler);
认证方式
Session 认证
npm install express-session
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000
}
}));
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (validateUser(username, password)) {
req.session.userId = user.id;
res.json({ message: '登录成功' });
} else {
res.status(401).json({ error: '用户名或密码错误' });
}
});
app.get('/logout', (req, res) => {
req.session.destroy();
res.json({ message: '已登出' });
});
JWT 认证
npm install jsonwebtoken bcryptjs
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const generateToken = (user) => {
return jwt.sign(
{ id: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
};
const verifyToken = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '请先登录' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Token 无效或已过期' });
}
};
app.post('/register', async (req, res) => {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({ username, password: hashedPassword });
const token = generateToken(user);
res.status(201).json({ user, token });
});
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user) {
return res.status(401).json({ error: '用户不存在' });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ error: '密码错误' });
}
const token = generateToken(user);
res.json({ user, token });
});
app.get('/profile', verifyToken, (req, res) => {
res.json({ user: req.user });
});
Passport.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(new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, async (email, password, done) => {
try {
const user = await User.findOne({ email });
if (!user) return done(null, false, { message: '用户不存在' });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return done(null, false, { message: '密码错误' });
return done(null, user);
} catch (err) {
return done(err);
}
}));
passport.use(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);
}
}));
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({ user: req.user });
}
);
权限控制
角色中间件
const checkRole = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '请先登录' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
};
app.delete('/users/:id',
verifyToken,
checkRole('admin'),
deleteUser
);
资源所有权检查
const checkOwnership = (model) => {
return async (req, res, next) => {
const resource = await model.findById(req.params.id);
if (!resource) {
return res.status(404).json({ error: '资源不存在' });
}
if (resource.userId.toString() !== req.user.id) {
return res.status(403).json({ error: '无权访问此资源' });
}
req.resource = resource;
next();
};
};
app.put('/posts/:id',
verifyToken,
checkOwnership(Post),
updatePost
);
安全最佳实践
- 使用 HTTPS:生产环境必须使用 HTTPS
- 环境变量:敏感信息存储在环境变量中
- 输入验证:验证所有用户输入
- SQL 注入防护:使用 ORM 或参数化查询
- XSS 防护:转义用户输入,使用 helmet
- CSRF 防护:使用 csurf 中间件
- 密码加密:使用 bcrypt 存储密码
- 日志记录:记录安全相关事件