使用 Tonic 和 Valtio 构建多租户应用的加密分片隔离层


在设计多租户系统时,最常见的隔离方法是在数据表的每一行增加一个 tenant_id 字段。所有的数据查询都必须带上 WHERE tenant_id = ? 的子句。这种方式简单直接,但在实践中极为脆弱。一个疏忽的后端开发者,一次忘记添加 WHERE 条件的复杂联表查询,就可能导致灾难性的数据泄露。这种依赖于“人不出错”的隔离机制,在安全层面是不可接受的。

我们的技术痛点很明确:需要一种即使在应用程序代码出现逻辑漏洞时,也能保证租户数据不被越权访问的强隔离机制。思路从逻辑隔离转向了物理或密码学隔离。最终,我们选择了一个被称为“加密分片”(Crypto-Sharding)的方案:为每个租户生成一个独立的数据加密密钥(DEK),该租户的所有持久化数据都使用其专属密钥加密。这样一来,即使查询逻辑出错,取到了属于其他租户的加密数据,由于没有正确的密钥,也无法解密,数据依然安全。

这个方案的核心在于一个高性能、高安全性的密钥管理与加解密服务。Rust 凭借其内存安全、无GC的特性,成为构建这个安全核心的理想选择。我们使用 Tonic 框架来构建基于 gRPC 的服务,它能提供强类型、高性能的跨语言通信能力。前端则需要一个能够清晰、响应式地管理当前会话上下文(如 tenant_id)的状态管理库,Valtio 以其极简的 Proxy 模型和对 React Hooks 的良好支持,成为我们的首选。

架构设计与技术选型决策

整个系统的核心是一个 gRPC 服务,我们称之为 CryptoShardingService。它负责处理两件关键的事情:

  1. 密钥管理:为新租户创建、存储和管理其专属的 DEK。在真实项目中,这部分会与硬件安全模块(HSM)或云厂商的 KMS(如 AWS KMS, Google Cloud KMS)集成。为了演示,我们将在服务内部实现一个简化的、内存中的密钥管理器。
  2. 数据加解密:提供 RPC 接口,接收明文数据和 tenant_id,在服务端使用该租户的 DEK 进行加密后返回密文;反之,接收密文和 tenant_id,解密后返回明文。

前端应用(React)通过 gRPC-Web 与后端通信。用户登录后,其 tenant_id 和认证凭证将被存储在 Valtio 的全局状态中。所有后续的数据操作请求,都会自动从 Valtio store 中获取 tenant_id,并发送给 CryptoShardingService

下面是整个流程的交互图:

sequenceDiagram
    participant FE as React App (Valtio Store)
    participant GW as gRPC-Web Proxy
    participant BE as Tonic Service (Rust)
    participant KMS as Key Management Service (In-Memory)
    participant DB as Database

    FE->>BE: 1. Login Request
    Note over BE: Authenticate user, get tenant_id
    BE->>FE: 2. Return auth_token, tenant_id
    Note over FE: Store context in Valtio Proxy state

    FE->>GW: 3. SetData(tenant_id, plaintext_data)
    GW->>BE: 4. gRPC SetData Request
    BE->>KMS: 5. Get DEK for tenant_id
    KMS-->>BE: 6. Return Tenant's DEK
    Note over BE: Encrypt plaintext_data with DEK
    BE->>DB: 7. Store encrypted_data
    DB-->>BE: 8. Confirm storage
    BE-->>GW: 9. Return Success
    GW-->>FE: 10. Confirm Success

    FE->>GW: 11. GetData(tenant_id, data_reference)
    GW->>BE: 12. gRPC GetData Request
    BE->>DB: 13. Fetch encrypted_data
    DB-->>BE: 14. Return encrypted_data
    BE->>KMS: 15. Get DEK for tenant_id
    KMS-->>BE: 16. Return Tenant's DEK
    Note over BE: Decrypt encrypted_data with DEK
    BE-->>GW: 17. Return plaintext_data
    GW-->>FE: 18. Return plaintext_data

第一步:定义 gRPC 服务接口

我们需要一个 .proto 文件来定义服务契约。这是前后端通信的基础。

proto/crypto_sharding.proto:

syntax = "proto3";

package crypto_sharding;

