spring-abac:属性化授权(ABAC)微服务拆解

作者:

🇬🇧 English

这套系统在演示什么

spring-abac 与同目录的 spring-rbac 是一对对照实验spring-rbac 项目文章):两套几乎一样的微服务骨架(Spring Boot 3.2.5 + Spring Cloud 2023.0.3 + Next.js),唯一差异在授权模型。

这篇文章主要回答三个问题:

  1. ABAC 比 RBAC 多解决了什么?
  2. 属性、策略和裁决如何落到微服务?
  3. 这套授权模型如何延伸到 Agent 的工具调用?
RBAC:  user ──belongs to──> role ──has──> permission ──> can access?
ABAC:  (subject, resource, environment) ──policy expression──> PERMIT / DENY

RBAC 问"你是什么角色",ABAC 问"你是(属性)、想碰什么(资源属性)、在什么环境下"。属性化授权能表达 RBAC 表达不了的东西:

  • 同一角色在不同密级、地域、时段下得出不同裁决;
  • 例外可以用一条高优先级 DENY 直接表达,不需要发明新角色;
  • 改属性即改权限,策略与代码解耦。

权限系统的演化,本质上是把授权从静态的关系配置推向动态的属性求值。RBAC 把权限折叠进角色,隐含假设是"人少、角色少、场景稳定";当部门、密级、属地、时段开始叠加,角色矩阵就会笛卡尔积爆炸——每加一个维度,就要手工合成一批新角色。ABAC 并不消灭复杂度,它把复杂度从"组合角色"转移到"组合条件":后者的表达力来自策略语言,可以组合、可以热更新、可以由非工程师维护。换句话说,RBAC 问"你是谁",ABAC 问"此时此刻,你、资源与环境是什么"——把"身份"从授权的主角降级为属性之一,正是这套系统想演示的转变。

四个组件与九个子服务

ABAC 标准组件与项目服务的对应关系:

组件 服务 职责
PAP 策略管理 abac-service /api/policies CRUD,策略四要素:作用域 / SpEL 条件 / 效果 / 优先级
PEP 策略执行点 gateway-service JWT 校验、动作映射、问 PDP、注入属性头、发射审计
PDP 策略决策点 abac-service 策略筛选、SpEL 求值、deny-override 合并、PIP 回源
PIP 属性提供点 auth + document + gateway 主体属性(JWT)、资源属性(回源接口)、环境属性(请求上下文)

