上周review一个Express项目时,我看到一段权限代码:用user.id === author.id || user.role判断是否显示编辑按钮。看起来干净,实则脆弱——一个边界条件遗漏,业务逻辑就可能被击穿。

这让我想起去年参与的一个医药项目。四个角色(药店、顾客、顾问、司机)交叉操作三类资源(库存、病历、配送),权限矩阵复杂到白板上画不下。当时团队选择了"最快实现":if/else嵌套、角色枚举、中间件硬编码。三个月后,代码变成了一团意大利面。

打开网易新闻 查看精彩图片

这篇文章想聊的是:当权限规则从"能不能访问"进化到"能不能操作特定数据",常见的快速方案为何失效,以及一种更可持续的组织方式。

打开网易新闻 查看精彩图片

权限的层级跃迁

最基础的权限是角色隔离。顾客读库存、顾问写病历、司机看配送——各管一摊,互不相干。Express的authorize(["consultant", "pharmacy"])中间件足够应对:路由层拦截,未授权角色直接返回403。

但业务很快提出新需求:顾客只能读自己的病历,药店只能改自己的库存,司机只能看指派给自己的订单。权限从"角色有无"变成了"角色+数据归属"的双重校验。

这时候authorize数组开始膨胀。["customer", "consultant", "driver", "pharmacy"]全塞进去,只是为了让他们都能"读订单"——但各自能读的订单范围完全不同。中间件只能判断角色,无法判断数据归属,逻辑被迫下沉到service层。

意大利面的形成机制

在NestJS这类框架中,常见的妥协是:controller层只做最粗放的保护,具体权限逻辑散落到各个service方法。于是你看到这样的代码:

如果角色是顾问,检查记录是否属于该顾问;如果是药店,检查库存归属;如果是司机,检查配送指派……if/else链条随着角色和资源类型指数级增长。更隐蔽的是,同一套"是否属于自己"的逻辑,在病历、库存、订单三个service里被重复实现了三遍,只是变量名不同。

打开网易新闻 查看精彩图片

这种结构的危险在于:新增一个角色或资源类型时,开发者需要遍历所有相关service,手动插入新的条件分支。遗漏一处,就是安全漏洞。

解耦的方向

根本问题出在"权限逻辑"与"业务逻辑"的纠缠。当service方法里既计算数据又判断权限,代码就丧失了组合性。一个更清晰的边界是:权限系统只回答"能不能做",业务系统只回答"怎么做"。

具体而言,可以将权限抽象为独立的策略(Policy)单元。每个策略封装一组规则:针对什么资源、在什么条件下、允许什么操作。service层不再写if/else,而是调用统一的权限检查接口,传入当前上下文(用户、资源、意图),获得布尔结果。

这种设计的代价是前期抽象成本,收益是后期扩展性。新增角色时,只需注册新的策略实现;修改规则时,只需调整对应策略,无需触碰业务代码。当权限矩阵复杂到需要文档才能理清时,这种显式化本身就是价值。

权限代码的腐烂往往不是技术债,而是认知债——团队对业务规则的理解没有及时转化为代码结构。if/else是最直观的翻译,但也是最懒惰的。当规则复杂度超过人脑的短期记忆容量时,就需要借助架构来外包这部分认知负荷。