人人都会AI编程

14.1 认证与鉴权

更新时间:2026-07-11

在 Web 应用中,认证(Authentication)鉴权(Authorization) 是两个紧密相关但职责不同的安全机制:

  • 认证 回答“你是谁?”——验证用户身份(如登录时核对用户名/密码)。
  • 鉴权 回答“你能做什么?”——控制已认证用户可访问的资源和操作。

一个健壮的认证与鉴权体系是 API 安全、数据保护和用户隐私的基石。Node.js 生态中相关的方案非常多,但核心模式主要围绕三个方面展开:会话机制的选择(Session-Cookie vs JWT)密码的安全存储(bcrypt)、以及权限模型的设计(如 RBAC)。本节将从这些要点出发,结合主流工具给出可落地的实现路径。

14.1.1 Session-Cookie 机制

Session-Cookie 机制是 Web 中最传统的身份保持方案,特别适合服务端渲染同源前后端的传统应用。

工作流程

  1. 用户提交登录表单,后端验证账号密码正确后,创建一个 Session 对象,通常保存在内存、Redis 或数据库中。
  2. 后端将唯一 Session ID 通过 Set-Cookie 头返回给浏览器。
  3. 浏览器自动在后续请求中携带该 Cookie,后端通过 Cookie 中的 Session ID 查找对应 Session,确定用户身份。
  4. 用户登出时,后端销毁 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-jwtpassport-google-oauth20 等。
  • 序列化/反序列化用户:Session 场景下,Passport 提供 serializeUserdeserializeUser 将用户对象转换成 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):权限的集合,如 admineditorviewer
  • 权限(Permission):对资源的原子操作,如 user:readarticle: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

一个实际项目的认证鉴权体系,往往是上述模块的组合。以下是一个典型的全栈方案:

  1. 注册:bcrypt 哈希密码 → 存库。
  2. 登录:Passport 本地策略验证 → 返回 JWT(Access Token + Refresh Token)。
  3. 请求认证:JWT 策略中间件解析令牌,挂载 req.user
  4. 权限控制:RBAC 中间件检查用户角色权限。
  5. 安全增强
  • 使用 HTTPS 传输所有敏感数据。
  • 对登录等敏感接口实现限流(如 express-rate-limit),防止暴力破解。
  • JWT 的 secret 保证高强度且定期轮换。
  • 在响应中统一处理认证/鉴权错误,避免泄露系统细节。
  • 考虑使用 helmet 设置安全头。

Node.js 生态提供了从低层原生模块到高层认证框架的完整能力链。通过合理组合 Session/JWT、bcrypt 加密、Passport 策略模式和 RBAC 模型,可以构建出既安全又灵活、且易于维护的认证鉴权体系,满足绝大多数 web 应用的需求。