输入关键词开始搜索

安全设计文档

智能家庭饮食管家 App - 安全设计文档

最后更新: 2026-05-18

一、文档修订记录

版本日期作者修改说明
v1.02026-05-18初始版本,定义数据安全与权限安全完整设计方案
v1.12026-05-19与接口设计文档 v1.6 对齐:完善 Token 刷新机制描述

二、安全设计概述

安全设计原则:

原则说明
纵深防御多层安全机制叠加,单一防护失效不影响整体安全
最小权限用户和组件仅获得完成任务所需的最小权限
默认安全安全配置开箱即用,无需额外配置
隐私优先敏感数据最小化采集,加密存储,脱敏展示

安全架构分层:

┌─────────────────────────────────────────────────────┐
│                    客户端层 (Qt/QML)                     │
│  ├── 本地 SQLite 加密 (SQLCipher)                      │
│  ├── 安全存储 (Keychain/CredentialStore)               │
│  └── 证书校验与域名校验                                 │
├─────────────────────────────────────────────────────┤
│                    网络通信层                            │
│  ├── HTTPS 强制 (TLS 1.2+)                            │
│  ├── WSS 加密 (TLS 1.2+)                              │
│  └── 证书固定 (Certificate Pinning)                    │
├─────────────────────────────────────────────────────┤
│                    云服务端                             │
│  ├── JWT 认证 + Token 刷新机制                       │
│  ├── RBAC 权限模型 + 家庭级数据隔离                     │
│  ├── SQL 注入防护 (参数化查询)                         │
│  ├── 请求频率限制 (Rate Limiting)                      │
│  └── 审计日志与异常检测                                │
└─────────────────────────────────────────────────────┘

三、数据安全设计

3.1 密码安全

密码策略:

规则要求
最小长度8 个字符
最大长度64 个字符
字符要求至少包含字母和数字
禁止规则禁止与 login_id 相同,禁止使用常见弱密码(如 123456)

密码哈希算法:

