为什么很多电商后台做了权限,最终还是会出现越权查数据?
电商系统的数据权限 SQL 应该怎么做?一次讲清店铺、仓库、类目隔离与动态查询设计
大家好,我是一名有 4 年工作经验的 Java 后端开发。
最近在系统整理后台系统和电商业务里的权限设计问题,准备沉淀成一个系列。
上一篇我写了电商后台的菜单权限、按钮权限、数据权限怎么设计,这一篇继续往下拆更实战的一层:数据权限在 SQL 里到底应该怎么落地。
🦅个人主页
🐼
文章目录
一、前言
很多团队在做权限系统时,菜单权限和按钮权限往往做得还不错:
- 菜单能看到哪些
- 按钮能不能点
- 接口有没有权限码
但一到数据权限,系统就开始变得很模糊。
比如:
- 商品运营只能看自己负责类目的商品,这个 SQL 怎么写?
- 仓库人员只能看自己仓库的订单,查询条件放哪?
- 客服只能看自己店铺的售后单,导出 Excel 时怎么限制?
- 财务只能看已结算店铺的数据,分页接口和统计接口怎么保持一致?
- 平台后台和商家后台共用一套表时,
merchant_id怎么隔离?
很多项目做到最后会出现这些问题:
- 前端做了筛选限制,但后端 SQL 没有限制
- 列表接口有限制,导出接口忘了限制
- 查询接口有限制,详情接口没限制
- SQL 全靠 if/else 拼来拼去,后面越来越难维护
- 数据范围逻辑散落在 Controller、Service、Mapper 到处都是
所以数据权限真正要解决的问题,不是“多加几个 where 条件”,而是:
如何把用户的数据可见范围稳定、统一、可维护地落到后端查询层。
这篇文章就结合电商后台场景,把数据权限 SQL 的设计思路、数据库建模、动态查询方式和常见坑点系统讲透。
二、业务场景
先假设这样一个典型的电商后台。
2.1 后台角色
系统里有这些典型角色:
- 商品运营
- 仓库人员
- 客服
- 财务
- 店长
- 平台运营
2.2 数据隔离维度
电商后台最常见的数据隔离维度有这些:
shop_id:店铺维度warehouse_id:仓库维度category_id:类目维度merchant_id:商户维度dept_id:组织维度creator_id:本人数据维度
2.3 真实业务要求
这个场景下,通常会有下面这些要求:
- 商品运营只能查看自己负责类目的商品
- 仓库人员只能查看自己仓库的待发货订单
- 客服只能查看自己店铺的订单和售后
- 财务只能查看自己负责商户的结算数据
- 平台管理员可以看全部数据
- 导出、详情、统计接口都必须遵守同一套数据权限规则
这就意味着:
数据权限不是某一个页面的特殊逻辑,而应该是整套后端查询体系的一部分。
三、问题现象
很多项目一开始做数据权限,通常是这样写的。
3.1 在 Controller 里随手塞几个查询条件
比如:
@GetMapping("/order/list")
public Result list(OrderQuery query) {
query.setShopId(LoginUserHolder.getShopId());
return Result.success(orderService.list(query));
}
这种写法的问题是:
- 逻辑分散
- 只覆盖了某一个接口
- 后面导出、详情、统计接口很容易忘记加
3.2 前端传什么范围,后端就查什么
比如前端请求:
{
"shopId": 1001,
"warehouseId": 3
}
然后后端就直接信任这个参数去查。
这种做法非常危险,因为:
- 前端参数可以被篡改
- 用户完全可能传入不属于自己的店铺 ID
也就是说:
数据权限永远不能靠前端约束,必须由后端根据登录人重新收敛。
3.3 列表接口做了数据权限,详情接口忘了做
这是实际项目里特别常见的坑。
比如:
- 订单列表只能看到自己店铺
- 但
GET /order/{id}却没做校验
结果就是:
- 列表看不到
- 手动拼一个订单 ID 却能直接看详情
3.4 导出接口和统计接口绕过了数据权限
很多系统列表页面限制做了,但:
- 导出接口没加
- 汇总统计 SQL 没加
- 大屏报表接口没加
这种问题在电商后台尤其危险,因为:
- 订单金额
- 成本价
- 结算数据
都很敏感。
四、原理分析
数据权限 SQL 真正要解决的,不只是“能不能查出来”,而是:
在任何查询入口上,都能稳定地把用户可见范围收敛成后端可信的查询条件。
4.1 数据权限和菜单权限、按钮权限有什么本质区别?
菜单权限和按钮权限更多是:
- 功能入口控制
而数据权限控制的是:
- 查询结果边界
- 可操作对象边界
也就是说:
- 菜单权限控制“能不能进页面”
- 按钮权限控制“能不能点操作”
- 数据权限控制“进入页面后能看到谁的数据”
这是三层不同问题。
4.2 数据权限本质上是“行级过滤”
大多数业务里的数据权限,本质上都可以理解成:
在原始 SQL 的基础上,附加一段与当前用户身份相关的 where 条件。
比如:
- 店铺权限:
shop_id in (...) - 仓库权限:
warehouse_id in (...) - 类目权限:
category_id in (...) - 本人权限:
creator_id = currentUserId
这就是为什么数据权限最终会落到 SQL 层。
4.3 为什么电商系统的数据权限往往是多维的?
因为电商后台里的可见范围不只有一个维度。
比如“订单列表”这个模块,可能同时受这些维度影响:
- 店铺维度
- 仓库维度
- 商户维度
- 数据状态维度
所以在真实项目里,数据权限通常不是一个简单的 dept_id = ? 就完了,而是:
多个维度共同约束。
4.4 为什么不能把数据权限逻辑写死在每个 Mapper 里?
因为一旦模块多了,你会看到下面这种情况:
- 订单列表写一套
- 订单导出写一套
- 订单统计写一套
- 售后列表再写一套
最后就会越来越难维护,而且很容易漏。
所以更合理的思路通常是:
- 先统一抽象“当前用户的数据范围”
- 再统一落到查询条件
五、数据库怎么设计最合适
如果你要做电商后台数据权限,我建议数据库至少这样建模。
5.1 基础角色表和权限表
这一层和普通 RBAC 一样:
sys_usersys_rolesys_user_rolesys_permissionsys_role_permission
5.2 数据范围表
电商系统建议按业务维度拆范围表。
sys_role_shop_scope
角色可见店铺范围
字段示例:
idrole_idshop_id
sys_role_warehouse_scope
角色可见仓库范围
字段示例:
idrole_idwarehouse_id
sys_role_category_scope
角色可见类目范围
字段示例:
idrole_idcategory_id
sys_role_merchant_scope
角色可见商户范围
字段示例:
idrole_idmerchant_id
5.3 为什么我更推荐“范围关系表”,而不是一个 scope_json 字段?
因为关系表更适合:
- SQL Join 查询
- 索引优化
- 权限配置管理
- 后续扩展
而 JSON 字段虽然看起来灵活,但:
- 查询不方便
- 索引能力弱
- 后期维护很麻烦
所以如果是正式项目,我更建议用关系表。
六、推荐的后端设计方式
这里给一版更贴近线上落地的思路。
6.1 先统一拿到当前用户的数据范围
比如登录后,后端可以把当前用户的数据范围整理成一个对象:
public class DataScopeContext {
private boolean allScope;
private Set<Long> shopIds;
private Set<Long> warehouseIds;
private Set<Long> categoryIds;
private Set<Long> merchantIds;
private Long userId;
}
这个对象可以来自:
- 用户角色
- 角色绑定的数据范围
- 平台管理员特殊规则
6.2 查询前,把数据范围转成查询条件
比如订单列表查询对象:
public class OrderQuery {
private Long shopId;
private Long warehouseId;
private Integer status;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Set<Long> allowedShopIds;
private Set<Long> allowedWarehouseIds;
}
然后在 Service 层做统一收敛:
public void fillDataScope(OrderQuery query, DataScopeContext scope) {
if (!scope.isAllScope()) {
query.setAllowedShopIds(scope.getShopIds());
query.setAllowedWarehouseIds(scope.getWarehouseIds());
}
}
这一步的核心意义是:
前端传的是业务筛选条件,后端补的是权限过滤条件。
两者不能混为一谈。
6.3 SQL 里永远以“后端收敛后的权限范围”为准
也就是说,用户前端即使传了:
shopId = 9999
但如果 9999 不在当前用户允许范围里,后端依然不能放行。
这也是数据权限设计里最重要的一条原则。
七、落地 SQL 和代码示例
下面给一版比较贴近实际项目思路的示例。
7.1 订单列表 SQL 怎么做数据权限
MyBatis XML 示例:
<select id="selectOrderPage" resultType="com.demo.OrderDTO">
select
id, order_no, shop_id, warehouse_id, status, total_amount, create_time
from order_info
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="shopId != null">
and shop_id = #{shopId}
</if>
<if test="warehouseId != null">
and warehouse_id = #{warehouseId}
</if>
<if test="allowedShopIds != null and allowedShopIds.size() > 0">
and shop_id in
<foreach collection="allowedShopIds" item="shopId" open="(" separator="," close=")">
#{shopId}
</foreach>
</if>
<if test="allowedWarehouseIds != null and allowedWarehouseIds.size() > 0">
and warehouse_id in
<foreach collection="allowedWarehouseIds" item="warehouseId" open="(" separator="," close=")">
#{warehouseId}
</foreach>
</if>
</where>
order by create_time desc
</select>
这个写法的核心点是:
shopId、warehouseId是业务筛选条件allowedShopIds、allowedWarehouseIds是权限过滤条件
这两个必须分开。
7.2 如果用户选择了一个自己没权限的店铺怎么办?
比如前端筛选:
shopId = 1005
但当前用户允许范围是:
shopIds = [1001, 1002]
后端有两种常见处理方式:
方式一:直接返回空数据
这适合列表查询。
方式二:直接抛权限异常
这更适合详情、导出、操作接口。
比如详情接口:
public OrderDetailDTO detail(Long orderId, Long currentUserId) {
Order order = orderMapper.selectById(orderId);
if (order == null) {
return null;
}
DataScopeContext scope = dataScopeService.getCurrentScope(currentUserId);
if (!scope.isAllScope() && !scope.getShopIds().contains(order.getShopId())) {
throw new RuntimeException("无权查看该订单");
}
return buildDetail(order);
}
7.3 导出接口必须复用同一套数据权限
这是最容易漏的地方。
错误做法:
- 列表接口带权限 SQL
- 导出接口重新写一套查询,忘了加范围限制
更好的做法是:
- 导出接口复用同一个 Query 对象
- 统一走同一个数据权限填充逻辑
比如:
public void exportOrder(OrderQuery query) {
DataScopeContext scope = dataScopeService.getCurrentScope(LoginUserHolder.getUserId());
fillDataScope(query, scope);
List<OrderDTO> rows = orderMapper.selectOrderForExport(query);
excelExporter.export(rows);
}
7.4 统计 SQL 也不能绕开数据权限
比如统计某个时间段订单金额:
select sum(total_amount)
from order_info
where status = 1
如果不加数据权限,这种报表最容易越权。
更合理的做法是:
select sum(total_amount)
from order_info
where status = 1
and shop_id in (...)
所以一定要记住:
数据权限不是只管列表,也必须管统计和报表。
7.5 更进一步:统一拦截器或注解式注入
如果项目规模更大,可以考虑:
- MyBatis 拦截器
- AOP 注解
- 自定义 DataScope 注解
比如:
@DataScope(shop = true, warehouse = true)
public PageResult<OrderDTO> page(OrderQuery query) {
return orderService.page(query);
}
然后在切面里统一把当前人的 shopIds、warehouseIds 注入查询对象。
这种方式的优点是:
- 权限逻辑更集中
- 不容易漏
- 维护成本更低
但缺点也有:
- 调试复杂度更高
- 需要团队规范统一
如果团队规模不大,我会建议先从“Service 层统一填充数据范围”开始,别一上来就做过度复杂的自动拦截。
八、为什么很多项目的数据权限最后会失控?
这也是线上项目非常常见的情况。
8.1 前端限制了筛选项,就以为安全了
这是最常见的误区。
前端不展示某个店铺,不代表别人不能手动传这个店铺 ID。
8.2 列表有限制,详情和导出忘了限制
这类问题在电商系统非常高频,而且影响很大。
8.3 数据权限逻辑散落在各层
Controller 里写一点,Service 里补一点,Mapper 里再拼一点,最后谁都说不清真正的限制条件是什么。
8.4 多维数据权限硬拼 OR / IN,SQL 越来越难维护
比如:
- 店铺一套
- 仓库一套
- 类目一套
- 商户一套
如果没有统一抽象,后面 SQL 会越来越乱。
8.5 没有管理员特权和商家维度的统一规则
比如平台管理员、商家管理员、店铺运营本来就不是一套范围模型。
如果没有统一定义,很容易写出很多特判。
8.6 只考虑查询,不考虑操作权限
比如发货、退款审核、库存调整这些动作,除了按钮权限,还必须校验目标数据对象是否在当前人的范围内。
也就是说:
数据权限不仅影响 select,也影响 update、export、approve 这些动作。
九、压测与监控怎么写,文章才更像做过项目的人写的?
数据权限这类文章如果只讲模型,不讲 SQL 成本和监控,容易显得太抽象。
所以建议补上观测和优化视角。
9.1 压测场景示例
这里给一个适合写进文章的测试场景:
场景配置:
- 订单表数据量:
5000 万 - 订单列表峰值 QPS:
3000 - 用户角色包含店铺和仓库双维度范围
- 应用实例数:
4 - MySQL:主从
对比方案:
- 方案 A:Controller 拼条件,接口各写各的
- 方案 B:Service 层统一填充数据范围
- 方案 C:统一数据权限注入 + 复合索引优化
9.2 压测结果示例
| 指标 | 各写各的 | Service 统一收敛 | 统一收敛 + SQL 优化 |
|---|---|---|---|
| 权限遗漏风险 | 高 | 中 | 低 |
| SQL 可维护性 | 差 | 中 | 好 |
| 查询 RT | 210ms | 180ms | 95ms |
| 导出一致性 | 容易出错 | 较好 | 好 |
| 项目可扩展性 | 差 | 中 | 好 |
从结果上可以看出:
- 数据权限最怕的不是多几个条件,而是逻辑散乱和遗漏
- 真正可维护的方式,一定是统一抽象、统一收敛、统一复用
说明:以上压测数据为示例写法,实际结果需要结合范围数据量、索引设计、SQL 条件组合方式综合评估。
9.3 线上建议重点监控哪些指标?
如果你准备真正落地数据权限 SQL,至少建议监控这些指标:
- 列表、详情、导出接口的越权审计日志
- 不同角色的查询 RT
- 范围过滤后的 SQL 扫描行数
- 导出接口耗时
- 统计报表接口耗时
- 数据权限命中规则数
- 管理员全量查询占比
- 店铺/仓库范围过大用户占比
这些指标一旦写进文章里,会明显更像真实线上经验总结。
十、面试中怎么回答这个问题?
如果面试官问你:
电商系统的数据权限 SQL 一般怎么做?
你可以这样回答。
10.1 回答思路
第一,电商系统的数据权限一般不是简单的部门维度,而是店铺、仓库、类目、商户等多维范围组合,所以需要先把当前用户的数据可见范围抽象出来。
第二,后端不能信任前端传入的范围参数,而是应该根据当前登录人的角色和范围配置,统一收敛出允许访问的 shopIds、warehouseIds、categoryIds 等,再把这些条件附加到 SQL 查询里。
第三,业务筛选条件和权限过滤条件必须分开,比如前端选择店铺是业务筛选,而当前人能看哪些店铺是权限条件,这两者不能混在一起。
第四,数据权限不能只作用于列表查询,详情、导出、统计、审批、发货这些接口也必须统一走同一套范围校验逻辑,否则非常容易出现越权。
第五,如果项目规模较小,可以先在 Service 层统一填充数据范围;如果规模更大,可以再考虑通过注解或拦截器统一注入数据权限条件。
10.2 面试官更想听到什么?
面试官真正想听的,通常不是一句“where 后面拼个 shop_id”,而是你有没有这些意识:
- 你知道前端参数不可信
- 你知道业务筛选和权限过滤要分开
- 你知道详情、导出、统计都必须统一管控
- 你知道数据权限是多维组合,不只是部门维度
- 你知道逻辑要统一收敛,不能散落在各层
- 你知道 SQL 性能和索引也要一起考虑
如果你能把这些点讲清楚,面试官会明显觉得你做过真实后台权限设计,而不只是知道 RBAC 名词。
十一、总结
电商系统的数据权限 SQL,真正难的不是“多加几个查询条件”,而是如何把:
- 当前用户的角色
- 当前用户的数据范围
- 业务筛选条件
- 统一的后端过滤逻辑
真正组合成一套稳定、统一、可复用的查询体系。
如果只记一句结论,我觉得可以记住这句:
电商系统的数据权限 SQL,核心不是让前端传什么查什么,而是后端根据当前用户统一收敛范围,再把权限条件稳定注入到所有查询入口。
这套思路不一定最轻,但通常更接近真实电商后台系统的做法。
十二、后续可以继续展开的内容
如果这篇你觉得还可以,后面这个系列我还可以继续写:
- 电商后台的订单状态机怎么设计?
- 平台后台和商家后台权限怎么隔离?
- 后台动态菜单和前端路由怎么结合?
- 电商系统的店铺、仓库、类目权限怎么统一抽象?
- 审批流和数据权限怎么结合?
十三、结尾
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注。
后面我会继续整理一些更偏实战的 Java 后端和电商系统设计文章。
更多推荐




所有评论(0)