服务端 API:策略与中间件(Policies & middlewares)

页面摘要: 就像 Strapi 核心一样,插件也可以拥有策略(policies)和中间件(middlewares)。插件策略在控制器操作之前运行,并返回 true 或 false 以允许或阻止请求。插件中间件围绕完整的请求/响应周期依次运行,并调用 next() 以继续。将策略和中间件声明为工厂函数对象,并在路由中通过其插件命名空间名称引用它们。

策略和中间件是插件服务端中拦截请求的两种机制。策略决定请求是否应当继续。中间件塑造请求的处理方式。

WARNING

在深入阅读本页概念之前,请确保你已经:

决策指南

在编写任何代码之前,使用下表选择合适的机制:

NeedMechanism
基于用户角色或状态阻止请求Policy
基于请求内容(body、headers)阻止请求Policy 或路由配置中的内联策略
当条件不满足时返回 403Policy
在多个路由间复用相同的访问规则具名 policy(按名称注册并引用)
为插件路由的每个响应添加 headers路由级中间件
在整个服务端记录或追踪每个请求服务端级中间件(strapi.server.use())
在到达控制器之前修改 ctx.query路由级中间件
在多个路由间共享逻辑具名 路由级中间件(按名称注册并引用)

策略(Policies)

策略是一个在给定路由的控制器操作之前运行的函数。它接收请求上下文,评估一个条件,并返回 true 以允许请求,或返回 false(或抛出)以通过 403 响应阻止它。

声明

策略作为普通函数(而非工厂函数)导出。每个策略接收以下 3 个参数:

  • policyContext 是 Koa 上下文对象的包装器。使用它来访问 policyContext.state.user、policyContext.request 等。
  • config 包含在附加策略时传入的每路由配置(例如 { name: 'plugin::my-plugin.hasRole', options: { role: 'editor' } },其中 config 是每策略的 options 对象)。
  • { strapi } 用于访问 Strapi 实例。
NOTE

policyContext.state.user 的确切形态取决于身份验证上下文(例如,管理面板身份验证 vs. Users & Permissions / Content API 身份验证)。请根据你的项目调整角色查找逻辑。

JavaScript

'use strict';

const hasRole = require('./has-role');

module.exports = {
  hasRole,
};
'use strict';

// Allow the request only if the user has the role specified in the route config
// Usage in route: { name: 'plugin::my-plugin.hasRole', options: { role: 'editor' } }
module.exports = (policyContext, config, { strapi }) => {
  const { user } = policyContext.state;
  const targetRole = config.role;

  if (!user || !targetRole) {
    return false;
  }

  // Supports both `user.role` and `user.roles` shapes depending on auth strategy.
  const roles = Array.isArray(user.roles)
    ? user.roles
    : user.role
      ? [user.role]
      : [];

  return roles.some((role) => {
    if (typeof role === 'string') return role === targetRole;
    return role?.code === targetRole || role?.name === targetRole;
  });
};

TypeScript

import hasRole from './has-role';

export default {
  hasRole,
};
import type { Core } from '@strapi/strapi';

type UserRole = { code?: string; name?: string };

// Allow the request only if the user has the role specified in the route config
// Usage in route: { name: 'plugin::my-plugin.hasRole', options: { role: 'editor' } }
export default (
  policyContext: Core.PolicyContext,
  config: { role?: string },
  { strapi }: { strapi: Core.Strapi }
) => {
  const { user } = policyContext.state;
  const targetRole = config?.role;

  if (!user || !targetRole) {
    return false;
  }

  // Supports both `user.role` and `user.roles` shapes depending on auth strategy.
  const userWithRoles = user as { roles?: UserRole[]; role?: UserRole };
  const roles: UserRole[] = Array.isArray(userWithRoles.roles)
    ? userWithRoles.roles
    : userWithRoles.role
      ? [userWithRoles.role]
      : [];

  return roles.some((role) => role?.code === targetRole || role?.name === targetRole);
};

在路由中的用法

声明后,使用 plugin::my-plugin.policy-name 命名空间从路由引用插件策略:

JavaScript

