签入版本

This commit is contained in:
2026-09-12 18:57:19 +08:00
parent d45b61d808
commit 2857e0f2da
295 changed files with 20713 additions and 13125 deletions
+6 -6
View File
@@ -46,7 +46,7 @@ from config.app import ( # noqa: E402
from config.database import ( # noqa: E402
DatabaseConfig, DB_PREFIX, DB_DRIVER, DB_HOST, DB_PORT, DB_DATABASE,
DB_USERNAME, DB_PASSWORD, DB_DEBUG, DB_SSL, DATABASE_URL,
DB_USERNAME, DB_PASSWORD, DB_DEBUG, DB_SSL, DATABASE_URL, friendly_db_error,
)
# Redis 缓存配置
@@ -59,8 +59,8 @@ from config.redis import ( # noqa: E402
# JWT + 权限 + 白名单配置
from config.auth import ( # noqa: E402
JWT_SALT, JWT_EXPIRE_MINUTES, LOGIN_FAIL_MAX, LOGIN_LOCK_SECONDS,
PERMISSION_ENABLED, INTERNAL_API_KEY,
JWT_SALT, JWT_EXPIRE_MINUTES, JWT_ABSOLUTE_TIMEOUT_MINUTES,
LOGIN_FAIL_MAX, LOGIN_LOCK_SECONDS, PERMISSION_ENABLED, INTERNAL_API_KEY,
INTERNAL_ALLOWED_IPS, LOGIN_WHITELIST_EXACT, LOGIN_WHITELIST_PREFIX,
LOGIN_WHITELIST_WILDCARD, LOGIN_WHITELIST_DEBUG, LOGIN_WHITELIST_LOG_ACCESS,
CAPTCHA_BYPASS_CODE,
@@ -108,15 +108,15 @@ __all__ = [
# 数据库配置
'DatabaseConfig', 'DB_PREFIX', 'DB_DRIVER', 'DB_HOST', 'DB_PORT', 'DB_DATABASE',
'DB_USERNAME', 'DB_PASSWORD', 'DB_DEBUG', 'DB_SSL', 'DATABASE_URL',
'DB_USERNAME', 'DB_PASSWORD', 'DB_DEBUG', 'DB_SSL', 'DATABASE_URL', 'friendly_db_error',
# Redis 配置
'REDIS_HOST', 'REDIS_PORT', 'REDIS_PASSWORD', 'REDIS_INDEX',
'REDIS_AUTH', 'REDIS_DECODE_RESPONSES', 'REDIS_URL',
# JWT + 权限 + 白名单配置
'JWT_SALT', 'JWT_EXPIRE_MINUTES', 'LOGIN_FAIL_MAX', 'LOGIN_LOCK_SECONDS',
'PERMISSION_ENABLED', 'INTERNAL_API_KEY',
'JWT_SALT', 'JWT_EXPIRE_MINUTES', 'JWT_ABSOLUTE_TIMEOUT_MINUTES',
'LOGIN_FAIL_MAX', 'LOGIN_LOCK_SECONDS', 'PERMISSION_ENABLED', 'INTERNAL_API_KEY',
'INTERNAL_ALLOWED_IPS', 'LOGIN_WHITELIST_EXACT', 'LOGIN_WHITELIST_PREFIX',
'LOGIN_WHITELIST_WILDCARD', 'LOGIN_WHITELIST_DEBUG', 'LOGIN_WHITELIST_LOG_ACCESS',
'CAPTCHA_BYPASS_CODE',
+1 -1
View File
@@ -21,7 +21,7 @@ FLASK_NAME = os.getenv('FLASK_NAME', 'DEMO')
# 应用版本号 - 用于版本追踪、API版本控制和更新检测
# 格式遵循语义化版本规范 v主版本.次版本.补丁版本
FLASK_VERSION = os.getenv('FLASK_VERSION', 'v2.8.0')
FLASK_VERSION = os.getenv('FLASK_VERSION', 'v3.0.0')
# 应用秘钥 - 用于Session加密、CSRF保护、签名生成
# 生产环境务必使用强随机字符串,长度至少32字符
+22 -2
View File
@@ -20,9 +20,22 @@ logger = logging.getLogger(__name__)
# 从环境变量读取,未配置则拒绝启动,防止使用已知弱密钥
JWT_SALT = os.getenv('JWT_SALT', '')
# JWT令牌过期时间(分钟)
# 滑动会话空闲超时(分钟):用户在此时间内无任何请求,会话过期需重新登录。
# 每收到一次有效请求,Redis 会话 TTL 被重置为该值(sliding session)。
JWT_EXPIRE_MINUTES = int(os.getenv('JWT_EXPIRE_MINUTES', 20))
# JWT 令牌绝对有效期(分钟):即使一直活跃,令牌最长有效时间,到期强制重新登录。
# 防止会话无限续期,需大于 JWT_EXPIRE_MINUTES(默认 1440 = 24小时)。
JWT_ABSOLUTE_TIMEOUT_MINUTES = int(os.getenv('JWT_ABSOLUTE_TIMEOUT_MINUTES', 1440))
# 启动校验:绝对有效期须 ≥ 空闲超时,否则滑动会话模型失效(一直活跃也会被绝对期限打断)
if JWT_ABSOLUTE_TIMEOUT_MINUTES < JWT_EXPIRE_MINUTES:
raise RuntimeError(
f"JWT_ABSOLUTE_TIMEOUT_MINUTES({JWT_ABSOLUTE_TIMEOUT_MINUTES}) "
f"不能小于 JWT_EXPIRE_MINUTES({JWT_EXPIRE_MINUTES}),"
"绝对有效期须大于等于滑动会话空闲超时"
)
# 登录失败次数上限(同一IP+用户名连续失败超过此次数后锁定)
LOGIN_FAIL_MAX = int(os.getenv('LOGIN_FAIL_MAX', 5))
@@ -40,12 +53,19 @@ PERMISSION_ENABLED = os.getenv('PERMISSION_ENABLED', 'True') == 'True'
# 调用方需在请求头中携带 X-Internal-API-Key 或 X-Scheduler-Key
INTERNAL_API_KEY = os.getenv('INTERNAL_API_KEY', '')
# 启动时校验:未配置则拒绝启动,防止定时任务权限验证失效
# 启动时校验:该密钥是权限全绕过的凭据,必须为强随机值,不符合则 fail-closed 拒绝启动。
# 规则化校验(长度 ≥32)而非黑名单枚举,任何弱值/新弱值都会被拦截。
if not INTERNAL_API_KEY:
raise RuntimeError(
"INTERNAL_API_KEY 未配置!请在 .env 中设置内部调用密钥。\n"
"生成方式:python -c \"import secrets; print(secrets.token_urlsafe(32))\""
)
if len(INTERNAL_API_KEY) < 32:
raise RuntimeError(
f"INTERNAL_API_KEY 长度不足 32 位(当前 {len(INTERNAL_API_KEY)} 位),"
"拒绝启动(fail-closed)。请更换为强随机密钥:\n"
"python -c \"import secrets; print(secrets.token_urlsafe(32))\""
)
# 内部调用白名单IP(逗号分隔,支持通配符如 127.0.0.*)
# 在此列表中的IP发起的请求将跳过权限验证
+127 -15
View File
@@ -1,21 +1,22 @@
# +======================================================================
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
# +----------------------------------------------------------------------
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
# | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有
# +----------------------------------------------------------------------
# | 开源协议: Apache-2.0
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
# +----------------------------------------------------------------------
# | 官方网站: https://www.djangoadmin.cn
# +----------------------------------------------------------------------
# | 研发团队: 一米阳光团队 荣誉出品
# | 研发团队: 小蚂蚁云团队 荣誉出品
# +======================================================================
"""
数据库配置模块
=============================================================
负责配置 MySQL 数据库连接及相关参数,包含连接池、SSL 加密等。
负责配置数据库连接及相关参数,包含连接池、SSL 加密等,支持多数据库驱动:
mysql(默认)/ postgresql(pgsql)/ mssql / sqlite / oracle。
依赖:config/__init__.py 中的 _load_env_file() 已提前加载 .env
"""
@@ -27,12 +28,46 @@ from config.app import FLASK_SECRET_KEY, FLASK_UPLOAD_DIR
# 数据库环境变量
# ============================================================
# 数据库驱动(mysql / pgsql / sqlite)
DB_DRIVER = os.getenv('DB_DRIVER', 'mysql')
def _normalize_driver(driver: str) -> str:
"""驱动别名归一化:别名 → 规范名;已是规范名的驱动原样返回。
- 规范名:mysql / postgresql / mssql / sqlite / oracle
- 别名:postgres、pgsql → postgresql;sqlserver → mssql;sqlite3 → sqlite
- oracle 本身即规范名,无需映射(经 fallback 直接透传),表中显式列出便于自文档化
与 scripts/migrate_db.py 共用(make_url 也调用),保证 .env / CLI 两端别名一致。
"""
return {
'postgres': 'postgresql',
'pgsql': 'postgresql',
'sqlserver': 'mssql',
'sqlite3': 'sqlite',
'oracle': 'oracle', # 规范名之一,显式列出便于自文档化
}.get(str(driver).lower(), str(driver).lower())
def _sqlite_url(database: str) -> str:
"""SQLite 连接 URL:DB_DATABASE 即文件路径(内存库用 ':memory:',Windows 反斜杠转正斜杠)。"""
if database == ':memory:':
return 'sqlite://'
return f"sqlite:///{database.replace(chr(92), '/')}"
# 数据库驱动(mysql / postgresql / mssql / sqlite / oracle,支持别名见 _normalize_driver)
DB_DRIVER = _normalize_driver(os.getenv('DB_DRIVER', 'mysql'))
# 数据库地址
DB_HOST = os.getenv('DB_HOST', '127.0.0.1')
# 数据库端口(MySQL默认3306,PostgreSQL默认5432)
DB_PORT = int(os.getenv('DB_PORT', 3306))
# 各驱动默认端口(未显式配置 DB_PORT 时按驱动回退)
_DEFAULT_PORT = {
'mysql': 3306,
'pgsql': 5432,
'postgresql': 5432,
'mssql': 1433,
'oracle': 1521,
'sqlite': 0, # sqlite 无端口
}
# 数据库端口(MySQL默认3306,PostgreSQL默认5432,MSSQL默认1433,Oracle默认1521)
DB_PORT = int(os.getenv('DB_PORT', _DEFAULT_PORT.get(DB_DRIVER, 3306)))
# 数据库名称
DB_DATABASE = os.getenv('DB_DATABASE', 'djangoadmin.flask.elevue')
# 数据库账号
@@ -67,8 +102,87 @@ DB_POOL_TIMEOUT = int(os.getenv('DB_POOL_TIMEOUT', 30))
# 使用前是否检测连接存活(默认True,防止使用已被MySQL关闭的连接)
DB_POOL_PRE_PING = os.getenv('DB_POOL_PRE_PING', 'True') == 'True'
# 方言 → SQLAlchemy URL 前缀映射(多库支持:只改 DB_DRIVER + 账号即可切换)
_DB_URI_PREFIX = {
'mysql': 'mysql+pymysql',
'pgsql': 'postgresql+psycopg2',
'postgresql': 'postgresql+psycopg2',
'mssql': 'mssql+pymssql',
'sqlite': 'sqlite',
'oracle': 'oracle+oracledb',
}
def _build_database_url() -> str:
"""
按 DB_DRIVER 构建数据库连接 URL。
支持:mysql(默认)/ postgresql / mssql / sqlite / oracle。
各库差异(端口、账号、ssl、charset、sqlite 无 host、oracle 用 SERVICE_NAME)在此统一封装。
"""
prefix = _DB_URI_PREFIX.get(DB_DRIVER, 'mysql+pymysql')
# SQLite:无 host/port/账号,DB_DATABASE 即文件路径(内存库用 ':memory:')
if DB_DRIVER == 'sqlite':
return _sqlite_url(DB_DATABASE)
cred = f"{quote_plus(DB_USERNAME)}:{quote_plus(DB_PASSWORD)}@{DB_HOST}:{DB_PORT}"
# Oracle:无"数据库名",DB_DATABASE 即 SERVICE_NAME(如 XEPDB1),走 URL 查询参数
if DB_DRIVER == 'oracle':
return f"{prefix}://{cred}/?service_name={quote_plus(DB_DATABASE)}"
url = f"{prefix}://{cred}/{DB_DATABASE}"
# MySQL(含未知驱动回退到 mysql):显式 utf8mb4 保证中文正确
if prefix == 'mysql+pymysql':
url += "?charset=utf8mb4"
# PostgreSQL:SSL 走 URL 参数(MySQL/MSSQL 的 SSL 在 _build_engine_options / pymssql 内配置)
elif DB_DRIVER in ('pgsql', 'postgresql') and DB_SSL:
url += "?sslmode=require"
return url
# 数据库连接URL(方便SQLAlchemy直接使用)
DATABASE_URL = f'mysql+pymysql://{quote_plus(DB_USERNAME)}:{quote_plus(DB_PASSWORD)}@{DB_HOST}:{DB_PORT}/{DB_DATABASE}'
DATABASE_URL = _build_database_url()
def friendly_db_error(e: Exception) -> str:
"""
把数据库连接异常翻译成可读的中文提示。
处理 psycopg2 在中文 Windows 下返回 GBK 错误消息、被按 UTF-8 解码抛出
UnicodeDecodeError 的怪癖:先还原字节并按 GBK 解码,再匹配常见错误。
"""
raw = str(e)
if isinstance(e, UnicodeDecodeError):
# UnicodeDecodeError.args = (encoding, bytes, start, end, reason);args[1] 是原始字节
try:
raw = e.args[1].decode('gbk', errors='ignore')
except Exception:
raw = str(e)
low = raw.lower()
if any(k in low for k in ('password authentication failed', '28p01', '认证失败', '密码错误', 'ora-01017')):
return '数据库认证失败:用户名或密码错误,请检查 .env 的 DB_USERNAME / DB_PASSWORD'
# 数据库不存在(PG SQLSTATE 3d000 / "database ... does not exist" / "数据库...不存在")
if '3d000' in low or ('database' in low and 'does not exist' in low) or ('数据库' in raw and '不存在' in raw):
return '数据库不存在:请先在目标库创建对应数据库(如 CREATE DATABASE <库名>)'
# 列/表类型不兼容(PG "type ... does not exist" / "类型...不存在"),区别于"数据库不存在"
if ('type' in low and 'does not exist' in low) or ('类型' in raw and '不存在' in raw):
return '目标表结构或列类型与数据库不兼容:请检查模型列类型(如 DATETIME 需用通用 DateTime)'
# Oracle 服务名/监听解析(ORA-12514 未知服务、ORA-12505 未知 SID、ORA-12154 无法解析连接标识符)
if any(k in low for k in ('ora-12514', 'ora-12505', 'ora-12154', 'could not resolve the connect identifier')):
return 'Oracle 服务名/监听解析失败:请检查 .env 的 DB_DATABASE(Oracle 填 SERVICE_NAME,如 XEPDB1)与监听配置'
if any(k in low for k in ('connection refused', '拒绝连接', 'cannot connect', 'target actively refused',
'ora-12541', 'ora-12560', 'ora-12518', 'ora-12170', 'ora-01034',
'oracle not available', 'no listener')):
return '数据库连接被拒绝:请检查 DB_HOST / DB_PORT 与数据库服务是否启动'
if any(k in low for k in ('timeout', '超时', 'timed out')):
return '数据库连接超时:请检查网络与 DB_HOST / DB_PORT'
if any(k in low for k in ('no module named', 'is not installed', '驱动未安装')):
return '数据库驱动未安装:请执行 pip install -r requirements.txt 安装对应驱动'
return f'数据库连接失败:{raw}'
def _build_engine_options():
@@ -86,7 +200,9 @@ def _build_engine_options():
'pool_timeout': DB_POOL_TIMEOUT,
'pool_pre_ping': DB_POOL_PRE_PING,
}
if DB_SSL:
# SSL:MySQL 走 connect_args;PG 走 URL sslmode(_build_database_url);MSSQL 由 pymssql 处理;
# Oracle:oracledb thin 模式默认走系统 CA,TLS 用 URL tcp+tls 或在 connect_args 传 ssl_context,本处不做额外处理
if DB_SSL and DB_DRIVER == 'mysql':
ssl_config = {}
if DB_SSL_CA:
ssl_config['ca'] = DB_SSL_CA
@@ -134,14 +250,10 @@ class DatabaseConfig:
WTF_CSRF_ENABLED = False
# ============================================================
# MySQL 数据库配置
# 数据库连接配置(按 DB_DRIVER 驱动切换,见 _build_database_url)
# ============================================================
# 构建 MySQL 连接 URI(使用 PyMySQL 驱动)
SQLALCHEMY_DATABASE_URI = (
f'mysql+pymysql://{quote_plus(DB_USERNAME)}:{quote_plus(DB_PASSWORD)}@'
f'{DB_HOST}:{DB_PORT}/{DB_DATABASE}'
)
SQLALCHEMY_DATABASE_URI = DATABASE_URL
# 不追踪对象修改(提升性能)
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 是否打印 SQL 语句(调试模式下开启)