服务端 API:策略与中间件(Policies & middlewares)
页面摘要: 就像 Strapi 核心一样,插件也可以拥有策略(policies)和中间件(middlewares)。插件策略在控制器操作之前运行,并返回
true或false以允许或阻止请求。插件中间件围绕完整的请求/响应周期依次运行,并调用next()以继续。将策略和中间件声明为工厂函数对象,并在路由中通过其插件命名空间名称引用它们。
策略和中间件是插件服务端中拦截请求的两种机制。策略决定请求是否应当继续。中间件塑造请求的处理方式。
在深入阅读本页概念之前,请确保你已经:
- 创建了一个 Strapi 插件,
- 阅读并理解了 Server API 的基础知识。
决策指南
在编写任何代码之前,使用下表选择合适的机制:
| Need | Mechanism |
|---|---|
| 基于用户角色或状态阻止请求 | Policy |
| 基于请求内容(body、headers)阻止请求 | Policy 或路由配置中的内联策略 |
| 当条件不满足时返回 403 | Policy |
| 在多个路由间复用相同的访问规则 | 具名 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 实例。
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 将每路由配置传递给该函数。
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`);
});
};
服务端级中间件会影响所有插件和应用程序本身的所有路由,而不仅仅是你插件的路由。一个抛出错误或从不调用 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参数中接收。这样可以避免重复类似的策略。