从 RBAC 鉴权长成的小型微服务系统:Spring Cloud + 零依赖 JWT + CRM + 审计 + BFF

作者:

🇬🇧 English

出发点:从最小 RBAC 起步,顺着探索长成小系统

作为偏前端的全栈开发,微服务鉴权一直是我认知里偏薄的一块。做 RBAC 时常见的做法是直接上 Spring Security + Nacos + Gateway 全套方案,偏重、上手慢,文档里一笔带过的版本与配置组合经常把人卡住。于是想换个更轻的思路:让 AI 把一套最小 RBAC 鉴权闭环写出来,我边读边学、把来龙去脉理清楚,再在它基础上探索。

起初确实只想做"最小可用":用户注册登录、JWT 签发验证、RBAC 角色权限。但鉴权这条线跑通后,自然就开始问下一个问题:真有个业务服务接在网关后面,这套鉴权能不能管住它?于是加了 CRM 客户域。又想看每次放行/拒绝到底发生了什么,于是加了跨服务审计。最后想有个界面点点看,于是加了 Next.js 前端。这篇记录的就是这个演进过程–重点不在"我写了什么",而在"每个环节到底在做什么、为什么这样搭"。

当前栈:Java 17 · Spring Boot 3.2.5 · Spring Cloud 2023.0.3 · Spring Cloud Gateway(WebFlux)· Eureka · Config Server(native)· Spring Data JPA · H2 · Resilience4j · Maven 多模块(7 个)· Next.js 14 前端。三套运行模式(裸 jar / Docker / k3s)零改配置,同一份源码直接跑。

架构总览

7 个后端服务 + 1 个 Next.js 前端:

