Sails Policies 权威指南:从 ACL 配置到授权中间件源码解析
2026/9/20 14:09:58 网站建设 项目流程
  • 后端

【免费下载链接】sails

Realtime MVC Framework for Node.js

项目地址:https://gitcode.com/gh_mirrors/sa/sails
点击查看免费下载

Policies(策略)是 Sails 实时 MVC 框架内置的授权与访问控制机制:它允许你在 action 执行之前运行一段逻辑,从而决定是否继续处理该请求,最常见的用途是把某些 action 限制为"仅登录用户可访问"。本文将基于 Sails 官方文档与当前仓库源码(lib/hooks/policies/index.jslib/router/bind.js等),系统讲解config/policies.js声明式 ACL 的完整语法(控制器、独立 action、全局通配符、优先级),并深入剖析 policy 的加载、绑定与执行链路,最后给出从简单登录校验到动态权限(Helper 方案)的完整实战示例。

Overview:Policies 到底是什么

在 Sails 中,Policies 是授权与访问控制的通用工具:它们在 action 运行之前执行某些逻辑,以此判断是否继续处理该请求。Policies 最常见的应用场景是将某些 action 限制为仅登录用户(logged-in users)可访问

注意:Policies作用于控制器(controllers)和 action,作用于视图(views)。如果你在 routes.js 配置文件 中定义了一条直接指向某个视图的路由,那么不会有任何 policy 应用到它。为确保 policy 生效,你应该改为定义一个"渲染该视图"的 action,然后把路由指向这个 action。

从实现上看,每个 policy 本质上是 Express 风格的中间件函数(req, res, next),在目标 action 之前按顺序执行。这一点在 config/policies.js 解剖文档 中有明确说明:policy 文件(如api/policies/isLoggedIn.js)可以放入api/policies/目录,之后即可通过其文件名(去掉扩展名,如isLoggedIn)来引用。

什么时候该用 Policies

Sails 官方建议避免在应用里实现过多或过于复杂的 policy。当实现细粒度、基于角色的权限时,应依靠你的 actions 来拒绝未经授权的访问;同时,action 也应当负责对响应中发送的视图局部变量(view locals)和 JSON 响应数据做必要的个性化处理。

例如,当你需要实现用户级或角色级权限时,最直接的做法是在控制器 action 的顶部处理相关校验——无论是内联编写,还是调用一个 helper。遵循这一最佳实践将显著提升代码的可维护性。

简单地说,Policies 擅长的是二元的(yes/no)访问控制:它非常适合检查"用户是否已登录""当前登录用户是否为超级管理员"这类简单判断。而复杂权限(取决于"谁"和"想做什么")更适合放到 action 或 helper 中处理(详见下文"动态权限"一节)。

用 Policies 保护 action 与控制器

Sails 内置了一个位于config/policies.jsACL(访问控制列表)。该文件用于把 policy 映射到 action 和控制器上。

这个文件是声明式(declarative)的:它描述的是应用权限应该是什么样(what),而不是如何实现(how)。这让新开发者更容易理解当前配置,也让应用在需求随时间变化时更加灵活。

config/policies.js是一个字典(dictionary),其属性和值的形态取决于你是在为控制器还是为独立 action 配置 policy。

将 Policies 应用于控制器

要将 policy 应用于控制器,请在config/policies.js字典中使用控制器名称作为属性名,并把它的值设为另一个字典,该字典将控制器中的 action 映射到要应用的 policy 上。使用*表示"所有未映射的 action"。policy 的名称与其文件名相同(去掉扩展名):

