seaislee1209 afcff9455f feat: v0.12.1 安全加固补充 + 短信测试按钮
①Refresh Token 轮换(ROTATE_REFRESH_TOKENS + BLACKLIST_AFTER_ROTATION)
②前端 token 刷新时保存新 refresh token(auth store + axios 拦截器)
③短信告警测试按钮(/admin/test-sms + 系统设置页按钮)
④安全审查完成:S2 git 历史无泄露、S4 无攻击面、S7 nginx 已配、S10 全接口有权限

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:38:42 +08:00

225 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Django settings for AirDrama backend."""
import os
from pathlib import Path
from datetime import timedelta
BASE_DIR = Path(__file__).resolve().parent.parent
# 自动加载 .env.local本地开发用不进 git
_env_local = BASE_DIR / '.env.local'
if _env_local.exists():
with open(_env_local, encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
# 去掉 export 前缀
if line.startswith('export '):
line = line[7:]
key, _, value = line.partition('=')
if key and _ == '=':
# 去掉引号
value = value.strip().strip('"').strip("'")
os.environ.setdefault(key.strip(), value)
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', '')
if not SECRET_KEY:
import warnings
warnings.warn('DJANGO_SECRET_KEY not set — using insecure dev key. Do NOT deploy like this.')
SECRET_KEY = 'django-insecure-dev-only-do-not-deploy'
DEBUG = os.environ.get('DJANGO_DEBUG', 'False').lower() in ('true', '1', 'yes')
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party
'rest_framework',
'rest_framework_simplejwt.token_blacklist',
'corsheaders',
# Local apps
'apps.accounts',
'apps.generation',
]
MIDDLEWARE = [
'utils.log_center.LogCenterMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database configuration
# TESTING=true → isolated test_db.sqlite3 (never pollutes dev data)
# USE_MYSQL=true → Aliyun RDS MySQL (production)
# Otherwise → SQLite db.sqlite3 (local dev)
TESTING = os.environ.get('TESTING', 'false').lower() in ('true', '1', 'yes')
if TESTING:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'test_db.sqlite3',
}
}
elif os.environ.get('USE_MYSQL', 'false').lower() in ('true', '1', 'yes'):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DB_NAME', 'video_auto'),
'USER': os.environ.get('DB_USER', 'ai_video'),
'PASSWORD': os.environ.get('DB_PASSWORD', ''),
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '3306'),
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
AUTH_USER_MODEL = 'accounts.User'
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 8}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# REST Framework
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'apps.accounts.authentication.SessionJWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
),
'EXCEPTION_HANDLER': 'utils.log_center.custom_exception_handler',
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '30/minute',
'user': '120/minute',
'login': '5/minute',
},
}
# JWT settings
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'AUTH_HEADER_TYPES': ('Bearer',),
}
# CORS
CORS_ALLOWED_ORIGINS = [
'http://localhost:5173',
'http://127.0.0.1:5173',
'http://localhost:3000',
]
_extra_cors = os.environ.get('CORS_ALLOWED_ORIGINS', '')
if _extra_cors:
CORS_ALLOWED_ORIGINS += [o.strip() for o in _extra_cors.split(',') if o.strip()]
CORS_ALLOW_CREDENTIALS = True
CSRF_TRUSTED_ORIGINS = [o for o in CORS_ALLOWED_ORIGINS if o.startswith('https://')]
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = True
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# ──────────────────────────────────────────────
# IP Geolocation
# ──────────────────────────────────────────────
ALIYUN_IP_GEO_APPCODE = os.environ.get('ALIYUN_IP_GEO_APPCODE', '93a86e9dfc9e4c71bcd44baa4008e662')
IP2REGION_DB_PATH = BASE_DIR / 'data' / 'ip2region.xdb'
# ──────────────────────────────────────────────
# Security headers (production)
# ──────────────────────────────────────────────
if not DEBUG:
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# ──────────────────────────────────────────────
# TOS (Volcano Engine Object Storage)
# ──────────────────────────────────────────────
TOS_ACCESS_KEY = os.environ.get('TOS_ACCESS_KEY', '')
TOS_SECRET_KEY = os.environ.get('TOS_SECRET_KEY', '')
TOS_ENDPOINT = os.environ.get('TOS_ENDPOINT', 'https://tos-cn-beijing.volces.com')
TOS_BUCKET = os.environ.get('TOS_BUCKET', 'airdrama-media')
TOS_REGION = os.environ.get('TOS_REGION', 'cn-beijing')
TOS_CDN_DOMAIN = os.environ.get('TOS_CDN_DOMAIN', 'https://airdrama-media.tos-cn-beijing.volces.com')
# ──────────────────────────────────────────────
# Seedance API (Volcano Engine ARK)
# ──────────────────────────────────────────────
ARK_API_KEY = os.environ.get('ARK_API_KEY', '')
ARK_BASE_URL = os.environ.get('ARK_BASE_URL', 'https://ark.cn-beijing.volces.com/api/v3')
# Set to True when Seedance model is activated on ARK platform
SEEDANCE_ENABLED = os.environ.get('SEEDANCE_ENABLED', 'false').lower() == 'true'
# Set to True to enable the Assets API (virtual avatar library)
ASSETS_API_ENABLED = os.environ.get('ASSETS_API_ENABLED', 'false').lower() == 'true'
# ──────────────────────────────────────────────
# Aliyun SMS (短信告警)
# ──────────────────────────────────────────────
ALIYUN_SMS_ACCESS_KEY = os.environ.get('ALIYUN_SMS_ACCESS_KEY', '')
ALIYUN_SMS_ACCESS_SECRET = os.environ.get('ALIYUN_SMS_ACCESS_SECRET', '')
ALIYUN_SMS_SIGN_NAME = os.environ.get('ALIYUN_SMS_SIGN_NAME', '广州气元科技')
ALIYUN_SMS_TEMPLATE_CODE = os.environ.get('ALIYUN_SMS_TEMPLATE_CODE', 'SMS_503445109')