browser ──► web :3000  (Next.js BFF:/api/* rewrite 到网关,无 CORS)
               │
               ▼
    gateway-service :4100   PEP:JWT 校验 + 权限裁决 + 审计旁路 + trace ID
               │   via Eureka (lb://)
   ┌───────────┼───────────┬──────────────┐
   ▼           ▼           ▼              ▼
 auth:4101   rbac:4102  customer:4103   audit:4104
 注册/登录    PDP         CRM+审批流      append-only 审计
 签发 JWT    /api/check   被保护资源     (网关异步发射 / GET 查询)

    config-server :8888 (native 配置中心)    eureka-server :8761 (服务注册)
服务 端口 职责
eureka-server 8761 服务注册中心(@EnableEurekaServer
config-server 8888 配置中心(@EnableConfigServer,native 后端)
gateway-service 4100 PEP:JWT 校验 + 权限裁决 + 审计旁路 + trace ID
auth-service 4101 注册 / 登录 / /api/me,签发 JWT
rbac-service 4102 PDP:角色 / 权限 / 授予,BFS 算有效权限
customer-service 4103 CRM 客户域 CRUD + 删除审批流
audit-service 4104 跨服务 append-only 审计日志
web 3000 Next.js BFF dashboard,权限驱动 UI

鉴权核心:PEP/PDP + 零依赖 JWT

鉴权严格分两层,"放行/拒绝"和"凭什么放行"各管各的:

  • PEP(Policy Enforcement Point) 在网关 gateway-service/.../filter/AuthGlobalFilter.java@Order(-1) GlobalFilter)。
  • PDP(Policy Decision Point)rbac-service,回答"这个用户有没有这个权限"。

AuthGlobalFilter 的处理流程:

  1. trace ID:读 X-Trace-Id,没有就生成一个 8 位 hex;回写响应头并往下传,全链路可追踪。
  2. 公开路由放行/api/login/api/register/health/actuator/** 直接放行;非 /api/** 也放行。
  3. 验 JWT:要求 Authorization: Bearer <token>,调 jwtUtil.verify;缺 token 返回 401(审计 auth:missing),无效返回 401(审计 auth:invalid)。
  4. 注入身份:校验通过则往下游请求加 X-User: <username> 头。
  5. 算所需权限mapPermission(path, method) 把路由映射到权限;映射不到(如 /api/me/api/check)则只需登录即可。
  6. 调 PDP:用 WebClienthttp://rbac-service/api/check?user=&permission=,包在熔断器 rbac-check 里;allowed 放行并发审计 ALLOW,否则 403 发审计 DENY。
  7. 审计旁路:每次裁决后异步 fire-and-forget 发给 audit-service(审计本身 fail-open,不影响业务)。

零依赖 JWT:没有引入 jjwtnimbus-jose-jwt,用 JDK 原生的 Mac + Base64 + Jackson 直接实现 HS256(auth-service/util/JwtUtil.javagateway-service/util/JwtUtil.java)。零依赖只是附带好处,真正的目的是把 JWT 到底在算什么摊开来看清–header / payload / base64url 怎么拼、HMAC 怎么签、签名怎么验,几十行代码就不再是黑盒。共享密钥经 app.jwt-secret 注入(auth 与 gateway 共用对称密钥),TTL app.jwt-ttl 默认 86400000(24h)。gateway 的 JwtUtil 刻意只保留 verify 一份。密码哈希用 PBKDF2WithHmacSHA256auth-service/util/PasswordUtil.java):迭代 65536、key 256 bit、salt 16 字节,输出 salt:hash

熔断兜底(fail-closed):网关对 PDP 的调用用 Resilience4j 包裹,instance rbac-check,参数:

  • slidingWindowType: COUNT_BASEDslidingWindowSize: 10
  • minimumNumberOfCalls: 5
  • failureRateThreshold: 50slowCallRateThreshold: 50
  • slowCallDurationThreshold: 800ms
  • waitDurationInOpenState: 5s
  • permittedNumberOfCallsInHalfOpenState: 3
  • automaticTransitionFromOpenToHalfOpenEnabled: true

fail-closed:熔断打开或 PDP 异常时,网关直接返回 403,绝不返回 200。安全组件里 deny-by-default 比 allow-by-default 稳妥。熔断状态经 Actuator 暴露:management.endpoints.web.exposure.include: health,circuitbreakers

RBAC 模型:角色、权限、授予

种子数据在 rbac-service/.../service/RbacService.javaseedIfEmpty(),由 CommandLineRunner 启动时注入(H2 ddl-auto,每次启动干净种子):

  • 角色admin / editor / viewer(三层,无继承–seed 里 parentId 全为 null)。
  • 权限 11 个
    • 用户/角色/权限管理:users:readusers:writeroles:readroles:writepermissions:read
    • CRM 业务域:customers:readcustomers:createcustomers:updatecustomers:deletecustomers:approve
    • 审计:audit:read
  • 授予
    • admin:全部 11 个(含 customers:approveaudit:read
    • editorusers:readroles:readpermissions:read + customers:read/create/update/delete(7 个;无 approve、无 audit:read)
    • viewerusers:readroles:readpermissions:readcustomers:read(4 个,纯只读)
  • 用户 -> 角色admin -> admin、user -> editor、viewer -> viewer
  • 登录账号admin/admin123user/user123viewer/viewer123

BFS 有效权限resolveEffectivePermissions(username) 沿 parentId 链 BFS 收集权限(visited 防环),支持任意深度继承。当前 seed 没启用继承,所以实际等于直授角色权限的并集–机制留着,需要多级继承时 seed 里给 parentId 即可。PDP 端点:GET /api/check?user=&permission=

业务域与审计:让鉴权真的管住东西

鉴权闭环跑通后,加业务资源才看得出它管不管用。

customer-service(:4103,CRM 客户域):CRUD + 检索,网关路由映射到 customers:read/create/update/delete。亮点是删除审批流DELETE /api/customers/{id} 时,若调用方有 customers:approve(admin)直接删(200);否则返回 202 并建一条 pending 审批。/api/approvals 提供列表 + approve / reject(需 customers:approve,approver 从 X-User 头取)。它自己也有一份 RbacClientrbac /api/check 来判断走直接删还是审批(出错 fail 到审批流)。H2 ./data/customerddl-auto=update

audit-service(:4104,跨服务 append-only 审计):网关是唯一发射方–PEP 每次裁决后异步 best-effort 发一条审计事件。写入仅允许网关经服务发现直连(带私有头 X-Internal-Audit: gateway),对外路由 /api/audit 只放行 GET,无法从外部伪造写入。查询:GET /api/audit(分页,可按 decision / traceId 过滤)、GET /api/audit/stats(今日概览)。读需 audit:read(仅 admin)。H2 ./data/auditddl-auto=update

这俩加上去之后,"鉴权"就不再是空中楼阁:customer 是被保护的业务资源,audit 让每次裁决可追溯。

前端 BFF:权限驱动的 dashboard

web(:3000,Next.js 14 + React 18 + TypeScript):采用 BFF 模式,浏览器只调同源 /api/*,Next.js 服务端 rewrite 到网关 :4100,无 CORS。dashboard 有 7 个 tab:Roles / Permissions / Users / Customers / Approvals / Audit / Check。

关键是权限驱动 UI:登录后对每个权限调一次 PDP /api/check,没权限的 tab 显示锁。前端不自己判断权限,只根据 PDP 结果显隐–权限逻辑始终集中在 rbac,UI 只是个可视化的壳。

设计取舍:为什么这样选

这套组合不是唯一解,也未必是"最优解",是在"最小依赖 + 能跑通 + 边探索边回答问题"这几个约束下的刻意取舍:

  • PDP 留在 rbac-service 内,不接 OPA / Casbin:先验证"网关只做 enforcement、决策集中一处"这个分层本身是否成立,而不是先背上策略引擎的复杂度。
  • 网关用 Spring Cloud Gateway,而不是 Kong / APISIX:后者是成熟的生产网关,但会引入额外运维实体。目标是"用 Spring 生态内最小代价把 PEP 跑起来",所以留在应用层。
  • JWT 用 JDK 原生实现,不引 jjwt / nimbus:与其说为了省依赖,不如说为了把签名 / 验证的机制摊开来看清–代价是失去成熟库的边缘 case 覆盖和审计保障。生产里还是该用成熟库,demo 阶段借这套实现把 JWT 本身搞懂。
  • 鉴权没上 Spring Security:同样是不过早引入"版本冲突 + 配置纠缠",先把 RBAC 最小闭环跑通。
  • customer / audit / web 是探索出来的,不是为完整而堆:每加一个都回答一个具体问题–业务资源怎么管住?裁决怎么追溯?有没有界面点点看?

往生产走,通常还要补这些:密钥进 KMS / Vault 而不是硬编码;PDP 拆独立服务并接策略引擎;补可观测性(trace / metric / 熔断事件告警);做水平扩展与无状态化;以及更严谨的鉴权强度(token 吊销、权限变更实时生效)。这篇文章验证的是"思路是否站得住",不是"这套能直接上生产"。

实现要点与注意事项

把这套组合真跑起来,有几个点值得提前知道。措辞与 DOCKER.md 对齐。

Eureka 冷启动期网关可能 503

Eureka 服务端响应缓存默认 30000ms、客户端拉取默认 30s,服务刚注册、网关还没拿到实例列表时,lb://service-name 解析会 503。调法:eureka-server 的 response-cache-update-interval-ms 调到 3000(默认 30000),gateway 的 registry-fetch-interval-seconds 调到 3(默认 30);docker-compose 里网关还多 sleep 6 缓冲(注释写"服务端缓存 3s + 网关拉取 3s")。

Map 类型配置在容器里用 -D 注入

eureka.client.serviceUrl 是 Map 类型,spring.config.import 是 bootstrap 期属性。这俩用扁平 env 变量(EUREKA_CLIENT_SERVICEURL_DEFAULTZONE / SPRING_CONFIG_IMPORT)注入不可靠:Map 绑不到 defaultZone 键、config import 的 URI 在 bootstrap 阶段解析时扁平 env 也常读不到,结果 client 回退到 application.yml 里的 localhost 默认值、注册失败。改用 JVM 系统属性(-D,优先级最高,Map 也能绑)经 JAVA_TOOL_OPTIONS 注入。这是 Docker / k3s 里实际生效的做法(详见 DOCKER.md)。

k8s 容器启动即退出(CrashLoopBackOff)

各服务 Dockerfile 只有 COPY ... app.jar + EXPOSE,没有 ENTRYPOINT / CMD;docker-compose 靠 command: 兜底能跑,但 k8s manifest 没设 command,容器无进程可跑、立刻退出(Completed = exit 0)-> CrashLoopBackOff。修:k8s 每个容器补 command: ["java","-Xmx256m","-jar","app.jar"](web 是 npm run start,网关带 sleep 5 缓冲)。

k3s 镜像拉不到(ContainerCreating 卡住)

imagePullPolicy: Never 在 OrbStack 下偶发无法解析本地镜像,kubelet 卡在创建容器。改成 IfNotPresent(本地有镜像就直接用、不拉取)。

initContainer 等的是 config,不是 eureka

manifest 里 initContainer 叫 wait-config(等 config-server:8888),网关那个等 config + auth + rbac + customer + audit 五个端口,web 那个等 gateway(wait-gateway)。不是等 eureka。

三套运行模式:一份源码零改配置

application.yml 从不修改,地址全靠 env / JAVA_TOOL_OPTIONS 注入。

裸 jar 模式

make build        # mvn clean package -DskipTests -> 七个 jar
make start        # 按序启动 7 个服务 + web,等待就绪
make demo         # bash scripts/demo.sh,经网关 :4100 走完整流程

Docker Compose 模式

make docker-up      # build + compose up -d --build
make docker-demo    # 等待就绪 + 运行演示
make docker-logs    # compose logs -f
make docker-stop    # compose down(保留数据卷)

k3s 模式

orb start k8s                 # OrbStack 启用 k3s(或:k3s server)
make k3s-build                # 实为 docker-build 别名
make k3s-deploy               # kubectl apply -f k8s/spring-rbac.yaml
make k3s-demo                 # port-forward 网关 41000 + web 3000,自检 Pod 全 Running,demo

k8s/spring-rbac.yaml:8 个 Deployment + 8 个 Service,命名空间 rbac-demo,initContainer 等依赖就绪,配置通过 env + JAVA_TOOL_OPTIONS 注入。三套模式直接复用同一份源码,不需要任何配置修改。

结果:从鉴权闭环到小型微服务系统

功能清单

  • 用户注册登录 + JWT 签发验证(零依赖,JDK 原生)
  • RBAC:3 角色 / 11 权限 / 3 授权 / BFS 有效权限(支持继承,seed 未启用)
  • 网关边缘鉴权:PEP + 熔断 fail-closed + trace ID + 审计旁路
  • PDP 集中决策:rbac /api/check
  • CRM 业务域:customer-service CRUD + 删除审批流
  • 跨服务审计:audit-service append-only,网关异步发射
  • 前端 BFF:Next.js 权限驱动 dashboard
  • 服务发现(Eureka)+ 配置中心(Config Server);三套运行模式零改配置

它仍然是个学习 / 探索性质的骨架,离生产还差得远–鉴权强度、密钥管理、可观测性、扩缩容、token 吊销等都还要补。但比当初"最小 RBAC"时完整得多,而且每个增量都对应一个具体问题。

源码导航

  • README.md / README.zh.md – 项目说明、架构总览、运行指南
  • pom.xml – 父 POM,声明 Spring Cloud BOM 和 7 个子模块
  • eureka-server/ – 服务注册中心(@EnableEurekaServer,响应缓存调 3000ms)
  • config-server/ – 配置中心(@EnableConfigServer,native 后端,配置在 src/main/resources/config-repo/,5 个业务服务配置)
  • auth-service/src/main/java/com/example/rbac/auth/util/JwtUtil.java – HS256 JWT 签发与验证
  • auth-service/src/main/java/com/example/rbac/auth/util/PasswordUtil.java – PBKDF2WithHmacSHA256 密码哈希
  • auth-service/src/main/java/com/example/rbac/auth/AuthApplication.java – seeded 登录账号
  • gateway-service/src/main/java/com/example/rbac/gateway/filter/AuthGlobalFilter.java – 网关 PEP:JWT 校验 + 权限裁决 + 审计旁路 + trace ID
  • gateway-service/src/main/java/com/example/rbac/gateway/util/JwtUtil.java – 网关侧 JWT 解析(verify-only)
  • gateway-service/src/main/resources/application.yml – 5 条路由(lb://)+ 熔断器配置 + 拉取间隔 3s
  • rbac-service/src/main/java/com/example/rbac/rbac/service/RbacService.java – seed 数据 + BFS 有效权限 + check
  • rbac-service/src/main/java/com/example/rbac/rbac/controller/RbacController.java – PDP 端点 /api/check
  • customer-service/ – CRM 客户域 + 删除审批流,含 RbacClient
  • audit-service/ – append-only 跨服务审计日志
  • web/ – Next.js 14 BFF 前端(next.config.mjs rewrite、lib/permissions.ts 权限驱动)
  • k8s/spring-rbac.yaml – k3s / Kubernetes 部署清单(8 Deployment + 8 Service)
  • docker-compose.yml – Docker Compose 编排(8 服务)
  • Makefile – 所有 make 目标
  • DOCKER.md – Docker Compose 与 k3s 运行手册(含各坑点详解)
  • ARCHITECTURE.md – 架构深度解析与设计决策

完整项目地址:https://github.com/erishen/spring-rbac

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

首页 简历 关于 隐私政策 商店 Web Chat Nsbp

@ 2026 ESN
沪ICP备2024079226号-1   沪公网安备31010502007082号