人人都会AI编程

24.3 中间层常见能力:鉴权、聚合、缓存、降级

更新时间:2026-07-10

在 BFF(Backend For Frontend)架构中,Node.js 中间层不仅仅是前端和后端微服务之间的“透明代理”。它最核心的价值,在于将散落在各个下游服务中的能力进行组合、剪裁和增强,为前端提供量身定制的接口。其中,鉴权、聚合、缓存和降级是中间层最常见的四类基础能力,它们在业务中几乎每天都会被用到,但实现得好坏差别巨大。本节我们会逐一展开这四种能力的作用、典型实现方式以及值得注意的工程细节。

24.3.1 鉴权:统一的安全网关

在微服务体系下,每个服务各自做身份验证会造成大量重复代码,也容易因为实现不一致而留下安全漏洞。将鉴权收敛到中间层,是最直接有效的做法。

中间层负责的前置校验通常包括:

  • 从请求头或 Cookie 中提取 Token(JWT、OAuth2 Access Token 等);
  • 验证 Token 有效性(签名、过期时间、吊销列表);
  • 解析用户身份,将 userIdrole 等信息挂载到请求上下文;
  • 根据 RBAC 或 ABAC 规则决定是否放行(也可以在后续聚合请求时附带这些信息)。

使用 Node.js 实现时,通常会借助 passportpassport-jwt 作为验证中间件,并将解析的结果注入到 req.user 或自定义上下文中。例如:

const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const { ExtractJwt } = require('passport-jwt');

passport.use(new JwtStrategy({
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.JWT_SECRET,
}, (payload, done) => {
  // 从 payload 中提取用户信息,可选地查询数据库或缓存
  return done(null, { id: payload.sub, role: payload.role });
}));

// 在路由中使用
app.get('/api/me', passport.authenticate('jwt', { session: false }), (req, res) => {
  res.json(req.user);
});

这样做的好处:

  • 前端只需要在请求时携带 Token,不用关心各个微服务各自的安全机制;
  • 下游微服务通常只接收内部请求,可以完全信任中间层注入的身份信息(比如通过请求头传递 X-User-Id),大幅简化服务自身的安全逻辑;
  • 权限规则变更时,只需在中间层调整,不需要修改所有业务服务。

一个常见的坑是,中间层作为全栈入口容易成为攻击焦点。为了强化安全,除了加密 Token 外,还应考虑接口限流、请求签名、防重放攻击等额外措施。这些能力可以结合 21 章介绍的安全防护体系来落地。

24.3.2 聚合:为前端定制单一接口

聚合是 BFF 中间层最体现“对前端友好”能力的一项。在微服务架构下,前端一个页面可能需要从用户服务、订单服务、商品服务分别拉取数据,如果直接在浏览器中发起多个请求,会增加网络开销、暴露服务拓扑,而且移动端弱网环境下体验很差。中间的 Node.js 层可以帮前端完成这些调用的编排,将多次请求合并为一个响应。

聚合的核心逻辑包括:

  • 接受前端传入的少量参数,分析出需要拉取哪些下游数据;
  • 并发调用下游服务(使用 Promise.allPromise.allSettled 或更高级的并发控制库);
  • 对返回数据进行合并、剪裁、重命名,使之符合前端 UI 所需的数据结构;
  • 返回给前端一个“刚好够用”的 JSON。

例如,在用户中心页,前端需要展示用户基本信息、最新订单列表和待处理通知数:

app.get('/api/user-center', authenticate, async (req, res) => {
  const userId = req.user.id;
  try {
    const [profile, orders, notifications] = await Promise.all([
      userService.getProfile(userId),
      orderService.getRecentOrders(userId, 5),
      notificationService.getUnreadCount(userId),
    ]);
    // 将三个服务的返回裁剪拼装为前端直接渲染的格式
    res.json({
      user: {
        name: profile.displayName,
        avatar: profile.avatarUrl,
      },
      recentOrders: orders.map(o => ({
        id: o.orderId,
        total: o.amount,
        status: o.status,
      })),
      unreadNotifications: notifications.count,
    });
  } catch (err) {
    res.status(500).json({ error: 'Failed to load user center' });
  }
});

提高聚合健壮性的几个实用技巧:

  • 使用 Promise.allSettled 取代 Promise.all 可以避免一个下游服务失败导致整个聚合请求崩溃,从而可以降级返回部分数据或默认值。
  • 对每一个下游调用设置合理的超时,避免“慢服务”拖垮整个接口。可以使用 axiostimeout 配置或 p-race 等手段。
  • 如果聚合中包含很多关联查询,可以考虑使用 GraphQL 作为聚合层的查询语言,由中间层解析 query 并自动拼接数据,但这会引入额外的学习成本和复杂度,团队需要评估是否必要。
  • 聚合层自身也可能成为性能瓶颈,可以把高频、低变化的数据放入缓存,避免每次都重复查询。

