
Ghost api-framework 权限系统详解api-framework 控制器的四种 permissions 模式与数据库权限落地【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost本文基于 Ghost 仓库中的 API 控制器权限指南.agents/skills/add-admin-api-endpoint/permissions.md系统讲解 Ghost 后端 api-framework 权限体系的五种请求处理阶段、四种permissions配置模式、Frame 上下文对象以及默认的数据库权限处理器permissions: true背后的查表机制与通过迁移脚本落地新权限的完整流程。读完后你可以为新管理端 API 端点正确配置权限并通过迁移把权限记录写入数据库避免安全漏洞。一、权限在请求处理管线中的位置Ghost 的后端 API 层基于tryghost/api-framework构建。每个控制器controller文件在 endpoints/index.js 中通过apiFramework.pipeline(require(./posts), localUtils)这样的方式接入框架框架将请求拆分为五个处理阶段输入校验Input validation输入序列化Input serialisation权限Permissions← 本文焦点查询执行Query即控制器的query方法输出序列化Output serialisation关键约束每个控制器方法都必须显式声明permissions属性。这是一项安全要求——显式声明防止了忘记写权限造成的安全漏洞也让每个端点的鉴权策略一目了然。如果缺少permissions属性框架会抛出IncorrectUsageError// 这会抛出 IncorrectUsageError edit: { query(frame) { return models.Post.edit(frame.data, frame.options); } // 缺少 permissions 属性 }仓库中真实控制器都遵循这一约定例如 automated-emails.js 中每个方法都显式带有permissions: true或permissions: { ... }。二、四种权限配置模式模式 1布尔值true—— 默认权限检查最常用的模式将权限判断委托给默认权限处理器edit: { headers: { cacheInvalidate: true }, options: [include], validation: { options: { include: { required: true, values: [tags] } } }, permissions: true, query(frame) { return models.Post.edit(frame.data, frame.options); } }适用场景标准 CRUD 操作默认权限处理器即可满足需求绝大多数需要登录的端点。默认权限处理器的工作原理设置permissions: true后框架会调用位于 api/endpoints/utils/permissions.js 的默认处理器。阅读该文件的nonePublicAuth函数L18-L83实际流程如下单数形式推导处理器将docName转换为单数——posts→postautomated_emails→automated_emailcategories→category源码中特判了ies→y的替换对应源码if (apiConfig.docName.match(/ies$/)) { singular apiConfig.docName.replace(/ies$/, y); } else { singular apiConfig.docName.replace(/s$/, ); }权限标识符identifier默认取frame.options.id控制器可通过apiConfig.identifier(frame)覆盖它比如编辑设置时用设置的 key改密码时用 body 中的 user id。权限检查调用permissions.canThis(frame.options.context)[method]singular以docName: posts、方法edit为例实际调用的是permissions.canThis(context).edit.post(postId, unsafeAttrs)。数据库查表权限服务ghost/core/core/server/services/permissions在permissions与permissions_roles两张表中查找action_type匹配方法如edit、object_type匹配单数 docName如post的权限记录并校验当前用户的角色是否被授予该权限。源码中还有一处值得注意的细节权限检查的返回值可以携带excludedAttrs列表处理器会把这些属性从请求数据中_.omit掉与直接抛NoPermissionError的unsafeAttrs不同它只是静默排除字段。源码注释说明这个机制目前主要为 posts 模型与 contributor 角色服务。默认处理器依赖的数据库配置要让permissions: true正常工作数据库中必须存在对应记录permissions表中的权限记录INSERT INTO permissions (name, action_type, object_type) VALUES (Browse posts, browse, post), (Read posts, read, post), (Edit posts, edit, post), (Add posts, add, post), (Delete posts, destroy, post);permissions_roles表中的角色-权限映射将上述权限授予 Administrator、Editor 等角色。这两类记录通常通过以下两种方式写入初始 fixturesghost/core/core/server/data/schema/fixtures/fixtures.json数据库迁移使用 ghost/core/core/server/data/migrations/utils/permissions.js 中的addPermissionWithRoles()工具见本文第四节。另外从 utils/permissions.js 的handle函数可以看到两个前置逻辑框架先调用permissions.parseContext(frame.options.context)解析上下文若上下文标记为publicContent API 与 Members API 的公开访问则直接放行不做权限检查。模式 2布尔值false—— 跳过权限检查完全绕过权限阶段browse: { options: [page, limit], permissions: false, query(frame) { return models.PublicResource.findAll(frame.options); } }适用场景不需要认证的公开端点健康检查、状态查询类端点对所有人可见的资源。警告务必谨慎使用。只有在确定端点应当公开可访问时才关闭权限检查。模式 3函数 —— 自定义权限逻辑完全掌控权限校验过程函数接收frame通过Promise.resolve()放行、Promise.reject()或throw拦截delete: { options: [id], permissions: async function(frame) { // 确保用户已认证 if (!frame.user || !frame.user.id) { const UnauthorizedError require(tryghost/errors).UnauthorizedError; return Promise.reject(new UnauthorizedError({ message: You must be logged in to perform this action })); } // 仅资源所有者或管理员可删除 const resource await models.Resource.findOne({id: frame.options.id}); if (resource.get(author_id) ! frame.user.id frame.user.role ! admin) { const NoPermissionError require(tryghost/errors).NoPermissionError; return Promise.reject(new NoPermissionError({ message: You do not have permission to delete this resource })); } return Promise.resolve(); }, query(frame) { return models.Resource.destroy(frame.options); } }适用场景依赖具体资源状态变化的复杂权限逻辑基于所有者的权限owner-based超出默认处理器的基于角色的访问控制权限决策需要查询数据库。模式 4配置对象 —— 默认处理 钩子将默认权限处理与配置选项、钩子函数结合edit: { options: [include], permissions: { unsafeAttrs: [author, status], before: async function(frame) { // 预加载权限检查所需的额外用户数据 frame.user.permissions await loadUserPermissions(frame.user.id); } }, query(frame) { return models.Post.edit(frame.data, frame.options); } }适用场景默认权限处理器足够但需要配置存在需要特殊权限处理的字段需要在权限检查前准备数据。三、Frame 对象与配置对象属性Frame 对象所有权限处理函数都接收一个frame对象其中包含完整的请求上下文Frame { // 请求数据 original: {}, // 原始未转换的输入 options: {}, // 查询/URL 参数 data: {}, // 请求体 // 用户上下文 user: {}, // 登录用户对象 // 文件上传 file: {}, // 单个上传文件 files: [], // 多个上传文件 // API 上下文 apiType: String, // content 或 admin docName: String, // 端点名称如 posts method: String, // 方法名如 browse、add、edit // HTTP 上下文由 HTTP 包装层注入 context: { api_key: {}, // API key 信息 user: userId, // 用户 ID 或 null integration: {}, // 集成详情 member: {} // 会员信息或 null } }配置对象属性模式 4unsafeAttrsArray声明需要特殊权限处理的属性。permissions: { unsafeAttrs: [author, visibility, status] }从源码看处理器会执行_.pick(frame.data[apiConfig.docName][0], apiConfig.unsafeAttrs)把这些属性挑出来传给权限检查函数做额外校验。适用于只有特定用户才能修改的字段例如只有管理员可以更换文章的作者。beforeFunction在默认权限处理器之前运行的钩子。permissions: { before: async function(frame) { // 准备权限检查所需的数据 const membership await loadMembership(frame.user.id); frame.user.membershipLevel membership.level; } }四、完整的控制器示例示例 1公开浏览端点module.exports { docName: articles, browse: { options: [page, limit, filter], validation: { options: { limit: { values: [10, 25, 50, 100] } } }, permissions: false, query(frame) { return models.Article.findPage(frame.options); } } };示例 2需要认证的 CRUD 控制器module.exports { docName: posts, browse: { options: [include, page, limit, filter, order], permissions: true, query(frame) { return models.Post.findPage(frame.options); } }, read: { options: [include], data: [id, slug], permissions: true, query(frame) { return models.Post.findOne(frame.data, frame.options); } }, add: { headers: { cacheInvalidate: true }, options: [include], permissions: { unsafeAttrs: [author_id] }, query(frame) { return models.Post.add(frame.data.posts[0], frame.options); } }, edit: { headers: { cacheInvalidate: true }, options: [include, id], permissions: { unsafeAttrs: [author_id, status] }, query(frame) { return models.Post.edit(frame.data.posts[0], frame.options); } }, destroy: { headers: { cacheInvalidate: true }, options: [id], permissions: true, statusCode: 204, query(frame) { return models.Post.destroy(frame.options); } } };示例 3基于所有者的权限module.exports { docName: user_settings, read: { options: [user_id], permissions: async function(frame) { // 用户只能读取自己的设置 if (frame.options.user_id ! frame.user.id) { const NoPermissionError require(tryghost/errors).NoPermissionError; return Promise.reject(new NoPermissionError({ message: You can only view your own settings })); } return Promise.resolve(); }, query(frame) { return models.UserSetting.findOne({user_id: frame.options.user_id}); } }, edit: { options: [user_id], permissions: async function(frame) { // 用户只能编辑自己的设置 if (frame.options.user_id ! frame.user.id) { const NoPermissionError require(tryghost/errors).NoPermissionError; return Promise.reject(new NoPermissionError({ message: You can only edit your own settings })); } return Promise.resolve(); }, query(frame) { return models.UserSetting.edit(frame.data, frame.options); } } };示例 4基于角色的访问控制module.exports { docName: admin_settings, browse: { permissions: async function(frame) { const allowedRoles [Owner, Administrator]; if (!frame.user || !allowedRoles.includes(frame.user.role)) { const NoPermissionError require(tryghost/errors).NoPermissionError; return Promise.reject(new NoPermissionError({ message: Only administrators can access these settings })); } return Promise.resolve(); }, query(frame) { return models.AdminSetting.findAll(); } }, edit: { permissions: async function(frame) { // 只有站点 owner 可以编辑管理设置 if (!frame.user || frame.user.role ! Owner) { const NoPermissionError require(tryghost/errors).NoPermissionError; return Promise.reject(new NoPermissionError({ message: Only the site owner can modify these settings })); } return Promise.resolve(); }, query(frame) { return models.AdminSetting.edit(frame.data, frame.options); } } };示例 5带数据准备的权限module.exports { docName: premium_content, read: { options: [id], permissions: { before: async function(frame) { // 加载用户的订阅状态 if (frame.user) { const subscription await models.Subscription.findOne({ user_id: frame.user.id }); frame.user.subscription subscription; } } }, async query(frame) { // query 中即可使用 frame.user.subscription const content await models.Content.findOne({id: frame.options.id}); if (content.get(premium) !frame.user?.subscription?.active) { const NoPermissionError require(tryghost/errors).NoPermissionError; throw new NoPermissionError({ message: Premium subscription required }); } return content; } } };五、最佳实践1. 始终显式声明权限// 好 —— 明确声明为公开 permissions: false // 好 —— 明确声明需要认证 permissions: true // 坏 —— 缺少 permissions会抛错 // permissions: undefined2. 选择恰当的模式场景推荐模式公开端点permissions: false标准认证 CRUDpermissions: true需要跟踪敏感字段permissions: { unsafeAttrs: [...] }复杂自定义逻辑permissions: async function(frame) {...}需要预处理数据permissions: { before: async function(frame) {...} }3. 权限函数保持职责单一权限函数只做权限检查不要夹带业务逻辑// 好 —— 只检查权限 permissions: async function(frame) { if (!frame.user || frame.user.role ! admin) { throw new NoPermissionError(); } } // 坏 —— 混入业务逻辑 permissions: async function(frame) { if (!frame.user) throw new NoPermissionError(); // 不要在权限里做这些 frame.data.processed true; await sendNotification(frame.user); }4. 使用有意义的错误信息permissions: async function(frame) { if (!frame.user) { throw new UnauthorizedError({ message: Please log in to access this resource }); } if (frame.user.role ! admin) { throw new NoPermissionError({ message: Administrator access required for this operation }); } }5. 校验资源所有权当资源归属于特定用户时务必验证所有权permissions: async function(frame) { const resource await models.Resource.findOne({id: frame.options.id}); if (!resource) { throw new NotFoundError({message: Resource not found}); } const isOwner resource.get(user_id) frame.user.id; const isAdmin frame.user.role admin; if (!isOwner !isAdmin) { throw new NoPermissionError({ message: You do not have permission to access this resource }); } }6. 用unsafeAttrs标记敏感字段permissions: { unsafeAttrs: [ author_id, // 只有管理员应能更换作者 status, // 发布需要特殊权限 visibility, // 修改可见性受限 featured // 只有编辑可置顶内容 ] }六、错误类型权限逻辑应使用tryghost/errors中语义匹配的错误类型UnauthorizedError—— 用户未认证NoPermissionError—— 用户已认证但缺乏权限默认处理器在查表失败时抛出的也是它并会把消息改写为 You do not have permission to {method} {docName}见 utils/permissions.jsNotFoundError—— 资源不存在慎用避免信息泄露ValidationError—— 输入校验失败。const { UnauthorizedError, NoPermissionError, NotFoundError } require(tryghost/errors);七、通过迁移落地新端点的权限当你新建一个使用默认权限处理器permissions: true的 API 端点时必须把对应权限写入数据库。Ghost 在 ghost/core/core/server/data/migrations/utils/permissions.js 中提供了addPermissionWithRoles工具该文件 L279 起定义。迁移工具导入const {combineTransactionalMigrations, addPermissionWithRoles} require(../../utils);示例为新资源添加一套 CRUD 权限// ghost/core/core/server/data/migrations/versions/X.X/YYYY-MM-DD-HH-MM-SS-add-myresource-permissions.js const {combineTransactionalMigrations, addPermissionWithRoles} require(../../utils); module.exports combineTransactionalMigrations( addPermissionWithRoles({ name: Browse my resources, action: browse, object: my_resource // docName 的单数形式 }, [ Administrator, Admin Integration ]), addPermissionWithRoles({ name: Read my resources, action: read, object: my_resource }, [ Administrator, Admin Integration ]), addPermissionWithRoles({ name: Edit my resources, action: edit, object: my_resource }, [ Administrator, Admin Integration ]), addPermissionWithRoles({ name: Add my resources, action: add, object: my_resource }, [ Administrator, Admin Integration ]), addPermissionWithRoles({ name: Delete my resources, action: destroy, object: my_resource }, [ Administrator, Admin Integration ]) );可分配的角色Administrator—— 完整管理端权限Admin Integration—— 具有 admin 范围的 API 集成Editor—— 可管理所有内容Author—— 可管理自己的内容Contributor—— 只能创建草稿Owner—— 站点所有者继承全部 Administrator 权限。权限命名约定name人类可读例如Browse automated emailsactionAPI 方法名 ——browse、read、edit、add、destroyobjectdocName的单数形式 ——automated_email而不是automated_emails必须与默认处理器在 utils/permissions.js 中推导出的单数形式一致否则查表会匹配不到。仅限管理员的端点若端点只允许管理员访问Editor、Author 等角色不能访问只将权限授予Administrator和Admin Integration两个角色addPermissionWithRoles({ name: Browse sensitive data, action: browse, object: sensitive_data }, [ Administrator, Admin Integration ])八、小结Ghost 的 api-framework 把权限做成请求管线中的显式一环控制器方法必须声明permissionstrue走基于permissions/permissions_roles两张数据表的默认检查false显式跳过函数与配置对象则分别提供完全自定义和默认 钩子的折中方案。新端点的开发闭环是控制器中声明权限 → 确认docName单数形式 → 用addPermissionWithRoles迁移写入权限与角色映射 → 由 默认处理器 在每次请求时完成鉴权。完整指南可参阅 permissions.md。【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考