概述

在开发 AI 代理应用时,安全性至关重要。本文将介绍如何保护 API 密钥、用户数据和应用安全的最佳实践。

为什么安全性很重要?

想象一下,如果你的 API 密钥被泄露,攻击者可能会:

  • 使用你的账户调用 AI 服务,产生巨额费用

  • 访问你的用户数据

  • 破坏你的应用

安全就像给你的应用加上"防盗门"和"保险箱",保护你的资产和用户的隐私。

1. API 密钥管理

1.1 永远不要硬编码密钥

❌ 危险的做法:硬编码密钥

// 千万不要这样做!
var apiKey = "sk-1234567890abcdef"; // 密钥直接写在代码里
var endpoint = "https://myopenai.openai.azure.com/";

问题

  • 代码提交到 Git 后,密钥会被永久记录

  • 任何能访问代码的人都能看到密钥

  • 密钥泄露后很难追踪

1.2 使用环境变量

✅ 安全的做法:使用环境变量

// 从环境变量读取密钥
var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");

if (string.IsNullOrEmpty(apiKey) || string.IsNullOrEmpty(endpoint))
{
    throw new InvalidOperationException("请配置 AZURE_OPENAI_API_KEY 和 AZURE_OPENAI_ENDPOINT 环境变量");
}

设置环境变量(Windows)

# PowerShell
$env:AZURE_OPENAI_API_KEY = "your-api-key"
$env:AZURE_OPENAI_ENDPOINT = "https://your-endpoint.openai.azure.com/"

设置环境变量(Linux/Mac)

export AZURE_OPENAI_API_KEY="your-api-key"
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"

1.3 使用配置文件(但不要提交到 Git)

创建 appsettings.Development.json(本地开发用):

{
  "AzureOpenAI": {
    "Endpoint": "https://your-endpoint.openai.azure.com/",
    "ApiKey": "your-api-key",
    "DeploymentName": "gpt-35-turbo"
  }
}

重要:在 .gitignore 中排除配置文件

# .gitignore
appsettings.Development.json
appsettings.*.json
*.env
.env

读取配置文件

using Microsoft.Extensions.Configuration;

public class SecureConfigurationManager
{
    private readonly IConfiguration _configuration;
    
    public SecureConfigurationManager()
    {
        _configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: true)
            .AddJsonFile("appsettings.Development.json", optional: true)
            .AddEnvironmentVariables() // 环境变量优先级最高
            .Build();
    }
    
    public string GetApiKey()
    {
        return _configuration["AzureOpenAI:ApiKey"] 
            ?? throw new InvalidOperationException("未配置 API 密钥");
    }
    
    public string GetEndpoint()
    {
        return _configuration["AzureOpenAI:Endpoint"] 
            ?? throw new InvalidOperationException("未配置 Endpoint");
    }
}

1.4 使用 Azure Key Vault(生产环境推荐)

Azure Key Vault 是专门用于存储密钥的安全服务。

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

public class KeyVaultSecretManager
{
    private readonly SecretClient _secretClient;
    
    public KeyVaultSecretManager(string keyVaultUrl)
    {
        // 使用托管标识或默认凭据
        _secretClient = new SecretClient(
            new Uri(keyVaultUrl),
            new DefaultAzureCredential()
        );
    }
    
    public async Task<string> GetSecretAsync(string secretName)
    {
        try
        {
            KeyVaultSecret secret = await _secretClient.GetSecretAsync(secretName);
            return secret.Value;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException($"无法获取密钥 {secretName}", ex);
        }
    }
}

// 使用示例
var keyVaultManager = new KeyVaultSecretManager("https://your-keyvault.vault.azure.net/");
var apiKey = await keyVaultManager.GetSecretAsync("AzureOpenAI-ApiKey");

1.5 密钥轮换策略

定期更换 API 密钥可以降低泄露风险。

public class ApiKeyRotationManager
{
    private string _currentKey;
    private string _backupKey;
    private DateTime _keyRotationDate;
    private readonly TimeSpan _rotationInterval = TimeSpan.FromDays(90);
    
    public ApiKeyRotationManager(string primaryKey, string secondaryKey)
    {
        _currentKey = primaryKey;
        _backupKey = secondaryKey;
        _keyRotationDate = DateTime.UtcNow;
    }
    
    public string GetCurrentKey()
    {
        // 检查是否需要轮换
        if (DateTime.UtcNow - _keyRotationDate > _rotationInterval)
        {
            Console.WriteLine("⚠️ 警告:API 密钥需要轮换");
        }
        
        return _currentKey;
    }
    
