Node.js 开发指南 / 第 15 章 · 认证与授权
第 15 章 · 认证与授权
15.1 认证 vs 授权
| 概念 | 说明 | 问题 |
|---|---|---|
| 认证(Authentication) | 你是谁? | 验证用户身份 |
| 授权(Authorization) | 你能做什么? | 验证用户权限 |
15.2 Session 认证
npm install express-session connect-redis
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const app = express();
// Redis 存储 session(生产环境推荐)
const redisClient = createClient();
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS
httpOnly: true, // 防止 XSS 访问
maxAge: 24 * 60 * 60 * 1000, // 24 小时
sameSite: 'lax', // CSRF 防护
},
}));
// 登录
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
const user = authenticate(username, password);
if (!user) {
return res.status(401).json({ error: '用户名或密码错误' });
}
req.session.userId = user.id;
req.session.role = user.role;
res.json({ message: '登录成功' });
});
// 认证中间件
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: '请先登录' });
}
next();
}
// 获取当前用户
app.get('/api/me', requireAuth, async (req, res) => {
const user = await db.users.findById(req.session.userId);
res.json({ user });
});
// 登出
app.post('/api/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: '登出失败' });
res.clearCookie('connect.sid');
res.json({ message: '已登出' });
});
});
15.3 JWT 认证
npm install jsonwebtoken bcrypt
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES = '24h';
const REFRESH_EXPIRES = '7d';
// 生成令牌
function generateTokens(user) {
const accessToken = jwt.sign(
{ id: user.id, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES }
);
const refreshToken = jwt.sign(
{ id: user.id },
process.env.REFRESH_SECRET,
{ expiresIn: REFRESH_EXPIRES }
);
return { accessToken, refreshToken };
}
// 验证令牌
function verifyToken(token) {
return jwt.verify(token, JWT_SECRET);
}
// 密码哈希
async function hashPassword(password) {
return bcrypt.hash(password, 12);
}
async function comparePassword(password, hash) {
return bcrypt.compare(password, hash);
}
// 登录路由
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findByEmail(email);
if (!user || !(await comparePassword(password, user.passwordHash))) {
return res.status(401).json({ error: '邮箱或密码错误' });
}
const tokens = generateTokens(user);
res.json({
user: { id: user.id, name: user.name, email: user.email, role: user.role },
...tokens,
});
});
// 认证中间件
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供认证令牌' });
}
try {
const token = authHeader.split(' ')[1];
req.user = verifyToken(token);
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: '令牌已过期', code: 'TOKEN_EXPIRED' });
}
res.status(401).json({ error: '令牌无效' });
}
}
// 角色授权中间件
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
}
// 刷新令牌
app.post('/api/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
const user = await db.users.findById(payload.id);
if (!user) return res.status(401).json({ error: '用户不存在' });
const tokens = generateTokens(user);
res.json(tokens);
} catch {
res.status(401).json({ error: '刷新令牌无效' });
}
});
// 使用
app.get('/api/admin/users', authenticate, authorize('admin'), (req, res) => {
res.json({ users: [] });
});
Session vs JWT 对比
| 特性 | Session | JWT |
|---|---|---|
| 存储位置 | 服务端 | 客户端 |
| 扩展性 | 需要共享存储 | 天然无状态 |
| 撤销 | 容易(删除 session) | 困难(需要黑名单) |
| 跨域 | Cookie 受域限制 | Header 自由传递 |
| 大小 | Cookie 只存 ID | 可能较大 |
| 适用场景 | 传统 Web 应用 | API / 微服务 / 移动端 |
15.4 OAuth2 认证
npm install passport passport-google-oauth20
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback',
}, async (accessToken, refreshToken, profile, done) => {
try {
// 查找或创建用户
let user = await db.users.findByGoogleId(profile.id);
if (!user) {
user = await db.users.create({
googleId: profile.id,
name: profile.displayName,
email: profile.emails[0].value,
avatar: profile.photos[0]?.value,
});
}
done(null, user);
} catch (err) {
done(err, null);
}
}));
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
const user = await db.users.findById(id);
done(null, user);
});
app.use(passport.initialize());
app.use(passport.session());
// 发起 OAuth 认证
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
// OAuth 回调
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
const tokens = generateTokens(req.user);
res.redirect(`/auth/success?token=${tokens.accessToken}`);
}
);
OAuth2 流程
用户 → 点击"使用 Google 登录"
↓
重定向到 Google 授权页面
↓
用户授权 → Google 回调到应用
↓
应用收到授权码 → 用授权码换取 Access Token
↓
用 Access Token 获取用户信息
↓
创建/更新本地用户 → 登录成功
15.5 RBAC 权限模型
// 基于角色的访问控制
const permissions = {
admin: ['users:read', 'users:write', 'users:delete', 'posts:read', 'posts:write', 'posts:delete'],
editor: ['posts:read', 'posts:write', 'users:read'],
viewer: ['posts:read', 'users:read'],
};
// 权限检查中间件
function requirePermission(permission) {
return (req, res, next) => {
const userPermissions = permissions[req.user.role] || [];
if (!userPermissions.includes(permission)) {
return res.status(403).json({ error: `需要权限: ${permission}` });
}
next();
};
}
// 使用
app.delete('/api/users/:id', authenticate, requirePermission('users:delete'), deleteUser);
注意事项
⚠️ JWT Secret 必须安全:使用足够长的随机字符串(至少 256 位),从环境变量读取。
⚠️ 永远不要在 JWT 中存储敏感信息:JWT 只是编码(Base64),不是加密,任何人都能解码。
⚠️ bcrypt 的 salt rounds:推荐 10-12 轮,更高会显著增加计算时间。
⚠️ HTTPS 必不可少:认证令牌在 HTTP 下传输等于明文暴露。
扩展阅读
上一章:第 14 章 · 数据库 下一章:第 16 章 · WebSocket 实时通信 — Socket.io、实时通信和房间管理。