'use strict';

module.exports = [
  {
    method: 'GET',
    path: '/dashboard',
    handler: 'dashboard.find',
    config: {
      // highlight-next-line
      policies: ['plugin::my-plugin.isActive'], // simple reference by namespaced name
    },
  },
  {
    method: 'DELETE',
    path: '/articles/:id',
    handler: 'article.delete',
    config: {
      // highlight-next-line
      policies: [{ name: 'plugin::my-plugin.hasRole', options: { role: 'editor' } }], // with per-route config
    },
  },
  {
    method: 'GET',
    path: '/public',
    handler: 'article.findAll',
    config: {
      // highlight-next-line
      policies: [(policyContext, config, { strapi }) => true], // inline policy, no registration needed
    },
  },
];

TypeScript

import type { Core } from '@strapi/strapi';

export default [
  {
    method: 'GET' as const,
    path: '/dashboard',
    handler: 'dashboard.find',
    config: {
      // highlight-next-line
      policies: ['plugin::my-plugin.isActive'], // simple reference by namespaced name
    },
  },
  {
    method: 'DELETE' as const,
    path: '/articles/:id',
    handler: 'article.delete',
    config: {
      // highlight-next-line
      policies: [{ name: 'plugin::my-plugin.hasRole', options: { role: 'editor' } }], // with per-route config
    },
  },
  {
    method: 'GET' as const,
    path: '/public',
    handler: 'article.findAll',
    config: {
      // highlight-next-line
      policies: [(policyContext: Core.PolicyContext, config: unknown, { strapi }: { strapi: Core.Strapi }) => true], // inline policy, no registration needed
    },
  },
];
策略返回值

返回 false 会导致 Strapi 发送 403 Forbidden 响应。返回空值(undefined)被视为宽松(允许),而不是阻止。务必显式返回 true 或 false。抛出异常会导致 Strapi 发送 500 响应,除非你抛出的是 Strapi HTTP 错误类(例如 new errors.PolicyError(...)、new errors.ForbiddenError(...) 或 new errors.UnauthorizedError(...))。

后端自定义

有关包含 GraphQL 支持和 policyContext API 的完整策略参考,请参阅 Policies。

中间件(Middlewares)

中间件是一个 Koa 风格的函数,包裹请求/响应周期。与 策略(通过/失败的守卫)不同,中间件可以在请求到达控制器之前读取并修改请求,并在控制器执行之后修改响应。

插件可以通过 2 种方式导出中间件:

  • 作为 路由级中间件,在 middlewares 导出(服务端入口文件)中声明,并在路由 config.middlewares 中引用
  • 作为 服务端级中间件,通过 register() 中的 strapi.server.use() 直接注册到 Strapi HTTP 服务端

路由级中间件

路由级中间件作用域限定于特定路由,并像策略一样声明:作为一个具名工厂函数对象,然后在路由配置中引用。

注意两级签名:外层函数接收 (config, { strapi }) 并返回实际的 Koa 中间件 async (ctx, next) => {}。这允许 Strapi 将每路由配置传递给该函数。

NOTE
  • middlewares 从插件导出中间件函数,以便它们可以在路由配置中被引用和复用。
  • strapi.server.use(...) 将中间件附加到全局服务端管道。
  • 中间件执行是基于请求的:一旦附加到路由或服务端管道,它会对每个匹配的请求运行。

JavaScript

'use strict';

const logRequest = require('./log-request');

module.exports = {
  logRequest,
};
'use strict';

module.exports = (config, { strapi }) => async (ctx, next) => {
  strapi.log.info(`[my-plugin] ${ctx.method} ${ctx.url}`);
  await next();
  strapi.log.info(`[my-plugin] → ${ctx.status}`);
};

TypeScript

import logRequest from './log-request';

export default {
  logRequest,
};
import type { Core } from '@strapi/strapi';

export default (config: unknown, { strapi }: { strapi: Core.Strapi }) =>
  async (ctx: any, next: () => Promise<void>) => {
    strapi.log.info(`[my-plugin] ${ctx.method} ${ctx.url}`);
    await next();
    strapi.log.info(`[my-plugin] → ${ctx.status}`);
  };

