你是不是也见过这种场景:HR 小张想查一下自己部门的员工档案,结果手滑在浏览器里把 URL 里的用户 ID 从 1001 改成了 1002,回车一按,另一个同事的身份证号、家庭住址、薪资明细竟然原封不动地跳了出来?而系统,像没看见一样,啥也没拦。
这听着像段子,但在现实中,这确实是很多企业管理系统的“隐形杀手”——水平越权(Horizontal Privilege Escalation)。
今天咱们不聊枯燥的安全理论,就聊聊怎么把这道防线真正堵死。毕竟,数据泄露的代价,可不是换个防火墙就能弥补的。
第一关:别信用户的嘴,只信服务器的记忆
很多初级开发者(甚至一些老手)在写接口时,喜欢这样写:
// ❌ 危险代码示例
@GetMapping("/api/user/info")
public Result getUserInfo(@RequestParam Long userId) {
// 直接从参数里拿 userId,然后去查库
User user = userService.getById(userId);
return Result.success(user);
}
表面上看,这挺合理啊,“我要查谁的信息,你告诉我 ID 不就行了?”
但问题就在于:这个 ID 是用户自己填的。
黑客或者好奇的员工,完全可以通过抓包工具(比如 Burp Suite)拦截这个请求,把 userId=1001 改成 userId=9999,服务器会老老实实返回 ID 为 9999 的人的信息。因为服务器根本没问:“你是谁?你有权限查这个人吗?”它只是机械地执行:“哦,你要查 ID 9999,给你。”
正确的做法:用 Session 或 Token 里的“身份锚点”
我们得让服务器记住:“查数据的人”是谁,而不是“用户请求里说他是谁”。
通常,用户登录后,系统会在 Session 或 JWT(JSON Web Token)里存一个 currentUserId。这个 ID 才是合法的、经过认证的身份标识。
// ✅ 安全代码示例
@GetMapping("/api/user/info")
public Result getUserInfo(@RequestParam Long targetUserId) {
// 1. 从当前登录用户的 Session/Token 中获取“当前操作人”的真实 ID
Long currentUserId = SecurityContextHolder.getCurrentUserId(); // 假设这是你封装的获取当前登录用户的方法
// 2. 绝对不要用请求参数里的 ID 作为“操作主体”,请求参数只作为“被查询对象”
// 3. 进入下一层校验:当前用户能查 targetUserId 吗?
if (!authorizationService.hasPermission(currentUserId, targetUserId)) {
return Result.fail("无权查看该用户信息");
}
User user = userService.getById(targetUserId);
return Result.success(user);
}
这里的关键思维转变是:
- 请求参数里的
userId:只是“我想看谁”的输入,不可信,可能是伪造的。 - Session/Token 里的
userId:是“我是谁”的证明,可信,由服务器签发。
💡 给小朋友打比方:这就好比你去图书馆借书,管理员不会只听你说“我是张三”,而是要看你的借书证(Session),确认上面印的名字真的是张三,而且照片跟你长得一样。如果光听你喊“我是张三”,那任何人都可以冒充张三借走珍稀古籍了。
第二关:每笔查询,必须“对账”
就算你从 Session 里拿到了当前用户 ID,还是不够。
因为 A 员工和 B 员工都是普通职员,A 凭什么能查 B 的资料?这时候就需要权限比对逻辑。
核心原则:资源归属 ID ≠ 操作者 ID
在大多数业务场景中,一个普通用户只能看自己的数据,或者自己下属/本部门的数据。
我们需要在代码里强制加入一段比对逻辑:
public boolean hasPermission(Long operatorId, Long resourceId) {
// 1. 首先,自己查自己,通常是可以的(比如查自己的档案)
if (operatorId.equals(resourceId)) {
return true;
}
// 2. 如果不是自己,再判断是否有特殊权限(比如 HR、管理员)
// 这里需要根据你的业务角色体系来判断
if (roleService.isAdmin(operatorId)) {
return true;
}
// 3. 或者判断是否在同一个部门(比如部门经理看下属)
if (deptService.isSameDepartment(operatorId, resourceId)) {
return true;
}
// 4. 都不满足,直接拒绝
return false;
}
不要依赖前端隐藏!
很多项目会犯一个低级错误:前端页面根据权限隐藏了“查看他人信息”的按钮,员工看不到入口,就以为安全了。
这是大错特错。
前端只是给真人看的“门帘”,黑客和懂技术的员工根本不看门帘,他们直接跟服务器对话。只要后端接口没拦,前端按钮藏不藏都没用。
🚨 真实案例提醒:某知名电商平台曾发生过一起内鬼泄露事件。一个普通客服没有管理员权限,后台也看不到其他用户的订单数据。但他发现,只要修改 API 请求中的
orderId,就能查到其他用户的订单详情。他每天批量爬取数据,卖了三年都没被发现。为什么?因为后端接口根本没校验“当前客服”和“订单归属用户”之间的关系。
第三关:敏感操作,得“再确认一次”,还要“留个底”
前两步解决的是“能不能看”的问题。但有些数据,看一眼都可能泄密。比如:身份证号、银行卡号、家庭住址、薪资。
这时候,光有权限校验还不够,得加上二次验证和全量日志。
1. 二次验证(2FA)
当用户要查看高敏感信息时,不要让他直接弹出数据。弹出一个窗口:“请确认您的身份”。
可以发一条短信验证码到手机号,或者让他输入登录密码。
@PostMapping("/api/user/sensitive-info")
public Result getSensitiveInfo(@RequestParam Long userId,
@RequestParam String smsCode) {
Long currentUserId = SecurityContextHolder.getCurrentUserId();
// 1. 先做基础权限校验
if (!hasPermission(currentUserId, userId)) {
throw new SecurityException("无权访问");
}
// 2. 二次验证:检查短信码是否正确,且未过期
if (!smsCodeService.validateAndConsume(currentUserId, smsCode)) {
return Result.fail("验证码错误或已失效");
}
// 3. 记录这条敏感操作日志
auditLogService.log("查看敏感信息", currentUserId, userId, "成功");
return Result.success(userService.getSensitiveInfo(userId));
}
2. 全量日志审计
“操作必有痕”。
不管查没查到,不管权限够不够,只要有人触发了敏感接口,就必须打日志。
日志里至少包含:
- 谁操作的(operatorId)
- 查了谁(targetId)
- 操作时间(timestamp)
- 操作结果(成功/失败)
- 来源 IP(用于后续追踪异常登录)
// 使用 AOP(面向切面编程)自动记录日志,避免业务代码里到处写 log
@Aspect
@Component
public class AuditLogAspect {
@AfterReturning(pointcut = "execution(* com.example.api.*.sensitive*(..))",
returning = "result")
public void logSensitiveOperation(JoinPoint joinPoint, Object result) {
Long operatorId = SecurityContextHolder.getCurrentUserId();
// 从参数里取 targetId
Object[] args = joinPoint.getArgs();
Long targetId = (Long) args[0];
AuditLog log = new AuditLog();
log.setOperatorId(operatorId);
log.setTargetId(targetId);
log.setOperationTime(LocalDateTime.now());
log.setResult(result != null && !"fail".equals(result.getCode()) ? "SUCCESS" : "FAIL");
auditLogRepository.save(log);
}
}
📊 为什么日志这么重要? 因为“水平越权”往往不是一次性的大规模泄露,而是像“蚂蚁搬家”一样,每天查几条,积少成多。如果没有日志,根本发现不了。一旦出事,日志就是追责和追溯的唯一线索。
总结一下:三步走,筑牢防线
| 步骤 | 核心动作 | 一句话口诀 |
|---|---|---|
| 1. 拒绝 URL 传 ID 作为身份依据 | 从 Session/Token 拿当前用户 ID,而不是从请求参数 | “我是谁,服务器说了算” |
| 2. 强制比对权限 | 每次查询都检查:当前用户 vs 目标资源 | “想看谁,先问权限够不够” |
| 3. 敏感操作二次验证+日志 | 发短信/密码确认,全程记录操作痕迹 | “大动作要再确认,所有操作留后账” |
最后说一句心里话
很多老板觉得:“我们系统又不值钱,谁会来黑?”
但你要知道,最大的威胁往往不是外面的黑客,而是内部的人。
那个想炫耀能力的程序员,那个想搞钱的内鬼,那个手滑想查同事八卦的普通员工……他们就在你的办公室里,用着你的系统。
权限校验不是技术问题,是管理问题,更是良心问题。
别等数据泄露上新闻了,再想起来补这三步。现在,就去检查一下你的代码,看看有没有那个“改个 ID 就能看别人数据”的漏洞。
毕竟,安全,从来不是锦上添花,而是活下去的本钱。
