在 Web 应用中,认证(Authentication) 和 鉴权(Authorization) 是两个紧密相关但职责不同的安全机制:
- 认证 回答“你是谁?”——验证用户身份(如登录时核对用户名/密码)。
- 鉴权 回答“你能做什么?”——控制已认证用户可访问的资源和操作。
一个健壮的认证与鉴权体系是 API 安全、数据保护和用户隐私的基石。Node.js 生态中相关的方案非常多,但核心模式主要围绕三个方面展开:会话机制的选择(Session-Cookie vs JWT)、密码的安全存储(bcrypt)、以及权限模型的设计(如 RBAC)。本节将从这些要点出发,结合主流工具给出可落地的实现路径。
14.1.1 Session-Cookie 机制
Session-Cookie 机制是 Web 中最传统的身份保持方案,特别适合服务端渲染或同源前后端的传统应用。
工作流程
- 用户提交登录表单,后端验证账号密码正确后,创建一个 Session 对象,通常保存在内存、Redis 或数据库中。
- 后端将唯一 Session ID 通过 Set-Cookie 头返回给浏览器。
- 浏览器自动在后续请求中携带该 Cookie,后端通过 Cookie 中的 Session ID 查找对应 Session,确定用户身份。
- 用户登出时,后端销毁 Session 并清除 Cookie。
在 Node.js 中实现
常用的库包括 express-session(内存存储,仅供开发环境)和 connect-redis(生产环境推荐用 Redis 存储 Session)。
安装依赖
npm install express-session connect-redis redis
配置 Express 应用
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redisClient = require('redis').createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key', // 签名 Cookie 的密钥
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // 仅 HTTPS 传输
httpOnly: true, // 防止 XSS 读取
maxAge: 1000 * 60 * 60 * 24 // 有效期 24 小时
}
}));
登录与登出路由
app.post('/login', (req, res) => {
// 验证用户名密码...
const user = { id: 1, username: req.body.username };
req.session.user = user; // 将用户信息写入 Session
res.json({ success: true });
});
app.post('/logout', (req, res) => {
req.session.destroy(err => {
res.clearCookie('connect.sid');
res.json({ success: true });
});
});
鉴权中间件
function authMiddleware(req, res, next) {
if (req.session && req.session.user) {
return next();
}
res.status(401).json({ error: '未登录' });
}
app.get('/profile', authMiddleware, (req, res) => {
res.json(req.session.user);
});
适用场景与权衡
- 优点:服务端可随时撤销会话(删除 Session 即可);Cookie 不传输用户数据本身,较为安全。
- 缺点:服务端需要存储 Session,水平扩展时需使用共享存储(如 Redis);不适用于跨域 API 服务,因为 Cookie 默认受同源策略限制。
- 现代倾向:对于前后端分离的 SPA 或移动端,Session-Cookie 逐渐被 JWT 替代,但在需要服务端主动失效会话的传统项目中仍然非常实用。
14.1.2 JWT 令牌认证
JWT(JSON Web Token) 是一种无状态的认证方案,已成为前后端分离架构的主流选择。它的核心思想是:服务器将用户信息(payload)编码到一个签名令牌中,客户端持有该令牌来证明身份,服务器无需保存任何会话信息。
JWT 结构
一个 JWT 由三部分组成,用点号分隔:header.payload.signature
- Header:声明令牌类型(JWT)和签名算法(如 HS256、RS256)。
- Payload:包含声明信息(如用户 ID、角色、过期时间等),该部分仅作 Base64 编码,不加密,不要存放敏感信息。
- Signature:对前两部分的签名,防止数据被篡改。生成方式为:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
在 Node.js 中实现
常用库为 jsonwebtoken。
安装
npm install jsonwebtoken
签发令牌
const jwt = require('jsonwebtoken');
app.post('/login', (req, res) => {
// 验证用户密码...
const payload = { id: user.id, role: user.role };
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '1h'
});
res.json({ token });
});
验证令牌的中间件
function jwtAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供令牌' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // 将用户信息挂载到请求对象
next();
} catch (err) {
res.status(403).json({ error: '令牌无效或已过期' });
}
}
app.get('/profile', jwtAuth, (req, res) => {
res.json(req.user);
});
刷新令牌与安全考量
- Access Token(短期,如15分钟) + Refresh Token(长期,存储在 httpOnly cookie 或安全存储)是推荐的实践。Refresh Token 用于无感获取新的 Access Token,避免用户频繁登录。
- JWT 一旦签发,在有效期内无法单方撤销(除非使用黑名单或版本号,但这会引入状态)。因此时效设置需权衡安全性与用户体验。
secret必须复杂且私密,生产环境推荐使用 RS256 非对称算法,便于微服务间验签。- Payload 不得包含密码、身份证等敏感信息。
Session-Cookie vs JWT 选型对比
| 特性 | Session-Cookie | JWT |
|------|----------------|-----|
| 状态 | 有状态,需要服务端存储 | 无状态,客户端持有 |
| 扩展性 | 需要 Redis 等共享存储 | 天然支持水平扩展 |
| 安全撤销 | 即时生效 | 需要额外机制(黑名单) |
| 跨域/移动端 | 不易(Cookie 受同源限制) | 方便(放在 Header 中) |
| 适用场景 | 传统 Web、服务端渲染 | SPA、移动端、微服务 |
14.1.3 密码加密:bcrypt
密码绝不能明文存储。bcrypt 是当前主流的密码哈希函数,具备两大特性:
- 加盐(Salt):自动生成随机盐值,使得相同密码产生不同哈希,抵抗彩虹表攻击。
- 计算成本可调节:通过
saltRounds(成本因子)控制哈希的计算量,拖延暴力破解速度。
基本用法
npm install bcrypt
注册时哈希密码
const bcrypt = require('bcrypt');
const saltRounds = 10; // 一般取 10-12,平衡安全与性能
app.post('/register', async (req, res) => {
const hashedPassword = await bcrypt.hash(req.body.password, saltRounds);
// 将 hashedPassword 存入数据库
});
登录时比对
const user = await User.findOne({ username: req.body.username });
if (!user) return res.status(401).json({ error: '用户不存在' });
const isMatch = await bcrypt.compare(req.body.password, user.password);
if (!isMatch) return res.status(401).json({ error: '密码错误' });
// 登录成功,签发 JWT 或创建 Session
为什么不使用 SHA256 或 MD5
- SHA256 是快速哈希函数,攻击者可以每秒进行数十亿次计算,配合彩虹表极易破解。
- bcrypt 的慢速特性和内置加盐,使暴力破解成本指数级上升。即便数据库泄露,攻击者也难以还原明文。
14.1.4 Passport 统一认证框架
Passport 是 Node.js 中最具影响力的认证中间件,它采用策略(Strategy)模式,将不同的认证方式(本地用户名密码、JWT、OAuth 等)封装为可插拔模块,对业务代码无侵入。
核心概念
- 策略(Strategy):一个独立的认证逻辑模块,例如
passport-local(用户名密码)、passport-jwt、passport-google-oauth20等。 - 序列化/反序列化用户:Session 场景下,Passport 提供
serializeUser和deserializeUser将用户对象转换成 ID / 根据 ID 恢复用户,实现状态保持。
集成示例:本地策略 + JWT
安装:
npm install passport passport-local passport-jwt jsonwebtoken
配置 Passport 本地策略
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('./models/User');
passport.use(new LocalStrategy(
{ usernameField: 'email' }, // 更改默认字段
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);
}
}
));
配置 JWT 策略
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET
};
passport.use(new JwtStrategy(opts, 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);
}
}));
在路由中使用
// 登录:使用本地策略
app.post('/login', passport.authenticate('local', { session: false }), (req, res) => {
// 认证成功,签发 JWT
const token = jwt.sign({ id: req.user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
});
// 受保护路由:使用 JWT 策略
app.get('/profile', passport.authenticate('jwt', { session: false }), (req, res) => {
res.json(req.user);
});
Passport 的价值在于策略解耦,当需要添加 GitHub 登录或企业 SAML 时,只需安装新策略模块并注册,不影响既有路由逻辑。
14.1.5 RBAC 权限模型设计与实现
RBAC(Role-Based Access Control,基于角色的访问控制) 是最常用的鉴权模型,将权限间接赋予角色,用户再被分配角色,从而简化权限管理。
核心实体及关系
- 用户(User):系统使用者。
- 角色(Role):权限的集合,如
admin、editor、viewer。 - 权限(Permission):对资源的原子操作,如
user:read、article:delete。
关系:用户 - 角色为多对多,角色 - 权限也为多对多。
数据库设计(MongoDB 示例)
// roleModel.js
const roleSchema = new mongoose.Schema({
name: { type: String, unique: true }, // 'admin', 'editor'
permissions: [{ type: String }] // ['article:create', 'user:read']
});
// userModel.js
const userSchema = new mongoose.Schema({
email: String,
password: String,
roles: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Role' }]
});
权限检查中间件
根据业务需求,通常封装一个“需要某权限”的中间件:
function requirePermission(requiredPerm) {
return async (req, res, next) => {
const user = req.user; // 认证中间件已挂载
if (!user) return res.status(401).json({ error: '未登录' });
// 加载用户的角色及权限
const populatedUser = await User.findById(user.id).populate({
path: 'roles',
populate: { path: 'permissions' }
});
// 收集该用户所有权限
const perms = new Set();
populatedUser.roles.forEach(role => {
role.permissions.forEach(p => perms.add(p));
});
if (perms.has(requiredPerm) || perms.has('admin')) { // admin 拥有所有权限
return next();
}
res.status(403).json({ error: '没有权限' });
};
}
// 使用:删除文章需要 'article:delete' 权限
app.delete('/articles/:id', requirePermission('article:delete'), (req, res) => {
// ...
});
细粒度权限扩展
- ABAC(基于属性):当规则复杂到需要根据资源属性(如“只能编辑自己创建的文章”)时,可在中间件中加入业务逻辑判断。
- 装饰器/注解:在 NestJS 等框架中,可以用
@Permissions('article:delete')装饰器声明式地控制权限。
生产实践提示
- 总是在数据库查询时也加入权限过滤,不能只依靠中间件(例如,
find查询只返回当前用户有读取权限的资源)。 - 角色和权限的映射可以缓存到 Redis,避免每次请求都查询数据库。
- 为关键操作(如删除用户、修改角色)增加审计日志。
14.1.6 整合方案与安全 checklist
一个实际项目的认证鉴权体系,往往是上述模块的组合。以下是一个典型的全栈方案:
- 注册:bcrypt 哈希密码 → 存库。
- 登录:Passport 本地策略验证 → 返回 JWT(Access Token + Refresh Token)。
- 请求认证:JWT 策略中间件解析令牌,挂载
req.user。 - 权限控制:RBAC 中间件检查用户角色权限。
- 安全增强:
- 使用 HTTPS 传输所有敏感数据。
- 对登录等敏感接口实现限流(如
express-rate-limit),防止暴力破解。 - JWT 的
secret保证高强度且定期轮换。 - 在响应中统一处理认证/鉴权错误,避免泄露系统细节。
- 考虑使用
helmet设置安全头。
Node.js 生态提供了从低层原生模块到高层认证框架的完整能力链。通过合理组合 Session/JWT、bcrypt 加密、Passport 策略模式和 RBAC 模型,可以构建出既安全又灵活、且易于维护的认证鉴权体系,满足绝大多数 web 应用的需求。