对话数据安全保证方法详解(AI 大模型评测平台的隐私设计)

私有对话数据一旦泄露,比丢几个账号严重得多,安全不能外包给云厂商的默认配置。当时我对照过两种思路:一种是把数据全部交给云厂商托管,靠平台的默认安全能力;另一种是脱敏做在前端,后端只存加工结果。权衡之后,我们选了”存储、传输、访问、销毁”四个环节全链路自控的方案,宁可多写代码,也不把风险外包给别人的默认配置。下面把这套设计完整拆开。

一、安全设计原则

安全设计不能靠零散补丁,先立原则再谈实现。我们定了五条,每条都能对应到具体的落地动作:

原则 落地
最小可见 用户只能看自己的数据,管理员也只能看脱敏后的数据
传输加密 全站 HTTPS,内部 RPC 用 mTLS
存储加密 敏感字段 AES-256-GCM 静态加密
操作审计 任何读/导出/删除都有日志
销毁可证 用户删除时硬删,并可下载销毁证明

五条原则里”销毁可证”最容易被人忽略——用户删数据不是点个删除按钮,而是要有可下载的销毁证明,这一点在面向企业客户时尤其重要。

二、传输层加密

传输层是数据离开内存后的第一道防线。对外全站 HTTPS,证书交给 Let’s Encrypt 自动续期,HSTS 强制跳转,用户访问的每个请求都走加密通道。对内,服务间调用如果还是明文,等于内网裸奔,所以我们上了 mTLS(Mutual TLS)。用服务网格管理证书最省心,Istio 一条 DestinationRule 就搞定:

# Istio DestinationRule
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: eval-service-mtls
spec:
  host: eval-service
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL

数据库连接同样不能省——MySQL、Redis 全部开 TLS,代码里禁掉明文连接串,从源头上杜绝”顺手连了个明文库”。传输加密整体可以归纳成三步:

  1. 对外统一 HTTPS 与 HSTS,证书自动续期;
  2. 内部服务间开启 mTLS,证书由服务网格管理;
  3. 数据库与缓存强制 TLS,关闭明文端口。

三、存储层加密

传输加密挡得住网络侧,挡不住存储侧。数据库明文落盘,拿到备份就拿到了一切。我们选择应用层加密而不是 TDE,原因在 FAQ 里展开,这里先看实现。对用户答案原文这类敏感字段,用 JPA 转换器在读写时自动加解密:

@Entity
@Table(name = "eval_output")
public class EvalOutput {
    @Id
    private Long id;
    private Long userId;
    
    // 加密字段
    @Convert(converter = AesGcmConverter.class)
    private String content;  // 用户答案原文
    
    // 索引用 hash
    private String contentHash;  // SHA-256(content) 用于去重,不暴露原文
}

AesGcmConverter 实现 JPA AttributeConverter,主密钥从环境变量或 KMS 读取,绝不落库,甚至不进配置文件:

public class AesGcmConverter implements AttributeConverter<String, String> {
    private static final String KEY = System.getenv("PLATFORM_AES_KEY");
    
    @Override
    public String convertToDatabaseColumn(String attribute) {
        return AesGcm.encrypt(attribute, KEY);
    }
    @Override
    public String convertToEntityAttribute(String dbData) {
        return AesGcm.decrypt(dbData, KEY);
    }
}

相比 MySQL 8.0 的 TDE,应用层加密可以精确到字段,score、duration 这类需要聚合的列保持明文,只有 content 走加密。Session 和缓存里如果放敏感数据同样要加密,我们封装了一个 SecureCache:

@Service
public class SecureCache {
    public void put(String key, String value) {
        String enc = AesGcm.encrypt(value, sessionKey);
        redisTemplate.opsForValue().set("sec:" + key, enc, Duration.ofHours(2));
    }
    public String get(String key) {
        String enc = redisTemplate.opsForValue().get("sec:" + key);
        return enc == null ? null : AesGcm.decrypt(enc, sessionKey);
    }
}

缓存的密钥用了独立的 sessionKey,跟数据库主密钥分开,一个泄露不至于全盘皆输。

四、访问控制

加密做完了,还要防”合法账号干坏事”。访问控制我们做了两层:API 层做用户级校验,数据层用拦截器强制注入 userId。API 层最朴素也最有效——每条查询都核对归属:

@GetMapping("/session/{id}")
public BaseResponse<EvalSession> get(@PathVariable long id) {
    EvalSession s = sessionService.getById(id);
    if (!s.getUserId().equals(currentUser.getId()) 
        && !currentUser.isAdmin()) {
        throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
    }
    return ResultUtils.success(s);
}

手写校验容易漏,所以又在数据层加了一道保险,用 MyBatis Flex 的拦截器自动往 SQL 里拼 WHERE user_id = ?,开发者想绕过都难:

@Interceptor(ignoreAlias = "user")
public class TenantInterceptor implements InnerInterceptor {
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, 
                            Object parameter, RowBounds rowBounds, 
                            ResultHandler resultHandler, BoundSql boundSql) {
        if (isTenantTable(boundSql.getSql())) {
            boundSql.setAdditionalParameter("userId", currentUser.getId());
            // 改 SQL 加 WHERE user_id = #{userId}
        }
    }
}

管理后台单独设计成脱敏视图,只展示前 50 字加省略号,要看完整内容必须二次审批,每一步都留审计。双保险下来,越权查询在测试阶段就被自动化用例拦住了。

五、操作审计

