- 后端
【免费下载链接】sails
Realtime MVC Framework for Node.js
Policies(策略)是 Sails 实时 MVC 框架内置的授权与访问控制机制:它允许你在 action 执行之前运行一段逻辑,从而决定是否继续处理该请求,最常见的用途是把某些 action 限制为"仅登录用户可访问"。本文将基于 Sails 官方文档与当前仓库源码(lib/hooks/policies/index.js、lib/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.js的ACL(访问控制列表)。该文件用于把 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/delete和user/login除外,原因见下一节)。
Policy 的排序与优先级
Policies 不会级联(do not cascade),这一点非常重要。在上面的例子中,isLoggedInpolicy 会应用于UserController.js文件中的所有 action(或位于api/controllers/user下的独立 action)但delete和login除外。如果希望给一个 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 中。
小结与最佳实践清单
- Policies 只保护 action/controller,不保护直接指向视图的路由——需要保护视图时,先把它包进 action。
- 保持 policy 简单:二元判断(是否登录、是否管理员)放 policy;细粒度、角色化权限放 action 或 helper。
- Policies 不级联:更具体的映射会覆盖更宽泛的映射(
user/delete覆盖user/*,user/*覆盖*);多个 policy 用数组。 - 利用内置值:
true放行、false拒绝,二者不能与其他 policy 混用;生产环境建议把全局默认从'*': true改为'*': false。 - 利用源码机制排障: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
相关推荐
Kubernetes权威指南:配置Kubelet API的RBAC授权机制
Kubernetes权威指南:配置Kubelet API的RBAC授权机制 引言:为什么需要Kubelet API的精细授权控制? 在Kubernetes集群中
文档/教程Apache RocketMQ 权限控制(ACL)实战指南:从配置部署到源码级原理解析
Apache RocketMQ 权限控制(ACL)实战指南:从配置部署到源码级原理解析 导读 Apache RocketMQ 的权限控制(Access Cont
消息队列后端微服务流处理Sails 之 config/local.js:本地开发环境配置的权威指南
Sails 之 config/local.js:本地开发环境配置的权威指南 导读 config/local.js 是 Sails 应用中专门用于承载 个人本地环
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考