技术 JV 的数据主权:接口契约与多租户隔离实践
一、比代码更难对齐的,是数据归属
技术 JV 的架构师常陷入一个幻觉:只要 REST 接口调通了,合作就跑起来了。真正导致项目翻车的,往往是数据归谁、谁能看、谁能改的灰色地带。
2024 年某车企与智驾公司的 JV 项目曾因「训练数据归属」对簿公堂——模型归双方共有,但原始路测数据只归一方,导致另一方无法独立复现模型效果。技术 JV 必须在架构层把「数据主权」写死,而不是写在合同附件里。
二、三层隔离模型
表格
层级 甲方 乙方 联合资产
原始数据 用户行为日志 传感器原始信号 —
特征层 用户画像标签 点云特征向量 联合标注数据集
模型层 — — 共同训练的预测模型
架构上,原始数据物理隔离,特征层通过联邦学习或安全多方计算交换,模型层存入双方共管的加密仓库。任何一方无法单方面导出完整模型。
三、API 契约:用代码代替口头约定
技术 JV 的接口不能「先写代码后补文档」。推荐 API-First 工作流:
双方架构师在 Swagger Editor 里对齐 OpenAPI 3.1 契约
契约入库,Git 分支保护,变更需 PR 审批
代码由契约自动生成(openapi-generator),杜绝「文档与代码两张皮」
代码实战:多租户 API 网关 + 字段级脱敏
以下是一个 Spring Cloud Gateway + 自定义过滤器 的实现,确保 JV 伙伴只能访问授权字段,且请求自动打上租户标签。
- OpenAPI 契约片段(双方签字版)
yaml
复制
jv-contract-v1.0.yaml
openapi: 3.1.0
info:
title: JV-UserProfile-API
version: "1.0.0"
x-jv-parties: [甲方, 乙方]
x-data-classification: PII-RESTRICTED
paths:
/users/{userId}/profile:
get:
x-jv-permission: [甲方:FULL, 乙方:MASKED] # 关键:乙方只能看脱敏字段
parameters:
- name: X-JV-Tenant
in: header
required: true
schema:
enum: [party-a, party-b]
responses:
200:
content:
application/json:
schema:
$ref: '#/components/schemas/UserProfile'
components:
schemas:
UserProfile:
type: object
properties:
userId: { type: string }
phone:
type: string
x-jv-masked-for: party-b # 乙方看到 138****8888
purchaseHistory:
type: array
x-jv-denied-for: party-b # 乙方完全不可见
jointCreditScore:
type: number
x-jv-shared: true # 联合计算,双方可见
2. 网关过滤器:租户校验 + 字段脱敏
java
复制
@Component
public class JvTenantFilter extends AbstractGatewayFilterFactory<JvTenantFilter.Config> {
private final ObjectMapper mapper;
private final Map<String, Set<String>> fieldPermissions; // 加载自契约 YAML
public JvTenantFilter() {
super(Config.class);
this.mapper = new ObjectMapper();
// 实际应从契约解析,这里硬编码示意
this.fieldPermissions = Map.of(
"party-b", Set.of("userId", "jointCreditScore") // 乙方白名单
);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String tenant = exchange.getRequest().getHeaders().getFirst("X-JV-Tenant");
if (!Set.of("party-a", "party-b").contains(tenant)) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
// 打上租户标签,后续日志、审计全链路透传
exchange.getAttributes().put("jv.tenant", tenant);
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 响应阶段:对乙方脱敏
if ("party-b".equals(tenant)) {
maskResponseForPartyB(exchange);
}
}));
};
}
private void maskResponseForPartyB(ServerWebExchange exchange) {
// 简化示意:实际用 JsonPath 或 Jackson 树模型遍历
// phone -> 138****8888
// purchaseHistory -> 直接删除节点
// jointCreditScore -> 保留原始值
}
public static class Config {}
}
3. 审计日志:每次 JV 数据访问留痕
java
复制
@Component
public class JvAuditLogger {
public void logAccess(String tenant, String api, Set<String> fields, String traceId) {
// 写入双方共管的审计库,任何一方不可单方面删除
AuditRecord record = AuditRecord.builder()
.timestamp(Instant.now())
.tenant(tenant)
.apiEndpoint(api)
.accessedFields(fields)
.traceId(traceId)
.integrityHash(hash(record)) // 防篡改
.build();
// 同步写入甲方 + 乙方 两个独立存储,形成「分布式证据」
primaryRepo.save(record);
partnerWebhook.notify(record); // 乙方实时收到审计副本
}
}

浙公网安备 33010602011771号