做业务系统的人,开局三件事永远绕不开:登录注册、权限管理、组织架构。尤其是当公司同时跑着订单系统、仓储系统、财务系统、报表看板,每个系统各建一套用户表,密码规则五花八门,员工离职还要挨个系统删账号。我在做Mole示例工程时,把以前踩过的这些坑重新梳理了一遍,最后落地了一套基于Mole用户中心的业务系统实现示例。它要解决的事情很聚焦:用一个用户中心统一管身份、管组织、管权限,让后续业务系统只按标准协议接入,不再重复造登录和权限的轮子。这篇内容适合后端开发、架构师,以及正在为海外仓等多仓业务做系统选型和集成的朋友,我会把关键代码、数据表设计、实战排查思路一起讲清楚。
1. 项目概述:Mole 示例工程到底解决了什么问题
1.1 为什么业务系统需要一个独立的用户中心
单体时代里,业务系统一般就在自己的用户表上加个is_admin字段,登录判断一下也就够了。但系统开始一分为多之后就变味了:ERP里一套账号,WMS里另一套账号,数据报表平台又来一套,账号不打通,密码策略不一致,权限定义也互相看不懂。最终结果就是:同一个员工在三个系统里是三张面孔,离职时漏删一个账号就可能成为安全隐患。
用户中心本质上就是公司的“工牌系统+门禁系统”。员工只需领一张工牌,就能进所有办公室;离职后收走工牌,所有门禁自动失效。Mole把这种思路沉淀成统一身份认证和授权服务,业务系统不再自己维护密码和角色表,只保留业务数据本身。这个设计在微服务架构里尤其重要,因为每个服务都单独校验身份会导致流程割裂,而独立的用户中心天然适合充当公共网关后的一层服务。
1.2 Mole示例工程的三个核心目标
在业务系统规模不大的时候,很多人会觉得单独搭一个用户中心是过度设计。但我的看法是:只要你想清楚未来会接入两个以上的子系统,用户中心就值得先做。Mole示例工程围绕这个判断设定了三个目标。
第一个目标是“证明可复用”。一个用户中心不能只是登录接口,还要支撑菜单权限、按钮权限、数据权限、组织树这些真实业务需求。示例工程覆盖了这些场景,让新业务系统可以直接参考复制。
第二个目标是“标准化接入方式”。如果每个团队接入用户中心时都发明一套自己的对接姿势,用户中心就会退化成又一个孤岛。Mole为业务系统提供统一的SDK和网关校验规范,接入方不用关心令牌怎么解析、权限怎么缓存,开发体验更像使用一个普通基础库。
第三个目标是“建立通用组织与数据权限模型”。尤其是多仓、多租户场景,业务系统最头疼的其实是“谁能看到哪些仓库的数据”。Mole把组织节点抽成数据范围,业务查询时只要带上用户可见的节点列表,就能天然隔离数据,而不是依赖散落在SQL里的各种条件拼接。
这三个目标带来的直接收益是开发提效。业务系统拿到Mole示例工程后,重点可以放在核心业务逻辑上,而不是在登录注册和权限管理上继续消耗人力。
2. 核心细节:Mole 用户中心的接口设计与数据模型
2.1 给业务系统开出的四类核心能力
用户中心不是简单的一个login接口,它至少要提供四类能力,业务系统才能完整跑起来。
第一类是认证能力。常见模式是OAuth2/OIDC或JWT。Mole默认支持账号密码登录,也支持授权码模式,业务系统通过code换取访问令牌,令牌里携带用户标识和有效期。令牌分access_token和refresh_token,前者短命,后者长命,这样既保证安全性又保证用户不需要频繁输入密码。
第二类是用户信息能力。前端需要展示头像、昵称、角色名称,后端需要知道用户属于哪些组织。Mole提供一个/userinfo接口,业务系统可以直接拿到当前用户的基本信息和角色权限点列表。
第三类是权限校验能力。这里分两层:操作权限和数据权限。操作权限解决“能不能点这个按钮、调这个接口”的问题,数据权限解决“能看到哪个仓库、哪个货主的数据”的问题。Mole在令牌里写入roles和org_ids,业务系统解析之后可以做接口级和数据级两层控制。
第四类是组织树能力。多仓业务系统里,“组织”不一定是公司部门,也可能是仓库、货主或区域中心。Mole维护统一组织树,父子节点天然形成层级关系,比如“英国仓”下面可以挂“伦敦仓”和“曼彻斯特仓”,权限配置挂在父节点时,子节点自动继承。
如果把用户中心只做成登录接口,那只完成了20%。真正决定项目成败的是权限模型和数据结构。
2.2 数据模型:用户、角色、权限点与组织的关系
Mole的数据模型遵循RBAC的经典套路,但增加了“组织”这个维度,让它能胜任多仓隔离。核心表就这么几张:
-- 用户表 create table sys_user ( id bigint primary key, username varchar(64) not null, password varchar(128) not null, nickname varchar(64), enabled tinyint default 1, create_time datetime ); -- 角色表 create table sys_role ( id bigint primary key, role_code varchar(64) not null, role_name varchar(64) not null, data_scope tinyint comment '1全部 2本组织 3本组织及以下 4自定义' ); -- 权限点表 create table sys_perm ( id bigint primary key, perm_code varchar(128) not null, perm_name varchar(128) not null, parent_id bigint default 0 ); -- 用户角色关系 create table sys_user_role ( user_id bigint not null, role_id bigint not null ); -- 角色权限关系 create table sys_role_perm ( role_id bigint not null, perm_id bigint not null ); -- 组织表(仓库、货主、部门都可抽象为组织) create table sys_org ( id bigint primary key, parent_id bigint default 0, org_type tinyint comment '1公司 2仓库 3货主 4区域', org_code varchar(64), org_name varchar(128) ); -- 用户与组织关系(数据权限核心) create table sys_user_org ( user_id bigint not null, org_id bigint not null );设计上的关键点在于:用户和角色的关系解决“能干什么”,用户和组织的关系解决“能看到哪些数据”。这两组关系最好分开存,否则一旦出现“某人拥有多个仓库权限但角色又不同”的组合,数据模型就会乱成一团。
在多仓场景里,org_id可以直接对应仓库ID。货主角色可能只关联某个仓库,仓管角色可以关联多个仓库,集团管理员则完全不关联组织节点,靠data_scope=1看全量数据。这样设计之后,库存查询等接口在最底层加一个组织维度过滤即可,干净利落。
2.3 对接方式:网关+SDK双保险
Mole不要求每个业务系统都直接调用用户中心接口来做权限校验,因为那样容易产生重复代码和性能损耗。更合理的做法是“网关做认证,业务系统做授权”。
请求先到达API网关,网关统一负责校验访问令牌是否有效,无效直接返回401。校验通过后,网关把用户信息以请求头透传给下游业务系统,比如X-User-Id、X-Org-Ids。业务系统只需要解析这几个请求头,再做本地接口权限校验即可。
Mole还提供一套SDK,把解析和校验动作封装好。业务系统引入SDK后,只需配置用户中心的地址和应用凭证,就能自动获得令牌解析、权限缓存、组织范围查询等能力。好处是业务代码里基本看不到JWT解析的底层逻辑,权限判断以注解或方法调用的方式出现,团队新成员也比较容易上手。
3. 实操过程:基于Mole用户中心搭建业务系统(海外仓示例)
3.1 初始化工程,先从依赖和配置说起
我以Java Spring Boot工程为例来说明整个接入过程,其他语言思路类似。首先在pom.xml里引入Mole提供的starter:
<dependency> <groupId>com.mole</groupId> <artifactId>mole-user-center-starter</artifactId> <version>1.0.0</version> </dependency>然后在application.yml里配置用户中心的连接信息:
mole: user-center: server-url: http://user-center.internal:8080 app-id: wms-biz app-secret: wms-secret-xxxxxxxx jwt-key: mole-jwt-secret-key-please-change这几个字段必须认真对待。server-url是用户中心的内网地址,app-id和app-secret相当于业务系统在用户中心注册时拿到的“应用身份证”,jwt-key用于本地解析令牌签名。这里最容易踩的坑是不同环境复制配置时忘记改jwt-key,导致本地联调时令牌一直校验不过。
3.2 完成登录与用户信息获取
接入Mole之后,登录流程通常由前端跳转到用户中心完成,用户中心登录成功后回跳回业务系统,并带一个授权码。业务系统拿到授权码,调用Mole的SDK换取令牌和用户信息:
@RestController @RequestMapping("/auth") public class AuthController { private final MoleClient moleClient; public AuthController(MoleClient moleClient) { this.moleClient = moleClient; } @GetMapping("/callback") public Result<UserInfo> callback(@RequestParam String code) { TokenResponse token = moleClient.getTokenByCode(code); UserInfo user = moleClient.getUserInfo(token.getAccessToken()); // 这里可以建立本地会话,如生成一个Short-Lived的Session ID return Result.ok(user); } }如果是系统间对接或API调用场景,不需要弹窗登录页面,业务系统也可以调用Mole的账号密码登录接口换取令牌,返回结果还是同一个格式。关键是业务系统不要自己保存密码,Mole用户中心存储密码时建议使用BCrypt或Argon2id加盐哈希,原样存储明文是绝对红线。
登录之后,所有业务请求都会带着Authorization: Bearer <access_token>。网关校验通过后,请求头里会注入用户信息,SDK把这些信息包装成MoleContext,业务方法里直接取用即可。
3.3 权限控制:用注解守住后端防线
前端隐藏按钮只是体验优化,真正的权限控制必须在后端。Mole的SDK提供了一个注解@RequirePerm,放在Controller方法上就能校验操作权限:
@RestController @RequestMapping("/wms/inventory") public class InventoryController { @GetMapping("/list") @RequirePerm(perm = "wms:inventory:list") public Result<List<InventoryVO>> page(InventoryQuery query) { Long userId = MoleContext.getUserId(); List<Long> orgIds = MoleContext.getOrgIds(); // 业务逻辑只处理orgIds范围内的数据 return Result.ok(inventoryService.query(userId, orgIds, query)); } }注意,这里我把orgIds也传进了业务服务。这就是数据权限的落地方式:操作权限拦住了没有wms:inventory:list权限码的用户,数据权限把可见组织传进SQL,保证用户只能看到自己有权限的仓库数据。
权限码建议统一规范成模块:子模块:动作的格式,比如wms:inbound:create、wms:outbound:audit。这样在权限点列表里看起来结构清晰,也方便按前缀批量授权。
3.4 多仓业务关键功能:货主档案与库存查询
海外仓业务里有一个典型场景:一个货主可能在美国仓和德国仓都有库存,但两个仓的负责人并不相同。货主可以查看自己所有仓库的库存,美国仓负责人只能看美国仓。要支持这种隔离,查询SQL只要加一个组织过滤就做完了:
SELECT i.warehouse_id, i.sku_code, i.available_qty FROM inventory i JOIN sys_user_org uo ON uo.org_id = i.warehouse_id WHERE uo.user_id = #{userId} AND i.sku_code = #{skuCode}这条SQL背后的思路是:仓库ID其实就是组织ID,用户与仓库的关系已经在用户中心里维护好了,业务系统查询时做一个JOIN,就天然只看得到自己有权限的仓库。不要试图在条件里写一堆复杂的OR判断,也不要在查询后再用Java代码过滤,数据库层直接做掉最稳妥。
货主档案管理也有类似问题。货主是一个组织节点,某位销售主管负责某一批货主,他登录系统后选择货主下拉框时,应该只看到自己负责的货主。实现方式同样是从MoleContext.getOrgIds()拿到组织范围,然后过滤货主下拉列表的数据源。
3.5 本地调试时最容易忽略的配置点
本地跑示例工程时,有几个配置容易让人卡住半小时以上。第一个是jwt-key不一致,网关服务和业务服务的密钥必须完全相同,否则令牌一提交换就变成篡改签名。第二个是时区设置,用户中心返回的令牌过期时间是UTC,业务服务如果没有统一时区,就会误判令牌过期。建议所有服务在启动参数里加-Duser.timezone=Asia/Shanghai,数据库连接串也用serverTimezone=Asia/Shanghai。
第三点是联调环境的server-url,本地要确认是走内网还是走代理,如果配置成公网地址,访问不稳定很正常。第四点是依赖缓存,Redis里可能堆积了旧的权限缓存,接入新角色后不生效,启动开发环境时最好主动清一次缓存。
4. 多国多仓海外仓场景:系统选型与WMS对比思路
4.1 海外仓业务对权限中心提出了什么要求
海外仓不是把多个国内仓搬出国,它面临的复杂度要高出不少。首先是地域分散,仓库分布在多个国家,时区、币种、计量单位都不一样。其次是业务角色复杂,有自营仓团队、代理仓团队、货主、承运商、报关行,不同角色对数据的可见范围完全不同。最后是数据合规要求,不同国家对个人数据的保护要求不一样,系统最好能在用户中心层面就做好数据分类和访问控制。
这种情况下,权限中心如果还停留在“用户表、角色表、菜单表”这种老三层,根本扛不住。Mole把组织维度引入权限体系后,业务系统可以把仓库作为组织节点,把货主作为绑定在仓库下的组织节点,权限分配时只需要选“用户+角色+组织范围”三个维度,清晰直接。系统上线后,新人入职开账号,或者货主新增仓库权限,都是在用户中心统一操作,不用进数据库手改关联表。
4.2 自研还是商用WMS,我的取舍标准
很多公司在建设海外仓业务系统时,都会纠结WMS到底是自研还是采购。我给一个很务实的判断标准:如果仓库数量少于3个,流程标准化,团队没有很强的开发能力,直接选商用WMS;如果仓库运营模式特殊,高度依赖定制,且已有技术团队,才考虑自研。自研的隐性成本远大于表面代码量,它需要持续投入维护,还要处理海关、物流渠道、平台API的各种变化。
| 维度 | 自研WMS | 老牌商用WMS | SaaS轻量WMS | 平台型WMS |
|---|---|---|---|---|
| 功能覆盖 | 灵活,但需定制开发 | 功能丰富,流程成熟 | 标准化程度高,灵活度低 | 与电商平台绑定,扩展受限 |
| 实施周期 | 数月到一年起 | 2到6个月 | 1到4周 | 通常较短,但配置受限 |
| 成本模型 | 人力成本高 | 授权费+实施费较高 | 按年订阅,门槛低 | 平台佣金或服务年费 |
| 适配场景 | 特殊流程和集团级多仓 | 大型制造、品牌跨境仓 | 中小卖家或业务初期 | 依赖特定电商渠道的卖家 |
我见过不少团队在只有两个人维护系统的情况下就启动自研WMS,结果半年后业务量起来,功能跟不上。哪怕最后仍要自研,也建议先跑一套商用SaaS或开源参照系统,把业务流程验证清楚,再动手设计和开发。
4.3 四款主流WMS的“形态对比”测评,以及选型标准
热门话题里反复提到的“多国多仓业务的海外仓系统怎么选”,其实不一定要死磕具体某几个产品,更重要的是先看懂WMS市场的几条主流路线。按我最近整理选型资料的经验,市面上的主流选择大致可以分成四类。
一是国际老牌系统。这类产品通常功能完整,支持多仓、多币种、多语言,适合全球化布局的成熟企业。缺点是贵、实施复杂、定制成本高,上线周期动辄半年以上。二是国内老牌WMS。这类系统本地化做得细,很多都在电商仓储领域打磨多年,价格相对国际品牌更低,对国内跨境仓、保税仓流程支持也更好。三是SaaS轻量WMS。开箱即用,月费门槛低,适合中小卖家快速起步,但功能天花板明显,复杂的波次策略和计费逻辑很难靠配置实现。四是自研或半自研平台。适合有开发团队、仓库流程又有明显差异化的集团。
选型标准可以归纳为六条:库存准确性、波次拣货策略、多仓多货主组织模型、开放API能力、权限模型、实施团队服务能力。我见过最大宗的选型错误是只看功能演示,忽略权限模型。仓配系统最怕“一个账号查所有仓”,如果选型时没把权限隔离当核心需求考察,后面每个仓库的数据风险都很大。
4.4 用Mole用户中心把业务系统和WMS串成一条线
无论是选型还是自研,最终业务系统都需要和WMS形成配合。Mole在整条链路里充当“账号和权限中枢”:业务平台、WMS、报表系统都对接同一个用户中心,用户登录一次,就能在多个系统间单点跳转,不需要重复输入密码。
具体串联方式也不复杂。用户中心里维护组织树,仓库和货主都是组织节点,WMS里的仓库ID与用户中心组织ID做好映射。WMS收到请求时,网关做认证,业务系统用Mole的SDK解析用户和权限范围,然后带着组织ID去查WMS的数据。这样WMS里不用再建一张权限表,所有账号的状态、角色、可用范围都由用户中心统一管控,审计日志也能做到一个入口查全部。
多仓业务最怕“系统间数据可见性不一致”,比如业务后台能看到某货主在德国仓的库存,但WMS里却查不到。只要权限判定统一落到用户中心,由网关下发org_ids,两个系统就不可能出现这种偏差。
5. 常见问题与排查技巧实录
5.1 登录成功但接口一直返回401
这个问题的排查主线很清楚。先用浏览器或调试工具看请求头里的Authorization是否正常带上了Bearer前缀,再看网关是否把这个头透传给下游业务服务。很多微服务网关默认只转发业务参数,自定义请求头被丢得一干二净。其次是确认JWT签名密钥配置一致,网关验签用的jwt-key和业务服务本地解析用的密钥必须是同一把。最后检查服务器时间是否准确,误差超过几秒就会导致nbf和exp判断异常。
排查时先用
curl -i看返回头里的错误码和提示,再逐层检查网关日志。不要上来就怀疑SDK有bug,这个场景八成是配置或透传问题。
5.2 数据权限失效,把所有仓库都查出来了
最常见的三个原因分别是:用户被赋予了全局管理员角色导致数据范围变为全部;查询SQL里漏了组织过滤条件;Redis缓存了旧权限数据。全局管理员角色适合给少数运维人员,但它会跳过数据权限校验,误给业务员这个角色就等于把全公司仓库都暴露了。SQL层面,我建议所有多仓查询统一走一个基础查询组件,组件内部强制追加org_ids过滤,而不是靠每个开发人员在Service里手写WHERE。缓存问题则可以通过角色分配后主动刷新用户权限缓存来解决。
5.3 令牌刷新竞态,刷新一次就掉线
前端一般会在接口返回401时用refresh_token换新的access_token。但多个接口同时401时,前端会同时发起多个刷新请求,导致用户中心的刷新令牌被旋转多次,旧令牌全部失效。更坏的情况是并发刷新之间相互踢掉对方的登录态。
处理方案是前端做一个统一的刷新管理:第一次遇到401时记录刷新Promise,后续401请求复用同一个Promise,不重复调用刷新接口。后端也建议在刷新令牌时做令牌旋转,每次刷新都签发新令牌吊销旧令牌,同时给刷新接口加短时间窗口内的防重放限制。这样既能延长会话,又不至于因为刷新风暴把人踢下线。
5.4 权限码大小写与缓存问题总是反复出现
权限码校验时我建议统一使用小写,并在注册权限点时就做好规范。实际项目里最容易出现“按钮不显示”或“接口报403”的情况,很多都是权限码大小写不一致造成的,比如前端写的是wms:Stock:list,后端注册的是wms:stock:list。另一个典型问题就是新增权限点或调整角色后不生效,需要确认权限缓存是否有TTL,是否能手动清理。Mole的SDK里通常支持按用户做缓存失效,角色调整后主动调用一下刷新接口,能减少非常多的线上疑问。
6. 实践体会与值得继续扩展的方向
Mole示例工程做完之后,我最深的一个体会是:权限模型看着简单,真正落地到处是坑。把组织节点和数据权限的边界提前定义清楚,后再做多仓、多业务系统的集成就会顺很多。如果一开始只图省事,在公司只有几个账号时用简单的is_admin字段顶上,后面用户量大了再迁移,代价会非常高。
后续值得继续扩展的方向也比较明确:第一是把多因素认证集成进用户中心,尤其是货主和外勤人员账号,风险更高;第二是给用户中心补上完整的审计日志,每次权限变更和登录行为都可追溯;第三是把数据脱敏规则也放进来,比如不同角色查看货主手机号时,看到的长度不一样。这些能力都沉淀在用户中心之后,业务系统的代码会越来越薄,安全性和一致性反而越来越高。