五级区划与行级数据权限,村级平台怎么把范围卡在服务端?

一、一个账号同时管着村和镇的那个上午

去年秋天在一个县做联调,镇农经站的一位同事登录之后要同时看本镇四个村的集体经济台账,我们把他的账号挂在镇一级,结果他打开村级的积分明细,页面是空的。前端按层级把菜单藏掉了,接口却没有拦住,同一个人换成直接请求,能把相邻一个镇的数据一起拉出来。那天下午我们拿他的令牌手工试了十一次,有七次越了界。

这套系统的账号两百三十多个,分布在县、镇、村三级,一个账号同时挂两三个层级是常态。页面六十七个,其中会碰数据的有四十四个,之前每个控制器里都写着一段区划判断,长短不一,谁写的谁熟悉,交接一次就多一处说不清的地方。

我们后来做过一次统计,四十四个接口里有九个的判断条件和别处不一样,比如村级论坛允许看本村加挂靠村,而三务公开只允许看本村。这些例外从来没人写下来过,全在读代码的时候才被发现。

二、边界不在菜单而在数据本身

拆开看,这件事分两层。一层是身份,你是谁,挂在哪个区划上;另一层是范围,你能看到哪些区划下的哪些数据。菜单只解决入口,解决不了范围,因为接口可以被绕开菜单直接调用,抓一次包就能拿到地址。

直觉做法是把判断放在前端。把当前账号的层级存进本地存储,页面按它决定要不要发请求,请求里顺手带一个范围参数。这条路两天就能写完,代价是前端传什么服务端就信什么,改一个参数就能换一个镇的数据,任何一次抓包都能发现。

另一个直觉是让每个接口自己判。写起来自由,坏处是规则散在四十四个地方,改一条规则要改几十处,漏掉一处也不会报错,只会安静地多给一点数据。我们判断这属于不能靠自觉守住的约束,必须由服务端从令牌里取,前端传的任何层级标识一律不参与判断。

三、三条路和各自的代价

第一条路是给每个数据域写一套查询接口。三务一套、积分一套、网格一套、论坛一套,判断写在各自的映射文件里。好处是容易看懂,坏处是四个域的账号范围一调整就要改四处,而且新增一个域就是新增一套,总量会一直往上涨。

第二条路是做一层统一的数据范围解析,把账号和区划的关系单独存一张表,查询执行时由框架统一注入条件。这条路前期要多写一层拦截,好处是范围逻辑只有一份,新增数据域只是在表里加一行,接口层看不到任何和区划有关的写法。

第三条路是交给现成的权限中间件,用它的角色能力再加一个自定义的数据范围注解。它能省掉一部分重复代码,只是这套村务规则里有跨级和例外,注解表达起来要么写死要么绕回自己写解析。权衡下来我们选了第二条,做成一层很薄的范围解析放在数据访问层,注解只用来标记这个接口属于哪个数据域。

四、把条件收进统一的拦截层

区划码直接沿用国标行政区划码,不另设一套编码体系。省两位、市四位、县六位、镇九位、村十二位,父级码就是子级的前缀。判断某条数据是否落在范围内,就是拿数据行的区划码做前缀匹配,写成 like 加前缀通配的形式,一位之差就落在不同的范围里。

这套权限判断后来被我们收进一个统一的拦截器,接口里再没出现过手写的区划条件。拦截器在进入业务方法之前把当前账号的可达区划集合算出来,压进这次请求的上下文,查询执行时由数据访问层读这个集合拼进条件里。

这套权限判断后来收进了万村乐数字乡村的统一拦截器,权限表只有三列,账号、区划码、数据域,命中即放行,数据域用枚举存,三务、积分、网格、论坛各占一位。一张表管住四十四个接口的范围,加人的时候只动这张表。

五、权限表与区划码的约定

请求上下文里放的是一个集合而不是一个字符串,因为跨级很常见。镇级账号的可达集合是它自己加上下面四个村的码,判断时遍历集合做前缀匹配,任意一个命中就放行。集合在请求开始时算一次,缓存在这次请求里,避免每个查询都去连表。

