自定义用户与权限插件路由
页面摘要: 用户与权限功能暴露了
/users和/auth路由,可以使用插件扩展系统进行扩展或覆盖。本指南展示了如何向 User 集合添加自定义策略、覆盖控制器以及添加新路由。
用户与权限功能随附了用于身份验证(/auth)和用户管理(/users)的内置路由。由于这些路由属于插件而非用户创建的内容类型,因此无法使用 createCoreRouter 进行自定义。相反,请通过使用 /src/extensions/users-permissions/ 文件夹中的 strapi-server 文件,通过插件扩展系统来扩展它们。
工作原理
用户与权限使用一套与标准内容类型不同的路由数组和控制器对象。在自定义它们之前,理解其结构至关重要。
路由结构
与你创建的内容类型(例如 api::restaurant.restaurant)不同,用户与权限插件在其 plugin.routes['content-api'].routes 数组中注册其路由。该数组包含所有的 /users、/auth、/roles 和 /permissions 路由定义。
每条路由都是一个具有以下形状的对象:
{
method: 'GET', // HTTP 方法
path: '/users', // URL 路径(相对于 /api)
handler: 'user.find', // controller.action
config: {
prefix: '', // 路径前缀(空表示 /api)
},
}
路由配置也可以包含可选的 policies 和 middlewares 数组(请参阅添加自定义策略)。
strapi-server 扩展文件 {#extend-routes}
对用户与权限插件的所有自定义都放在一个文件中:
JavaScript
module.exports = (plugin) => {
// Your customizations here
return plugin;
};
TypeScript
export default (plugin) => {
// Your customizations here
return plugin;
};
该函数接收完整的插件对象,并且必须返回该插件。在返回之前,你可以修改 plugin.routes、plugin.controllers、plugin.policies 和 plugin.services。
可用的操作 {#available-actions}
user 控制器是一个暴露以下操作的普通对象:
| 操作 | 方法 | 路径 | 描述 |
|---|---|---|---|
user.count | GET | /users/count | 统计用户数量 |
user.find | GET | /users | 查找所有用户 |
user.me | GET | /users/me | 获取已通过身份验证的用户 |
user.findOne | GET | /users/:id | 查找单个用户 |
user.create | POST | /users | 创建用户 |
user.update | PUT | /users/:id | 更新用户 |
user.destroy | DELETE | /users/:id | 删除用户 |
auth 控制器是一个工厂函数 ({ strapi }) => ({...}),暴露以下操作:
| 操作 | 方法 | 路径 | 是否限流 |
|---|---|---|---|
auth.callback | POST | /auth/local | 是 |
auth.callback | GET | /auth/:provider/callback | 否 |
auth.register | POST | /auth/local/register | 是 |
auth.connect | GET | /connect/(.*) | 是 |
auth.forgotPassword | POST | /auth/forgot-password | 是 |
auth.resetPassword | POST | /auth/reset-password | 是 |
auth.changePassword | POST | /auth/change-password | 是 |
auth.emailConfirmation | GET | /auth/email-confirmation | 否 |
auth.sendEmailConfirmation | POST | /auth/send-email-confirmation | 否 |
auth.refresh | POST | /auth/refresh | 否 |
auth.getSessions | GET | /auth/sessions | 否 |
auth.revokeSession | DELETE | /auth/sessions/:sessionId | 否 |
auth.logout | POST | /auth/logout | 否 |
由于 user 和 auth 控制器具有不同的类型(普通对象 vs. 工厂函数),它们需要不同的覆盖模式(请参阅覆盖 user 控制器操作和覆盖 auth 控制器操作)。
自定义路由 {#customize-routes}
你可以通过修改扩展文件中的 plugin.routes['content-api'].routes 数组来添加策略、注册新的端点或删除现有的端点。
添加自定义策略 {#add-custom-policy}
一个常见需求是限制谁可以更新或删除用户账户:例如,确保用户只能更新自己的个人资料。
1. 创建策略文件
创建一个全局策略,用于检查已通过身份验证的用户是否与目标用户匹配。策略函数接收 Koa 上下文(可访问 state.user 和 params)、一个可选的配置对象以及 { strapi }:
JavaScript
"use strict";
module.exports = (policyContext, config, { strapi }) => {
const currentUser = policyContext.state.user;
if (!currentUser) {
return false;
}
const targetUserId = Number(policyContext.params.id);
if (currentUser.id !== targetUserId) {
return false;
}
return true;
};
TypeScript
export default (policyContext, config, { strapi }) => {
const currentUser = policyContext.state.user;
if (!currentUser) {
return false;
}
const targetUserId = Number(policyContext.params.id);
if (currentUser.id !== targetUserId) {
return false;
}
return true;
};
上面的 is-own-user 策略特别适用于用户与权限插件路由。对于标准内容类型上的类似模式(限制访问条目作者),请参阅 is-owner 中间件示例 和 is-owner-review 策略示例。
2. 将策略附加到用户路由
在插件扩展文件中,找到 update 和 delete 路由并添加该策略:
JavaScript
module.exports = (plugin) => {
// Find the routes that need the policy
const routes = plugin.routes['content-api'].routes;
// Add the 'is-own-user' policy to the update route
const updateRoute = routes.find(
(route) => route.handler === 'user.update'
);
if (updateRoute) {
updateRoute.config = updateRoute.config || {};
updateRoute.config.policies = updateRoute.config.policies || [];
updateRoute.config.policies.push('global::is-own-user');
}
// Add the same policy to the delete route
const deleteRoute = routes.find(
(route) => route.handler === 'user.destroy'
);
if (deleteRoute) {
deleteRoute.config = deleteRoute.config || {};
deleteRoute.config.policies = deleteRoute.config.policies || [];
deleteRoute.config.policies.push('global::is-own-user');
}
return plugin;
};
TypeScript
export default (plugin) => {
// Find the routes that need the policy
const routes = plugin.routes['content-api'].routes;
// Add the 'is-own-user' policy to the update route
const updateRoute = routes.find(
(route) => route.handler === 'user.update'
);
if (updateRoute) {
updateRoute.config = updateRoute.config || {};
updateRoute.config.policies = updateRoute.config.policies || [];
updateRoute.config.policies.push('global::is-own-user');
}
// Add the same policy to the delete route
const deleteRoute = routes.find(
(route) => route.handler === 'user.destroy'
);
if (deleteRoute) {
deleteRoute.config = deleteRoute.config || {};
deleteRoute.config.policies = deleteRoute.config.policies || [];
deleteRoute.config.policies.push('global::is-own-user');
}
return plugin;
};
通过此配置,如果已通过身份验证的用户与 URL 中的 :id 不匹配,PUT /api/users/:id 和 DELETE /api/users/:id 将返回 403 Forbidden 错误。
要获得更具信息量的错误消息,请抛出 PolicyError 而不是返回 false:
const { errors } = require('@strapi/utils');
const { PolicyError } = errors;
// Inside the policy:
throw new PolicyError('You can only modify your own account');
有关策略模式和错误处理的更多详情,请参阅策略文档。
添加新路由 {#add-new-route}
你可以向用户与权限插件添加自定义路由。例如,按如下方式添加一个用于停用用户账户的端点:
JavaScript
module.exports = (plugin) => {
// Add a new controller action
plugin.controllers.user.deactivate = async (ctx) => {
const { id } = ctx.params;
const user = await strapi
.plugin('users-permissions')
.service('user')
.edit(id, { blocked: true });
ctx.body = { message: `User ${user.username} has been deactivated` };
};
// Register the route
plugin.routes['content-api'].routes.push({
method: 'POST',
path: '/users/:id/deactivate',
handler: 'user.deactivate',
config: {
prefix: '',
policies: ['global::is-own-user'],
},
});
return plugin;
};
TypeScript
export default (plugin) => {
// Add a new controller action
plugin.controllers.user.deactivate = async (ctx) => {
const { id } = ctx.params;
const user = await strapi
.plugin('users-permissions')
.service('user')
.edit(id, { blocked: true });
ctx.body = { message: `User ${user.username} has been deactivated` };
};
// Register the route
plugin.routes['content-api'].routes.push({
method: 'POST',
path: '/users/:id/deactivate',
handler: 'user.deactivate',
config: {
prefix: '',
policies: ['global::is-own-user'],
},
});
return plugin;
};
重启 Strapi 后,POST /api/users/:id/deactivate 即可使用。在管理面板中,为应当访问此端点的角色,在 用户与权限插件 > Roles 下授予相应的权限。
删除路由 {#remove-route}
你可以通过将路由从路由数组中过滤掉来禁用某条路由。例如,按如下方式禁用用户计数端点:
JavaScript
module.exports = (plugin) => {
plugin.routes['content-api'].routes = plugin.routes['content-api'].routes.filter(
(route) => route.handler !== 'user.count'
);
return plugin;
};
TypeScript
export default (plugin) => {
plugin.routes['content-api'].routes = plugin.routes['content-api'].routes.filter(
(route) => route.handler !== 'user.count'
);
return plugin;
};
覆盖控制器 {#override-controllers}
除了路由级别的自定义之外,你还可以覆盖控制器操作本身,以改变插件处理请求的方式。user 和 auth 控制器使用不同的模式,因此各自需要特定的方法。
覆盖 user 控制器操作 {#override-controller}
user 控制器是一个普通对象,因此你可以在扩展文件中直接读取并替换其方法。例如,要向 me 端点添加自定义逻辑:
JavaScript
module.exports = (plugin) => {
const originalMe = plugin.controllers.user.me;
plugin.controllers.user.me = async (ctx) => {
// Call the original controller
await originalMe(ctx);
// Add extra data to the response
if (ctx.body) {
ctx.body.timestamp = new Date().toISOString();
}
};
return plugin;
};
TypeScript
export default (plugin) => {
const originalMe = plugin.controllers.user.me;
plugin.controllers.user.me = async (ctx) => {
// Call the original controller
await originalMe(ctx);
// Add extra data to the response
if (ctx.body) {
ctx.body.timestamp = new Date().toISOString();
}
};
return plugin;
};
当包装一个控制器时,请始终先调用原始函数以保留默认行为。跳过原始函数意味着你接管了完整的请求处理,包括清理(sanitization)和错误处理。
覆盖 auth 控制器操作 {#override-auth-route}
auth 控制器使用工厂模式:它导出一个函数 ({ strapi }) => ({...}),而不是普通对象。当你的扩展代码运行时,Strapi 尚未解析该工厂。因此,plugin.controllers.auth 是一个函数,而不是带有方法的对象。
要覆盖一个 auth 操作,请包装工厂本身:
JavaScript
module.exports = (plugin) => {
const originalAuthFactory = plugin.controllers.auth;
plugin.controllers.auth = ({ strapi }) => {
// Resolve the original factory to get the controller methods
const originalAuth = originalAuthFactory({ strapi });
// Override the register method
const originalRegister = originalAuth.register;
originalAuth.register = async (ctx) => {
// Call the original register logic
await originalRegister(ctx);
// Custom post-registration logic
if (ctx.body && ctx.body.user) {
strapi.log.info(`New user registered: ${ctx.body.user.email}`);
}
};
return originalAuth;
};
return plugin;
};
TypeScript
export default (plugin) => {
const originalAuthFactory = plugin.controllers.auth;
plugin.controllers.auth = ({ strapi }) => {
// Resolve the original factory to get the controller methods
const originalAuth = originalAuthFactory({ strapi });
// Override the register method
const originalRegister = originalAuth.register;
originalAuth.register = async (ctx) => {
// Call the original register logic
await originalRegister(ctx);
// Custom post-registration logic
if (ctx.body && ctx.body.user) {
strapi.log.info(`New user registered: ${ctx.body.user.email}`);
}
};
return originalAuth;
};
return plugin;
};
不要直接访问 plugin.controllers.auth.register。由于在扩展时 auth 还是一个工厂函数,其方法在 Strapi 调用该工厂之前是不可访问的。请始终像上面所示那样包装工厂。
完整示例 {#combine-customizations}
以下示例在单个文件中组合了多项自定义:它向 update 和 delete 添加策略、包装 me 控制器,并添加一个新 profile 路由。
JavaScript
module.exports = (plugin) => {
const routes = plugin.routes['content-api'].routes;
// 1. Add 'is-own-user' policy to update and delete
for (const route of routes) {
if (route.handler === 'user.update' || route.handler === 'user.destroy') {
route.config = route.config || {};
route.config.policies = route.config.policies || [];
route.config.policies.push('global::is-own-user');
}
}
// 2. Wrap the 'me' controller to include the user's role
const originalMe = plugin.controllers.user.me;
plugin.controllers.user.me = async (ctx) => {
await originalMe(ctx);
if (ctx.state.user && ctx.body) {
const user = await strapi
.plugin('users-permissions')
.service('user')
.fetch(ctx.state.user.id, { populate: ['role'] });
ctx.body.role = user.role;
}
};
// 3. Add a custom route
plugin.controllers.user.profile = async (ctx) => {
const user = await strapi
.plugin('users-permissions')
.service('user')
.fetch(ctx.state.user.id, { populate: ['role'] });
ctx.body = {
username: user.username,
email: user.email,
role: user.role?.name,
createdAt: user.createdAt,
};
};
routes.push({
method: 'GET',
path: '/users/profile',
handler: 'user.profile',
config: { prefix: '' },
});
return plugin;
};
TypeScript
export default (plugin) => {
const routes = plugin.routes['content-api'].routes;
// 1. Add 'is-own-user' policy to update and delete
for (const route of routes) {
if (route.handler === 'user.update' || route.handler === 'user.destroy') {
route.config = route.config || {};
route.config.policies = route.config.policies || [];
route.config.policies.push('global::is-own-user');
}
}
// 2. Wrap the 'me' controller to include the user's role
const originalMe = plugin.controllers.user.me;
plugin.controllers.user.me = async (ctx) => {
await originalMe(ctx);
if (ctx.state.user && ctx.body) {
const user = await strapi
.plugin('users-permissions')
.service('user')
.fetch(ctx.state.user.id, { populate: ['role'] });
ctx.body.role = user.role;
}
};
// 3. Add a custom route
plugin.controllers.user.profile = async (ctx) => {
const user = await strapi
.plugin('users-permissions')
.service('user')
.fetch(ctx.state.user.id, { populate: ['role'] });
ctx.body = {
username: user.username,
email: user.email,
role: user.role?.name,
createdAt: user.createdAt,
};
};
routes.push({
method: 'GET',
path: '/users/profile',
handler: 'user.profile',
config: { prefix: '' },
});
return plugin;
};
验证
做出更改后,重启 Strapi 并验证你的自定义是否生效:
- 运行
yarn strapi routes:list以确认你的新路由或修改过的路由已出现。 - 在未经身份验证的情况下测试受保护的路由,以验证策略返回
403 Forbidden。 - 使用已通过身份验证的用户进行测试,以确认预期的行为。
- 检查 Strapi 服务器日志,查看启动期间是否有错误。
故障排查
| 症状 | 可能的原因 |
|---|---|
| 找不到路由(404) | 新路由未被推送到 plugin.routes['content-api'].routes,或者其 prefix 属性缺失。 |
| 策略未应用 | 策略名称不正确。全局策略需要 global:: 前缀(例如 global::is-own-user)。 |
| 控制器返回 500 | 控制器操作名称与路由定义中的 handler 值不匹配。 |
| 更改未生效 | 修改扩展文件后未重启 Strapi。扩展在启动时加载。 |
| 权限被拒绝(403) | 新操作未对该角色启用。在 用户与权限插件 > Roles 中启用它。 |
无法读取 auth 控制器的属性 | auth 控制器是一个工厂函数,而不是普通对象。请包装工厂,而不是直接访问方法(请参阅覆盖 auth 控制器操作)。 |