其余服务:eureka-server(注册中心)、config-server(配置中心)、audit-service(append-only 审计)、risk-service(交易风控)、agent-service(Agent 前置校验)、web(Next.js BFF,/api/* rewrite 到网关)。

属性从哪来

策略读三类属性,各有可信来源:

主体属性 subject.* —— 登录时由 auth-service 把 department / clearance / region / title 快照进 JWT 的 attrs,网关解析后随裁决请求一起发给 PDP。属性随令牌走,避免 PDP 每次回查身份服务;代价是属性是快照——改属性后旧 token 仍带旧值直到过期(默认 24h),目标用户重新登录才生效。

资源属性 resource.* —— PDP 只有资源 id 时,经 PIP 回源 document-service /internal/attributes/{id} 补齐密级、部门等特征。回源失败按 app.pip-fail-closed = true 处理:"拿不到属性就不知道该不该放行,那就别放行"。

环境属性 env.* —— 网关每次请求实时生成 hour / dayOfWeek / ip,随裁决请求带给 PDP。

信任边界:属性不允许自报

属性是策略的唯一输入,属性源必须可信。项目走过一次弯路:注册接口最初接受客户端提交 clearance / title,等于任何人都能注册成 clearance=5, title=admin——直接击穿整个 ABAC。安全审查后修复并加了回归测试:

  • POST /api/register 只收账号密码,新账号一律落为默认低权限(ENG / clearance 1 / CN / engineer);
  • 属性修改仅 admin:网关策略 USR-60(USER/UPDATE 仅 admin)是第一道,auth-service 业务层再校验调用者 title == 'admin' 是第二道——内网直连绕过网关也改不了;
  • 内部接口的写防伪头 X-Internal-Audit 从硬编码改为环境变量 APP_INTERNAL_SECRET,网关与 audit-service 从配置中心取同一个值。

策略与求值

策略存在 H2,四个要素:作用域(资源类型 + 动作)、SpEL 条件效果优先级

DOC-100  DENY   DOCUMENT / DELETE    env.hour < 9 || env.hour >= 18        priority 100
DOC-95   DENY   DOCUMENT / READ      resource.classification == 'CONFIDENTIAL'
                                      && subject.region != 'CN'            priority 95
DOC-90   DENY   DOCUMENT / READ      subject.clearance < resource.requiredClearance   90
DOC-30   PERMIT DOCUMENT / READ      resource.classification == 'PUBLIC'   30
DOC-20   PERMIT DOCUMENT / READ      resource.department == subject.department        20
DOC-18   PERMIT DOCUMENT / LIST      (无条件;列表放行,行级过滤兜底)     18
DOC-10   PERMIT DOCUMENT / *         resource.owner == subject.username     10
DOC-05   PERMIT DOCUMENT / *         subject.title == 'admin'               5

三条关键语义:

  • deny-override:DENY 优先级最高,一旦命中立即短路。所以"管理员全权"(DOC-05)翻不了"非工作时间禁止删除"(DOC-100)——这正是 RBAC 里 ROLE_ADMIN 一把梭做不到的;
  • 默认拒绝:没有任何策略命中 = DENY,不给隐式放行;
  • 效果三态:PERMIT 放行、DENY 拒绝、REVIEW 转人工复核(如 TRD-85 单笔大额、EML-99 群发)——REVIEW 由业务服务落队列,manager/admin 批准后才生效。

LIST 是集合动作:列表接口拿不到具体资源属性,按 READ 问会全部落空撞默认拒绝,所以网关对 GET /api/documents 问的是"能不能列这个域",真正的行级边界在业务服务。

SpEL 沙箱:四层防护

策略条件用 SpEL 表达式,而 SpEL 原生支持 T(...) 类型调用(语法上 T(java.lang.Runtime).getRuntime().exec(...) 完全合法),所以求值前做了四层纵深防御:

  1. 正则黑名单拦截危险片段;
  2. 类型定位器在解析期抛错,阻止黑名单类实例化;
  3. 自定义只读 MapPropertyAccessor,缺失属性返回 null 而不抛全局异常;
  4. ConcurrentHashMap 缓存已解析表达式,降低热更新时的重复解析开销。

在当前策略表达式约束下,表达式只能读取授权属性并进行比较,无法访问受限类型或执行 JVM 方法调用。

两道闸门:边缘 PEP + 行级过滤

GET /api/documents ──> gateway(PEP):JWT ✓ → 映射 (DOCUMENT, LIST) → 问 PDP
                          │ PERMIT,注入 X-User / X-Attr-*
                          ▼
                    document-service:逐行调 /api/decide/batch(带完整属性)
                          只放行 permitted == true 的行 → 再分页

两道闸门问的是同一个 PDP,不存在策略不一致:

  • 第 1 道(网关) 防"整个接口能不能碰",成本低,一处配置全局生效;
  • 第 2 道(业务服务)行级过滤——网关只认得 URL 里的 id,不知道列表里每一行的密级,只有持有数据的服务能做这个过滤。

PDP 不可用时两道都 fail-closed:网关熔断打开直接 403,业务服务侧调用异常抛 ForbiddenException。宁可拒绝不能放行。

审计:异步、append-only、防伪造

gateway 每次裁决后异步发射审计事件(actor、动作、路径、裁决、命中策略),best-effort、fail-open——审计集群抖动不拖慢主业务。audit-service 只追加不修改,字段最小化(不含请求体与文档内容)。写入防伪靠网关 ↔ audit 共享的 APP_INTERNAL_SECRET

风控与 Agent 域:REVIEW 三态落地

  • risk-service(交易风控):下单预裁,TRD-85 单笔大额转 REVIEW 人工复核、TRD-70 当日累计超限拒绝;
  • agent-service(Agent 前置校验):工具级策略,EML-99 群发复核、COD-75 高危代码、PAY-90 大额转账。

两域与 document 同款模式:网关按 URL 裁第一道,服务内带完整资源属性裁第二道;命中 REVIEW 不是直接放行,而是落成本服务的人工复核队列,批准后才生效。

与 spring-rbac 的对照

维度 spring-rbac spring-abac
授权依据 用户 → 角色 → 权限(静态绑定) 属性 → 策略表达式(动态求值)
判定入口 POST /api/check?user&permission POST /api/decide(完整属性包)
谁能看什么 权限绑定角色(数据播种) 改属性即改权限,不动代码
管理员 admin 角色全权 title == 'admin' 的 PERMIT 仍受 DENY 约束
行级过滤 无(列表全量返回) 逐行问 PDP,只放行读得到的行
冲突处理 权限叠加,无法表达例外 deny-override:高优先级 DENY 天然表达例外
端口块 8761 / 8888 / 41xx / web 3000 8762 / 8889 / 411x / web 3001

关联项目:tsm-hub

这套授权语义不是孤立的演示:我的另一套项目 tsm-hub(LLM 网关与能力池)的 Key 鉴权正是 ABAC 的极简形态——主体 = Key、动作 = 路由或工具调用,只是缺少资源与环境属性、策略引擎与复核态。本文描述的能力调用语义(动作 + 属性包 → 策略裁决、默认拒绝、REVIEW 转人工复核)恰好是能力池下一层需要的授权模型。

设计哲学:四件从演示里带走的东西

拒绝优先,默认拒绝

授权系统的安全不来自"列全所有允许",而来自"没说的都是拒绝"。deny-override 让例外以"补一条高优先级 DENY"的方式表达——这正是防火墙的哲学:白名单加显式例外,而不是黑名单加默认放行。代码里所有策略求值都短路上这一点:匹配到 DENY 立即返回,后面的 PERMIT 读都不读。

授权发生在数据附近

网关只能裁决"这个接口能不能碰",行级过滤只能由持有数据的服务完成。授权不是一次调用,而是一道从边缘到数据的链路——每一层都向同一个 PDP 求证,因此没有策略分歧,也没有"接口能进、数据裸奔"的中间态。

决策与审批分离:REVIEW 三态

授权系统本质是决策系统:机器能判的判掉,判不了的交给人。REVIEW 不是性能问题的妥协,而是决策链路完整性的要求——把人工复核从"业务系统里的状态"提升为"授权协议的一等公民"。交易风控与 Agent 域都复用同一语义:命中 REVIEW 落队列,批准才生效。

信任边界比规则更重要

属性是策略的唯一输入,属性源就必须可信。注册不接受属性自报、属性修改仅 admin 双校验、内部接口头走共享密钥——这三件事不是 ABAC 的附属品,而是 ABAC 成立的前提。规则写得再漂亮,属性可以被伪造,整个系统就是纸糊的。

前瞻:Agent 时代的授权

Agent 的每次工具调用就是一个动作,携带主体(谁在驱动)、资源(工具/文件/账户)与环境(会话上下文)三类属性,策略表达"这个 Agent 在这个场景下能不能调这个工具"。项目里的 agent-service 正是按这套语义实现的:授权问题正从"人访问系统"扩展到"程序访问系统",属性化授权刚好承接。ABAC 演示里沉淀的四条原则,会直接复用到下一波自动化场景。

测试与质量

六模块共 63 个用例(auth 11、abac 15、document 23、gateway 7、risk 3、agent 4),make testmvn test 全量运行。安全链路的回归保护是刻意安排的:PEP 认证边界 → PDP 裁决契约 → 行级过滤 → fail-closed,每一环都有测试——改策略语义或接入新域时不至于悄悄破坏安全边界。

运行方式三选一

make start         # 裸 jar:后台启动九服务 + 前端 :3001
make docker-up     # Docker Compose
make k3s-build && make k3s-deploy   # k3s(OrbStack / 单节点)

浏览器打开 http://localhost:3001。演示账号 admin / carol / alice / bob 属性差异明显,同一条策略给出不同裁决。

隐私与合规口径

演示数据仅收集用户名 + PBKDF2 密码哈希 + 部门/密级/属地/岗位属性,用于演示授权语义。生产部署需自行补齐隐私政策、告知同意、账号删除等个保法(PIPL)义务;JWT 密钥默认 dev-only-secret-change-me-please,生产设 APP_JWT_SECRET 覆盖即可,无需改代码。

源码导航

项目仓库

文档

  • README.md — 英文快速开始
  • README.zh.md — 中文版
  • ARCHITECTURE.md — 架构说明:属性来源、求值算法、沙箱、两道闸门、审计
  • docs/adr/ — 架构决策记录:为什么选 ABAC(0001)、SpEL 作策略语言(0002)、属性放 JWT(0003)、deny-override(0004)、两道闸门(0005)、LIST 集合动作(0006)

关键代码入口

  • PolicyEngine(abac-service)— PDP 求值:策略筛选、SpEL 求值、deny-override 短路
  • PipClient(abac-service)— PIP 回源:补齐资源属性,失败按 fail-closed 拒绝
  • AuthGlobalFilter(gateway-service)— PEP:JWT 校验、动作映射、注入属性头、发射审计
  • AttributesController(document-service)— 资源属性接口 /internal/attributes/{id}
  • DocumentService(document-service)— 行级过滤:逐行问 PDP,只放行读得到的行
  • JwtUtil(common)— JWT 属性签发与解析
  • AuditService(audit-service)— append-only 审计
  • RiskService / AgentService(risk / agent)— REVIEW 人工复核队列

运行与部署

  • make start / make stop — 裸 jar 一键起停
  • docker-compose.yml — Docker Compose
  • k8s/spring-abac.yaml — k3s 部署清单
  • scripts/demo.sh — 演示脚本

从这套演示里能带走的,不是九个服务的骨架,而是四条可迁移的原则:显式优于隐式(默认拒绝)、例外用高优先级表达(deny-override)、决策发生在数据附近(行级过滤)、信任边界先于规则(属性可信)。把它们落到任何鉴权设计里,RBAC 时代的"角色爆炸"问题就有了退路——属性化授权的真正价值不是更复杂,而是把复杂性放在它该在的地方。

主体属性(如职级、密级)变更后为何不能立即生效?

属性被快照进 JWT Claims,换取 PDP 无需跨网络回查身份服务;旧 token 带旧属性直到过期(默认 24h),目标用户重新登录才生效。演示口径接受此延迟,生产可引入属性版本号或 token 黑名单做热失效。

注册接口为什么不接收 clearance / title 等属性?

属性是策略的唯一输入,允许自报等于任何人都能注册成管理员,击穿整个 ABAC。注册只收账号密码,新账号统一为默认低权限,提权由管理员在用户属性面板操作(网关 USR-60 + auth-service 业务层双校验)。

策略条件用 SpEL 求值,如何防止恶意代码执行?

四层纵深防御:正则黑名单拦危险片段、类型定位器在解析期抛错阻止黑类实例化、只读 MapPropertyAccessor 防缺失属性引发全局报错、表达式缓存降低重复解析开销。在当前表达式约束下,表达式只能读取授权属性并进行比较,无法访问受限类型或执行 JVM 方法调用。

PIP 回源拿不到资源属性时会怎样?

app.pip-fail-closed = true 处理:”拿不到属性就不知道该不该放行,那就别放行”,PDP 直接拒绝,不做降级代理。这保障最小特权,代价是内部属性接口抖动会切断上游业务。

多条策略冲突时如何裁决?

deny-override 合并语义:粗筛作用域后按优先级降序求值,一旦命中 DENY 立即短路返回,后续 PERMIT 不会被读取;没有任何策略命中时默认拒绝,不给隐式放行。

评论

发表回复

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

商店 Web Chat Nsbp 关于 隐私政策

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