24.3.3 缓存:降低延迟与后端压力

在中间层引入缓存,通常有两种目的:一是减少对下游服务的重复调用,二是加快接口响应速度。由于 Node.js 处于前端和内部微服务之间,天然就是设置缓存的好位置。

缓存的三个典型层级:

  1. 内存缓存:适合极高频、总量小、允许进程间不一致的数据,例如配置信息、字典表。Node.js 中可以使用 node-cache 或简单的 Map,但重启即丢失,多个进程实例间不会共享。
  2. Redis 缓存:适合需要跨进程共享、持久化的缓存场景。可以用于频繁查询的用户信息、商品库存(需注意一致性)等。Redis 的数据结构(String、Hash、List)也为缓存策略提供了很多灵活性。
  3. HTTP 缓存头:如果中间层直接与客户端交互,还可以设置 Cache-ControlETag 等响应头,让 CDN 或浏览器缓存完整响应。

实现一个带缓存的聚合接口示例(使用 Redis):

const redis = require('ioredis');
const client = new redis(process.env.REDIS_URL);

app.get('/api/hot-products', async (req, res) => {
  const cacheKey = 'hot:products';
  const cached = await client.get(cacheKey);
  if (cached) {
    return res.json(JSON.parse(cached));
  }
  // 缓存未命中,查询下游
  const products = await productService.getHotProducts();
  // 缓存 5 分钟
  await client.setex(cacheKey, 300, JSON.stringify(products));
  res.json(products);
});

缓存的真正难度在于失效策略:

  • 采用 TTL(过期时间)是最简单的方式,适合对实时性要求不高的数据。
  • 对于更新频繁的数据,应在数据变更时主动更新或删除缓存(Cache-Aside 模式)。可以在中间层监听消息队列的事件,当商品服务发布“商品更新”消息时,中间层删除对应的缓存键。
  • 如果下游接口返回错误,应避免缓存错误结果,以防止雪崩效应。需要在代码中对错误做判断,仅缓存成功的响应。

合理使用缓存可以显著提升吞吐,但需要注意避免缓存引入的数据不一致问题,特别是涉及用户私有数据时,务必将 userId 等标识纳入缓存键中,防止跨用户数据泄露。

24.3.4 降级:保障核心体验的最后防线

当在中间层聚合多个下游服务时,其中一个服务不可用或严重超时,如果直接报错或长时间等待,前端可能出现白屏或不完整的错误页面。降级的意义就在于:在部分能力暂时不可用时,仍然维持应用的可用性,或至少给出优雅的提示。

常见的降级策略包括:

  • 返回缓存数据:如果下游服务失败但缓存中有旧数据,可以暂时使用旧数据(对页面影响通常小),同时在后台发出告警。
  • 省略非关键信息:例如用户中心页中,如果“最近订单”服务超时,可以先返回用户基本信息和默认的订单占位符(或直接不展示订单模块),而不是整个接口 500。
  • 提供兜底默认值:当价格、库存等数字无法获取时,可以用 null- 代替,并配合前端展示“数据加载失败”。
  • 服务熔断:当某个下游服务在短时间内大量失败时,应当临时停止请求,直接走降级逻辑。可以使用 opossum 这类熔断器库来实现:
  const CircuitBreaker = require('opossum');
  const orderBreaker = new CircuitBreaker(orderService.getRecentOrders, {
    timeout: 2000,      // 请求超时 2 秒
    errorThresholdPercentage: 50, // 失败率超过 50% 时熔断
    resetTimeout: 30000 // 30 秒后尝试恢复
  });
  

在 BFF 路由中调用 orderBreaker.fire(userId, 5) 即可自动获得熔断保护。

设计降级功能的三条原则:

  1. 明确优先级:先确定哪些数据对页面是“致命”的(核心数据),哪些是“锦上添花”的。前者的降级可能只能返回静默错误或通用提示,后者的降级则可以采用忽略或占位符。
  2. 减少客户端判断逻辑:中间层应该尽可能给出确定的响应结构,前端只需根据返回的字段做展示/隐藏,而不是在浏览器端实现重试、降级判断,以免业务逻辑分散。
  3. 监控与报警不可缺:一旦触发降级,说明某处出现了问题,必须有对应的日志和告警,否则团队不会意识到系统正在“带病”工作。

鉴权、聚合、缓存、降级这四种能力相互关联,常常组合使用。例如,已鉴权的用户请求聚合数据,中间层先查询缓存,如果缓存未命中则并发调用下游,并设置降级逻辑保护部分数据。最终,这些基础能力让 BFF 中间层从一个简单的转发网关,升级为真正对前端业务负责的服务入口。

在实际项目中,可以根据团队资源逐步引入这些能力,优先从鉴权和聚合开始,然后根据性能数据和可靠性要求逐步完善缓存和降级。它们都不是“银弹”,但确是 BFF 中间层最具工程价值的四块基石。