使用 Argon2id(当前最佳实践)或 bcrypt 作为备选。

  • Argon2id 参数
    • 内存消耗:64 MB(m=65536
    • 迭代次数:3(t=3
    • 并行度:4(p=4
    • 输出长度:32 字节(hashlen=32
    • 盐长度:16 字节(saltlen=16,随机生成)

C++ 实现示例:

#include <argon2.h>

QByteArray PasswordService::hashPassword(const QString &plainPassword) {
    QByteArray passwordBytes = plainPassword.toUtf8();
    QByteArray salt = SecurityUtils::generateRandomBytes(16);

    char hash[32];
    int result = argon2id_hash_raw(
        3,        // t = 迭代次数
        65536,    // m = 内存消耗 (KB)
        4,        // p = 并行度
        passwordBytes.constData(),
        passwordBytes.size(),
        salt.constData(),
        salt.size(),
        hash,
        32        // hashlen
    );

    if (result != ARGON2_OK) {
        qWarning() << "Password hashing failed:" << result;
        return QByteArray();
    }

    // 返回格式: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
    return QString("$argon2id$v=19$m=65536,t=3,p=4$%1$%2")
        .arg(salt.toBase64())
        .arg(QByteArray(hash, 32).toBase64())
        .toUtf8();
}

bool PasswordService::verifyPassword(const QString &plainPassword,
                                    const QByteArray &storedHash) {
    // 不使用 timing-safe 比较,直接验证
    auto computedHash = hashPassword(plainPassword);
    return CryptoUtils::timingSafeEqual(computedHash, storedHash);
}

安全存储规范:

数据存储位置加密方式
登录密码云端 PostgreSQLArgon2id 哈希(不可逆)
本地 TokenQt Keychain/iOS Keychain/Android Keystore平台安全存储
本地 SQLite应用私有目录SQLite Encryption Extension
缓存数据应用私有目录不加密(无敏感数据)

禁止行为:

  • ❌ 禁止在日志中打印密码或密码哈希
  • ❌ 禁止在 URL 参数中传递密码
  • ❌ 禁止将密码以明文或 Base64 编码存储
  • ❌ 禁止使用 MD5/SHA1 等弱哈希算法存储密码

3.2 通信安全

传输层加密:

通信类型协议TLS 版本证书要求
REST APIHTTPSTLS 1.2+有效 CA 证书
WebSocketWSSTLS 1.2+有效 CA 证书
资源下载HTTPSTLS 1.2+有效 CA 证书

证书固定策略(Certificate Pinning):

客户端使用证书固定,防止中间人攻击。

void NetworkConfig::setupCertificatePinning() {
    // 预置服务器证书的公钥指纹(SHA-256)
    QSet<QByteArray> pinnedFingerprints = {
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // 主证书
        "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="  // 备用证书(证书轮换)
    };

    QSslConfiguration config;
    config.setCaCertificates(QSslConfiguration::systemCaCertificates());

    // 自定义证书验证回调
    config.setPeerVerifyMode(QSslSocket::VerifyPeer);

    // 仅在 Debug 模式下跳过证书验证,生产环境必须验证
#ifndef QT_DEBUG
    // 生产环境:启用证书固定
    auto certificate = serverCertificate();
    auto fingerprint = QCryptographicHash::hash(
        certificate.publicKey().toDer(),
        QCryptographicHash::Sha256
    );
    Q_ASSERT(pinnedFingerprints.contains(fingerprint));
#endif
}

C++ 证书验证回调:

bool HttpClient::verifyCertificate(const QSslCertificate &certificate,
                                   const QList<QSslCertificate> &chain,
                                   QSslSocket::PeerVerifyMode mode) {
    Q_UNUSED(chain);
    Q_UNUSED(mode);

    // 获取证书公钥指纹
    auto publicKeyDer = certificate.publicKey().toDer();
    auto fingerprint = QCryptographicHash::hash(publicKeyDer, QCryptographicHash::Sha256);

    // 检查是否在固定列表中
    if (pinnedFingerprints_.contains(fingerprint)) {
        return true;
    }

    qWarning() << "Certificate fingerprint mismatch:"
               << fingerprint.toBase64();
    return false;
}

HSTS(HTTP Strict Transport Security):

服务端响应头配置:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

敏感数据不通过 URL 传递:

  • ❌ 禁止:GET /api/v1/users/123 在 URL 中暴露用户 ID(可接受,但需配合其他校验)
  • ❌ 禁止:GET /api/v1/families/invite-code 直接返回邀请码
  • ✅ 正确:邀请码通过 POST 请求体获取,且校验邀请码状态

3.3 敏感数据保护

数据分类与保护策略:

数据类别示例存储加密传输保护脱敏展示
高敏感登录密码、Token✅ Argon2id/TEE✅ HTTPSN/A
中敏感身高/体重/年龄、TDEE、营养档案❌(业务数据)✅ HTTPS✅ 仅本人可见
低敏感菜谱名称、家庭名称✅ HTTPS
公开数据系统内置菜谱/食材✅ HTTPS

用户身体数据保护:

用户的身高、体重、年龄、TDEE 等数据属于个人健康数据,必须严格保护:

  1. 访问控制:仅用户本人和同一家庭成员可访问(已加入家庭的成员)
  2. 接口校验:获取他人档案时,必须校验 family_id 关联
  3. 脱敏展示:在家庭成员列表中,不展示具体体重,仅展示”健康目标”
  4. 数据隔离:不同家庭的数据严格隔离,无跨家庭查询能力

C++ 数据隔离示例:

UserProfile UserService::getUserProfile(const QString &requestingUserId,
                                        const QString &targetUserId) {
    // Step 1: 获取请求者所在的家庭
    auto requestingMember = repository_->getFamilyMemberByUserId(requestingUserId);

    // Step 2: 获取目标用户的家庭
    auto targetMember = repository_->getFamilyMemberByUserId(targetUserId);

    // Step 3: 校验是否在同一家庭
    if (requestingMember.family_id != targetMember.family_id) {
        // 非家庭成员无权访问他人详细档案
        return UserProfile::redacted(targetUserId); // 仅返回脱敏数据
    }

    // Step 4: 校验是否为本人
    if (requestingUserId == targetUserId) {
        return repository_->getFullProfile(targetUserId); // 返回完整数据
    } else {
        return repository_->getPublicProfile(targetUserId); // 返回公开数据
    }
}

隐私合规要点:

  • 不采集不必要的个人数据(最小化原则)
  • 用户可查看和删除自己的数据(GDPR/个人信息保护法合规)
  • 身体数据不用于非业务目的的分析
  • 云端数据库字段加密(PostgreSQL TDE 或应用层加密)

3.4 本地数据安全

SQLite 本地加密:

void DatabaseManager::initEncryptedDatabase() {
    // 使用 SQLite Encryption Extension (SEE) 或 SQLCipher
    // 密钥存储在 Qt Keychain 中
    auto encryptionKey = KeychainService::getDatabaseKey();
    if (encryptionKey.isEmpty()) {
        // 首次启动,生成随机密钥
        encryptionKey = SecurityUtils::generateRandomBytes(32);
        KeychainService::storeDatabaseKey(encryptionKey);
    }

    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName(":/data/app.db");
    db.setConnectOptions("QSQLITE_ENABLE_CIPHER"
                         " PRAGMA key = '" + encryptionKey + "'");

    if (!db.open()) {
        qFatal("Failed to open encrypted database");
    }
}

安全清理(敏感数据删除):

void SecurityService::secureDeleteUserData(const QString &userId) {
    // 1. 删除本地 Token
    KeychainService::deleteToken(userId);

    // 2. 删除本地 SQLite 数据库文件(而非仅软删除)
    QFile::remove(getLocalDatabasePath(userId));

    // 3. 清理内存中的敏感数据
    memoryCache_.remove(userId);

    // 4. 请求服务端彻底删除账户数据
    apiClient_.post("/users/me/delete-account", {});
}

四、身份认证设计

4.1 认证流程

整体认证架构:

┌──────────────┐     1. POST /auth/login      ┌──────────────┐
│              │ ───────────────────────────→ │              │
│   Qt Client  │                              │   云端 API   │
│              │ ←─────────────────────────── │   Server     │
└──────────────┘     2. { user, token }        └──────────────┘

       │ 3. 存储 Token

┌──────────────┐
│ Qt Keychain  │  ← 操作系统安全存储(不可提取明文)
└──────────────┘

认证流程:

1. 用户输入 login_id + password
2. 客户端 POST /auth/login { login_id, password }
3. 服务端:
   a. 参数校验(非空、格式)
   b. 查询用户(login_id)
   c. 读取 password_hash
   d. 使用 Argon2id 验证密码
   e. 验证成功 → 生成 JWT (access_token) + 刷新令牌 (refresh_token)
   f. 返回 { user, access_token, refresh_token, expires_at }
4. 客户端:
   a. 存储 access_token 到 Keychain
   b. 存储 refresh_token 到 Keychain
   c. 跳转首页
5. 后续请求 Header 携带:Authorization: Bearer <access_token>

JWT Token 结构:

字段说明过期时间
access_token短期令牌,访问 API15 分钟
refresh_token长期令牌,刷新 access_token7 天

JWT Payload 示例:

{
  "iss": "food-manager-api",
  "sub": "user-uuid-123",
  "aud": "food-manager-client",
  "exp": 1747574400,
  "iat": 1747570800,
  "jti": "token-uuid-unique",
  "family_id": "family-uuid-456",
  "role": "owner"
}

C++ Token 生成示例:

QByteArray TokenService::generateAccessToken(const QString &userId,
                                            const QString &familyId,
                                            const QString &role) {
    // Header
    QJsonObject header = {
        { "alg", "HS256" },
        { "typ", "JWT" }
    };

    // Payload
    QJsonObject payload = {
        { "iss", "food-manager-api" },
        { "sub", userId },
        { "aud", "food-manager-client" },
        { "exp", QDateTime::currentSecsSinceEpoch() + 900 }, // 15 min
        { "iat", QDateTime::currentSecsSinceEpoch() },
        { "jti", QUuid::createUuid().toString() },
        { "family_id", familyId },
        { "role", role }
    };

    return JwtEncoder::encode(header, payload, secretKey_);
}

4.2 Token 刷新机制

刷新流程:

1. access_token 即将过期(剩余 < 5 分钟)
2. 客户端 POST /auth/refresh { refresh_token }
3. 服务端:
   a. 校验 refresh_token 签名
   b. 校验 refresh_token 未被撤销
   c. 校验 refresh_token 未过期
   d. 生成新的 access_token
   e. 更新 refresh_token(可选:滚动刷新)
4. 客户端存储新的 tokens
5. 重试失败的请求

Token 撤销(登出):

void TokenService::revokeRefreshToken(const QString &userId,
                                      const QString &tokenId) {
    // 将 tokenId 加入撤销列表(Redis 或数据库表)
    refreshTokenStore_.addRevokedToken(tokenId,
        QDateTime::currentSecsSinceEpoch() + 7 * 24 * 3600);

    // 如果需要全局撤销(修改密码等情况)
    if (globalRevoke) {
        refreshTokenStore_.revokeAllUserTokens(userId);
    }
}

Token 安全存储:

存储位置AndroidiOSDesktop (Qt)
安全存储Android KeystoreiOS KeychainQt Keychain
加密方式AES-256-GCMKeychain Servicesplatform-native
生物识别可选(指纹/面容)可选(Face ID/Touch ID)N/A
QByteArray KeychainService::storeToken(const QString &key,
                                        const QByteArray &token) {
#ifdef Q_OS_IOS
    return KeychainStore::write(key, token, KeychainStore::AccessibilityWhenUnlocked);
#elif defined(Q_OS_ANDROID)
    return KeychainStore::write(key, token, AndroidKeystore::AES_256_GCM);
#else
    // Desktop: 使用 Qt Keychain 或系统密钥链
    return KeychainStore::write(key, token, QtKeychain::GenericSecretService);
#endif
}

4.3 认证安全防护

暴力破解防护:

防护机制阈值措施
登录尝试限制5 次/分钟/IP返回 429 Too Many Requests
密码错误次数5 次/账号临时锁定 15 分钟 + 发送告警
验证码连续 3 次失败下次登录需图形验证码
账户锁定10 次失败需管理员或邮箱验证解锁

C++ 限流实现示例:

bool RateLimitService::checkLoginAttempts(const QString &loginId,
                                          const QString &ipAddress) {
    auto now = QDateTime::currentSecsSinceEpoch();

    // 按 IP 限流
    auto ipKey = QString("login:ip:%1").arg(ipAddress);
    auto ipCount = rateLimitStore_.increment(ipKey, 1, 60); // 60秒窗口
    if (ipCount > 60) {
        qWarning() << "IP login rate limit exceeded:" << ipAddress;
        return false;
    }

    // 按账号限流
    auto accountKey = QString("login:account:%1").arg(loginId);
    auto accountCount = rateLimitStore_.increment(accountKey, 1, 300); // 5分钟窗口
    if (accountCount > 5) {
        qWarning() << "Account login rate limit exceeded:" << loginId;
        // 触发账户锁定
        accountLockoutService_.lockAccount(loginId, 900); // 锁定15分钟
        return false;
    }

    return true;
}

五、权限安全设计

5.1 权限模型

RBAC + 家庭数据隔离模型:

┌─────────────────────────────────────────────────┐
│                  用户身份层                         │
│  ┌─────────────┐   ┌──────────────────────────┐  │
│  │   系统管理员  │   │      家庭成员 (role)        │  │
│  │  (平台运维)   │   │  owner / member           │  │
│  └─────────────┘   └──────────────────────────┘  │
├─────────────────────────────────────────────────┤
│                  权限层                           │
│  can_confirm_menu / can_manage_inventory          │
├─────────────────────────────────────────────────┤
│                  身份层                           │
│  daily_menus.chef_id (当日掌勺人)                 │
└─────────────────────────────────────────────────┘

权限校验层次(由内到外):

层级校验类型说明
1身份校验Token 中的 user_id 是否有效
2家庭归属校验请求的资源是否属于用户的家庭
3角色校验family_members.role 是否满足要求
4权限字段校验can_confirm_menu / can_manage_inventory
5掌勺人校验daily_menus.chef_id 是否匹配(核销场景)

权限校验流程(C++):

PermissionResult PermissionService::checkCookingPermission(
    const QString &userId,
    const QString &menuId) {

    // Step 1: 获取用户成员记录
    auto member = repository_->getFamilyMemberByUserId(userId);
    if (!member) {
        return PermissionResult::denied("USER_NOT_IN_FAMILY");
    }

    // Step 2: 户主拥有最高权限
    if (member.role == "owner") {
        return PermissionResult::allowed();
    }

    // Step 3: 获取菜单信息
    auto menu = repository_->getMenu(menuId);
    if (!menu) {
        return PermissionResult::denied("MENU_NOT_FOUND");
    }

    // Step 4: 校验是否为当日掌勺人(核心:与 can_confirm_menu 解耦)
    if (menu.chef_id == userId) {
        return PermissionResult::allowed();
    }

    // Step 5: 户主可以替他人核销(承担管理责任)
    // 该分支在上面已覆盖

    // Step 6: can_confirm_menu 权限不能用于核销(职责分离)
    return PermissionResult::denied("NOT_CHEF_OR_OWNER");
}

PermissionResult PermissionService::checkMenuConfirmationPermission(
    const QString &userId,
    const QString &menuId) {

    auto member = repository_->getFamilyMemberByUserId(userId);
    if (!member) {
        return PermissionResult::denied("USER_NOT_IN_FAMILY");
    }

    // 户主可以确认
    if (member.role == "owner") {
        return PermissionResult::allowed();
    }

    // 拥有 can_confirm_menu 权限的成员可以确认
    if (member.can_confirm_menu) {
        return PermissionResult::allowed();
    }

    return PermissionResult::denied("NO_CONFIRM_PERMISSION");
}

权限模型与接口的对应关系:

接口权限要求校验函数
POST /daily-menus/{id}/complete-cooking掌勺人或户主checkCookingPermission()
POST /daily-menus/{id}/confirm掌勺人/户主或 can_confirm_menucheckMenuConfirmationPermission()
POST /families/{id}/inventory/batches掌勺人或户主或 can_manage_inventorycheckInventoryPermission()
DELETE /families/{id}户主checkIsOwner()
PUT /families/{id}/transfer-ownership户主checkIsOwner()
POST /wishlist/{id}/adopt掌勺人/户主或 can_confirm_menucheckMenuConfirmationPermission()

5.2 家庭数据隔离

核心原则:用户只能访问自己家庭的数据,无法访问其他家庭的数据。

隔离策略:

隔离维度实现方式防护级别
家庭级隔离所有数据查询强制携带 family_idP0
跨家庭查询拦截Service 层校验 family_id 归属P0
邀请码隔离邀请码仅对特定家庭有效P1
成员列表隔离只能查看同一家庭成员P1

跨家庭访问防护(C++):

template<typename T>
T FamilyDataService::getFamilyData(const QString &userId,
                                   const QString &familyId,
                                   const QString &dataId) {
    // Step 1: 校验用户属于该家庭
    auto member = repository_->getFamilyMemberByUserIdAndFamilyId(userId, familyId);
    if (!member) {
        qWarning() << "User" << userId << "attempted to access family"
                   << familyId << "without membership";
        return T(); // 返回空或抛出异常
    }

    // Step 2: 校验数据确实属于该家庭
    auto data = repository_->getDataById(dataId);
    if (!data || data.family_id != familyId) {
        qWarning() << "User" << userId << "attempted to access data"
                   << dataId << "from different family";
        return T();
    }

    return data;
}

邀请码安全校验:

bool InviteCodeService::validateAndConsume(const QString &code,
                                           const QString &targetFamilyId) {
    auto invite = repository_->getInviteByCode(code);

    // 校验码是否存在
    if (!invite) {
        return false;
    }

    // 校验码是否匹配目标家庭
    if (invite.family_id != targetFamilyId) {
        qWarning() << "Invite code" << code << "does not belong to family"
                   << targetFamilyId;
        return false;
    }

    // 校验是否过期
    if (invite.expires_at && invite.expires_at < QDateTime::currentDateTime()) {
        qWarning() << "Invite code" << code << "has expired";
        return false;
    }

    // 校验是否已被使用(可选:一次性码 vs 多次使用码)
    if (invite.is_single_use && invite.used) {
        return false;
    }

    // 标记为已使用(如果是一次性码)
    if (invite.is_single_use) {
        repository_->markInviteUsed(code);
    }

    return true;
}

5.3 接口权限校验

云端 API 权限中间件:

// 所有受保护接口的通用校验逻辑
bool ApiMiddleware::authorize(const HttpRequest &request,
                              const JwtToken &token) {
    // 1. 校验 Token 有效性
    if (!jwtService_.verify(token)) {
        response_.setStatus(401);
        response_.setBody({ "code": "1002", "message": "Token无效或已过期" });
        return false;
    }

    // 2. 校验 Token 中的 family_id 与请求中的 family_id 是否一致
    if (request.hasFamilyId()) {
        if (token.family_id != request.familyId()) {
            response_.setStatus(403);
            response_.setBody({ "code": "1003", "message": "无权访问该家庭数据" });
            return false;
        }
    }

    // 3. 校验接口特定权限
    auto requiredPermission = getRequiredPermission(request.path());
    if (requiredPermission && !permissionService_.check(token.userId,
                                                         request.familyId(),
                                                         requiredPermission)) {
        response_.setStatus(403);
        response_.setBody({ "code": "1003", "message": "权限不足" });
        return false;
    }

    return true;
}

各接口权限矩阵(云端强制校验):

接口权限要求额外校验
GET /families/{id}家庭成员family_id 匹配
GET /families/{id}/inventory家庭成员family_id 匹配
POST /families/{id}/inventory/batchesowner 或 can_manage_inventoryfamily_id 匹配
POST /daily-menus/{id}/complete-cookingowner 或 chef_id 匹配menu.family_id 匹配
PUT /families/{id}/members/{userId}/permissionsownerfamily_id 匹配
DELETE /families/{id}ownerfamily_id 匹配

六、会话与状态管理

6.1 会话生命周期
状态触发条件处理方式
正常Token 有效正常处理请求
即将过期Token 剩余 < 5 分钟客户端自动刷新
已过期Token 超过有效期返回 401,客户端跳转登录
已撤销登出或修改密码返回 401,客户端清除本地数据
多设备冲突同一账号同时在线允许(最多 5 个设备)
异常登录新设备/新 IP可选:发送告警通知
6.2 多设备管理
struct DeviceSession {
    QString deviceId;
    QString deviceName;
    QString lastIp;
    QDateTime lastActive;
    QDateTime createdAt;
};

bool SessionService::validateSession(const QString &userId,
                                     const QString &tokenId) {
    auto sessions = sessionStore_.getUserSessions(userId);

    // 校验 Token 是否属于已注册的设备
    auto session = sessions.findByTokenId(tokenId);
    if (!session) {
        // 新设备登录,需注册
        if (sessions.count() >= 5) {
            // 达到最大设备数,拒绝
            return false;
        }
        registerDevice(userId, tokenId);
        return true;
    }

    // 更新最后活跃时间
    session.lastActive = QDateTime::currentDateTime();
    sessionStore_.update(session);

    return true;
}

七、安全审计与监控

7.1 审计日志

必须记录的审计事件:

事件类型记录内容保留时间
登录/登出user_id, IP, 设备, 时间, 结果180 天
权限变更操作者, 目标用户, 变更内容, 时间永久
家庭解散操作者, 家庭ID, 时间永久
敏感操作操作者, 操作类型, 资源ID, 时间180 天
安全告警事件类型, 详情, IP, 时间永久

C++ 审计日志写入:

void AuditService::logSecurityEvent(const SecurityEvent &event) {
    AuditLog log = {
        .event_type = event.type,
        .user_id = event.userId,
        .ip_address = event.ipAddress,
        .device_id = event.deviceId,
        .details = QJsonDocument(event.details).toJson(),
        .result = event.result,
        .timestamp = QDateTime::currentDateTime().toString(Qt::ISODate)
    };

    // 异步写入,不阻塞主流程
    asyncWrite([log]() {
        auditRepository_->insert(log);
    });
}

// 使用示例
void AuthService::onLoginFailed(const QString &loginId,
                                  const QString &ipAddress,
                                  int attemptCount) {
    SecurityEvent event;
    event.type = SecurityEvent::LOGIN_FAILED;
    event.userId = loginId; // 此时用户ID可能未知,用loginId替代
    event.ipAddress = ipAddress;
    event.details = {
        { "attempt_count", attemptCount },
        { "reason", "invalid_password" }
    };
    event.result = "denied";

    auditService_->logSecurityEvent(event);

    // 连续失败 5 次,触发账户锁定
    if (attemptCount >= 5) {
        accountLockoutService_->lockAccount(loginId, 900);
        event.type = SecurityEvent::ACCOUNT_LOCKED;
        auditService_->logSecurityEvent(event);
    }
}
7.2 异常检测与告警

实时告警阈值:

告警类型阈值告警方式
暴力破解5 次/分钟/IP即时邮件 + 封禁 IP
异常权限访问3 次/用户/小时邮件通知
账户锁定1 次邮件通知用户
多设备异常5 个新设备/天站内通知
大量数据导出100 条/分钟审计日志 + 管理员通知

C++ 异常检测:

void SecurityMonitor::analyzeRequest(const HttpRequest &request) {
    auto userId = request.token().userId;
    auto ip = request.remoteIp();

    // 1. 检测暴力破解
    auto loginAttempts = rateLimitStore_.get("login:" + ip);
    if (loginAttempts > 5) {
        securityAlert(AlertType::BRUTE_FORCE, { ip, loginAttempts });
        ipBlocklist_.add(ip, 3600); // 封禁1小时
    }

    // 2. 检测异常权限访问
    auto deniedCount = accessDenialStore_.increment(userId, 1, 3600);
    if (deniedCount > 3) {
        securityAlert(AlertType::PERMISSION_ANOMALY,
                     { userId, deniedCount });
    }

    // 3. 检测数据异常访问模式
    if (detectRapidEnumeration(request)) {
        securityAlert(AlertType::ENUMERATION_ATTACK, { userId, ip });
        rateLimitStore_.set("ratelimit:" + userId, 1, 60);
    }
}

八、数据备份与恢复安全

8.1 备份策略
备份类型频率保留时间加密
全量备份每日 00:0030 天✅ AES-256
增量备份每小时7 天✅ AES-256
事务日志实时7 天✅ AES-256
异地容灾每日90 天✅ AES-256
8.2 备份数据访问控制
// 备份数据访问审批流程
bool BackupRestoreService::authorizeBackupAccess(const QString &operatorId,
                                                 const QString &familyId) {
    // 仅管理员和技术运维可访问备份数据
    auto operator_ = repository_->getOperator(operatorId);
    if (!operator_ || !operator_.isAdmin) {
        qWarning() << "Non-admin operator" << operatorId
                   << "attempted to access backup data";
        auditService_->logSecurityEvent({
            .type = "UNAUTHORIZED_BACKUP_ACCESS",
            .operatorId = operatorId,
            .familyId = familyId
        });
        return false;
    }

    // 所有备份访问操作均需记录
    auditService_->logSecurityEvent({
        .type = "BACKUP_ACCESS",
        .operatorId = operatorId,
        .familyId = familyId,
        .timestamp = QDateTime::currentDateTime()
    });

    return true;
}

九、合规与隐私

9.1 隐私合规要点
法规适用要求实现方式
《个人信息保护法》告知同意、最小采集、可删除隐私协议、用户数据导出/删除功能
GDPR数据可携权、删除权数据导出 API、账户删除 API
《网络安全法》网络安全等级保护数据加密、访问控制、审计日志
9.2 用户数据权利

用户可行使的权利:

权利实现接口说明
知情权GET /users/me查看自己的完整档案
访问权GET /users/me/data-export导出个人全部数据(JSON)
更正权PUT /users/me/profile修改个人档案
删除权DELETE /users/me/account注销账户(软删除 + 云端数据清除)
撤回同意DELETE /users/me/account注销账户即撤回所有同意

账户注销流程:

void UserService::deleteAccount(const QString &userId,
                                 const QString &password) {
    // Step 1: 再次验证密码
    if (!passwordService_.verifyPassword(password, userId)) {
        throw AuthException("密码错误");
    }

    // Step 2: 写入软删除标记
    repository_->softDeleteUser(userId);

    // Step 3: 撤销所有 Token
    tokenService_.revokeAllUserTokens(userId);

    // Step 4: 请求云端删除个人数据(30天内完成)
    apiClient_.asyncPost("/users/me/request-deletion", {});

    // Step 5: 删除本地数据
    localDataService_.deleteAllUserData(userId);

    // Step 6: 发送确认邮件
    emailService_.sendAccountDeletionConfirmation(userId);
}

十、安全设计清单

10.1 开发阶段安全检查
检查项优先级说明
密码使用 Argon2id 哈希P0禁止 MD5/SHA1
HTTPS/WSS 全链路加密P0禁止 HTTP 明文
Token 存储在 KeychainP0禁止存储在 SharedPreferences
家庭数据隔离校验P0Service 层必须校验 family_id
核销权限与掌勺人绑定P0can_confirm_menu 不能用于核销
禁止 SQL 拼接P0必须使用参数化查询
禁止 URL 传密码P0使用请求体
日志禁止打印敏感信息P1密码、Token、完整身份证号等
输入校验P1长度、格式、范围
错误信息脱敏P1不暴露服务端路径、SQL 错误
10.2 发布前安全检查
检查项说明
HTTPS 证书有效检查过期时间
HSTS 头配置Strict-Transport-Security
证书固定客户端已配置
API 限流配置Rate Limiting 已启用
审计日志关键操作已记录
Token 过期时间access_token ≤ 1 小时
安全测试通过权限越权测试 100% 通过
隐私协议更新描述数据采集和使用方式