访问控制做对了,不等于可以信任,关键是出了事能查。任何对敏感数据的读、导出、删除、下载操作,都写一条审计日志:

CREATE TABLE data_audit_log (
  id          BIGINT PRIMARY KEY,
  user_id     BIGINT NOT NULL,         -- 操作者
  action      VARCHAR(32) NOT NULL,    -- READ/EXPORT/DELETE/DOWNLOAD
  target_type VARCHAR(32),             -- EVAL_SESSION / PROMPT_LAB
  target_id   BIGINT,
  ip          VARCHAR(64),
  user_agent  VARCHAR(256),
  created_at  DATETIME,
  INDEX idx_user_time (user_id, created_at)
);

审计日志只增不改,定期归档到只读 S3 桶,即使数据库被删,审计链也不会断。这个表给安全事件复盘提供了全部依据。

六、数据销毁

数据安全最后一环是销毁。用户注销账号,不是把数据标记删掉就完了,而是要有完整流程:先软删让业务立即不可见,再异步排期硬删,最后出一份可验证的销毁证明:

public void deleteUserData(long userId) {
    // 1) 软删
    sessionService.softDeleteByUser(userId);
    outputService.softDeleteByUser(userId);
    labService.softDeleteByUser(userId);
    
    // 2) 异步硬删(30 天后执行)
    destroyTaskService.schedule(new DestroyUserDataTask(userId), 
                                  Duration.ofDays(30));
}

@Scheduled(cron = "0 0 3 * * ?")  // 每天凌晨 3 点
public void runDestroyTasks() {
    for (var task : destroyTaskService.listDue()) {
        // 硬删所有表数据
        hardDeleteByUser(task.getUserId());
        // 写销毁证明
        certificateService.generate(task.getUserId(), "GDPR-compliant");
    }
}

销毁证明包含销毁时间、销毁范围、独立校验码,用户可下载作为凭证。这里有个容易被忽略的点:硬删要连 binlog 和备份一起处理,重要数据甚至要 DROP TABLE 重建,否则 DELETE 之后数据仍然残留在底层存储里。

七、API Key 安全

说完用户数据,再说平台自己的密钥。我们管理 LLM API Key 有几条铁律:

措施 落地
不存明文 AES-256-GCM 加密存数据库
不进日志 Logback Pattern 脱敏
不进前端 后端代理,前端只调平台 API
定期轮换 90 天自动轮换,旧 key 保留 7 天灰度
权限隔离 管理员查 key 也只能看尾号 4 位

表格里的规则执行起来不复杂,核心代码就是一个脱敏工具,把密钥只露出头尾:

public class ApiKeyUtil {
    public static String mask(String key) {
        if (key == null || key.length() < 8) return "***";
        return key.substring(0, 6) + "..." + key.substring(key.length() - 4);
    }
}

管理员查 Key 只能看到尾号四位,想看完整 Key 必须重新生成。90 天自动轮换加上 7 天灰度期,即使 Key 泄露,影响窗口也被压得很小。

八、踩过的坑

方案设计得再完备,落地过程也踩了不少坑,写出来给后来人参考:

  • 加密字段不能模糊查询:LIKE '%keyword%' 在加密字段上无法使用。要么用 hash 索引(不支持模糊),要么用 pg_trgm(PG)或全文搜索引擎。
  • KMS 性能瓶颈:所有加密都过 KMS 中心化服务会成为瓶颈。平台用”本地 KEK + 远程 KMS 主密钥”双层,主密钥解 KEK 后本地用 KEK 加解密。
  • 审计日志膨胀:读写都要审计的话一天 1 亿行。改为”只审计敏感操作(导出、删除、查全量)”,普通读不审计。
  • 销毁不彻底:MySQL DELETE 后 binlog 仍存,硬盘残留。重要数据 DROP TABLE 重建。
  • 内部人员偷数据:所有查询走 API 限流 + 异常查询告警(短时间大量查询触发审查)。

这些坑集中在两类:一类是”功能与安全打架”(加密字段不能模糊查询、KMS 性能瓶颈),一类是”你以为删了其实没删”(binlog 残留、审计膨胀)。安全设计最容易出问题的地方,恰恰是这些边界。

九、合规检查清单

为了不流于口号,我们把安全要求做成检查清单,每个发布周期过一遍:

  • [ ] 所有页面 HTTPS
  • [ ] 敏感字段加密存储
  • [ ] API Key 不落日志
  • [ ] 跨用户访问 100% 拦截(自动化测试覆盖)
  • [ ] 审计日志 90 天可查
  • [ ] 数据销毁 30 天可执行
  • [ ] 第三方依赖无已知高危漏洞
  • [ ] 员工安全培训记录

清单不是一次性动作,平台每季度自审一轮,新员工入职也要过一遍流程培训。数据安全没有一劳永逸,只有持续检查。

常见问题(FAQ)

Q1:为什么用应用层加密而不是 MySQL TDE?

TDE 是表级加密,性能开销小但粒度粗。平台按字段加密更灵活,例如只对 content 加密,score、duration 仍明文方便聚合。

Q2:用户能看到自己的 API Key 明文吗?

不能。用户添加 Key 时平台只存加密版本和”尾号 4 位”。要看完整 Key 必须重新生成,这是金融级安全实践。

Q3:怎么验证加密没漏?

DBA 定期跑脚本:直接 SELECT 加密字段,应该全部是乱码 base64。任何明文命中都触发安全告警。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
赵其鑫的头像赵其鑫管理团队

相关推荐

返回顶部