fix: 修复后端 8 项审计问题 (P1/P2) #51
Regular → Executable
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
# ==================== 应用配置 ====================
|
||||
APP_NAME=小虾 SaaS
|
||||
BASE_URL=http://localhost:3000
|
||||
APP_BASE_URL=http://localhost:3000
|
||||
|
||||
# ==================== 数据库配置 ====================
|
||||
DATABASE_URL=postgresql://xiaoxia_user:your_password@localhost:5432/xiaoxia_saas
|
||||
|
||||
Regular → Executable
+7
-6
@@ -45,12 +45,13 @@ CELERY_WORKER_MAX_TASKS_PER_CHILD=1000
|
||||
# =======================
|
||||
# MinIO 对象存储配置
|
||||
# =======================
|
||||
MINIO_ENDPOINT=47.98.113.167:9000
|
||||
MINIO_ACCESS_KEY=CHANGE_ME_PRODUCTION
|
||||
MINIO_SECRET_KEY=CHANGE_ME_PRODUCTION
|
||||
MINIO_BUCKET=xiaoxia-assets
|
||||
MINIO_SECURE=false
|
||||
MINIO_PUBLIC_URL=http://47.98.113.167:9000
|
||||
# OSS 对象存储配置 (原 MINIO_* 配置已统一为 OSS_* 命名)
|
||||
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
|
||||
OSS_ACCESS_KEY_ID=CHANGE_ME_PRODUCTION
|
||||
OSS_ACCESS_KEY_SECRET=CHANGE_ME_PRODUCTION
|
||||
OSS_BUCKET_NAME=xiaoxia-assets
|
||||
OSS_SECURE=false
|
||||
OSS_PUBLIC_URL=http://47.98.113.167:9000
|
||||
|
||||
# =======================
|
||||
# 日志配置
|
||||
|
||||
Regular → Executable
+7
-6
@@ -45,12 +45,13 @@ CELERY_WORKER_MAX_TASKS_PER_CHILD=1000
|
||||
# =======================
|
||||
# MinIO 对象存储配置
|
||||
# =======================
|
||||
MINIO_ENDPOINT=47.98.113.167:9000
|
||||
MINIO_ACCESS_KEY=CHANGE_ME_STAGING
|
||||
MINIO_SECRET_KEY=CHANGE_ME_STAGING
|
||||
MINIO_BUCKET=xiaoxia-assets
|
||||
MINIO_SECURE=false
|
||||
MINIO_PUBLIC_URL=http://47.98.113.167:9000
|
||||
# OSS 对象存储配置 (原 MINIO_* 配置已统一为 OSS_* 命名)
|
||||
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
|
||||
OSS_ACCESS_KEY_ID=CHANGE_ME_STAGING
|
||||
OSS_ACCESS_KEY_SECRET=CHANGE_ME_STAGING
|
||||
OSS_BUCKET_NAME=xiaoxia-assets
|
||||
OSS_SECURE=false
|
||||
OSS_PUBLIC_URL=http://47.98.113.167:9000
|
||||
|
||||
# =======================
|
||||
# 日志配置
|
||||
|
||||
Regular → Executable
+1
-1
@@ -93,7 +93,7 @@ async def register(
|
||||
):
|
||||
use_case = RegisterUserUseCase(
|
||||
user_repository=user_repository,
|
||||
base_url="http://localhost:3000",
|
||||
base_url=settings.APP_BASE_URL,
|
||||
email_service=email_service,
|
||||
)
|
||||
response, error = use_case.execute(
|
||||
|
||||
Regular → Executable
+11
-1
@@ -11,6 +11,11 @@ class Settings(BaseSettings):
|
||||
ENVIRONMENT: str = "development"
|
||||
DEBUG: bool = True
|
||||
|
||||
# 应用基础 URL,用于生成认证邮件中的链接
|
||||
# 开发环境默认 http://localhost:3000
|
||||
# 生产环境应通过环境变量 APP_BASE_URL 设置
|
||||
APP_BASE_URL: str = "http://localhost:3000"
|
||||
|
||||
# Container bind address; external expose is controlled by Docker/Nginx.
|
||||
API_HOST: str = "0.0.0.0" # nosec: B104
|
||||
API_PORT: int = 8000
|
||||
@@ -20,7 +25,7 @@ class Settings(BaseSettings):
|
||||
"postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas"
|
||||
)
|
||||
DATABASE_POOL_SIZE: int = 20
|
||||
DATABASE_MAX_OVERFLOW: int = 40
|
||||
DATABASE_MAX_OVERFLOW: int = 10 # 调整为合理值:pool_size(20) + max_overflow(10) = 最大30连接
|
||||
DATABASE_POOL_TIMEOUT: int = 30
|
||||
DATABASE_POOL_RECYLE: int = 3600
|
||||
USE_IN_MEMORY_DB: bool = False
|
||||
@@ -32,6 +37,11 @@ class Settings(BaseSettings):
|
||||
|
||||
# JWT secret key - MUST be set via environment variable, no default allowed
|
||||
JWT_SECRET_KEY: Optional[str] = None
|
||||
# 旧的 JWT secret key(用于密钥轮换期间验证旧 token)
|
||||
# 在密钥轮换时,先设置新密钥,旧密钥保留在此处直到所有旧 token 过期
|
||||
JWT_SECRET_KEY_OLD: Optional[str] = None
|
||||
# 密钥轮换天数(到达此天数后建议更换密钥)
|
||||
SECRET_ROTATION_DAYS: int = 90
|
||||
|
||||
@field_validator("JWT_SECRET_KEY", mode="before")
|
||||
@classmethod
|
||||
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
# 安全密钥轮换指南
|
||||
|
||||
本文档说明如何手动轮换小虾 SaaS 的安全密钥。
|
||||
|
||||
## 概述
|
||||
|
||||
为了保障系统安全,建议定期轮换以下密钥:
|
||||
- `JWT_SECRET_KEY` - JWT 签名密钥
|
||||
|
||||
## 配置项
|
||||
|
||||
在 `apps/api/app/config.py` 中定义:
|
||||
|
||||
```python
|
||||
# JWT secret key - MUST be set via environment variable, no default allowed
|
||||
JWT_SECRET_KEY: Optional[str] = None
|
||||
```
|
||||
|
||||
## 轮换流程
|
||||
|
||||
### 1. 准备新密钥
|
||||
|
||||
生成一个新的强随机密钥(至少 32 字符):
|
||||
|
||||
```bash
|
||||
# 使用 Python 生成
|
||||
python3 -c "import secrets; print(secrets.token_urlsafe(48))"
|
||||
|
||||
# 或使用 openssl
|
||||
openssl rand -base64 48
|
||||
```
|
||||
|
||||
### 2. 更新环境变量
|
||||
|
||||
在生产/预发布环境中更新环境变量文件:
|
||||
|
||||
**`.env.production` / `.env.staging`**:
|
||||
|
||||
```bash
|
||||
# 旧密钥(保留用于平滑迁移期间验证旧 token)
|
||||
JWT_SECRET_KEY_OLD=your-old-secret-key
|
||||
|
||||
# 新密钥
|
||||
JWT_SECRET_KEY=your-new-secret-key
|
||||
```
|
||||
|
||||
### 3. 通知用户(可选)
|
||||
|
||||
如果需要用户重新登录,可以提前发布公告。
|
||||
|
||||
### 4. 部署新版本
|
||||
|
||||
```bash
|
||||
# 使用 docker-compose 重启服务
|
||||
docker-compose down && docker-compose up -d
|
||||
|
||||
# 或使用 K8s
|
||||
kubectl rollout restart deployment/xiaoxia-api
|
||||
```
|
||||
|
||||
### 5. 验证
|
||||
|
||||
- 检查服务是否正常启动
|
||||
- 验证新用户可以正常登录
|
||||
- 确认 API 请求正常工作
|
||||
|
||||
### 6. 清理旧密钥
|
||||
|
||||
在确认所有功能正常后,可以移除旧密钥:
|
||||
|
||||
```bash
|
||||
# 编辑 .env.production
|
||||
# 删除 JWT_SECRET_KEY_OLD 行
|
||||
```
|
||||
|
||||
## 自动轮换支持
|
||||
|
||||
当前版本支持在环境变量中同时配置新旧密钥:
|
||||
|
||||
```python
|
||||
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY")
|
||||
JWT_SECRET_KEY_OLD: Optional[str] = os.getenv("JWT_SECRET_KEY_OLD")
|
||||
```
|
||||
|
||||
这允许:
|
||||
1. 先部署新密钥(旧 token 仍可用旧密钥验证)
|
||||
2. 等待旧 token 自然过期
|
||||
3. 清理旧密钥
|
||||
|
||||
## 推荐的轮换周期
|
||||
|
||||
| 环境 | 建议轮换周期 |
|
||||
|------|-------------|
|
||||
| 生产环境 | 每 90 天 |
|
||||
| 预发布环境 | 每 90 天 |
|
||||
| 开发环境 | 按需 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **不要在代码中硬编码密钥** - 始终使用环境变量
|
||||
2. **密钥强度** - 使用至少 32 字符的随机字符串
|
||||
3. **备份** - 在轮换前确保旧密钥有备份
|
||||
4. **监控** - 轮换后监控异常登录行为
|
||||
5. **零停机** - 推荐使用双密钥机制实现平滑迁移
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [环境配置指南](./ENVIRONMENT-CONFIG.md)
|
||||
- [生产部署清单](./PRODUCTION-CHECKLIST.md)
|
||||
@@ -63,7 +63,11 @@ class AssetModel(Base):
|
||||
asset_library_id = Column(String(36), nullable=False, index=True)
|
||||
name = Column(String(500), nullable=False)
|
||||
file_type = Column(String(20), nullable=False, index=True)
|
||||
file_size = Column(Float, nullable=False)
|
||||
# storage_key: OSS 对象键(相对路径),用于内部存储和操作
|
||||
storage_key = Column(String(255), nullable=False)
|
||||
# file_size: 文件大小(字节),使用 Integer 类型以确保精确性
|
||||
file_size = Column(Integer, nullable=False)
|
||||
# file_url: 完整可访问的 URL,用于客户端直接访问文件
|
||||
file_url = Column(String(1000), nullable=False)
|
||||
thumbnail_url = Column(String(1000), nullable=True)
|
||||
duration = Column(Float, nullable=True)
|
||||
@@ -194,8 +198,10 @@ class GeneratedVideoModel(Base):
|
||||
project_id = Column(String(32), nullable=False, index=True)
|
||||
generation_task_id = Column(String(32), nullable=False, index=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
# file_url: 完整可访问的 URL,用于客户端直接访问视频
|
||||
file_url = Column(String(1000), nullable=False)
|
||||
file_size = Column(Float, nullable=False)
|
||||
# file_size: 文件大小(字节),使用 Integer 类型以确保精确性
|
||||
file_size = Column(Integer, nullable=False)
|
||||
duration = Column(Float, nullable=False)
|
||||
thumbnail_url = Column(String(1000), nullable=True)
|
||||
width = Column(Float, nullable=False)
|
||||
|
||||
Regular → Executable
+9
-13
@@ -248,20 +248,16 @@ class RefreshTokenUseCase:
|
||||
"""
|
||||
通过 refresh_token 查找 session
|
||||
|
||||
遍历所有 session 查找匹配的 refresh_token(生产环境应使用索引优化)
|
||||
使用 Redis 中的反向索引 (refresh_token -> session_id) 快速查找 session。
|
||||
反向索引在 save_session 时创建,确保了 O(1) 的查找复杂度。
|
||||
|
||||
Args:
|
||||
refresh_token: 刷新令牌
|
||||
|
||||
Returns:
|
||||
Session 数据字典,包含 session_id, user_id 等信息;如果不存在返回 None
|
||||
"""
|
||||
# Scan Redis for all session keys and check refresh_token
|
||||
# This is a simplified implementation - in production, use a reverse index
|
||||
# e.g., store refresh_token -> session_id mapping in Redis
|
||||
|
||||
# For now, iterate through user sessions
|
||||
# In a real implementation, you would maintain a reverse index:
|
||||
# refresh_token:session_id -> session_id
|
||||
# or use Redis SCAN to find sessions with matching refresh_token
|
||||
|
||||
# Simplified: iterate users (not scalable for production)
|
||||
# In production, add: session_store.save_refresh_token_index(refresh_token, session_id)
|
||||
return None # Placeholder - implement with reverse index in production
|
||||
return self.session_store.get_session_by_refresh_token(refresh_token)
|
||||
|
||||
|
||||
class LogoutRequest:
|
||||
|
||||
Regular → Executable
+10
-1
@@ -23,10 +23,19 @@ class RequestPasswordResetUseCase:
|
||||
def __init__(
|
||||
self,
|
||||
user_repository,
|
||||
base_url: str = "http://localhost:3000",
|
||||
base_url: str,
|
||||
token_expire_hours: int = 1,
|
||||
email_service=None,
|
||||
):
|
||||
"""
|
||||
初始化请求密码重置用例
|
||||
|
||||
Args:
|
||||
user_repository: 用户仓储
|
||||
base_url: 应用基础 URL(用于生成重置链接),必须由调用方传入
|
||||
token_expire_hours: 令牌过期时间(小时)
|
||||
email_service: 邮件服务
|
||||
"""
|
||||
self.user_repository = user_repository
|
||||
self.base_url = base_url
|
||||
self.token_expire_hours = token_expire_hours
|
||||
|
||||
Regular → Executable
+2
-2
@@ -52,7 +52,7 @@ class RegisterUserUseCase:
|
||||
def __init__(
|
||||
self,
|
||||
user_repository,
|
||||
base_url: str = "http://localhost:3000",
|
||||
base_url: str,
|
||||
email_service=None,
|
||||
):
|
||||
"""
|
||||
@@ -60,7 +60,7 @@ class RegisterUserUseCase:
|
||||
|
||||
Args:
|
||||
user_repository: 用户仓储
|
||||
base_url: 应用基础 URL(用于生成验证链接)
|
||||
base_url: 应用基础 URL(用于生成验证链接),必须由调用方传入
|
||||
"""
|
||||
self.user_repository = user_repository
|
||||
self.base_url = base_url
|
||||
|
||||
Reference in New Issue
Block a user