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

147 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 安全上下文契约 v1.0
## 1. 上下文字段定义
### 必填字段
| 字段 | 类型 | 来源 | 说明 |
|------|------|------|------|
| customerId | String | 网关 Header: X-Customer-Id | 租户标识 |
| orgId | String | 网关 Header: X-Org-Id | 组织标识 |
| userId | String | 网关 Header: X-User-Id | 操作人标识 |
| roles | List<String> | 网关 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<TenantContext> 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<OrderItemRequest> 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);