    public void RotateKeys(string newKey)
    {
        _backupKey = _currentKey;
        _currentKey = newKey;
        _keyRotationDate = DateTime.UtcNow;
        Console.WriteLine("✓ API 密钥已轮换");
    }
    
    public string GetBackupKey() => _backupKey;
}

2. 数据保护策略

2.1 敏感数据脱敏

在发送给 AI 模型之前,应该脱敏敏感信息。

using System.Text.RegularExpressions;

public class DataMasker
{
    // 脱敏手机号
    public string MaskPhoneNumber(string text)
    {
        // 匹配中国手机号:1[3-9]\d{9}
        return Regex.Replace(text, @"1[3-9]\d{9}", m =>
        {
            var phone = m.Value;
            return phone.Substring(0, 3) + "****" + phone.Substring(7);
        });
    }
    
    // 脱敏身份证号
    public string MaskIdCard(string text)
    {
        // 匹配身份证号:18位数字
        return Regex.Replace(text, @"\d{17}[\dXx]", m =>
        {
            var id = m.Value;
            return id.Substring(0, 6) + "********" + id.Substring(14);
        });
    }
    
    // 脱敏邮箱
    public string MaskEmail(string text)
    {
        return Regex.Replace(text, @"[\w\.-]+@[\w\.-]+\.\w+", m =>
        {
            var email = m.Value;
            var parts = email.Split('@');
            if (parts[0].Length <= 2)
                return "***@" + parts[1];
            return parts[0].Substring(0, 2) + "***@" + parts[1];
        });
    }
    
    // 脱敏银行卡号
    public string MaskBankCard(string text)
    {
        return Regex.Replace(text, @"\d{16,19}", m =>
        {
            var card = m.Value;
            return card.Substring(0, 4) + " **** **** " + card.Substring(card.Length - 4);
        });
    }
    
    // 综合脱敏
    public string MaskSensitiveData(string text)
    {
        text = MaskPhoneNumber(text);
        text = MaskIdCard(text);
        text = MaskEmail(text);
        text = MaskBankCard(text);
        return text;
    }
}

使用示例

var masker = new DataMasker();
var userMessage = "我的手机号是13812345678,邮箱是zhangsan@example.com";
var maskedMessage = masker.MaskSensitiveData(userMessage);
// 结果: "我的手机号是138****5678,邮箱是zh***@example.com"

// 发送脱敏后的消息给 AI
await thread.AddUserMessageAsync(maskedMessage);

2.2 数据加密存储

如果需要存储对话历史,应该加密存储。

using System.Security.Cryptography;
using System.Text;

public class DataEncryption
{
    private readonly byte[] _key;
    private readonly byte[] _iv;
    
    public DataEncryption(string encryptionKey)
    {
        // 从密钥派生加密密钥和 IV
        using var sha256 = SHA256.Create();
        var keyBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(encryptionKey));
        _key = keyBytes;
        _iv = keyBytes.Take(16).ToArray();
    }
    
    // 加密文本
    public string Encrypt(string plainText)
    {
        using var aes = Aes.Create();
        aes.Key = _key;
        aes.IV = _iv;
        
        using var encryptor = aes.CreateEncryptor();
        var plainBytes = Encoding.UTF8.GetBytes(plainText);
        var encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
        
        return Convert.ToBase64String(encryptedBytes);
    }
    
    // 解密文本
    public string Decrypt(string encryptedText)
    {
        using var aes = Aes.Create();
        aes.Key = _key;
        aes.IV = _iv;
        
        using var decryptor = aes.CreateDecryptor();
        var encryptedBytes = Convert.FromBase64String(encryptedText);
        var decryptedBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
        
        return Encoding.UTF8.GetString(decryptedBytes);
    }
}

// 使用示例
var encryption = new DataEncryption(Environment.GetEnvironmentVariable("ENCRYPTION_KEY"));

// 加密存储
var message = "用户的敏感对话内容";
var encrypted = encryption.Encrypt(message);
await SaveToDatabase(encrypted);

// 解密读取
var encryptedFromDb = await LoadFromDatabase();
var decrypted = encryption.Decrypt(encryptedFromDb);

2.3 限制数据访问

实现基于角色的访问控制(RBAC)。

public enum UserRole
{
    User,        // 普通用户
    Admin,       // 管理员
    Developer    // 开发者
}

public class AccessControl
{
    private readonly Dictionary<UserRole, HashSet<string>> _permissions = new()
    {
        [UserRole.User] = new HashSet<string> { "chat", "view_history" },
        [UserRole.Admin] = new HashSet<string> { "chat", "view_history", "view_all_users", "manage_users" },
        [UserRole.Developer] = new HashSet<string> { "chat", "view_history", "view_logs", "debug" }
    };
    