// 核心数据服务
service TenantDataService {
  // 创建一个新租户,并为其生成一个加密密钥
  rpc CreateTenant(CreateTenantRequest) returns (CreateTenantResponse);

  // 加密并存储一个租户的数据
  rpc SetData(SetDataRequest) returns (SetDataResponse);

  // 获取并解密一个租户的数据
  rpc GetData(GetDataRequest) returns (GetDataResponse);
}

message CreateTenantRequest {
  string tenant_name = 1;
}

message CreateTenantResponse {
  string tenant_id = 1;
}

message SetDataRequest {
  // 必须提供租户ID以确定使用哪个密钥
  string tenant_id = 1;
  // 数据的唯一标识符,用于后续检索
  string data_key = 2;
  // 待加密的原始数据
  bytes raw_data = 3;
}

message SetDataResponse {
  // 确认操作成功
  bool success = 1;
}

message GetDataRequest {
  string tenant_id = 1;
  string data_key = 2;
}

message GetDataResponse {
  // 解密后的原始数据
  bytes raw_data = 1;
}

这里的关键点在于 SetDataRequestGetDataResponse 中的 raw_data 字段类型是 bytes。这意味着我们的服务是数据无关的,它可以加密任何二进制数据,无论是 JSON 字符串、图片还是其他文件。

第二步:实现 Tonic 后端服务

这是整个系统的核心。我们将使用 Rust 构建这个 gRPC 服务。

项目结构:

.
├── Cargo.toml
├── build.rs
├── proto/
│   └── crypto_sharding.proto
└── src/
    ├── server.rs
    └── crypto.rs

Cargo.toml 依赖项:

[package]
name = "crypto-sharding-service"
version = "0.1.0"
edition = "2021"

[dependencies]
tonic = "0.10"
prost = "0.12"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
uuid = { version = "1.6", features = ["v4"] }
aes-gcm = "0.10.3"
rand = "0.8.5"
hex = "0.4.3"
dashmap = "5.5.3" # 用于线程安全的内存存储
thiserror = "1.0.50"
anyhow = "1.0.75"
log = "0.4"
env_logger = "0.10"
config = "0.13"

[build-dependencies]
tonic-build = "0.10"

build.rs 文件用于在编译时从 .proto 文件生成 Rust 代码:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::configure()
        .build_server(true)
        .build_client(false) // 我们只在这个 crate 中构建服务端
        .compile(&["proto/crypto_sharding.proto"], &["proto/"])?;
    Ok(())
}

加密与密钥管理模块 (src/crypto.rs)

这个模块封装了所有密码学操作和密钥存储。在生产环境中,KeyManagementService 会与外部 KMS 通信。这里我们用 DashMap 模拟一个线程安全的内存数据库来存储租户ID到加密密钥的映射。

// src/crypto.rs
use aes_gcm::{
    aead::{Aead, KeyInit, OsRng},
    Aes256Gcm, Nonce, Key,
};
use dashmap::DashMap;
use std::sync::Arc;
use thiserror::Error;
use uuid::Uuid;

const KEY_SIZE: usize = 32; // 256 bits for AES-256
const NONCE_SIZE: usize = 12; // 96 bits for GCM

#[derive(Error, Debug)]
pub enum CryptoError {
    #[error("Encryption failed: {0}")]
    EncryptionError(String),
    #[error("Decryption failed: {0}")]
    DecryptionError(String),
    #[error("Key not found for tenant: {0}")]
    KeyNotFound(String),
}

// 定义一个类型别名,方便使用
type TenantKey = Key<Aes256Gcm>;

// 模拟的密钥管理服务
#[derive(Clone, Debug, Default)]
pub struct KeyManagementService {
    // 使用 DashMap 来实现线程安全的并发读写
    // Key: tenant_id (String)
    // Value: Tenant's Data Encryption Key (DEK)
    tenant_keys: Arc<DashMap<String, TenantKey>>,
}

impl KeyManagementService {
    pub fn new() -> Self {
        Self {
            tenant_keys: Arc::new(DashMap::new()),
        }
    }

    /// 为新租户生成并存储一个新的DEK
    pub fn generate_key_for_tenant(&self, tenant_id: &str) {
        let key = Aes256Gcm::generate_key(&mut OsRng);
        self.tenant_keys.insert(tenant_id.to_string(), key);
        log::info!("Generated and stored new DEK for tenant_id: {}", tenant_id);
    }