使用与策略相同的 plugin::my-plugin.middleware-name 命名空间在路由中引用路由级中间件:

JavaScript

'use strict';

module.exports = [
  {
    method: 'POST',
    path: '/articles',
    handler: 'article.create',
    config: {
      // highlight-next-line
      middlewares: ['plugin::my-plugin.logRequest'],
    },
  },
  {
    method: 'GET',
    path: '/articles',
    handler: 'article.find',
    config: {
      middlewares: [
        // highlight-next-line
        async (ctx, next) => {
          // inline middleware, no registration needed
          ctx.query.pageSize = ctx.query.pageSize || '10';
          await next();
        },
      ],
    },
  },
];

TypeScript

export default [
  {
    method: 'POST' as const,
    path: '/articles',
    handler: 'article.create',
    config: {
      // highlight-next-line
      middlewares: ['plugin::my-plugin.logRequest'],
    },
  },
  {
    method: 'GET' as const,
    path: '/articles',
    handler: 'article.find',
    config: {
      middlewares: [
        async (ctx: any, next: () => Promise<void>) => {
          // inline middleware, no registration needed
          ctx.query.pageSize = ctx.query.pageSize || '10';
          await next();
        },
      ],
    },
  },
];

服务端级中间件

服务端级中间件直接注册到 Strapi HTTP 服务端,并对每个请求运行,而不仅仅对插件路由运行。在 register() 中使用 strapi.server.use() 注册它:

JavaScript

'use strict';

module.exports = ({ strapi }) => {
  // Attached to the global server pipeline — runs per matching request
  strapi.server.use(async (ctx, next) => {
    const start = Date.now();
    await next();
    const ms = Date.now() - start;
    ctx.set('X-Response-Time', `${ms}ms`);
  });
};

TypeScript

import type { Core } from '@strapi/strapi';

export default ({ strapi }: { strapi: Core.Strapi }) => {
  // Attached to the global server pipeline — runs per matching request
  strapi.server.use(async (ctx: any, next: () => Promise<void>) => {
    const start = Date.now();
    await next();
    const ms = Date.now() - start;
    ctx.set('X-Response-Time', `${ms}ms`);
  });
};
WARNING

服务端级中间件会影响所有插件和应用程序本身的所有路由,而不仅仅是你插件的路由。一个抛出错误或从不调用 next() 的服务端级中间件会破坏服务端上的每个请求,而不仅仅是你插件的端点。当关注点特定于你插件的端点时,请使用路由级中间件。

版本/运行时行为

对于路由声明,验证接受为 policies 和 middlewares 塑造成 { name, options } 的对象条目(参见 services/server/routing.ts)。

在运行时,某些内部实现仍然在中间件解析器(services/server/middleware.ts)中支持 { resolve, config } 形式,但标准路由文件中的路由验证不接受该形态。

为避免验证错误,请在路由配置中使用 { name, options }。

后端自定义

有关完整的中间件参考,请参阅 Middlewares。

最佳实践

  • 在策略中使用 policyContext,而不是 ctx。 策略的第一个参数是 policyContext,它是 Koa 上下文的包装器。正确使用它可以确保策略对 REST 和 GraphQL 解析器都能工作。

  • 从策略中显式返回。 返回 undefined 的策略被视为宽松(允许)。务必返回 true 以允许或 false 以拒绝。如果意图是阻止请求,切勿隐式返回。

  • 优先使用路由级中间件而非服务端级中间件。 服务端级中间件在 Strapi 整个服务端的每个请求上运行。除非行为确实适用于所有流量,否则请将中间件作用域限定在插件路由。

  • 始终在中间件中调用 await next()。 忘记 next() 意味着请求链被中断,控制器永远不会执行,导致请求挂起且无响应。

  • 为可复用策略使用 options。 当相同策略逻辑需要按路由接收不同参数(例如所需的角色名)时,从路由的 { name, options } 对象传入。这些值在策略函数的 config 参数中接收。这样可以避免重复类似的策略。