安全设计文档
智能家庭饮食管家 App - 安全设计文档
最后更新: 2026-05-18
一、文档修订记录
| 版本 | 日期 | 作者 | 修改说明 |
|---|---|---|---|
| v1.0 | 2026-05-18 | 初始版本,定义数据安全与权限安全完整设计方案 | |
| v1.1 | 2026-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,随机生成)
- 内存消耗:64 MB(
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);
}
安全存储规范:
| 数据 | 存储位置 | 加密方式 |
|---|---|---|
| 登录密码 | 云端 PostgreSQL | Argon2id 哈希(不可逆) |
| 本地 Token | Qt Keychain/iOS Keychain/Android Keystore | 平台安全存储 |
| 本地 SQLite | 应用私有目录 | SQLite Encryption Extension |
| 缓存数据 | 应用私有目录 | 不加密(无敏感数据) |
禁止行为:
- ❌ 禁止在日志中打印密码或密码哈希
- ❌ 禁止在 URL 参数中传递密码
- ❌ 禁止将密码以明文或 Base64 编码存储
- ❌ 禁止使用 MD5/SHA1 等弱哈希算法存储密码
3.2 通信安全
传输层加密:
| 通信类型 | 协议 | TLS 版本 | 证书要求 |
|---|---|---|---|
| REST API | HTTPS | TLS 1.2+ | 有效 CA 证书 |
| WebSocket | WSS | TLS 1.2+ | 有效 CA 证书 |
| 资源下载 | HTTPS | TLS 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 | ✅ HTTPS | N/A |
| 中敏感 | 身高/体重/年龄、TDEE、营养档案 | ❌(业务数据) | ✅ HTTPS | ✅ 仅本人可见 |
| 低敏感 | 菜谱名称、家庭名称 | ❌ | ✅ HTTPS | ❌ |
| 公开数据 | 系统内置菜谱/食材 | ❌ | ✅ HTTPS | ❌ |
用户身体数据保护:
用户的身高、体重、年龄、TDEE 等数据属于个人健康数据,必须严格保护:
- 访问控制:仅用户本人和同一家庭成员可访问(已加入家庭的成员)
- 接口校验:获取他人档案时,必须校验
family_id关联 - 脱敏展示:在家庭成员列表中,不展示具体体重,仅展示”健康目标”
- 数据隔离:不同家庭的数据严格隔离,无跨家庭查询能力
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 | 短期令牌,访问 API | 15 分钟 |
| refresh_token | 长期令牌,刷新 access_token | 7 天 |
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 安全存储:
| 存储位置 | Android | iOS | Desktop (Qt) |
|---|---|---|---|
| 安全存储 | Android Keystore | iOS Keychain | Qt Keychain |
| 加密方式 | AES-256-GCM | Keychain Services | platform-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_menu | checkMenuConfirmationPermission() |
POST /families/{id}/inventory/batches | 掌勺人或户主或 can_manage_inventory | checkInventoryPermission() |
DELETE /families/{id} | 户主 | checkIsOwner() |
PUT /families/{id}/transfer-ownership | 户主 | checkIsOwner() |
POST /wishlist/{id}/adopt | 掌勺人/户主或 can_confirm_menu | checkMenuConfirmationPermission() |
5.2 家庭数据隔离
核心原则:用户只能访问自己家庭的数据,无法访问其他家庭的数据。
隔离策略:
| 隔离维度 | 实现方式 | 防护级别 |
|---|---|---|
| 家庭级隔离 | 所有数据查询强制携带 family_id | P0 |
| 跨家庭查询拦截 | 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/batches | owner 或 can_manage_inventory | family_id 匹配 |
POST /daily-menus/{id}/complete-cooking | owner 或 chef_id 匹配 | menu.family_id 匹配 |
PUT /families/{id}/members/{userId}/permissions | owner | family_id 匹配 |
DELETE /families/{id} | owner | family_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:00 | 30 天 | ✅ 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 存储在 Keychain | P0 | 禁止存储在 SharedPreferences |
| 家庭数据隔离校验 | P0 | Service 层必须校验 family_id |
| 核销权限与掌勺人绑定 | P0 | can_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% 通过 |
| 隐私协议更新 | 描述数据采集和使用方式 |