module.exports.policies = { UserController: { // 默认要求请求来自已登录用户 // (运行 api/policies/isLoggedIn.js 中的 policy) '*': 'isLoggedIn', // 只允许管理员用户删除其他用户 // (运行 api/policies/isAdmin.js 中的 policy) 'delete': 'isAdmin', // 允许任何人访问 login action,即使未登录 'login': true } };

将 Policies 应用于独立 action

要为某个或多个独立 action 应用 policy,请使用action 路径(相对于api/controllers)作为config/policies.js字典中的属性名,并把值设为应应用于这些 action 的 policy 或 policy 数组。通过在 action 路径末尾使用通配符*,你可以把 policy 应用于所有以该路径开头的 action。下面是与上面相同的策略集,改写为应用于独立 action 的写法:

module.exports.policies = { 'user/*': 'isLoggedIn', 'user/delete': 'isAdmin', 'user/login': true }

注意:这个例子与基于控制器的 policy 写法略有不同——isLoggedInpolicy 将应用于api/controllers/user文件夹及其子文件夹中的所有 action(但user/deleteuser/login除外,原因见下一节)。

Policy 的排序与优先级

Policies 不会级联(do not cascade),这一点非常重要。在上面的例子中,isLoggedInpolicy 会应用于UserController.js文件中的所有 action(或位于api/controllers/user下的独立 action)deletelogin除外。如果希望给一个 action 应用多个 policy,请用数组列出这些 policy:

'getEncryptedData': ['isLoggedIn', 'isInValidRegion']

从源码角度来印证这种"非级联、精确覆盖"的机制:在 lib/hooks/policies/index.js 的buildPolicyMap()中,所有 policy 键会被按字母序排序,使更具体的键(如user/foo)排在更宽泛的键(如user/*)之后;随后,对于*全局键,会为排在它后面的每个目标追加!<target>形式的排除项,对于/*通配符键也会对后续匹配到的目标追加!前缀,从而保证更精确的映射覆盖更宽泛的映射——注释里明确写着"for now policies are NOT cumulative"(目前 policies 不会累积)。最终这些target,!exclude1,!exclude2形式的逗号分隔字符串会通过sails.registerActionMiddleware(policies, targets)注册(见 bindPolicies)。

而真正匹配时,lib/router/bind.js 会把每个注册键按,拆分成目标数组并排序(!开头的排前面),先检查否定目标(!...)是否匹配 action identity,再检查肯定目标(*xxx/*通配、精确匹配),从而决定该 policy 是否加入待执行中间件链。

对 Blueprint Action 使用 Policies

Sails 内置的 blueprint API 是使用普通 Sails action 实现的,唯一区别在于 blueprint action 是**隐式(implicit)**的。

要对 blueprint action 应用 policy,只需像上面的例子一样设置 policy 映射,只不过把目标指向控制器中相应隐式 blueprint action 的名称(或作为独立 action)。例如:

module.exports.policies = { UserController: { // 将 'isLoggedIn' policy 应用于 'UserController' 的 'update' action update: 'isLoggedIn' } };

module.exports.policies = { 'user/update': 'isLoggedIn' };

全局 Policies

你可以通过*属性把某个 policy 应用到所有未被显式映射的 action。例如:

module.exports.policies = { '*': 'isLoggedIn', 'user/login': true };

这会把isLoggedInpolicy 应用到除api/controllers/user/login.js中的loginaction(或api/controllers/UserController.js中的loginaction)之外的所有 action。

内置 Policies

Sails 提供了两个内置 policy,可以全局应用,也可以应用于特定控制器或 action:

  • true:公共访问(允许任何人访问被映射的 controller/action)
  • false禁止访问(不允许任何人访问被映射的 controller/action)

'*': true是所有控制器和 action 的默认 policy。在生产环境中,好的做法是将其改为false,以防止访问任何你可能无意暴露的逻辑。

这两个内置值在源码中有明确的对应实现:lib/hooks/policies/index.js 定义了neverAllow(调用res.forbidden())和alwaysAllow(直接调用next())两个中间件函数,并分别标记为POLICY: false (neverAllow)POLICY: true (alwaysAllow);同时,true/false会先被包装成单元素数组,且如果数组中还有其他 policy 会直接抛错E_INVALID_POLICY_CONFIG),也就是说true/false必须是该目标唯一的策略。

此外,配置校验非常严格(buildPolicyMap):policy 可以是字符串(必须对应已加载的 policy 名称,否则抛出E_INVALID_POLICY_CONFIG)、函数,或true/false,其他任何类型都会在应用启动时直接报错终止。

编写你的第一个 Policy

下面是一个简单的isLoggedInpolicy,用于阻止未认证用户访问。它检查 session 中是否有userId属性,如果找不到,就发送默认的forbidden响应。对许多应用来说,这可能就是唯一需要的 policy。下面的例子假设在认证用户的控制器 action 中,你把req.session.userId设置成了一个真值(truthy)。

// api/policies/isLoggedIn.js module.exports = async function (req, res, proceed) { // 如果设置了 `req.me`,我们就知道这个请求来自已登录用户, // 因此可以安全地继续执行下一个 policy—— // 或者,如果这是最后一个 policy,则继续执行相应 action。 // > 关于 `req.me` 从哪来,请查看本应用的自定义 hook(`api/hooks/custom/index.js`)。 if (req.me) { return proceed(); } //--• // 否则,这个请求不是来自已登录用户。 return res.forbidden(); };

Policy 的加载机制

从源码看,policy 文件的加载发生在 policies hook 的loadMiddleware()中(lib/hooks/policies/index.js):它调用sails.modules.loadPolicies(),后者(lib/hooks/moduleloader/index.js)使用includeAll.optional()sails.config.paths.policies(即api/policies/)目录加载所有非md/txt的模块,并扁平化、保留目录结构。加载后:

  • 如果通过编程方式在sails.config.policies.moduleDefinitions中提供了 policy 函数,会覆盖从磁盘加载的同名 policy;
  • 每个加载的 policy 都会被校验必须是函数,否则抛出E_INVALID_POLICY错误;
  • 每个 policy 函数会被打上_middlewareType = 'POLICY: <name>'标记,便于日志和调试。

在路由上直接绑定 Policy

除了config/policies.js的声明式映射,policies hook 还监听了route:typeUnknown事件(lib/hooks/policies/index.js),允许你在config/routes.js中通过policy: '...'路由选项把 policy手动绑定到显式路由上。若引用的 policy 不存在,会调用 errors/fatal.js 中的__UnknownPolicy__打印错误(Unknown policy, "xxx", referenced in ...)并终止进程。

进阶:动态权限与 Helper 方案

生成一个带示例的 Web App

要查看访问控制的实际示例——以及登录、认证和密码恢复——请生成一个 starter web app:

sails new foo # 然后选择 "Web App"

动态权限:结合数据库的细粒度控制

对于更复杂的权限方案,比如请求方主体的访问权同时取决于他们是谁(who they are)他们想做什么(what they're trying to do),你需要引入数据库。虽然你也可以用 policy 实现这一点,但通常更直接、更易维护的做法是使用 helper。

例如,你可以创建api/helpers/check-permissions.js

module.exports = { friendlyName: 'Check permissions', description: 'Look up a user\'s "rights" within a particular organization.', inputs: { userId: { type: 'number', required: true }, orgId: { type: 'number', required: true } }, exits: { success: { outputFriendlyName: 'Rights', outputDescription: `A user's "rights" within an org.`, outputType: ['string'] }, orgNotFound: { description: 'No such organization exists.' } }, fn: async function(inputs, exits) { var org = await Organization.findOne(inputs.orgId) .populate('adminUsers', { id: inputs.userId }) .populate('regularUsers', { id: inputs.userId }); if (!org) { throw 'orgNotFound'; } var rights = []; if (org.regularUsers.length !== 0) { rights = ['basicAccess', 'inviteRegularUsers']; } else if (org.adminUsers.length !== 0) { rights = ['basicAccess', 'inviteRegularUsers', 'removeRegularUsers', 'inviteOrgAdmins']; } else if (org.owner === inputs.userId) { rights = ['basicAccess', 'inviteRegularUsers', 'removeRegularUsers', 'inviteOrgAdmins', 'removeOrDemoteOrgAdmins']; } // ^^这里可以按你的需要做到任意简单或精细,例如 // ['basicAccess', 'inviteRegularUsers', 'inviteOrgAdmins', 'removeRegularUsers', 'removeOrDemoteOrgAdmins'] return exits.success(rights); } };

你的 action——例如api/controllers/demote-org-admin.js——可能长这样:

//… var rights = await checkPermissions(this.req.session.userId, inputs.orgId) .intercept('orgNotFound', 'notFound'); if (!_.contains(rights, 'removeOrDemoteOrgAdmins')) { throw 'forbidden'; } await Organization.removeFromCollection(inputs.orgId, 'adminUsers', inputs.targetUserId); await Organization.addToCollection(inputs.orgId, 'regularUsers', inputs.targetUserId); return exits.success();

注意:请记住,虽然我们在这里使用了checkPermissions(…,…),但我们也可以使用.with()切换到命名参数:

await checkPermissions.with({ userId: this.req.session.userId, orgId: inputs.orgId });

你可以在不同场景中选择不同的 helper 调用方式来增强代码可读性。拿不定主意时,一个很好的最佳实践是:先追求显式(explicitness),再追求可读性(readability),最后才追求简洁(conciseness)。当然,当你更频繁地使用某个 helper 并逐渐熟悉它之后,这些优先级可能会发生变化。

这种"policy 做二元放行 + action/helper 做细粒度业务授权"的组合,正是 Sails 官方推荐的权限架构:policy 保持简单,复杂判断下沉到可复用、可测试的 helper 与 action 中。

小结与最佳实践清单

  1. Policies 只保护 action/controller,不保护直接指向视图的路由——需要保护视图时,先把它包进 action。
  2. 保持 policy 简单:二元判断(是否登录、是否管理员)放 policy;细粒度、角色化权限放 action 或 helper。
  3. Policies 不级联:更具体的映射会覆盖更宽泛的映射(user/delete覆盖user/*user/*覆盖*);多个 policy 用数组。
  4. 利用内置值true放行、false拒绝,二者不能与其他 policy 混用;生产环境建议把全局默认从'*': true改为'*': false
  5. 利用源码机制排障:policy 名必须与api/policies/下的文件名一致;引用不存在的 policy 会导致启动失败(Unknown policy)。配置错误(E_INVALID_POLICY_CONFIG)同样会在启动时被拦截,而不是在请求时才暴露。

相关深入阅读:Policies 概念文档、Access Control and Permissions、config/policies.js 解剖、策略 hook 源码、action middleware 注册、路由绑定与匹配。

  • 后端

【免费下载链接】sails

Realtime MVC Framework for Node.js

项目地址:https://gitcode.com/gh_mirrors/sa/sails
点击查看免费下载
上一篇:Zotero Style:让文献管理变得高效实用的视觉化插件指南
下一篇:Byzer Notebook详解:数据科学家的理想工作环境

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询