- 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]>
147 lines
5.8 KiB
Markdown
147 lines
5.8 KiB
Markdown
# 安全上下文契约 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); |