    /// 根据租户ID获取其DEK
    fn get_key(&self, tenant_id: &str) -> Result<TenantKey, CryptoError> {
        self.tenant_keys
            .get(tenant_id)
            .map(|kv| *kv.value())
            .ok_or_else(|| CryptoError::KeyNotFound(tenant_id.to_string()))
    }
}

// 核心加解密逻辑
#[derive(Clone)]
pub struct CryptoService {
    kms: KeyManagementService,
}

impl CryptoService {
    pub fn new(kms: KeyManagementService) -> Self {
        Self { kms }
    }

    /// 使用指定租户的密钥加密数据
    /// 返回的 Vec<u8> 结构是: [nonce (12 bytes) || ciphertext]
    pub fn encrypt(&self, tenant_id: &str, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
        let key = self.kms.get_key(tenant_id)?;
        let cipher = Aes256Gcm::new(&key);
        let nonce = Aes256Gcm::generate_nonce(&mut OsRng);

        let ciphertext = cipher
            .encrypt(&nonce, plaintext)
            .map_err(|e| CryptoError::EncryptionError(e.to_string()))?;

        // 将 nonce 和密文拼接在一起存储,解密时需要
        let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
        result.extend_from_slice(nonce.as_slice());
        result.extend_from_slice(&ciphertext);

        Ok(result)
    }

    /// 使用指定租户的密钥解密数据
    pub fn decrypt(&self, tenant_id: &str, encrypted_data: &[u8]) -> Result<Vec<u8>, CryptoError> {
        if encrypted_data.len() < NONCE_SIZE {
            return Err(CryptoError::DecryptionError("Invalid encrypted data length".to_string()));
        }

        let key = self.kms.get_key(tenant_id)?;
        let cipher = Aes256Gcm::new(&key);

        // 从存储的数据中分离 nonce 和 ciphertext
        let (nonce_bytes, ciphertext) = encrypted_data.split_at(NONCE_SIZE);
        let nonce = Nonce::from_slice(nonce_bytes);

        let plaintext = cipher
            .decrypt(nonce, ciphertext)
            .map_err(|e| CryptoError::DecryptionError(e.to_string()))?;

        Ok(plaintext)
    }
}

gRPC 服务实现 (src/server.rs)

这里我们将实现 .proto 文件中定义的服务。

// src/server.rs
use tonic::{transport::Server, Request, Response, Status};
use uuid::Uuid;
use dashmap::DashMap;
use std::sync::Arc;
use std::net::SocketAddr;

mod crypto;
use crypto::{CryptoService, KeyManagementService, CryptoError};

// 引入自动生成的代码
pub mod crypto_sharding {
    tonic::include_proto!("crypto_sharding");
}

use crypto_sharding::{
    tenant_data_service_server::{TenantDataService, TenantDataServiceServer},
    CreateTenantRequest, CreateTenantResponse,
    SetDataRequest, SetDataResponse,
    GetDataRequest, GetDataResponse,
};

// 模拟一个持久化存储,存储加密后的数据
// Key: data_key, Value: encrypted_data
type TenantDB = Arc<DashMap<String, Vec<u8>>>;

#[derive(Debug)]
pub struct MyTenantDataService {
    crypto_service: CryptoService,
    // Key: tenant_id, Value: TenantDB
    // 每个租户有自己的数据 "命名空间"
    storage: Arc<DashMap<String, TenantDB>>, 
    kms: KeyManagementService,
}

impl MyTenantDataService {
    fn new(crypto_service: CryptoService, kms: KeyManagementService) -> Self {
        Self {
            crypto_service,
            storage: Arc::new(DashMap::new()),
            kms,
        }
    }
}

// 实现 gRPC 服务 trait
#[tonic::async_trait]
impl TenantDataService for MyTenantDataService {
    async fn create_tenant(
        &self,
        request: Request<CreateTenantRequest>,
    ) -> Result<Response<CreateTenantResponse>, Status> {
        let tenant_name = &request.get_ref().tenant_name;
        let tenant_id = Uuid::new_v4().to_string();

        log::info!("Creating new tenant '{}' with id: {}", tenant_name, tenant_id);
        
        // 为新租户生成密钥
        self.kms.generate_key_for_tenant(&tenant_id);

        // 为新租户创建数据存储空间
        self.storage.insert(tenant_id.clone(), Arc::new(DashMap::new()));

        let response = CreateTenantResponse { tenant_id };
        Ok(Response::new(response))
    }

