水平越权和垂直越权是什么?代码和架构层面怎么防范?
- 标签:Spring Security、认证授权、后端
🏛 业务架构师
水平越权与垂直越权——OWASP 常见的Broken Access Control 的两个典型形态:
- 水平越权:同权限级别用户访问别人的资源——把 URL 里的
orderId=1024改成1025,看到别人的订单。本质:只验证了"登录",没验证"资源归属"; - 垂直越权:低权限用户执行高权限操作——普通用户直接调
/api/admin/deleteUser(前端隐藏了按钮但接口裸奔)。本质:只做了前端显隐,没做后端权限校验;或低权限用户伪造请求带上 admin 的 action。
代码层防范:
- 垂直越权:每个接口按"权限注解"显式声明——Spring Security 的
@PreAuthorize("hasAuthority('user:delete')")方法级覆盖;默认拒绝(deny-all 起步,按需放开)而不是默认放行;后台管理接口独立分组+独立鉴权链(即使内网也不裸奔); - 水平越权(更难、更高发):权限注解只管"角色",资源归属要在业务代码校验——三种写法:显式校验(
order.userId == currentUser不符抛 403);SpEL 参数校验(@PreAuthorize("#id == authentication.name"));数据层收敛(查询天然带归属条件where user_id = ?——最稳,从 SQL 上杜绝越权数据可达)。ID 用 UUID/雪花代替自增(防遍历),敏感操作加二次确认。
架构层防范:
- 统一数据权限模型:行级权限(部门/区域/租户隔离)做成框架级组件(MyBatis 拦截器自动拼 scope 条件)——业务开发无感知地安全,比"人人记得写 if"可靠;
- 越权是授权测试的必测项:安全测评用两个账号互测(普通 A 改 B 的资源、普通用户打 admin 接口)写进上线 checklist;
- 审计与告警:403 频率异常监控(扫描越权的行为特征)。
业务视角的定调:越权漏洞占 Web 安全事故的大头——它不挑技术栈,专挑"认证做了、授权偷懒"的团队。防越权不是框架配置而是编码纪律+架构收敛的组合拳,要进代码评审的固定检查项。
🏗 系统架构师
水平越权(同级越权)= 访问同权限等级的其他用户的数据(用户 A 查看用户 B 的订单);垂直越权 = 低权限者执行高权限操作(普通用户调管理接口)。OWASP 的 Broken Access Control——多年蝉联漏洞 Top。
代码层防范:
- 水平越权(数据归属校验):
- 每个资源访问强制校验归属:
order.getUserId().equals(currentUser.getId())——从服务端会话取当前用户,绝不信请求参数里的 userId(GET /order/123?userId=456的 userId 是攻击面); - 数据层兜底:查询条件恒带归属(
where id = ? and user_id = ?)——用查询本身保证隔离,不依赖先查后判(TOCTOU 窗口);MyBatis 拦截器自动注入数据权限条件(见 RBAC 专题); - 关键 IDs 用不透明引用(订单号防遍历:连续自增 ID + 无归属校验 = 批量拖库脚本);
- 每个资源访问强制校验归属:
- 垂直越权(权限点校验):
- 方法级
@PreAuthorize("hasRole('ADMIN')")全覆盖敏感接口——默认拒绝(没有注解的接口要过 Code Review 清单:这个端点为什么可以匿名/登录即可?); - 前端隐藏按钮只是体验(前端控显、后端控权双层纪律);
- 管理接口网络隔离(独立域名/内网 + 二次认证)。
- 方法级
架构层防范(体系化):
- 统一授权收口:网关粗粒度(路径×角色)+ 服务细粒度(数据归属)两层,规则集中配置可审计;
- API 设计规避:资源路径用"我的视角"(
/me/orders而非传 userId)、管理操作独立路径前缀(便于统一管控); - 测试与审计:越权用例进 CI(两个测试账号交叉访问的自动化用例——每个新端点必配);生产流量审计(异常访问模式告警:单用户大量跨实体查询);
- 纵深防御:越权发生后的爆炸半径控制(敏感数据脱敏返回、操作二次确认、审计留痕可追溯)。
根因规律:越权漏洞几乎都源于信任了不该信任的输入(参数里的身份/权限)——原则一句话:身份从会话来,权限从服务端规则来,请求参数只提供"操作什么",不提供"我是谁/我能"。
💻 全栈开发专家
水平越权(越权访问同层级数据):用户 A 访问用户 B 的资源——GET /api/order/1001 把订单号换成别人的,接口没校验归属就返回了。 垂直越权(低权限用高权限功能):普通用户调管理接口——前端藏了按钮但后端没防(/admin/refund 直接 POST 成功)。
防范分代码与架构两层:
代码层(防御写法):
java
// 1. 垂直:方法级注解拦角色/权限
@PreAuthorize("hasRole('ADMIN')")
public void refund(Long orderId) { ... }
// 2. 水平:资源归属强制校验(核心!)——用当前用户做查询条件
public Order detail(Long orderId) {
Long uid = SecurityUtils.currentUserId();
return orderRepo.findByIdAndUserId(orderId, uid) // WHERE user_id = ?
.orElseThrow(() -> new AccessDeniedException("无权访问"));
}
// 反面教材:orderRepo.findById(orderId) 直接返回——越权窗口
// 3. 更新/删除同理:UPDATE ... WHERE id=? AND user_id=?(影响行数=0 即越权)
@Modifying
@Query("update Order o set o.status=?2 where o.id=?1 and o.userId=?3")架构层:
- 默认拒绝:SecurityFilterChain
anyRequest().authenticated()+ 白名单显式开放; - URL 层防面(角色粗筛)+ 方法层防点(业务权限)双层;
- ID 不可预测做纵深(雪花/UUID——但只能提高猜测成本,不是鉴权);
- 服务端永远不信任前端传的 userId——身份只从 token/SecurityContext 取,请求参数里的 userId 只能是「资源定位」不能是「身份声明」;
- 越权监控:403 集中告警,同一用户高频 403 = 爬虫或攻击侦察。
测试手段:安全用例进回归——两个测试账号互访对方资源、普通账号调管理接口,CI 每次跑(越权是回归最容易引入的漏洞)。