# 服务器策略和中间件

> Source: https://strapi.nodejs.cn/cms/plugins-development/server-policies-middlewares

🌐 Server API: Policies & middlewares

就像 Strapi 核心一样，插件也可以有策略和中间件。插件策略在控制器动作之前运行，并返回 `true` 或 `false` 来允许或阻止请求。插件中间件在整个请求/响应周期中按顺序运行，并调用 `next()` 以继续。将策略和中间件声明为工厂函数的对象，并在路由中通过插件命名空间的名称引用它们。

策略和中间件是插件服务器中拦截请求的两种机制。策略决定请求是否应该继续。中间件决定它如何被处理。

🌐 Policies and middlewares are the two mechanisms for intercepting requests in a plugin server. Policies decide whether a request should proceed. Middlewares shape how it is processed.

:::prerequisites

在深入了解本页的概念之前，请确保你已经：

🌐 Before diving deeper into the concepts on this page, please ensure you have:

- [创建了一个 Strapi 插件](/cms/plugins-development/create-a-plugin)，
- 已阅读并理解了[服务器 API](/cms/plugins-development/server-api)的基础知识

:::

## 决策指南 {#decision-guide}

🌐 Decision guide

在编写任何代码之前，请使用此表选择正确的机制：

🌐 Use this table to pick the right mechanism before writing any code:

