Files
Bruno GaoandQwen-Coder 43d1229bd7 M1: Add complete documentation structure and contract specifications
- Add comprehensive docs/ directory with standardized structure
- Include architecture specification and contract documents
- Add prompt templates for development workflows
- Establish M1 deliverables as frozen specifications

Co-authored-by: Qwen-Coder <[email protected]>
2026-02-11 23:41:04 +08:00

5.8 KiB
Raw Permalink Blame History

安全上下文契约 v1.0

1. 上下文字段定义

必填字段

字段 类型 来源 说明
customerId String 网关 Header: X-Customer-Id 租户标识
orgId String 网关 Header: X-Org-Id 组织标识
userId String 网关 Header: X-User-Id 操作人标识
roles List 网关 Header: X-User-Roles 角色列表(逗号分隔)

可选字段

字段 类型 来源 说明
traceId String Header: X-Trace-Id 链路 ID(无则服务自动生成 UUID)
requestId String Header: X-Request-Id 网关生成的请求唯一 ID(排障用)
clientId String Header: X-Client-Id 来源系统标识(管理端/大屏/移动端)

2. 上下文注入流程

网关(JWT 验证 → 解析 claims → 注入 Header) → TraceIdFilter(生成或接收 traceId/requestId,写入 MDC) → SecurityContextFilter(解析 Header → 构造 TenantContext 对象 → 写入 ThreadLocal) → Controller → UseCase → Port → Adapter

3. 宪法级规则

规则 1:可信来源——禁"来源",不禁"显式参数"

核心原则:customerId/orgId/userId 的【值】只能来源于网关注入的 Header, 不能来源于请求 DTO 中客户端传入的字段。

具体条款: (a) Controller/DTO 层: - 请求 DTO 中【禁止】定义 customerId/orgId/userId 字段用于业务查询 - 如果 DTO 中存在这些字段(如管理后台的"代操作"场景), 【必须】在 Controller 中校验"DTO 传入值 == TenantContext 中的值",否则拒绝

(b) Application 层(UseCase): - 【默认】从 TenantContext.current() 获取 customerId - 【允许】将 TenantScope(只读对象,包含 customerId/orgId)作为 显式参数传入 UseCase.execute(command, tenantScope),便于单元测试 - 【禁止】从 Command/Query 对象中读取 customerId 作为查询条件

(c) Port / Adapter 层: - Out Port 接口【允许】将 customerId 作为显式方法参数 例如:OrderPersistencePort.findByIdAndCustomerId(OrderId id, String customerId) - 这样做的目的是让 Port 接口具有明确的契约,提高可测试性

规则 2:多租户数据隔离

  • 所有数据查询【必须】携带 customerId 作为过滤条件
  • 实现方式(二选一,在项目初始化时决定): (a) 显式 WHERE:每个查询方法都带 customerId 参数 (b) Hibernate @Filter:全局自动注入(需要在配置中启用)
  • 写入操作:create_user_id/update_user_id 从安全上下文自动填充

规则 3:上下文在线程间的传播

(a) 同步请求链(Controller → UseCase → Repo): ThreadLocal + MDC 正常工作,无需额外处理

(b) 异步场景(@Async / CompletableFuture / 线程池 / 调度任务): 【必须】使用 TaskDecorator 或 ContextWrapper 显式传播 TenantContext 【禁止】在异步代码中直接调用 TenantContext.current()(除非已确认上下文已传播)

(c) Virtual Threads 注意事项: Java 21 Virtual Threads 默认继承 ThreadLocal,但在大量并发下应 评估是否迁移到 ScopedValue(JEP 481,Preview)。 模板默认使用 ThreadLocal,预留迁移接口。

规则 4:SecurityContext 类定义

位置:${name}-application/src/.../port/in/TenantContext.java

public final class TenantContext { private static final ThreadLocal HOLDER = new ThreadLocal<>();

private final String customerId;
private final String orgId;
private final String userId;
private final List<String> roles;

// static current(), set(), clear()
// 构造器、getter(不可变)

}

位置:${name}-application/src/.../port/in/TenantScope.java(只读快照,用于传参)

public record TenantScope(String customerId, String orgId) { public static TenantScope fromContext() { var ctx = TenantContext.current(); return new TenantScope(ctx.getCustomerId(), ctx.getOrgId()); } }


✅ 可自动检查项

规则 检查方式
Request DTO 中禁止出现 customerId 字段 ArchUnit: classes in ..api.dto.. 的 fields 不含名为 customerId/orgId/userId 的字段(管理后台例外需 @SuppressWarning 注解标注)
Command/Query 中禁止出现 customerId 字段 ArchUnit: classes in ..port.in.. 的 fields 不含名为 customerId/orgId 的字段
Out Port 接口允许 customerId 参数 不做限制(这是允许的)
异步代码不直接调用 TenantContext.current() 人工评审 + 编码规范约束(ArchUnit 难以静态检测)

❌ 典型违规示例

违规 1:从 DTO 透传 customerId

// 错误!DTO 的 customerId 不可信 public record CreateOrderRequest( String customerId, // 客户端传入 → 不可信 List items ) {}

@PostMapping("/orders") public ApiResponse create(@RequestBody CreateOrderRequest req) { createOrderUseCase.execute(new CreateOrderCommand( req.customerId(), // 直接把 DTO 的值当查询条件 → 违规! req.items() )); }

// 正确:从安全上下文获取 @PostMapping("/orders") public ApiResponse create(@RequestBody CreateOrderRequest req) { var scope = TenantScope.fromContext(); createOrderUseCase.execute(new CreateOrderCommand(req.items()), scope); }

违规 2:异步任务中直接使用 TenantContext

// 错误!异步线程中 ThreadLocal 可能为空 @Async public void sendNotification(OrderId orderId) { var customerId = TenantContext.current().getCustomerId(); // 可能 NPE! // ... }

// 正确:在发起方捕获上下文,显式传入 var scope = TenantScope.fromContext(); asyncNotificationService.sendNotification(orderId, scope);