技术的路,没有白走的坑
背景介绍:一次“看似简单”的重构需求

事情发生在去年一个平常的下午。我们团队接到一个任务,对现有项目中的权限管理模块进行重构。听起来不复杂,功能也很清晰:原来的权限控制逻辑分散在各个前端组件和后端接口中,耦合严重,难以维护。产品经理希望我们能够统一权限体系、提升用户体验,并为后续扩展留出空间。
但就是这样一个“看起来不大”的改动,却让我踩了不少坑。
我们的系统是一个中型 SaaS 平台,用户量几万,每天上百万次请求,技术栈是 Vue + Spring Boot,部署在阿里云 ECS 上,用的是 MySQL 和 Redis 作为主要数据支撑。权限模型已经存在了一段时间,但最初是为内部员工设计的,后来业务扩张,变成了多租户结构,原有的模型开始显得捉襟见肘。
我心想,这事儿就交给我来搞吧,结果没想到,这个活儿硬生生干了快两个月。期间各种技术选型、架构调整、兼容性问题层出不穷。最后回头看,那些“小问题”其实都指向了一个核心:技术探索与实践必须基于真实业务场景,脱离实际的技术理想主义往往会吃大亏。
问题描述:权限重构的三大难题

1. 权限粒度过粗,无法满足灵活配置
原始的权限系统只支持角色级别的控制,比如管理员可以访问 A 页面,普通用户只能看 B 页面。但随着产品迭代,不同客户的需求差异很大,有的客户希望某位员工可以看到部分特定页面,而不能操作敏感功能;有的则需要更细粒度的按钮级控制。
这个问题的核心在于:权限模型是否支持动态可配置,以及是否具备足够的表达能力。
2. 前端与后端权限校验分离,容易出错
当时的前端使用 Vue 实现了简单的路由守卫来做权限控制,而后端则在 Controller 层做了方法拦截(@PreAuthorize)。但由于缺乏统一标准,经常出现前端显示了某个按钮,但点击后返回 403 的情况,用户体验非常差。
这种前后端分离导致的一致性问题,如果没有良好的沟通机制或中间层抽象,很容易变成责任模糊地带。
3. 多租户环境下权限继承与隔离处理复杂
我们系统支持多个客户共用一套平台,每个客户有自己的角色和权限配置。最开始采用的是“全局角色 + 客户自定义角色”,但在实际使用过程中发现权限继承关系混乱,跨客户的数据泄露风险也在增加。
这里的问题是:如何在保证租户之间数据隔离的前提下,实现权限的层级继承?
解决方案:从 RBAC 到 ABAC 的进阶之路
面对这些挑战,我们决定采用一种混合型权限模型 —— RBAC + ABAC(基于属性的访问控制),并重新设计整个权限体系。
RBAC 为主,ABAC 补充
RBAC 是 Role-Based Access Control 的缩写,是经典的权限模型。它的核心思想是将权限分配给角色,再将角色分配给用户。适合组织结构相对稳定、权限变动较少的系统。
而 ABAC 是 Attribute-Based Access Control,它可以根据对象的各种属性(比如用户所在部门、IP 地址、时间等)来做判断。它更灵活,但也更复杂。
我们在项目初期尝试过纯 RBAC,后来发现无法覆盖所有业务场景,于是引入了 ABAC 来处理一些特殊情况,比如:
- 销售人员只能查看自己负责区域的客户信息;
- 某些 API 接口仅限特定设备类型调用;
- 数据导出功能限制每天最多触发 5 次。
这样既保持了整体结构的稳定性,又不失灵活性。
统一权限中心服务化
为了减少前后端重复判断、权限校验不一致的问题,我们开发了一个权限中心服务(Permission Center),通过 RESTful API 对外暴露接口,集中处理权限计算逻辑。
例如,当用户点击“删除订单”按钮时,前端会先调用 /api/permission/check?resource=order&action=delete,拿到布尔值之后再决定是否渲染该操作按钮。而在后端,同样在入口处增加一个统一的权限拦截器,避免越权请求直接访问数据库。
这样做有几点好处:
- 前后端共享一套权限逻辑,避免分歧;
- 权限变更只需更新权限中心,无需改其他服务;
- 支持缓存,提高性能。
权限模型设计与存储优化
我们在数据库层面做了表结构调整,主要增加了几个核心表:
-- 用户角色关联表
CREATE TABLE user_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL
);
-- 角色权限映射表
CREATE TABLE role_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_id BIGINT NOT NULL,
permission_type ENUM('MENU', 'BUTTON', 'API', 'CUSTOM') NOT NULL,
permission_key VARCHAR(64) NOT NULL,
effect BOOLEAN NOT NULL DEFAULT TRUE
);
-- 自定义规则表(用于 ABAC)
CREATE TABLE abac_rule (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(64),
expression TEXT,
description TEXT
);
其中 permission_key 就是我们用来标识某个具体权限点的 key,比如 "order_delete"、“customer_export_excel”。
而 ABAC 部分的规则,则借助 Lua 表达式引擎来实现,我们封装了一层工具类来执行这些规则脚本,便于复用。
踩坑经验分享:技术选型背后的故事
在这个过程中,有几个关键的技术决策值得拿出来聊聊。
1. 为什么不直接使用开源权限框架?
一开始我也想,市面上不是有很多成熟的权限框架吗?比如 Apache Shiro、Spring Security、Casbin 等。特别是 Casbin,在 Go 社区中很有名,还支持多种策略语言。
但我们团队主要是 Java 栈,最终选择了自研+轻量级封装的方式。因为:
- 开源框架的学习成本高,而且有些概念不适合当前业务场景;
- 我们已经在使用 Spring Security,但它是面向认证和方法级别控制的,不具备统一权限中心的能力;
- Casbin 虽然强大,但中文社区支持一般,文档不够详细,出了问题不好排查;
- 最重要的是,我们需要快速响应业务变化,自研更容易定制。
所以不要盲目追求热门框架,要根据团队能力和业务特点做取舍。
2. 为何放弃基于 JWT 的权限携带方案?
在权限传递方面,我们曾考虑在 JWT 中携带权限信息,每次请求自动带入权限列表,省去频繁查库的成本。
但在实践中发现几个问题:
- Token 很容易变大,尤其对于权限复杂的用户来说,可能超过 Cookie 的限制;
- 权限更新后 Token 不同步,容易产生安全漏洞;
- 多租户环境下,权限上下文切换麻烦,得在 Token 中嵌套多层结构,维护成本很高。
所以我们最后选择在 Token 中只保留 basic 用户信息和租户 ID,权限在校验时由权限中心实时计算。
3. ABAC 引擎的选择与落地难点
ABAC 是个好东西,但真的要在生产环境跑起来并不容易。我们最开始尝试用 SpEL 表达式,后来觉得语法太复杂,用户难以上手;也尝试过 Drools,但启动慢、内存占用高,不适合我们的微服务架构。
最后我们用了 Aviator,这是一个轻量的、支持动态表达式的 Java 库。比如我们可以定义一条规则:
user.dept == "销售" && request.resourceType == "customer" && request.action == "export"
然后把它解析成 Aviator 的表达式,并传入相应的变量参数执行。
不过这也带来一个问题:规则如何管理和调试?为此我们专门开发了一个后台界面,让运营人员可以在界面上添加和测试 ABAC 规则。
效果总结:不仅仅是功能上线,更是架构升级
这次重构上线后,我们得到了几个明显的收益:
- 权限配置从原来的代码修改变为可视化操作,减少了上线频率;
- 用户反馈明显减少,权限异常基本归零;
- 新功能上线时权限部分开发效率提高 40%;
- 后续接入新租户变得更简单,基本上只要复制模板即可。
此外,权限中心作为一个独立的服务模块,也为未来的权限审计、权限分析等功能打下了基础。
最重要的是,整个团队对权限系统的理解更深了,协作更加顺畅。以前遇到权限相关的问题大家都互相推,现在大家都知道流程在哪里、谁来改、怎么改。
经验建议:写给正在探索的你
如果你也正在做类似的权限系统或者权限重构工作,我想给你几点建议:
1. 先画清楚你的权限树
别着急编码,先从角色、资源、动作三个维度梳理清楚你们的权限结构。可以画出一棵“权限树”或“资源矩阵”,帮助你更清晰地理解系统边界。
2. 权限和服务解耦,尽早做统一中心
不要把权限逻辑写死在接口里,也不要让每个服务都自己处理权限。尽早搭建一个权限中心,哪怕只是一个轻量的鉴权服务,也能帮你避免后期大量的返工。
3. 前后端权限一致性比你想的重要得多
前端控制权限是用户体验的一部分,但后端必须兜底。最好的做法是在前端做展示控制,在网关层或统一拦截器中做最终校验。记住一句话:前端可以不用权限,但不能绕过权限。
4. 不要迷信技术名词,选型要务实
RBAC、ABAC、OAuth、JWT、Casbin、ACL……这些词听起来很专业,但它们都是为了解决某些特定问题而存在的。你要问自己:这些问题在我这儿也存在吗?有没有更简单的替代方案?
5. 权限是个系统工程,涉及人和流程
很多时候权限问题并不是技术问题,而是流程上的疏漏。比如,运维误删了某个角色权限导致整个客户群体无法访问某个功能。所以除了技术之外,还需要配套的权限审批、发布流程。
写在最后:技术的温度,藏在每一个细节里
回头来看,那次权限重构只是日常工作中的一朵浪花。但正是这些不断出现的小问题、小冲突,构成了我们作为技术人员的成长轨迹。
我记得有一次晚上加班到十一点,还在修一个权限拦截器的 Bug,同事发来消息说:“能不能快点,用户已经反馈没法点了。” 我一边调试代码一边笑:“你说权限是不是应该叫‘阻碍’?”
确实如此。技术的本质是解决问题,而好的技术方案不仅要解决表面的问题,更要照顾用户体验、团队协作,甚至还要预判未来可能出现的新需求。
如果你也在路上,不妨记住一点:所有的坑,都不是白踩的。它会让我们在未来的设计中更加从容,也会在某个不经意的瞬间,让你回想起那段折腾的日子,嘴角微微一笑。
技术这条路,越往前走越明白一件事:不是我们驾驭技术,而是技术教会了我们如何思考、如何面对复杂。
愿你在每一次探索中,都能找到那份属于自己的成长与热爱。

评论 0