    public bool HasPermission(UserRole role, string permission)
    {
        return _permissions.TryGetValue(role, out var perms) && perms.Contains(permission);
    }
    
    public void CheckPermission(UserRole role, string permission)
    {
        if (!HasPermission(role, permission))
        {
            throw new UnauthorizedAccessException($"角色 {role} 没有权限执行 {permission}");
        }
    }
}

// 使用示例
public class SecureAgentService
{
    private readonly AccessControl _accessControl = new();
    
    public async Task<List<string>> GetUserHistoryAsync(UserRole role, string userId)
    {
        // 检查权限
        _accessControl.CheckPermission(role, "view_history");
        
        // 如果不是管理员,只能查看自己的历史
        if (role != UserRole.Admin && userId != GetCurrentUserId())
        {
            throw new UnauthorizedAccessException("只能查看自己的对话历史");
        }
        
        return await LoadHistoryFromDatabase(userId);
    }
    
    private string GetCurrentUserId() => "current-user-id"; // 实际实现
    private Task<List<string>> LoadHistoryFromDatabase(string userId) => Task.FromResult(new List<string>());
}

3. 输入验证和清理

3.1 验证用户输入

public class InputValidator
{
    private const int MaxMessageLength = 4000;
    private const int MaxMessagesPerMinute = 20;
    
    private readonly Dictionary<string, Queue<DateTime>> _userRequestTimes = new();
    
    // 验证消息长度
    public bool ValidateMessageLength(string message, out string error)
    {
        if (string.IsNullOrWhiteSpace(message))
        {
            error = "消息不能为空";
            return false;
        }
        
        if (message.Length > MaxMessageLength)
        {
            error = $"消息长度不能超过 {MaxMessageLength} 字符";
            return false;
        }
        
        error = null;
        return true;
    }
    
    // 速率限制
    public bool CheckRateLimit(string userId, out string error)
    {
        var now = DateTime.UtcNow;
        
        if (!_userRequestTimes.TryGetValue(userId, out var times))
        {
            times = new Queue<DateTime>();
            _userRequestTimes[userId] = times;
        }
        
        // 移除一分钟前的请求
        while (times.Count > 0 && (now - times.Peek()).TotalMinutes > 1)
        {
            times.Dequeue();
        }
        
        if (times.Count >= MaxMessagesPerMinute)
        {
            error = $"请求过于频繁,每分钟最多 {MaxMessagesPerMinute} 次请求";
            return false;
        }
        
        times.Enqueue(now);
        error = null;
        return true;
    }
    
    // 检测恶意内容
    public bool DetectMaliciousContent(string message, out string error)
    {
        var maliciousPatterns = new[]
        {
            @"<script",           // XSS 攻击
            @"javascript:",       // JavaScript 注入
            @"onerror=",         // 事件处理器注入
            @"eval\(",           // 代码执行
            @"exec\(",           // 命令执行
        };
        
        foreach (var pattern in maliciousPatterns)
        {
            if (Regex.IsMatch(message, pattern, RegexOptions.IgnoreCase))
            {
                error = "检测到潜在的恶意内容";
                return true;
            }
        }
        
        error = null;
        return false;
    }
    
    // 综合验证
    public bool ValidateInput(string userId, string message, out string error)
    {
        if (!ValidateMessageLength(message, out error))
            return false;
            
        if (!CheckRateLimit(userId, out error))
            return false;
            
        if (DetectMaliciousContent(message, out error))
            return false;
            
        return true;
    }
}

3.2 清理输出内容

public class OutputSanitizer
{
    // 移除潜在的危险内容
    public string SanitizeOutput(string output)
    {
        // 移除 HTML 标签
        output = Regex.Replace(output, @"<[^>]+>", "");
        
        // 移除 JavaScript
        output = Regex.Replace(output, @"<script.*?</script>", "", RegexOptions.IgnoreCase | RegexOptions.Singleline);
        
        // 转义特殊字符
        output = output
            .Replace("<", "&lt;")
            .Replace(">", "&gt;")
            .Replace("\"", "&quot;")
            .Replace("'", "&#39;");
        
        return output;
    }
}

4. 安全配置示例

4.1 完整的安全配置类

public class SecureAgentConfiguration
{
    // 从安全来源加载配置
    public static SecureAgentConfiguration LoadFromSecureSource()
    {
        return new SecureAgentConfiguration
        {
            ApiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"),
            Endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"),
            EncryptionKey = Environment.GetEnvironmentVariable("ENCRYPTION_KEY"),
            MaxRequestsPerMinute = 20,
            MaxMessageLength = 4000,
            EnableDataMasking = true,
            EnableEncryption = true,
            EnableRateLimit = true
        };
    }
    