    async fn set_data(
        &self,
        request: Request<SetDataRequest>,
    ) -> Result<Response<SetDataResponse>, Status> {
        let req = request.into_inner();
        log::info!("SetData request for tenant_id: {}, data_key: {}", req.tenant_id, req.data_key);

        let tenant_storage = self.storage.get(&req.tenant_id).ok_or_else(|| {
            log::error!("Attempted to set data for non-existent tenant: {}", req.tenant_id);
            Status::not_found(format!("Tenant with id {} not found", req.tenant_id))
        })?;

        // 核心加密逻辑
        let encrypted_data = self.crypto_service.encrypt(&req.tenant_id, &req.raw_data)
            .map_err(|e| match e {
                CryptoError::KeyNotFound(_) => Status::not_found(e.to_string()),
                _ => Status::internal(e.to_string()),
            })?;
        
        // 存储密文
        tenant_storage.insert(req.data_key, encrypted_data);

        Ok(Response::new(SetDataResponse { success: true }))
    }

    async fn get_data(
        &self,
        request: Request<GetDataRequest>,
    ) -> Result<Response<GetDataResponse>, Status> {
        let req = request.into_inner();
        log::info!("GetData request for tenant_id: {}, data_key: {}", req.tenant_id, req.data_key);

        let tenant_storage = self.storage.get(&req.tenant_id).ok_or_else(|| {
            log::error!("Attempted to get data for non-existent tenant: {}", req.tenant_id);
            Status::not_found(format!("Tenant with id {} not found", req.tenant_id))
        })?;

        let encrypted_data = tenant_storage.get(&req.data_key).ok_or_else(|| {
            Status::not_found(format!("Data with key {} not found for tenant {}", req.data_key, req.tenant_id))
        })?;

        // 核心解密逻辑
        let raw_data = self.crypto_service.decrypt(&req.tenant_id, &encrypted_data)
            .map_err(|e| {
                log::error!("Decryption failed for tenant {}: {}", req.tenant_id, e);
                Status::internal("Decryption failed. Data might be corrupted or key is incorrect.")
            })?;

        Ok(Response::new(GetDataResponse { raw_data }))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    env_logger::init();
    
    let addr: SocketAddr = "127.0.0.1:50051".parse()?;
    
    let kms = KeyManagementService::new();
    let crypto_service = CryptoService::new(kms.clone());
    let service = MyTenantDataService::new(crypto_service, kms);
    let server = TenantDataServiceServer::new(service);

    log::info!("Crypto Sharding Service listening on {}", addr);

    Server::builder()
        .add_service(server)
        .serve(addr)
        .await?;

    Ok(())
}

这个实现包含了完整的错误处理。例如,如果请求一个不存在的租户,会返回 Status::not_found。如果解密失败(可能是因为数据损坏或密钥错误),会返回 Status::internal,并记录详细的错误日志。这是生产级代码的基本要求。

第三步:构建 React 前端与 Valtio 状态管理

现在我们转向前端。我们需要设置 gRPC-Web 客户端,并使用 Valtio 来管理应用的全局状态,特别是认证和租户上下文。

项目初始化与依赖安装:

npx create-react-app crypto-sharding-ui --template typescript
cd crypto-sharding-ui
npm install valtio @improbable-eng/grpc-web protobuf google-protobuf ts-protoc-gen

生成 gRPC-Web 客户端代码:
我们需要从 .proto 文件生成 TypeScript 客户端代码。

protoc \
  --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" \
  --js_out="import_style=commonjs,binary:./src/proto" \
  --ts_out="service=grpc-web:./src/proto" \
  ../proto/crypto_sharding.proto

Valtio 状态管理 (src/store.ts)

我们将创建一个简单的 store 来存放当前用户的会话信息。Valtio 的美妙之处在于其简洁性,你只需导出一个普通的对象,它就会被 proxy 包装成响应式状态。

// src/store.ts
import { proxy } from 'valtio';

interface AuthState {
  isAuthenticated: boolean;
  tenantId: string | null;
  tenantName: string | null;
  authToken: string | null; // In a real app, this would be a JWT or similar
}

export const authStore = proxy<AuthState>({
  isAuthenticated: false,
  tenantId: null,
  tenantName: null,
  authToken: null,
});

// 定义一些 action 来修改 state,这不是必须的,但有助于组织代码
export const authActions = {
  login: (tenantId: string, tenantName: string, token: string) => {
    authStore.isAuthenticated = true;
    authStore.tenantId = tenantId;
    authStore.tenantName = tenantName;
    authStore.authToken = token;
  },
  logout: () => {
    authStore.isAuthenticated = false;
    authStore.tenantId = null;
    authStore.tenantName = null;
    authStore.authToken = null;
  },
};

API 客户端封装 (src/apiClient.ts)

封装 gRPC 调用,使其更易于在组件中使用。

// src/apiClient.ts
import { grpc } from '@improbable-eng/grpc-web';
import { TenantDataServiceClient } from './proto/crypto_sharding_pb_service';
import {
  CreateTenantRequest,
  SetDataRequest,
  GetDataRequest,
} from './proto/crypto_sharding_pb';

// 假设 gRPC-Web 代理运行在 8080 端口
const host = "http://localhost:8080";
export const client = new TenantDataServiceClient(host);

export const api = {
  createTenant: async (tenantName: string): Promise<string> => {
    const request = new CreateTenantRequest();
    request.setTenantName(tenantName);

    return new Promise((resolve, reject) => {
      client.createTenant(request, (err, response) => {
        if (err) {
          return reject(err);
        }
        if (response) {
          resolve(response.getTenantId());
        } else {
          reject(new Error("No response received"));
        }
      });
    });
  },

  setData: async (tenantId: string, key: string, data: Uint8Array): Promise<boolean> => {
    const request = new SetDataRequest();
    request.setTenantId(tenantId);
    request.setDataKey(key);
    request.setRawData(data);
    
    return new Promise((resolve, reject) => {
      client.setData(request, (err, response) => {
        if (err) return reject(err);
        resolve(response?.getSuccess() || false);
      });
    });
  },

  getData: async (tenantId: string, key: string): Promise<Uint8Array> => {
    const request = new GetDataRequest();
    request.setTenantId(tenantId);
    request.setDataKey(key);

    return new Promise((resolve, reject) => {
      client.getData(request, (err, response) => {
        if (err) return reject(err);
        resolve(response?.getRawData_asU8() || new Uint8Array());
      });
    });
  },
};

React 组件实现 (src/App.tsx)

这个组件将演示创建租户、设置和获取数据的完整流程。它使用 useSnapshot hook 来订阅 Valtio store 的变化,当 authStore 中的状态改变时,组件会自动重新渲染。

// src/App.tsx
import React, { useState, useEffect } from 'react';
import { useSnapshot } from 'valtio';
import { authStore, authActions } from './store';
import { api } from './apiClient';
import { TextEncoder, TextDecoder } from 'text-encoding'; // Polyfill for some environments

function App() {
  const authSnap = useSnapshot(authStore);
  const [tenantNameToCreate, setTenantNameToCreate] = useState('');
  const [dataKey, setDataKey] = useState('profile');
  const [dataValue, setDataValue] = useState('{"name": "Alice", "role": "admin"}');
  const [retrievedData, setRetrievedData] = useState('');
  const [statusMessage, setStatusMessage] = useState('');
  
  const handleCreateTenant = async () => {
    if (!tenantNameToCreate) return;
    try {
      setStatusMessage(`Creating tenant ${tenantNameToCreate}...`);
      const tenantId = await api.createTenant(tenantNameToCreate);
      // 模拟登录
      authActions.login(tenantId, tenantNameToCreate, 'fake-jwt-token');
      setStatusMessage(`Tenant created and logged in! ID: ${tenantId}`);
    } catch (error: any) {
      setStatusMessage(`Error: ${error.message}`);
    }
  };

  const handleSetData = async () => {
    if (!authSnap.tenantId || !dataKey || !dataValue) return;
    try {
      setStatusMessage('Encrypting and setting data...');
      // 必须将字符串转换为 Uint8Array
      const encoder = new TextEncoder();
      const rawData = encoder.encode(dataValue);
      await api.setData(authSnap.tenantId, dataKey, rawData);
      setStatusMessage(`Data for key '${dataKey}' stored successfully.`);
    } catch (error: any) {
      setStatusMessage(`Error: ${error.message}`);
    }
  };

  const handleGetData = async () => {
    if (!authSnap.tenantId || !dataKey) return;
    try {
      setStatusMessage('Fetching and decrypting data...');
      const rawData = await api.getData(authSnap.tenantId, dataKey);
      // 将返回的 Uint8Array 转换回字符串
      const decoder = new TextDecoder();
      const decodedString = decoder.decode(rawData);
      setRetrievedData(decodedString);
      setStatusMessage(`Data for key '${dataKey}' retrieved.`);
    } catch (error: any) {
      setRetrievedData('');
      setStatusMessage(`Error: ${error.message}`);
    }
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>Crypto-Sharding Demo</h1>
      <hr />
      
      {!authSnap.isAuthenticated ? (
        <div>
          <h2>1. Create a Tenant</h2>
          <input 
            type="text" 
            placeholder="Tenant Name (e.g., acme-corp)"
            value={tenantNameToCreate}
            onChange={(e) => setTenantNameToCreate(e.target.value)}
          />
          <button onClick={handleCreateTenant}>Create and Login</button>
        </div>
      ) : (
        <div>
          <h2>Welcome, {authSnap.tenantName} (ID: {authSnap.tenantId})</h2>
          <button onClick={authActions.logout}>Logout</button>
          <hr />
          
          <h2>2. Set Tenant Data (JSON)</h2>
          <div>
            <label>Data Key: </label>
            <input value={dataKey} onChange={e => setDataKey(e.target.value)} />
          </div>
          <div>
            <label>Data Value: </label>
            <textarea 
              rows={3}
              cols={50}
              value={dataValue} 
              onChange={e => setDataValue(e.target.value)}
            />
          </div>
          <button onClick={handleSetData}>Set Data</button>

          <hr />

          <h2>3. Get Tenant Data</h2>
          <div>
            <label>Data Key to Get: </label>
            <input value={dataKey} onChange={e => setDataKey(e.target.value)} />
          </div>
          <button onClick={handleGetData}>Get Data</button>
          
          <h3>Retrieved & Decrypted Data:</h3>
          <pre style={{ border: '1px solid #ccc', padding: '10px', background: '#f5f5f5' }}>
            {retrievedData || '(no data retrieved yet)'}
          </pre>
        </div>
      )}
      
      <hr />
      <h3>Status Log:</h3>
      <p><i>{statusMessage}</i></p>
    </div>
  );
}

export default App;

这个前端应用清晰地展示了整个流程。当用户未登录时,只能创建租户。创建成功后,authStore 被更新,isAuthenticated 变为 true,UI 响应式地切换到数据操作界面。所有的操作都强依赖于 authStore.tenantId,确保了请求的上下文正确性。

方案的局限性与未来迭代路径

我们实现的这套基于 Tonic 和 Valtio 的加密分片架构,有效地将租户数据隔离从应用逻辑层面提升到了密码学层面。然而,这并非一个没有代价的银弹。

首先,当前实现的密钥管理是内存级的,这在生产环境中是完全不可接受的。下一步必须将其替换为与专业 KMS 系统的集成,例如 HashiCorp Vault 或云厂商提供的服务。这会引入网络延迟和额外的成本,但却是保障密钥安全的必要步骤。

其次,所有数据都加密存储,这意味着我们失去了在数据库层面进行查询、索引和聚合的能力。对加密数据的任何操作都必须先将数据拉取到服务端,解密后再在内存中处理,这对于大数据量的分析场景是低效的。解决方案包括可搜索加密(Searchable Encryption)或为需要索引的字段保留明文或哈希索引,但这会削弱一部分安全性,需要在具体业务场景中进行权衡。

最后,密钥轮换(Key Rotation)是一个未被覆盖的关键安全实践。需要设计一套机制,能够定期更换每个租户的DEK,并将旧数据用新密钥重新加密,或者采用信封加密的变体来简化这个过程。这增加了系统的复杂性,但对长期的数据安全至关重要。


  目录