| 需求 | 机制 |
| --- | --- |
| 根据用户角色或状态阻止请求 | [策略](#policies) |
| 根据请求内容（body, headers）阻止请求 | [策略](#policies) 或路由配置中的内联策略 |
| 当条件不满足时返回 403 | [策略](#policies) |
| 在多个路由间重用相同访问规则 | 命名 [策略](#policies)（已注册并通过名称引用） |
| 为插件的路由的每个响应添加头信息 | [路由级中间件](#route-level-middlewares) |
| 在整个服务器上记录或跟踪每个请求 | [服务器级中间件](#server-level-middlewares) (`strapi.server.use()`) |
| 在到达控制器之前修改 `ctx.query` | [路由级中间件](#route-level-middlewares) |
| 在多个路由间共享逻辑 | 命名 [路由级中间件](#route-level-middlewares)（已注册并通过名称引用） |

## 政策 {#policies}

🌐 Policies

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

🌐 A policy is a function that runs before the controller action for a given route. It receives the request context, evaluates a condition, and returns `true` to allow the request or `false` (or throws) to block it with a 403 response.

### 声明 {#declaration}

🌐 Declaration

策略以普通函数的形式导出（不是工厂函数）。每个策略接收以下三个参数：

🌐 Policies are exported as plain functions (not factory functions). Each policy receives the following 3 arguments:

- `policyContext` 是 Koa 上下文对象的一个封装。使用它可以访问 `policyContext.state.user`、`policyContext.request` 等。
- `config` 包含在附加策略时传入的每条路由配置（例如，`{ name: 'plugin::my-plugin.hasRole', options: { role: 'editor' } }`，其中 `config` 是每策略的选项对象）。
- `{ strapi }` 提供对 Strapi 实例的访问。

:::note

`policyContext.state.user` 的确切形状取决于认证上下文（例如，管理员面板认证与用户与权限 / 内容 API 认证）。请将角色查找逻辑调整为适应你的项目。

🌐 The exact shape of `policyContext.state.user` depends on the authentication context (for example, admin panel authentication vs. Users & Permissions / Content API authentication). Adapt the role lookup logic to your project.

:::

```js title="/src/plugins/my-plugin/server/src/policies/index.js"
'use strict';

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

module.exports = {
  hasRole,
};
```

```js title="/src/plugins/my-plugin/server/src/policies/has-role.js"
'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;
  });
};
```
```ts title="/src/plugins/my-plugin/server/src/policies/index.ts"

  hasRole,
};
```

```ts title="/src/plugins/my-plugin/server/src/policies/has-role.ts"

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' } }

  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);
};
```

### 在路由中的使用 {#usage-in-routes}

🌐 Usage in routes

一旦声明，可以使用 `plugin::my-plugin.policy-name` 命名空间从路由中引用插件策略：

🌐 Once declared, reference a plugin policy from a route using the `plugin::my-plugin.policy-name` namespace:

```js title="/src/plugins/my-plugin/server/src/routes/index.js"
'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
    },
  },
];
```
```ts title="/src/plugins/my-plugin/server/src/routes/index.ts"

  {
    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
    },
  },
];
```

:::caution Policy return values

返回 `false` 会导致 Strapi 发送 `403 Forbidden` 响应。返回空 (`undefined`) 被视为允许（允许），而不是阻止。始终明确返回 `true` 或 `false`。抛出错误会导致 Strapi 发送 `500` 响应，除非你抛出 Strapi HTTP 错误类（例如 `new errors.PolicyError(...)`、`new errors.ForbiddenError(...)` 或 `new errors.UnauthorizedError(...)`）。

🌐 Returning `false` causes Strapi to send a `403 Forbidden` response. Returning nothing (`undefined`) is treated as permissive (allowed), not as a block. Always return `true` or `false` explicitly. Throwing an error causes Strapi to send a `500` response unless you throw a Strapi HTTP error class (e.g., `new errors.PolicyError(...)`, `new errors.ForbiddenError(...)`, or `new errors.UnauthorizedError(...)`).

:::

:::strapi Backend customization

有关包括 GraphQL 支持和 `policyContext` API 在内的完整政策参考，请参阅 [Policies](/cms/backend-customization/policies)。

🌐 For the full policy reference including GraphQL support and the `policyContext` API, see [Policies](/cms/backend-customization/policies).

:::

## 中间件 {#middlewares}

🌐 Middlewares

中间件是一个 Koa 风格的函数，用于封装请求/响应周期。与 [策略](#policies)（它们是通过/不通过的守卫）不同，中间件可以在请求到达控制器之前读取和修改请求，并在控制器执行后修改响应。

🌐 A middleware is a Koa-style function that wraps the request/response cycle. Unlike [policies](#policies) (which are pass/fail guards), middlewares can read and modify the request before it reaches the controller, and modify the response after the controller has executed.

插件可以通过两种方式导出中间件：

🌐 Plugins can export middlewares in 2 ways:

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

### 路由级中间件 {#route-level-middlewares}

🌐 Route-level middlewares

路由级中间件作用于特定路由，并且声明方式类似于策略：作为命名工厂函数的对象，然后在路由配置中引用。

🌐 Route-level middlewares are scoped to a specific route and are declared like policies: as an object of named factory functions, then referenced in the route config.

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

🌐 Note the two-level signature: the outer function receives `(config, { strapi })` and returns the actual Koa middleware `async (ctx, next) => {}`. This allows Strapi to pass per-route configuration to the function.

:::note

- `middlewares` 从插件中导出中间件函数，以便在路由配置中引用和重用。
- `strapi.server.use(...)` 将一个中间件附加到全局服务器管道上。
- 中间件的执行是基于请求的：一旦附加到路由或服务器管道，它会在每个匹配的请求中运行。

:::

```js title="/src/plugins/my-plugin/server/src/middlewares/index.js"
'use strict';

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

module.exports = {
  logRequest,
};
```

```js title="/src/plugins/my-plugin/server/src/middlewares/log-request.js"
'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}`);
};
```
```ts title="/src/plugins/my-plugin/server/src/middlewares/index.ts"

  logRequest,
};
```

```ts title="/src/plugins/my-plugin/server/src/middlewares/log-request.ts"

  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` 命名空间引用路由级中间件：

🌐 Reference a route-level middleware in a route using the same `plugin::my-plugin.middleware-name` namespace as policies:

```js title="/src/plugins/my-plugin/server/src/routes/index.js"
'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();
        },
      ],
    },
  },
];
```
```ts title="/src/plugins/my-plugin/server/src/routes/index.ts"

  {
    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();
        },
      ],
    },
  },
];
```

### 服务器级中间件 {#server-level-middlewares}

🌐 Server-level middlewares

服务器级中间件直接注册在 Strapi HTTP 服务器上，并且针对每个请求运行，而不仅仅是插件路由。在 `register()` 中使用 `strapi.server.use()` 注册它：

🌐 A server-level middleware is registered on the Strapi HTTP server directly and runs for every request, not just plugin routes. Register it in `register()` using `strapi.server.use()`:

```js title="/src/plugins/my-plugin/server/src/register.js"
'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`);
  });
};
```
```ts title="/src/plugins/my-plugin/server/src/register.ts"

  // 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`);
  });
};
```

:::caution

服务器级中间件会影响所有插件和应用本身的所有路由，而不仅仅是你插件的路由。一个抛出异常或从未调用 `next()` 的服务器级中间件会破坏服务器上的每一个请求，而不仅仅是你插件的端点。当关心的问题仅涉及你插件的端点时，请使用路由级中间件。

🌐 Server-level middlewares affect all routes across all plugins and the application itself, not just your plugin's routes. A server-level middleware that throws or never calls `next()` will break every request on the server, not just your plugin's endpoints. Use route-level middlewares when the concern is specific to your plugin's endpoints.

:::

:::note Version/runtime behavior

对于路由声明，验证接受形状为 `{ name, options }` 的对象条目，用于 `policies` 和 `middlewares`（见 `services/server/routing.ts`）。

🌐 For route declarations, validation accepts object entries shaped as `{ name, options }` for both `policies` and `middlewares` (see `services/server/routing.ts`).

在运行时，一些内部仍然在中间件解析器（`services/server/middleware.ts`）中引用 `{ resolve, config }` 支持，但标准路由文件中的路由验证不接受该形状。

🌐 At runtime, some internals still reference `{ resolve, config }` support in the middleware resolver (`services/server/middleware.ts`), but that shape is not accepted by route validation in standard route files.

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

🌐 To avoid validation errors, use `{ name, options }` in route configurations.

:::

:::strapi Backend customization

有关完整的中间件参考，请参见 [中间件](/cms/backend-customization/middlewares)。

🌐 For the full middleware reference, see [Middlewares](/cms/backend-customization/middlewares).

:::

## 最佳实践 {#best-practices}

🌐 Best practices

- **在策略中使用 `policyContext`，而不是 `ctx`。** 策略的第一个参数是 `policyContext`，它是 Koa 上下文的一个封装。正确使用它可以确保策略在 REST 和 GraphQL 解析器中都能正常工作。
- **明确地从策略中返回。** 返回 `undefined` 的策略被视为允许（允许）的策略。要允许请始终返回 `true`，要拒绝请始终返回 `false`。如果意图是阻止请求，请勿隐式返回。
- **优先使用路由级中间件而非服务器级中间件。** 服务器级中间件会在整个 Strapi 服务器的每个请求上运行。除非该行为确实适用于所有流量，否则将中间件范围限制在插件路由上。
- **在中间件中始终调用 `await next()`。** 忘记 `next()` 意味着请求链被中断，控制器永远不会执行，导致请求挂起且没有响应。
- **使用 `options` 来实现可重用的策略。** 当相同的策略逻辑在不同路由中需要不同的参数（例如，必需的角色名称）时，可以从路由的 `{ name, options }` 对象中传递这些参数。这些值会在策略函数的 `config` 参数中接收。这可以避免重复类似的策略。