    public string ApiKey { get; set; }
    public string Endpoint { get; set; }
    public string EncryptionKey { get; set; }
    public int MaxRequestsPerMinute { get; set; }
    public int MaxMessageLength { get; set; }
    public bool EnableDataMasking { get; set; }
    public bool EnableEncryption { get; set; }
    public bool EnableRateLimit { get; set; }
    
    // 验证配置
    public void Validate()
    {
        if (string.IsNullOrEmpty(ApiKey))
            throw new InvalidOperationException("未配置 API 密钥");
            
        if (string.IsNullOrEmpty(Endpoint))
            throw new InvalidOperationException("未配置 Endpoint");
            
        if (EnableEncryption && string.IsNullOrEmpty(EncryptionKey))
            throw new InvalidOperationException("启用加密但未配置加密密钥");
    }
}

4.2 安全的代理服务

public class SecureAgentService
{
    private readonly ChatCompletionAgent _agent;
    private readonly SecureAgentConfiguration _config;
    private readonly InputValidator _validator;
    private readonly DataMasker _masker;
    private readonly DataEncryption _encryption;
    private readonly OutputSanitizer _sanitizer;
    
    public SecureAgentService()
    {
        _config = SecureAgentConfiguration.LoadFromSecureSource();
        _config.Validate();
        
        var chatClient = new AzureOpenAIClient(
            new Uri(_config.Endpoint),
            new ApiKeyCredential(_config.ApiKey)
        ).GetChatClient("gpt-35-turbo");
        
        _agent = new ChatCompletionAgent(
            chatClient: chatClient,
            name: "SecureAgent",
            instructions: "你是一个安全的助手"
        );
        
        _validator = new InputValidator();
        _masker = new DataMasker();
        _encryption = new DataEncryption(_config.EncryptionKey);
        _sanitizer = new OutputSanitizer();
    }
    
    public async Task<string> ProcessMessageAsync(string userId, string message)
    {
        // 1. 验证输入
        if (!_validator.ValidateInput(userId, message, out var error))
        {
            throw new ArgumentException(error);
        }
        
        // 2. 脱敏敏感数据
        if (_config.EnableDataMasking)
        {
            message = _masker.MaskSensitiveData(message);
        }
        
        // 3. 调用 AI 代理
        var thread = new AgentThread();
        await thread.AddUserMessageAsync(message);
        var response = await _agent.InvokeAsync(thread);
        var output = response.Content;
        
        // 4. 清理输出
        output = _sanitizer.SanitizeOutput(output);
        
        // 5. 加密存储(如果需要)
        if (_config.EnableEncryption)
        {
            var encrypted = _encryption.Encrypt(output);
            await SaveToSecureStorage(userId, encrypted);
        }
        
        return output;
    }
    
    private Task SaveToSecureStorage(string userId, string data)
    {
        // 实际实现:保存到数据库
        return Task.CompletedTask;
    }
}

5. 安全检查清单

在部署应用之前,使用这个清单检查安全性:

  • [ ] API 密钥管理

    • [ ] 密钥不在代码中硬编码

    • [ ] 使用环境变量或 Key Vault

    • [ ] 配置文件已添加到 .gitignore

    • [ ] 实施密钥轮换策略

  • [ ] 数据保护

    • [ ] 敏感数据已脱敏

    • [ ] 存储的数据已加密

    • [ ] 实施了访问控制

    • [ ] 定期清理过期数据

  • [ ] 输入验证

    • [ ] 验证消息长度

    • [ ] 实施速率限制

    • [ ] 检测恶意内容

    • [ ] 清理用户输入

  • [ ] 输出安全

    • [ ] 清理 AI 输出

    • [ ] 移除潜在危险内容

    • [ ] 转义特殊字符

  • [ ] 网络安全

    • [ ] 使用 HTTPS

    • [ ] 实施 CORS 策略

    • [ ] 配置防火墙规则

  • [ ] 日志和监控

    • [ ] 记录安全事件

    • [ ] 不记录敏感信息

    • [ ] 配置告警

小结

安全是一个持续的过程,关键要点:

  1. 永远不要硬编码密钥:使用环境变量或 Key Vault

  2. 保护用户数据:脱敏、加密、访问控制

  3. 验证所有输入:长度、速率、恶意内容

  4. 清理所有输出:移除危险内容

  5. 持续监控:记录安全事件,及时响应

记住:安全不是一次性的工作,而是持续的实践

更多AIGC文章

RAG技术全解:从原理到实战的简明指南

更多VibeCoding文章

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