902 lines
25 KiB
Markdown
902 lines
25 KiB
Markdown
# Backup Service 实施指导
|
||
|
||
## 1. 概述
|
||
|
||
### 1.1 服务定位
|
||
|
||
`backup-service` 是 RWA Durian 平台的 MPC 备份分片存储服务,负责安全存储用户的 Backup Share (Party 2)。
|
||
|
||
**核心职责:**
|
||
- 安全存储 MPC 2-of-3 的第三个分片 (Backup Share)
|
||
- 提供分片的存取 API
|
||
- 支持用户账户恢复时的分片验证和获取
|
||
- 审计日志记录
|
||
|
||
### 1.2 安全要求
|
||
|
||
**关键安全原则:必须部署在与 identity-service 不同的物理服务器上!**
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ MPC 分片物理隔离架构 │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 物理服务器 A 物理服务器 B 用户设备 │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────┐ │
|
||
│ │identity-service │ │ backup-service │ │Flutter App │ │
|
||
│ │Server Share (0) │ │Backup Share (2) │ │Client (1) │ │
|
||
│ └─────────────────┘ └─────────────────┘ └────────────┘ │
|
||
│ │
|
||
│ 任意单点被攻破,攻击者只能获得 1 个分片,无法重建私钥 │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
如果 Server Share 和 Backup Share 在同一台物理服务器,攻击者攻破该服务器后可获得 2 个分片,直接重建私钥,**MPC 安全性完全失效**!
|
||
|
||
### 1.3 技术栈
|
||
|
||
与 identity-service 保持一致:
|
||
- **框架**: NestJS 10.x
|
||
- **语言**: TypeScript 5.x
|
||
- **数据库**: PostgreSQL 15+ (独立数据库实例)
|
||
- **ORM**: Prisma 5.x
|
||
- **缓存**: Redis (可选)
|
||
- **消息队列**: Kafka (可选,用于接收 identity-service 的事件)
|
||
|
||
---
|
||
|
||
## 2. 目录结构
|
||
|
||
参考 identity-service 的 DDD + Hexagonal 架构:
|
||
|
||
```
|
||
backup-service/
|
||
├── prisma/
|
||
│ ├── schema.prisma # 数据库模型
|
||
│ └── migrations/ # 数据库迁移
|
||
├── src/
|
||
│ ├── api/ # 接口层 (Adapters - Driving)
|
||
│ │ ├── controllers/
|
||
│ │ │ └── backup-share.controller.ts
|
||
│ │ ├── dto/
|
||
│ │ │ ├── request/
|
||
│ │ │ │ ├── store-share.dto.ts
|
||
│ │ │ │ └── retrieve-share.dto.ts
|
||
│ │ │ └── response/
|
||
│ │ │ └── share-info.dto.ts
|
||
│ │ └── api.module.ts
|
||
│ │
|
||
│ ├── application/ # 应用层 (Use Cases)
|
||
│ │ ├── commands/
|
||
│ │ │ ├── store-backup-share/
|
||
│ │ │ │ ├── store-backup-share.command.ts
|
||
│ │ │ │ └── store-backup-share.handler.ts
|
||
│ │ │ └── revoke-share/
|
||
│ │ │ ├── revoke-share.command.ts
|
||
│ │ │ └── revoke-share.handler.ts
|
||
│ │ ├── queries/
|
||
│ │ │ └── get-backup-share/
|
||
│ │ │ ├── get-backup-share.query.ts
|
||
│ │ │ └── get-backup-share.handler.ts
|
||
│ │ ├── services/
|
||
│ │ │ └── backup-share-application.service.ts
|
||
│ │ └── application.module.ts
|
||
│ │
|
||
│ ├── domain/ # 领域层 (Core Business Logic)
|
||
│ │ ├── entities/
|
||
│ │ │ └── backup-share.entity.ts
|
||
│ │ ├── repositories/
|
||
│ │ │ └── backup-share.repository.interface.ts
|
||
│ │ ├── services/
|
||
│ │ │ └── share-encryption.domain-service.ts
|
||
│ │ ├── value-objects/
|
||
│ │ │ ├── share-id.vo.ts
|
||
│ │ │ └── encrypted-data.vo.ts
|
||
│ │ └── domain.module.ts
|
||
│ │
|
||
│ ├── infrastructure/ # 基础设施层 (Adapters - Driven)
|
||
│ │ ├── persistence/
|
||
│ │ │ ├── prisma/
|
||
│ │ │ │ └── prisma.service.ts
|
||
│ │ │ └── repositories/
|
||
│ │ │ └── backup-share.repository.impl.ts
|
||
│ │ ├── crypto/
|
||
│ │ │ └── aes-encryption.service.ts
|
||
│ │ └── infrastructure.module.ts
|
||
│ │
|
||
│ ├── shared/ # 共享模块
|
||
│ │ ├── guards/
|
||
│ │ │ └── service-auth.guard.ts # 服务间认证
|
||
│ │ ├── filters/
|
||
│ │ │ └── global-exception.filter.ts
|
||
│ │ └── interceptors/
|
||
│ │ └── audit-log.interceptor.ts
|
||
│ │
|
||
│ ├── config/
|
||
│ │ └── index.ts
|
||
│ ├── app.module.ts
|
||
│ └── main.ts
|
||
│
|
||
├── test/
|
||
│ ├── unit/
|
||
│ └── e2e/
|
||
│ └── backup-share.e2e-spec.ts
|
||
│
|
||
├── .env.example
|
||
├── .env.development
|
||
├── package.json
|
||
├── tsconfig.json
|
||
├── nest-cli.json
|
||
└── Dockerfile
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 数据库设计
|
||
|
||
### 3.1 Prisma Schema
|
||
|
||
```prisma
|
||
// prisma/schema.prisma
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
// 备份分片存储
|
||
model BackupShare {
|
||
shareId BigInt @id @default(autoincrement()) @map("share_id")
|
||
|
||
// 用户标识 (来自 identity-service)
|
||
userId BigInt @unique @map("user_id")
|
||
accountSequence BigInt @unique @map("account_sequence")
|
||
|
||
// MPC 密钥信息
|
||
publicKey String @unique @map("public_key") @db.VarChar(130)
|
||
partyIndex Int @default(2) @map("party_index") // Backup = Party 2
|
||
threshold Int @default(2)
|
||
totalParties Int @default(3) @map("total_parties")
|
||
|
||
// 加密的分片数据 (AES-256-GCM 加密)
|
||
encryptedShareData String @map("encrypted_share_data") @db.Text
|
||
encryptionKeyId String @map("encryption_key_id") @db.VarChar(64) // 密钥轮换支持
|
||
|
||
// 状态管理
|
||
status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, ROTATED
|
||
|
||
// 访问控制
|
||
accessCount Int @default(0) @map("access_count") // 访问次数限制
|
||
lastAccessedAt DateTime? @map("last_accessed_at")
|
||
|
||
// 时间戳
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
revokedAt DateTime? @map("revoked_at")
|
||
|
||
// 索引
|
||
@@index([publicKey], name: "idx_backup_public_key")
|
||
@@index([status], name: "idx_backup_status")
|
||
@@index([createdAt], name: "idx_backup_created")
|
||
@@map("backup_shares")
|
||
}
|
||
|
||
// 访问审计日志
|
||
model ShareAccessLog {
|
||
logId BigInt @id @default(autoincrement()) @map("log_id")
|
||
|
||
shareId BigInt @map("share_id")
|
||
userId BigInt @map("user_id")
|
||
|
||
action String @db.VarChar(20) // STORE, RETRIEVE, REVOKE, ROTATE
|
||
sourceService String @map("source_service") @db.VarChar(50) // identity-service, recovery-service
|
||
sourceIp String @map("source_ip") @db.VarChar(45)
|
||
|
||
success Boolean @default(true)
|
||
errorMessage String? @map("error_message") @db.Text
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
@@index([shareId], name: "idx_log_share")
|
||
@@index([userId], name: "idx_log_user")
|
||
@@index([action], name: "idx_log_action")
|
||
@@index([createdAt], name: "idx_log_created")
|
||
@@map("share_access_logs")
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. API 设计
|
||
|
||
### 4.1 存储备份分片
|
||
|
||
**POST /backup-share/store**
|
||
|
||
由 identity-service 在创建账户时调用。
|
||
|
||
```typescript
|
||
// Request DTO
|
||
class StoreBackupShareDto {
|
||
@IsNotEmpty()
|
||
userId: string; // identity-service 的用户ID
|
||
|
||
@IsNotEmpty()
|
||
accountSequence: number; // 账户序列号
|
||
|
||
@IsNotEmpty()
|
||
publicKey: string; // MPC 公钥
|
||
|
||
@IsNotEmpty()
|
||
encryptedShareData: string; // 已加密的分片数据
|
||
|
||
@IsOptional()
|
||
threshold?: number; // 默认 2
|
||
|
||
@IsOptional()
|
||
totalParties?: number; // 默认 3
|
||
}
|
||
|
||
// Response
|
||
{
|
||
"success": true,
|
||
"shareId": "123",
|
||
"message": "Backup share stored successfully"
|
||
}
|
||
```
|
||
|
||
### 4.2 获取备份分片
|
||
|
||
**POST /backup-share/retrieve**
|
||
|
||
用于账户恢复场景,需要额外的身份验证。
|
||
|
||
```typescript
|
||
// Request DTO
|
||
class RetrieveBackupShareDto {
|
||
@IsNotEmpty()
|
||
userId: string;
|
||
|
||
@IsNotEmpty()
|
||
publicKey: string;
|
||
|
||
@IsNotEmpty()
|
||
recoveryToken: string; // 恢复令牌 (由 identity-service 签发)
|
||
|
||
@IsOptional()
|
||
deviceId?: string; // 新设备ID
|
||
}
|
||
|
||
// Response
|
||
{
|
||
"success": true,
|
||
"encryptedShareData": "...",
|
||
"partyIndex": 2,
|
||
"publicKey": "..."
|
||
}
|
||
```
|
||
|
||
### 4.3 撤销分片
|
||
|
||
**POST /backup-share/revoke**
|
||
|
||
用于密钥轮换或账户注销。
|
||
|
||
```typescript
|
||
// Request DTO
|
||
class RevokeShareDto {
|
||
@IsNotEmpty()
|
||
userId: string;
|
||
|
||
@IsNotEmpty()
|
||
publicKey: string;
|
||
|
||
@IsNotEmpty()
|
||
reason: string; // ROTATION, ACCOUNT_CLOSED, SECURITY_BREACH
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 服务间认证
|
||
|
||
### 5.1 认证机制
|
||
|
||
backup-service 不对外公开,只接受来自内部服务的请求。使用 **服务间 JWT** 或 **mTLS** 进行认证。
|
||
|
||
```typescript
|
||
// src/shared/guards/service-auth.guard.ts
|
||
|
||
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||
import { ConfigService } from '@nestjs/config';
|
||
import * as jwt from 'jsonwebtoken';
|
||
|
||
@Injectable()
|
||
export class ServiceAuthGuard implements CanActivate {
|
||
constructor(private configService: ConfigService) {}
|
||
|
||
canActivate(context: ExecutionContext): boolean {
|
||
const request = context.switchToHttp().getRequest();
|
||
const serviceToken = request.headers['x-service-token'];
|
||
|
||
if (!serviceToken) {
|
||
throw new UnauthorizedException('Missing service token');
|
||
}
|
||
|
||
try {
|
||
const secret = this.configService.get<string>('SERVICE_JWT_SECRET');
|
||
const payload = jwt.verify(serviceToken, secret) as { service: string };
|
||
|
||
// 只允许特定服务访问
|
||
const allowedServices = ['identity-service', 'recovery-service'];
|
||
if (!allowedServices.includes(payload.service)) {
|
||
throw new UnauthorizedException('Service not authorized');
|
||
}
|
||
|
||
request.sourceService = payload.service;
|
||
return true;
|
||
} catch (error) {
|
||
throw new UnauthorizedException('Invalid service token');
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.2 identity-service 调用示例
|
||
|
||
在 identity-service 中添加调用 backup-service 的客户端:
|
||
|
||
```typescript
|
||
// identity-service/src/infrastructure/external/backup/backup-client.service.ts
|
||
|
||
@Injectable()
|
||
export class BackupClientService {
|
||
private readonly backupServiceUrl: string;
|
||
private readonly serviceToken: string;
|
||
|
||
constructor(
|
||
private readonly httpService: HttpService,
|
||
private readonly configService: ConfigService,
|
||
) {
|
||
this.backupServiceUrl = this.configService.get('BACKUP_SERVICE_URL');
|
||
this.serviceToken = this.generateServiceToken();
|
||
}
|
||
|
||
async storeBackupShare(params: {
|
||
userId: string;
|
||
accountSequence: number;
|
||
publicKey: string;
|
||
encryptedShareData: string;
|
||
}): Promise<void> {
|
||
await firstValueFrom(
|
||
this.httpService.post(
|
||
`${this.backupServiceUrl}/backup-share/store`,
|
||
params,
|
||
{
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Service-Token': this.serviceToken,
|
||
},
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
private generateServiceToken(): string {
|
||
const secret = this.configService.get<string>('SERVICE_JWT_SECRET');
|
||
return jwt.sign(
|
||
{ service: 'identity-service', iat: Math.floor(Date.now() / 1000) },
|
||
secret,
|
||
{ expiresIn: '1h' },
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 核心实现
|
||
|
||
### 6.1 领域实体
|
||
|
||
```typescript
|
||
// src/domain/entities/backup-share.entity.ts
|
||
|
||
export class BackupShare {
|
||
private constructor(
|
||
private readonly _shareId: bigint | null,
|
||
private readonly _userId: bigint,
|
||
private readonly _accountSequence: bigint,
|
||
private readonly _publicKey: string,
|
||
private readonly _partyIndex: number,
|
||
private readonly _threshold: number,
|
||
private readonly _totalParties: number,
|
||
private _encryptedShareData: string,
|
||
private _encryptionKeyId: string,
|
||
private _status: BackupShareStatus,
|
||
private _accessCount: number,
|
||
private readonly _createdAt: Date,
|
||
) {}
|
||
|
||
static create(params: {
|
||
userId: bigint;
|
||
accountSequence: bigint;
|
||
publicKey: string;
|
||
encryptedShareData: string;
|
||
encryptionKeyId: string;
|
||
}): BackupShare {
|
||
return new BackupShare(
|
||
null,
|
||
params.userId,
|
||
params.accountSequence,
|
||
params.publicKey,
|
||
2, // Backup = Party 2
|
||
2, // threshold
|
||
3, // totalParties
|
||
params.encryptedShareData,
|
||
params.encryptionKeyId,
|
||
BackupShareStatus.ACTIVE,
|
||
0,
|
||
new Date(),
|
||
);
|
||
}
|
||
|
||
recordAccess(): void {
|
||
if (this._status !== BackupShareStatus.ACTIVE) {
|
||
throw new DomainError('Cannot access revoked share');
|
||
}
|
||
this._accessCount++;
|
||
}
|
||
|
||
revoke(reason: string): void {
|
||
if (this._status === BackupShareStatus.REVOKED) {
|
||
throw new DomainError('Share already revoked');
|
||
}
|
||
this._status = BackupShareStatus.REVOKED;
|
||
}
|
||
|
||
rotate(newEncryptedData: string, newKeyId: string): void {
|
||
this._encryptedShareData = newEncryptedData;
|
||
this._encryptionKeyId = newKeyId;
|
||
this._status = BackupShareStatus.ACTIVE;
|
||
}
|
||
|
||
// Getters...
|
||
get shareId(): bigint | null { return this._shareId; }
|
||
get userId(): bigint { return this._userId; }
|
||
get publicKey(): string { return this._publicKey; }
|
||
get encryptedShareData(): string { return this._encryptedShareData; }
|
||
get status(): BackupShareStatus { return this._status; }
|
||
get accessCount(): number { return this._accessCount; }
|
||
}
|
||
|
||
export enum BackupShareStatus {
|
||
ACTIVE = 'ACTIVE',
|
||
REVOKED = 'REVOKED',
|
||
ROTATED = 'ROTATED',
|
||
}
|
||
```
|
||
|
||
### 6.2 应用服务
|
||
|
||
```typescript
|
||
// src/application/services/backup-share-application.service.ts
|
||
|
||
@Injectable()
|
||
export class BackupShareApplicationService {
|
||
private readonly logger = new Logger(BackupShareApplicationService.name);
|
||
|
||
constructor(
|
||
@Inject(BACKUP_SHARE_REPOSITORY)
|
||
private readonly repository: BackupShareRepository,
|
||
private readonly encryptionService: AesEncryptionService,
|
||
private readonly auditService: AuditLogService,
|
||
) {}
|
||
|
||
async storeBackupShare(command: StoreBackupShareCommand): Promise<string> {
|
||
this.logger.log(`Storing backup share for user: ${command.userId}`);
|
||
|
||
// 检查是否已存在
|
||
const existing = await this.repository.findByUserId(BigInt(command.userId));
|
||
if (existing) {
|
||
throw new ApplicationError('Backup share already exists for this user');
|
||
}
|
||
|
||
// 二次加密 (identity-service 已加密一次,这里再加密一次)
|
||
const { encrypted, keyId } = await this.encryptionService.encrypt(
|
||
command.encryptedShareData,
|
||
);
|
||
|
||
// 创建实体
|
||
const share = BackupShare.create({
|
||
userId: BigInt(command.userId),
|
||
accountSequence: BigInt(command.accountSequence),
|
||
publicKey: command.publicKey,
|
||
encryptedShareData: encrypted,
|
||
encryptionKeyId: keyId,
|
||
});
|
||
|
||
// 保存
|
||
const saved = await this.repository.save(share);
|
||
|
||
// 记录审计日志
|
||
await this.auditService.log({
|
||
shareId: saved.shareId!,
|
||
userId: BigInt(command.userId),
|
||
action: 'STORE',
|
||
sourceService: command.sourceService,
|
||
sourceIp: command.sourceIp,
|
||
success: true,
|
||
});
|
||
|
||
this.logger.log(`Backup share stored: shareId=${saved.shareId}`);
|
||
return saved.shareId!.toString();
|
||
}
|
||
|
||
async retrieveBackupShare(query: GetBackupShareQuery): Promise<BackupShareDto> {
|
||
this.logger.log(`Retrieving backup share for user: ${query.userId}`);
|
||
|
||
const share = await this.repository.findByUserIdAndPublicKey(
|
||
BigInt(query.userId),
|
||
query.publicKey,
|
||
);
|
||
|
||
if (!share) {
|
||
throw new ApplicationError('Backup share not found');
|
||
}
|
||
|
||
if (share.status !== BackupShareStatus.ACTIVE) {
|
||
throw new ApplicationError('Backup share is not active');
|
||
}
|
||
|
||
// 记录访问
|
||
share.recordAccess();
|
||
await this.repository.save(share);
|
||
|
||
// 解密
|
||
const decrypted = await this.encryptionService.decrypt(
|
||
share.encryptedShareData,
|
||
share.encryptionKeyId,
|
||
);
|
||
|
||
// 记录审计日志
|
||
await this.auditService.log({
|
||
shareId: share.shareId!,
|
||
userId: BigInt(query.userId),
|
||
action: 'RETRIEVE',
|
||
sourceService: query.sourceService,
|
||
sourceIp: query.sourceIp,
|
||
success: true,
|
||
});
|
||
|
||
return {
|
||
encryptedShareData: decrypted,
|
||
partyIndex: share.partyIndex,
|
||
publicKey: share.publicKey,
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 环境配置
|
||
|
||
### 7.1 .env.example
|
||
|
||
```bash
|
||
# Database (必须是独立的数据库实例,不能与 identity-service 共享!)
|
||
DATABASE_URL="postgresql://postgres:password@backup-db-server:5432/rwa_backup?schema=public"
|
||
|
||
# Server
|
||
APP_PORT=3002
|
||
APP_ENV="development"
|
||
|
||
# Service Authentication
|
||
SERVICE_JWT_SECRET="your-super-secret-service-jwt-key"
|
||
ALLOWED_SERVICES="identity-service,recovery-service"
|
||
|
||
# Encryption (用于二次加密备份分片)
|
||
BACKUP_ENCRYPTION_KEY="your-256-bit-encryption-key-in-hex"
|
||
BACKUP_ENCRYPTION_KEY_ID="key-v1"
|
||
|
||
# Rate Limiting
|
||
MAX_RETRIEVE_PER_DAY=3 # 每用户每天最多获取 3 次
|
||
MAX_STORE_PER_MINUTE=10 # 每分钟最多存储 10 个
|
||
|
||
# Audit
|
||
AUDIT_LOG_RETENTION_DAYS=365 # 审计日志保留 365 天
|
||
|
||
# Monitoring
|
||
PROMETHEUS_ENABLED=true
|
||
PROMETHEUS_PORT=9102
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 部署要求
|
||
|
||
### 8.1 物理隔离
|
||
|
||
**强制要求:**
|
||
|
||
```yaml
|
||
# docker-compose.yml (仅供参考架构,生产环境必须分开部署)
|
||
|
||
# ❌ 错误示例 - 同一台机器
|
||
services:
|
||
identity-service:
|
||
...
|
||
backup-service: # 不能在这里!
|
||
...
|
||
|
||
# ✅ 正确示例 - 分开部署
|
||
# 机器 A: docker-compose-main.yml
|
||
services:
|
||
identity-service:
|
||
...
|
||
|
||
# 机器 B: docker-compose-backup.yml (异地机房)
|
||
services:
|
||
backup-service:
|
||
...
|
||
```
|
||
|
||
### 8.2 网络安全
|
||
|
||
```yaml
|
||
# backup-service 的网络策略
|
||
- 只允许来自 identity-service 的入站连接
|
||
- 禁止任何公网访问
|
||
- 使用 VPN 或专线连接主机房和备份机房
|
||
```
|
||
|
||
### 8.3 数据库安全
|
||
|
||
```
|
||
- 使用独立的 PostgreSQL 实例
|
||
- 启用 TLS 加密连接
|
||
- 定期备份到第三方存储
|
||
- 启用审计日志
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 测试
|
||
|
||
### 9.1 E2E 测试
|
||
|
||
```typescript
|
||
// test/e2e/backup-share.e2e-spec.ts
|
||
|
||
describe('BackupShare (e2e)', () => {
|
||
let app: INestApplication;
|
||
let serviceToken: string;
|
||
|
||
beforeAll(async () => {
|
||
const moduleFixture = await Test.createTestingModule({
|
||
imports: [AppModule],
|
||
}).compile();
|
||
|
||
app = moduleFixture.createNestApplication();
|
||
await app.init();
|
||
|
||
// 生成测试用的服务令牌
|
||
serviceToken = jwt.sign(
|
||
{ service: 'identity-service' },
|
||
process.env.SERVICE_JWT_SECRET,
|
||
);
|
||
});
|
||
|
||
describe('POST /backup-share/store', () => {
|
||
it('should store backup share successfully', async () => {
|
||
const response = await request(app.getHttpServer())
|
||
.post('/backup-share/store')
|
||
.set('X-Service-Token', serviceToken)
|
||
.send({
|
||
userId: '12345',
|
||
accountSequence: 1001,
|
||
publicKey: '02' + 'a'.repeat(64),
|
||
encryptedShareData: 'encrypted-share-data-base64',
|
||
})
|
||
.expect(201);
|
||
|
||
expect(response.body.success).toBe(true);
|
||
expect(response.body.shareId).toBeDefined();
|
||
});
|
||
|
||
it('should reject duplicate share', async () => {
|
||
// First store
|
||
await request(app.getHttpServer())
|
||
.post('/backup-share/store')
|
||
.set('X-Service-Token', serviceToken)
|
||
.send({
|
||
userId: '99999',
|
||
accountSequence: 9999,
|
||
publicKey: '02' + 'b'.repeat(64),
|
||
encryptedShareData: 'data',
|
||
});
|
||
|
||
// Duplicate
|
||
await request(app.getHttpServer())
|
||
.post('/backup-share/store')
|
||
.set('X-Service-Token', serviceToken)
|
||
.send({
|
||
userId: '99999',
|
||
accountSequence: 9999,
|
||
publicKey: '02' + 'b'.repeat(64),
|
||
encryptedShareData: 'data',
|
||
})
|
||
.expect(400);
|
||
});
|
||
|
||
it('should reject unauthorized service', async () => {
|
||
await request(app.getHttpServer())
|
||
.post('/backup-share/store')
|
||
.set('X-Service-Token', 'invalid-token')
|
||
.send({})
|
||
.expect(401);
|
||
});
|
||
});
|
||
|
||
describe('POST /backup-share/retrieve', () => {
|
||
it('should retrieve backup share with valid recovery token', async () => {
|
||
// Setup: store a share first
|
||
const storeResponse = await request(app.getHttpServer())
|
||
.post('/backup-share/store')
|
||
.set('X-Service-Token', serviceToken)
|
||
.send({
|
||
userId: '77777',
|
||
accountSequence: 7777,
|
||
publicKey: '02' + 'c'.repeat(64),
|
||
encryptedShareData: 'test-encrypted-data',
|
||
});
|
||
|
||
// Retrieve
|
||
const response = await request(app.getHttpServer())
|
||
.post('/backup-share/retrieve')
|
||
.set('X-Service-Token', serviceToken)
|
||
.send({
|
||
userId: '77777',
|
||
publicKey: '02' + 'c'.repeat(64),
|
||
recoveryToken: 'valid-recovery-token',
|
||
})
|
||
.expect(200);
|
||
|
||
expect(response.body.encryptedShareData).toBeDefined();
|
||
expect(response.body.partyIndex).toBe(2);
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 与 identity-service 的集成
|
||
|
||
### 10.1 修改 identity-service
|
||
|
||
在 identity-service 的 `autoCreateAccount` 中添加调用 backup-service:
|
||
|
||
```typescript
|
||
// identity-service/src/application/services/user-application.service.ts
|
||
|
||
// 在保存服务端分片后,调用 backup-service 存储备份分片
|
||
await this.mpcKeyShareRepository.saveServerShare({...});
|
||
|
||
// 新增: 调用 backup-service 存储备份分片
|
||
if (mpcResult.backupShareData) {
|
||
try {
|
||
await this.backupClient.storeBackupShare({
|
||
userId: account.userId.toString(),
|
||
accountSequence: account.accountSequence.value,
|
||
publicKey: mpcResult.publicKey,
|
||
encryptedShareData: mpcResult.backupShareData,
|
||
});
|
||
this.logger.log(`Backup share stored to backup-service for user: ${account.userId}`);
|
||
} catch (error) {
|
||
this.logger.error(`Failed to store backup share: ${error.message}`);
|
||
// 根据业务需求决定是否回滚或继续
|
||
// 建议: 记录失败,后续通过补偿任务重试
|
||
}
|
||
}
|
||
```
|
||
|
||
### 10.2 配置更新
|
||
|
||
在 identity-service 的 `.env` 中添加:
|
||
|
||
```bash
|
||
# Backup Service
|
||
BACKUP_SERVICE_URL="http://backup-server:3002"
|
||
SERVICE_JWT_SECRET="your-super-secret-service-jwt-key"
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 监控和告警
|
||
|
||
### 11.1 关键指标
|
||
|
||
```
|
||
- backup_share_store_total # 存储总数
|
||
- backup_share_retrieve_total # 获取总数
|
||
- backup_share_revoke_total # 撤销总数
|
||
- backup_share_store_latency_ms # 存储延迟
|
||
- backup_share_retrieve_latency_ms # 获取延迟
|
||
- backup_share_error_total # 错误总数
|
||
```
|
||
|
||
### 11.2 告警规则
|
||
|
||
```yaml
|
||
# 异常访问告警
|
||
- alert: HighBackupShareRetrieveRate
|
||
expr: rate(backup_share_retrieve_total[5m]) > 10
|
||
for: 5m
|
||
labels:
|
||
severity: warning
|
||
annotations:
|
||
summary: "Backup share retrieve rate is high"
|
||
|
||
# 服务不可用告警
|
||
- alert: BackupServiceDown
|
||
expr: up{job="backup-service"} == 0
|
||
for: 1m
|
||
labels:
|
||
severity: critical
|
||
```
|
||
|
||
---
|
||
|
||
## 12. 开发步骤
|
||
|
||
### 12.1 初始化项目
|
||
|
||
```bash
|
||
# 1. 创建 NestJS 项目
|
||
cd backend/services/backup-service
|
||
npx @nestjs/cli new . --skip-git
|
||
|
||
# 2. 安装依赖
|
||
npm install @nestjs/config @prisma/client class-validator class-transformer
|
||
npm install -D prisma
|
||
|
||
# 3. 初始化 Prisma
|
||
npx prisma init
|
||
|
||
# 4. 复制 schema 并生成客户端
|
||
npx prisma generate
|
||
npx prisma migrate dev --name init
|
||
```
|
||
|
||
### 12.2 开发顺序
|
||
|
||
1. **基础设施层** - Prisma 配置、数据库连接
|
||
2. **领域层** - BackupShare 实体、Repository 接口
|
||
3. **应用层** - 存储/获取/撤销命令处理
|
||
4. **接口层** - REST API Controller
|
||
5. **安全层** - ServiceAuthGuard、审计日志
|
||
6. **测试** - 单元测试、E2E 测试
|
||
|
||
---
|
||
|
||
## 13. 检查清单
|
||
|
||
开发完成后,确保以下事项:
|
||
|
||
- [ ] 数据库部署在独立服务器
|
||
- [ ] 服务间认证已实现
|
||
- [ ] 分片数据已二次加密
|
||
- [ ] 审计日志已启用
|
||
- [ ] 访问频率限制已实现
|
||
- [ ] E2E 测试通过
|
||
- [ ] 与 identity-service 集成测试通过
|
||
- [ ] 监控指标已配置
|
||
- [ ] 告警规则已配置
|
||
- [ ] 生产环境部署在异地机房
|