拦截器要能识别接口属于哪个数据域,我们在注解上写了域标识,还允许声明是否跨级。跨级这件事不能靠前缀碰巧覆盖,比如某个村级码的上级并不在这个账号的管辖里,业务上不该看到,就必须在权限表里显式声明,而不是让匹配规则自己兜。

六、三个坑的现场记录

第一个坑是菜单藏了接口没拦。现象是某个村的入口在菜单里看不到,把地址拿来手工拼一次请求,数据照样返回。根因是范围判断只写在页面和接口的分支里,导出、统计、下拉选项这些旁路接口没有覆盖到。改法是把拦截挂在数据访问层而不是控制器,凡是走这一层的查询都要过一遍范围解析,旁路的也一样。

第二个坑是跨级统计。现象是镇里看四个村的汇总,总数比四个村相加少了两个村。根因是账号挂的镇级码只覆盖本级,下面四个村级码并不天然被它的前缀包含,而代码依赖了这种巧合。改法是在权限表里为这个人显式写入四个村级码,同时在界面上把他能覆盖的几个村标出来,让他自己看得见。

第三个坑出在账号调整管辖范围之后历史数据的归属。现象是换岗的人调走了,之前经手的数据在新范围里消失,统计口径对不上。根因是数据归属按当前权限算,而业务上要求按发生时间判定。改法是在业务表上加一个归属区划字段,写入时记的是发生当时的区划,不随账号调整而变,范围判断优先读这个字段。

七、这层拦不住的事

这层拦得住范围,拦不住合理的越权请求。有人确实有查看某个村的正当理由,比如临时抽调做核查,权限表里没有这条记录,他就只能找人加一行。我们的做法是把这类临时授权做成带到期时间的记录,时间一到自动失效,避免一年后没人记得收回。

它也解决不了数据本身的准确度。区划码写错一位,范围判断会忠实执行,把不相干的记录算进来,这种问题只能靠录入校验和定期比对码表。后面我们补了一个校验任务,每周比对一次区划码与上级码表,不一致的行单独列出人工确认。

八、把范围写成可执行的规则

回头看,这几个月真正省事的地方是把范围写成了能执行的规则,而不是写在文档或者某个人的记忆里。新增一个数据域不用改接口,评审的时候看这张表就知道谁能看什么,例外也摆在明面上,不用靠口头交代。

这套判断在万村乐数字乡村里管着二十多个村的账号,每加一个数据域只要在权限表里补一行。上线到现在改过三次规则,每次都是先改表再跑一遍回归,没有一次是去动接口代码的。

相关推荐
喵了几个咪5 天前
Go 写业务,Rust 扛底盘:一套可落地的混合架构
微服务·架构·golang·rust·多租户·gowind·rushwind
衡石科技5 天前
让问数更自然:衡石 Data Agent 实践
大数据·bi·数据权限·data agent·自然语言问数
RuoyiOffice20 天前
SpringBoot3+Vue3 企业级管理平台架构:多租户、RBAC、数据权限、工作流与微服务一次讲透
spring boot·vue3·springboot3·rbac·多租户·数据权限·ruoyi office
EatFan21 天前
多租户系统到底怎么设计?从 tenant_id 到 JWT 租户隔离的真实实践
前端·后端·全栈·多租户
thesky1234561 个月前
27届大模型面试准备(七十一):多租户 GPU 虚拟化与推理隔离工程——MIG、MPS 与噪声邻居治理
大模型·mig·多租户·mps·gpu虚拟化·显存隔离·噪声邻居
linweidong1 个月前
阿里Lazada Java面经及参考答案
大数据·高并发·反射·拦截器·session·幂等性·redis性能
CSharp精选营1 个月前
用 .NET 8 + uni-app 做一套多租户畜牧 SaaS:我们在秦巴牧云踩过的 6 个坑
uni-app·efcore·.net8·多租户·saas踩坑
AI砖家2 个月前
多商户多租户系统架构设计文档(Java版)
java·开发语言·系统架构·多租户·多商户
这是谁的博客?2 个月前
【中阶·融合】如何隔离多租户 AI 推理平台的 GPU 资源:从 Namespace 到 MIG/Kata 的五层纵深防御
人工智能·ai·云原生·kubernetes·gpu·多租户·ai安全