数据主权合规核心是物理落点可控,需显式控制路径、加密、元数据、生命周期及临时文件:路径从配置读取并指定云区域;静态加密用托管密钥,传输加密用tls 1.2+;抹除时间戳、禁用bom、脱敏文件名;文件操作封装带留存策略;临时文件须重定向至受控目录。

文件路径和存储位置必须显式控制
数据主权的核心是“数据物理落点可控”,C# 本身不决定文件存在哪,File.WriteAllText、FileStream 这些 API 只管写,不管服务器在哪、磁盘挂载在哪、云存储区域选哪。你调用 File.WriteAllText(@"C:datauser.json", content),如果这台机器在德国法兰克福机房,那数据就在欧盟;如果在新加坡虚拟机里跑,就可能违反 GDPR 或中国《数据出境安全评估办法》。
实操建议:
- 所有路径必须从配置读取,禁止硬编码本地路径(如
"C:\temp"),改用Configuration["Storage:Path"]或环境变量Environment.GetEnvironmentVariable("DATA_ROOT") - 若用云存储(Azure Blob / AWS S3),必须显式指定区域参数:Azure 中用
new BlobServiceClient(connectionString, new BlobClientOptions { Transport = ..., Retry = ... })不够,关键要看连接字符串里的EndpointSuffix(如core.windows.netvscore.chinacloudapi.cn) - 容器名/桶名不能含用户标识——避免通过命名泄露归属地,比如
eu-user-data-2024比user-12345更易触发审计风险
文件内容加密必须区分“传输中”和“静态”场景
光靠 HTTPS 传文件不行,数据主权法规(如巴西 LGPD、韩国 PIPA)普遍要求静态数据加密(at-rest encryption)。但 C# 的 ProtectedData.Protect 是 Windows DPAPI,密钥绑定本机,换服务器就打不开;而 Aes.Create() 手动加解密又容易因 IV 复用或密钥硬编码翻车。
实操建议:
- 静态加密优先用平台托管密钥:Azure Key Vault +
KeyEncryptionKey,AWS KMS +EncryptRequest,别自己生成Rfc2898DeriveBytes密钥 - 传输中加密靠 TLS 1.2+ 就够,但要验证证书链:
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12,并禁用ServerCertificateValidationCallback的无条件返回true - 加密后文件名也需脱敏:别用
user_12345_encrypted.bin,改用 UUID 命名 + 元数据表映射,否则日志或备份目录可能暴露主体信息
文件元数据和日志必须剥离可识别个人字段
很多人只盯着文件内容,忘了 File.GetCreationTimeUtc、File.GetAttributes、甚至 StreamWriter 默认的 BOM 字节头,都可能成为“间接标识符”。GDPR 明确把设备 ID、时间戳组合视为个人数据;中国《个人信息保护法》第4条也涵盖“可间接识别特定自然人的信息”。
实操建议:
- 写文件前统一抹除时间戳:
File.SetCreationTimeUtc(path, DateTime.MinValue)、File.SetLastWriteTimeUtc(path, DateTime.MinValue)(注意:某些合规审计要求保留原始时间,此时应加密存储而非删除) - 禁用 BOM:用
new StreamWriter(fileStream, Encoding.UTF8, bufferSize: 1024, emitBom: false),否则 UTF-8 文件头EF BB BF虽小,但属于可提取特征 - 日志中禁止记录完整路径:
logger.LogInformation("Saved to {Path}", path)→ 改为logger.LogInformation("Saved to region-{RegionId}", regionId),regionId 查配置映射表
文件生命周期管理必须匹配法规留存期限
不是“删了就没事”。欧盟要求删除请求响应时间 ≤ 72 小时,但若文件已归档到冷存储(如 Azure Archive Tier)、或被备份系统抓走,File.Delete 根本无效。更麻烦的是,.NET 的 File.Delete 在 Linux 上对符号链接默认不递归,可能只删了链接没删源文件。
实操建议:
- 所有文件操作封装成服务类,强制带
RetentionPolicy参数:fileService.SaveAsync(content, new RetentionPolicy { CountryCode = "CN", MaxDays = 180 }) - 备份策略与主存储解耦:用
rsync --exclude="*.tmp"同步时,要额外排除retention=forever标签的文件;Azure Backup 不会自动遵守你代码里的Delete调用 - 定期扫描残留:用
Directory.GetFiles(root, "*", SearchOption.AllDirectories)配合正则匹配日期格式文件名,比依赖LastAccessTime更可靠(该属性在 NTFS 上默认关闭更新)
最常被忽略的一点:临时文件。Path.GetTempFileName() 返回的路径不受你管控,且 .NET 6+ 默认用 %TEMP%,Windows 下可能是系统盘,Linux 下可能是 /tmp —— 这些位置往往不在你的合规存储区域内。必须重写临时路径逻辑,指向受控目录,并加清理钩子。










