授权与访问控制:别让权限成了摆设
授权这事儿,说白了就是「谁,能对什么,做什么」。听起来简单吧?可我在审计过的项目里,十个有八个都在这上面栽过跟头。要么权限给得太粗,要么干脆没控制,结果就是——一个普通用户,愣能翻到别人的订单、改掉别人的配置,甚至把整个服务给停了。
今天咱们就掰扯清楚,怎么把授权这堵墙砌结实。
细粒度访问控制的两大模式:RBAC 与 ABAC
你可能会问,我直接给每个用户单独配权限不行吗?行,但那是小作坊做法。一旦用户上千、资源上万,你得累死。所以业界搞出了两种成熟的抽象方式。
1. RBAC(基于角色的访问控制)
RBAC 的思路很简单:把权限打包成「角色」,再把角色分配给用户。比如「运营专员」角色只能读订单、「管理员」角色能删订单。我在项目中习惯这样设计表结构:
-- 用户表
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
);
-- 角色表
CREATE TABLE `role` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL, -- 'admin', 'operator', 'auditor'
PRIMARY KEY (`id`)
);
-- 用户-角色关联
CREATE TABLE `user_role` (
`user_id` int NOT NULL,
`role_id` int NOT NULL,
PRIMARY KEY (`user_id`, `role_id`)
);
-- 权限表(资源+操作)
CREATE TABLE `permission` (
`id` int NOT NULL AUTO_INCREMENT,
`resource` varchar(128) NOT NULL, -- 'order', 'user', 'report'
`action` varchar(32) NOT NULL, -- 'read', 'write', 'delete'
PRIMARY KEY (`id`)
);
-- 角色-权限关联
CREATE TABLE `role_permission` (
`role_id` int NOT NULL,
`permission_id` int NOT NULL,
PRIMARY KEY (`role_id`, `permission_id`)
);
优点很明显:管理简单,新人入职直接绑角色就行。但缺点也有——不够灵活。比如你想让「运营专员 A」只能看北京区域的订单、「运营专员 B」只能看上海区域,RBAC 就抓瞎了,你得给每个区域建一个角色,角色数量瞬间爆炸。
2. ABAC(基于属性的访问控制)
这就是对付上面那个痛点的。ABAC 不看角色,看属性:用户属性、资源属性、环境属性。然后写一条策略,比如:
# 伪代码:允许北京的运营专员读北京区域的订单
ALLOW read ON order
WHERE user.department == 'operations'
AND user.region == resource.region
AND resource.region == 'Beijing'
我个人觉得 ABAC 是更现代的方案,尤其在微服务和多租户系统里特别好使。但代价是策略引擎复杂,性能开销大。你想想看,每次请求都要跑一个策略评估,写不好真的会卡死。
| 维度 | RBAC | ABAC |
|---|---|---|
| 抽象粒度 | 粗(角色) | 细(多属性组合) |
| 管理成本 | 低(角色数量可控) | 高(策略数量可能很大) |
| 动态性 | 差(改权限需改角色) | 好(运行时评估属性) |
| 适用场景 | 部门、岗位固定的企业系统 | 多租户、地理位置敏感的云平台 |
最小权限原则:给得刚刚好,别多给
这原则听起来像废话——「只给完成工作所需的最小权限」。但我在实际审计里,发现大量接口的权限设计是反过来的:默认给最大的,等到出问题了再往回缩。
落地时我建议你盯死这三点:
- 按需分配,不是按身份分配:别因为某人是部门总监,就给他所有模块的写权限。总监可能只需要看报表,不一定要删订单。
- 默认拒绝,白名单模式:所有接口在未显式授权时,直接返回403。我曾经见过一个系统,忘了给新接口配权限策略,结果所有人默认都能调——那太要命了。
- 定期回收,权限会腐烂:用户转岗、离职之后,旧权限还在。我建议每季度跑一次权限审计脚本,把超过90天未使用的权限标记出来,通知管理员确认。
避坑清单:
- 不要在代码里写死 user.role == 'admin' 这样的硬编码判断。用配置中心和策略引擎统一管理。
- 不要让接口自己去查"我有没有权限"。应该统一走一个鉴权中间件。
- 不要为了性能把权限缓存到前端。前端只展示菜单,后端必须重新验证。
防止越权操作:水平越权与垂直越权
越权分两种,我分别讲。
垂直越权
低权限用户尝试访问高权限接口。比如普通用户试图调 /admin/deleteUser。这通常发生在两个地方:
- 前端隐藏了按钮,但后端没拦住直接调 API。
- 角色判断有漏洞,比如用整数表示角色等级(0=普通, 1=管理员),攻击者改一下请求里的 role 字段就上去了。
防法:后端统一拦截,校验 JWT 或 session 中的角色是否匹配接口要求的角色等级。不要相信前端传的任何角色信息。
水平越权
同一个角色、同一个接口,A 用户看到了 B 用户的数据。比如查询订单接口:/order/detail?orderId=12345。攻击者把 orderId 改成 12346,就看到了别人的订单。
我曾经在审计一个电商平台时发现,他们的订单详情接口只校验了用户是否登录,没校验这个订单是不是当前用户的。结果换几个数字就能遍历全站订单。嗯,那漏洞报告写得可不短。
防法:传参时必须带上用户标识,后端比对。比如:
// 错误做法:只查 orderId,不验证归属
Order order = orderMapper.selectById(orderId);
// 正确做法:带上 userId 一起查
Order order = orderMapper.selectByIdAndUserId(orderId, currentUserId);
特别提醒:不要只在业务层做校验。
我建议你把权限检查做成一个注解或 AOP 切面,每个接口自动触发。比如用 Spring Security 的 @PreAuthorize。这样新来的同事加接口时,忘了写权限检查,编译都不会过。
我的实战习惯:
每次写完一个接口,我都手动构造一个「别人的 」去测试——换一个 userId、换一个 orderId。如果还能正常返回,那说明权限校验没到位。
小结几个硬道理
授权这东西,偷懒一时爽,审计火葬场。我自己总结了几条铁律:
- 接口权限不要自己写 if-else,用策略引擎统一管控。哪怕你现在就两个接口,也值得引入。
- 最小权限不是口号,是代码。每个接口都问一句:这次调用真的需要这个权限吗?
- 越权测试要写在自动化用例里。每次发版前跑一遍,换几个 userId 打一遍,确保没人能偷看到别人的数据。
- 权限变更要有审计日志。谁、什么时候、给谁加了什么权限,全记下来。出了事能追溯。
嗯,说实话,我见过太多因为「懒得写权限」或者「以为前端藏住了按钮就安全了」导致的严重事故。授权这块,你多花一个小时设计,后面就能省下一百个小时的应急响应。