签入版本
This commit is contained in:
+6
-6
@@ -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
@@ -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
@@ -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
@@ -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 语句(调试模式下开启)
|
||||
|
||||
Reference in New Issue
Block a user