From 1214bfdf4af866d806525ce7326cec746ed36604 Mon Sep 17 00:00:00 2001 From: bwstudio Date: Sat, 5 Sep 2026 20:46:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(ops):=20=E7=8E=AF=E5=A2=83=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=8C=96=E9=85=8D=E7=BD=AE=20+=20=E5=81=A5=E5=BA=B7?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=20+=20=E5=90=AF=E5=8A=A8=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=20+=20=E9=83=A8=E7=BD=B2/=E5=AE=89=E5=85=A8=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要变更: config - 引入 python-dotenv,从 .env 加载 SECRET_KEY / MAIL_* / SQLALCHEMY_DATABASE_URI - 拆分 BaseConfig / DevConfig / ProConfig,PEAR_ENV 控制切换 - ProConfig 在 init_app() 校验 SECRET_KEY,禁止使用默认值 ops - 新增 /healthz 端点:DB 探活 + 环境名 + 时间戳,失败返回 503 - 新增 start.bat (Windows) / Makefile (跨平台) 一键启动 / 初始化 - 新增 .env.example 模板;.gitignore 排除 .env / logs / vscode 等 - 新增 docs/DEPLOY.md(部署指南)和 docs/SECURITY.md(安全清单) - app.py 不再硬编码 debug=False,交给配置类决定 本提交不包含上一次会话的其它工作树修改(模板/导航相关),后续单独 PR。 --- .env.example | 40 +++++++++ .gitignore | 23 ++++- Makefile | 69 ++++++++++++++ app.py | 10 ++- applications/__init__.py | 34 +++++-- applications/config.py | 164 ++++++++++++++++++++++------------ applications/view/__init__.py | 7 +- applications/view/health.py | 71 +++++++++++++++ docs/DEPLOY.md | 164 ++++++++++++++++++++++++++++++++++ docs/SECURITY.md | 89 ++++++++++++++++++ requirements.txt | 3 +- start.bat | 63 +++++++++++++ 12 files changed, 665 insertions(+), 72 deletions(-) create mode 100644 .env.example create mode 100644 Makefile create mode 100644 applications/view/health.py create mode 100644 docs/DEPLOY.md create mode 100644 docs/SECURITY.md create mode 100644 start.bat diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c37547f --- /dev/null +++ b/.env.example @@ -0,0 +1,40 @@ +# ============================================================ +# Pear Admin Flask - 环境变量示例 +# ============================================================ +# 用法: +# 1) 复制本文件:cp .env.example .env +# 2) 按需修改 .env 中的值(不要提交 .env 到 git) +# 3) 应用启动时由 applications/__init__.py 自动 load_dotenv 读取 +# +# 加载优先级:系统环境变量 > .env > 代码默认值 +# ============================================================ + +# ---- 运行模式 ---- +# dev: 开发模式(DEBUG=True,自动生成 SECRET_KEY,无需外部传入) +# production: 生产模式(DEBUG=False,强制要求 SECRET_KEY) +PEAR_ENV=dev + +# ---- 安全 ---- +# 生产模式必填。可用以下命令生成: +# python -c "import secrets; print(secrets.token_urlsafe(48))" +SECRET_KEY= + +# ---- 数据库 ---- +# 默认是项目根目录下的 SQLite pear.db;生产推荐 MySQL。 +# MySQL 示例: +# SQLALCHEMY_DATABASE_URI=mysql+pymysql://user:pass@127.0.0.1:3306/pear?charset=utf8mb4 +# SQLALCHEMY_DATABASE_URI= + +# ---- 邮件(Flask-Mail)---- +# QQ 邮箱示例 +MAIL_SERVER=smtp.qq.com +MAIL_USE_TLS=false +MAIL_USE_SSL=true +MAIL_PORT=465 +MAIL_USERNAME=your_account@qq.com +MAIL_PASSWORD=your_authorization_code_here +MAIL_DEFAULT_SENDER=your_account@qq.com + +# ---- Flask ---- +# FLASK_APP=app.py +# FLASK_DEBUG=1 diff --git a/.gitignore b/.gitignore index 503480c..eb28e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,25 @@ static/upload/ flask_session/ # WorkBuddy 运行时目录(不应纳入版本管理) -.workbuddy/ \ No newline at end of file +.workbuddy/ + +# ---- Pear Admin Flask 额外忽略 ---- +# 私有环境变量模板:保留 .env.example 入库;本地 .env 不入 +.env +.env.* +!.env.example + +# 系统监控日志(如果产生) +*.pid +*.sock + +# 运行日志 +logs/ +*.log + +# pycharm / vscode +.vscode/ + +# 临时下载/构建产物 +tmp/ +build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..80d701b --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +# ============================================================ +# Pear Admin Flask - Makefile(跨平台,Windows 下需用 Git Bash / WSL) +# +# 用法(在项目根目录): +# make help 查看所有命令 +# make venv 创建虚拟环境 +# make install 安装依赖 +# make migrate 执行 db migrate + upgrade +# make seed 导入 admin 基础数据(app_data/init.json) +# make init 等价于 migrate + seed +# make run 开发模式启动(PEAR_ENV=dev) +# make run-prod 生产模式启动(PEAR_ENV=production,要求 .env SECRET_KEY) +# make clean 清理缓存与数据库(危险!请先备份) +# make health curl /healthz +# ============================================================ + +PY ?= python +VENV ?= venv +PYBIN := $(VENV)/bin/python +PIP := $(PYBIN) -m pip +FLASK := $(PYBIN) -m flask +APP := app.py + +.PHONY: help venv install migrate seed init run run-prod clean health + +help: + @echo "可用命令:" + @echo " make venv 创建虚拟环境" + @echo " make install 安装依赖" + @echo " make migrate db migrate + upgrade" + @echo " make seed flask admin init" + @echo " make init migrate + seed" + @echo " make run 开发模式启动(默认端口 5000)" + @echo " make run-prod 生产模式启动(强制要求 .env SECRET_KEY)" + @echo " make health curl http://127.0.0.1:5000/healthz" + @echo " make clean 清理 pyc / __pycache__ / pear.db / flask_session" + +venv: + @test -d "$(VENV)" || $(PY) -m venv $(VENV) + @echo "[venv] 已创建 $(VENV)" + +install: venv + $(PIP) install --upgrade pip + $(PIP) install -r requirements.txt + +migrate: + $(FLASK) db migrate + $(FLASK) db upgrade + +seed: + $(FLASK) admin init + +init: migrate seed + +run: + PEAR_ENV=dev $(PYBIN) $(APP) + +run-prod: + PEAR_ENV=production $(PYBIN) $(APP) + +health: + @curl -sS http://127.0.0.1:5000/healthz | python -m json.tool + +clean: + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete + rm -rf flask_session + @echo "[clean] 已清理缓存。注意:pear.db 请手动备份后删除。" diff --git a/app.py b/app.py index a5622a5..8841d94 100644 --- a/app.py +++ b/app.py @@ -3,4 +3,12 @@ from applications import create_app app = create_app() if __name__ == '__main__': - app.run() + # 绑定 0.0.0.0 后,同局域网手机可通过本机 IP 访问(例如 http://192.168.1.10:5000/site/) + # debug 标志交给配置类决定:PEAR_ENV=dev → DEBUG=True;PEAR_ENV=production → DEBUG=False + # 防止重载进程抢占端口;局域网访问保持稳定 + app.run( + host='0.0.0.0', + port=5000, + debug=app.config.get('DEBUG', False), + use_reloader=False, + ) diff --git a/applications/__init__.py b/applications/__init__.py index d2b460b..b4cf3ca 100644 --- a/applications/__init__.py +++ b/applications/__init__.py @@ -1,24 +1,40 @@ import os + +from dotenv import load_dotenv from flask import Flask + from applications.common.script import init_script -from applications.config import BaseConfig +from applications.config import BaseConfig, get_config_by_name from applications.extensions import init_plugs from applications.view import init_bps +# 项目根目录 = 本文件父目录的父目录 +BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + def create_app(): - app = Flask(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + # 1) 先加载 .env(在 BaseConfig 读取 os.environ 之前) + # override=False:环境变量优先级高于 .env,便于 CI/CD 在部署时再覆盖 + load_dotenv(os.path.join(BASE_DIR, ".env"), override=False) - # 引入配置 - app.config.from_object(BaseConfig) + app = Flask(BASE_DIR) - # 注册flask组件 + # 2) 根据 PEAR_ENV / FLASK_ENV 选择配置类,默认 dev + env_name = os.environ.get("PEAR_ENV") or os.environ.get("FLASK_ENV") or "dev" + config_cls = get_config_by_name(env_name) + app.config.from_object(config_cls) + + # 3) 把 BaseConfig 默认值里没写、但 .env 可能写了的 SQLALCHEMY_DATABASE_URI 等再覆盖一次 + if os.environ.get("SQLALCHEMY_DATABASE_URI"): + app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["SQLALCHEMY_DATABASE_URI"] + + # 3.5) 让配置类有机会做运行时校验(例如 ProConfig 强制要求 SECRET_KEY) + if hasattr(config_cls, "init_app"): + config_cls.init_app(app) + + # 4) 注册 flask 组件 / 蓝图 / 命令 init_plugs(app) - - # 注册蓝图 init_bps(app) - - # 注册命令 init_script(app) return app diff --git a/applications/config.py b/applications/config.py index b5369f0..88835de 100644 --- a/applications/config.py +++ b/applications/config.py @@ -1,84 +1,130 @@ import logging +import os +import secrets from datetime import timedelta -class BaseConfig: - # 超级管理员账号 - SUPERADMIN = 'admin' +def _env_bool(key: str, default: bool = False) -> bool: + """读取布尔环境变量,兼容 true/1/yes/on(大小写不敏感)。""" + val = os.environ.get(key) + if val is None: + return default + return val.strip().lower() in {"1", "true", "yes", "on"} - # 系统名称 + +def _env_str(key: str, default: str = "") -> str: + val = os.environ.get(key) + return val if val is not None and val != "" else default + + +class BaseConfig: + """通用配置:所有环境共有的项。子类按需覆盖。""" + + # ---- 应用基础 ---- + SUPERADMIN = 'admin' SYSTEM_NAME = 'Pear Admin' - # 主题面板的链接列表配置 SYSTEM_PANEL_LINKS = [ - { - "icon": "layui-icon layui-icon-auz", - "title": "官方网站", - "href": "http://www.pearadmin.com" - }, - { - "icon": "layui-icon layui-icon-auz", - "title": "开发文档", - "href": "http://www.pearadmin.com" - }, - { - "icon": "layui-icon layui-icon-auz", - "title": "开源地址", - "href": "https://gitee.com/Jmysy/Pear-Admin-Layui" - } + {"icon": "layui-icon layui-icon-auz", "title": "官方网站", "href": "http://www.pearadmin.com"}, + {"icon": "layui-icon layui-icon-auz", "title": "开发文档", "href": "http://www.pearadmin.com"}, + {"icon": "layui-icon layui-icon-auz", "title": "开源地址", "href": "https://gitee.com/Jmysy/Pear-Admin-Layui"}, ] - # 上传图片目标文件夹 + # ---- 上传 ---- UPLOADED_PHOTOS_DEST = 'static/upload' UPLOADED_FILES_ALLOW = ['gif', 'jpg', 'jpeg', 'png', 'webp'] UPLOADS_AUTOSERVE = True - # JSON 配置 + # ---- JSON ---- JSON_AS_ASCII = False - # 配置多个数据库连接的连接串写法示例 - # HOSTNAME: 指数据库的IP地址、USERNAME:指数据库登录的用户名、PASSWORD:指数据库登录密码、PORT:指数据库开放的端口、DATABASE:指需要连接的数据库名称 - # MSSQL: f"mssql+pymssql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=cp936" - # MySQL: f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8mb4" - # Oracle: f"oracle+cx_oracle://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}" - # SQLite "sqlite:/// database.db" - # Postgres f"postgresql+psycopg2://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}" - # Oracle的第二种连接方式 - # dsnStr = cx_Oracle.makedsn({HOSTNAME}, 1521, service_name='orcl') - # connect_str = "oracle://%s:%s@%s" % ('{USERNAME}', ' {PASSWORD}', dsnStr) - - # 在SQLALCHEMY_BINDS 中设置:'{数据库连接别名}': '{连接串}' - # 最后在models的数据模型class中,在__tablename__前设置 __bind_key__ = '{数据库连接别名}' 即可,表示该数据模型不使用默认的数据库连接,改用“SQLALCHEMY_BINDS”中设置的其他数据库连接 - # SQLALCHEMY_BINDS = { - # 'testMySQL': 'mysql+pymysql://test:123456@192.168.1.1:3306/test?charset=utf8', - # 'testMsSQL': 'mssql+pymssql://test:123456@192.168.1.1:1433/test?charset=cp936', - # 'testOracle': 'oracle+cx_oracle://test:123456@192.168.1.1:1521/test', - # 'testSQLite': 'sqlite:///database.db - # } - - # 数据库的配置信息 + # ---- 数据库 ---- + # 默认走项目根目录下的 pear.db(与 instance/ 同一级,Flask 2.x 行为)。 + # 生产环境推荐通过 SQLALCHEMY_DATABASE_URI 环境变量切换到 MySQL。 SQLALCHEMY_DATABASE_URI = 'sqlite:///../pear.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False - # 默认日志等级 + # ---- 日志 ---- LOG_LEVEL = logging.WARN - # 发信设置 - MAIL_SERVER = 'smtp.qq.com' - MAIL_USE_TLS = False - MAIL_USE_SSL = True - MAIL_PORT = 465 - MAIL_USERNAME = '123@qq.com' - MAIL_PASSWORD = 'XXXXX' # 生成的授权码 - MAIL_DEFAULT_SENDER = MAIL_USERNAME + # ---- Session ---- + PERMANENT_SESSION_LIFETIME = timedelta(days=7) + SESSION_TYPE = "filesystem" + SESSION_PERMANENT = False + SESSION_USE_SIGNER = True - # 插件配置,填写插件的文件名名称,默认不启用插件。 + # ---- 安全 / 密钥 ---- + # SECRET_KEY 的优先级: + # 1. 环境变量 SECRET_KEY + # 2. .env 中的 SECRET_KEY + # 3. 自动生成一个临时随机值(仅用于开发启动;生产模式强制要求外部传入) + SECRET_KEY = os.environ.get("SECRET_KEY") or "pear-system-flask" + + # ---- Mail(默认占位;环境变量可覆盖)---- + MAIL_SERVER = _env_str("MAIL_SERVER", "smtp.qq.com") + MAIL_USE_TLS = _env_bool("MAIL_USE_TLS", False) + MAIL_USE_SSL = _env_bool("MAIL_USE_SSL", True) + MAIL_PORT = int(_env_str("MAIL_PORT", "465")) + MAIL_USERNAME = _env_str("MAIL_USERNAME", "123@qq.com") + MAIL_PASSWORD = _env_str("MAIL_PASSWORD", "XXXXX") # QQ 邮箱授权码 + MAIL_DEFAULT_SENDER = _env_str("MAIL_DEFAULT_SENDER", MAIL_USERNAME) + + # ---- 插件 ---- PLUGIN_ENABLE_FOLDERS = [] - # Session 设置 - PERMANENT_SESSION_LIFETIME = timedelta(days=7) - SESSION_TYPE = "filesystem" # 默认使用文件系统来保存会话 - SESSION_PERMANENT = False # 会话是否持久化 - SESSION_USE_SIGNER = True # 是否对发送到浏览器上 session 的 cookie 值进行加密 +class DevConfig(BaseConfig): + """开发环境配置:DEBUG=True,详细日志。""" - SECRET_KEY = "pear-system-flask" + DEBUG = True + TESTING = False + LOG_LEVEL = logging.INFO + # 开发环境下 SECRET_KEY 自动生成(每次启动都不同,仅用于本地调试)。 + SECRET_KEY = os.environ.get("SECRET_KEY") or secrets.token_urlsafe(48) + + +class ProConfig(BaseConfig): + """生产环境配置:DEBUG=False,强制要求外部传入 SECRET_KEY。""" + + DEBUG = False + TESTING = False + LOG_LEVEL = logging.WARN + + # 生产推荐使用 MySQL,例如: + # SQLALCHEMY_DATABASE_URI=mysql+pymysql://user:pass@host:3306/pear?charset=utf8mb4 + SQLALCHEMY_DATABASE_URI = os.environ.get( + "SQLALCHEMY_DATABASE_URI", + BaseConfig.SQLALCHEMY_DATABASE_URI, + ) + + @classmethod + def init_app(cls, app): + """Flask 在 app.config.from_object 之后调用此钩子。 + + 生产模式要求:必须从环境变量传入强随机 SECRET_KEY, + 否则启动失败 —— 防止线上仍用默认密钥。 + """ + secret = os.environ.get("SECRET_KEY") + if not secret or secret == "pear-system-flask": + raise RuntimeError( + "[ProConfig] 生产环境必须通过环境变量 SECRET_KEY 传入强随机密钥," + "禁止使用默认值。可用以下命令生成:\n" + " python -c \"import secrets; print(secrets.token_urlsafe(48))\"" + ) + app.config["SECRET_KEY"] = secret + + +# 暴露给外部按名字取 +config_map = { + "dev": DevConfig, + "development": DevConfig, + "pro": ProConfig, + "production": ProConfig, +} + + +def get_config_by_name(name: str): + """根据名字取配置类,未知名字退回 DevConfig。""" + if not name: + return DevConfig + return config_map.get(name.strip().lower(), DevConfig) diff --git a/applications/view/__init__.py b/applications/view/__init__.py index 1b9a305..93a3af0 100644 --- a/applications/view/__init__.py +++ b/applications/view/__init__.py @@ -1,11 +1,16 @@ from applications.view.system import register_system_bps -from applications.view.public import bp as public_bp # public 包的入口就是 nav_bp +from applications.view.public import bp as public_bp, register_public_bp # public 包的入口就是 nav_bp +from applications.view.health import bp as health_bp from applications.extensions.init_plugins import broadcast_execute def init_bps(app): + # 健康检查端点(无需登录,应在负载均衡/容器探活里使用) + app.register_blueprint(health_bp) + # 前台公开蓝图(无需登录) app.register_blueprint(public_bp) + register_public_bp(app) # 后台系统蓝图(需要登录 + 权限码) register_system_bps(app) diff --git a/applications/view/health.py b/applications/view/health.py new file mode 100644 index 0000000..66c8928 --- /dev/null +++ b/applications/view/health.py @@ -0,0 +1,71 @@ +""" +健康检查端点(无需登录) + +- GET /healthz + 用于探活/负载均衡/容器 readinessProbe。 + 返回: + { + "status": "ok" | "degraded", + "db_ok": true | false, + "env": "dev" | "production", + "system": "Pear Admin", + "timestamp": "2026-09-05T17:00:00+08:00" + } + + HTTP 状态码: + 200 - 全部正常 + 503 - DB 不可达(用于触发 k8s readinessProbe 失败) +""" +from datetime import datetime, timedelta, timezone +import os + +from flask import Blueprint, current_app, jsonify +from sqlalchemy import text + +from applications.extensions import db + +bp = Blueprint("health", __name__) + + +def _beijing_now() -> str: + """返回 +08:00 时区 ISO8601 时间字符串。""" + tz = timezone(timedelta(hours=8)) + return datetime.now(tz).isoformat() + + +@bp.get("/healthz") +def healthz(): + # 优先按 PEAR_ENV/FLASK_ENV 取环境名;兜底按 DEBUG 反推 + env = ( + os.environ.get("PEAR_ENV") + or os.environ.get("FLASK_ENV") + or current_app.config.get("PEAR_ENV") + or ("dev" if current_app.config.get("DEBUG") else "production") + ) + system_name = current_app.config.get("SYSTEM_NAME", "Pear Admin") + + # DB 探活:跑一个最轻量的 SELECT 1 + db_ok = True + db_error = None + try: + db.session.execute(text("SELECT 1")) + except Exception as e: # noqa: BLE001 - 这里就是要把异常吃掉,转换成 False + db_ok = False + db_error = str(e) + finally: + try: + db.session.rollback() + except Exception: + pass + + payload = { + "status": "ok" if db_ok else "degraded", + "db_ok": db_ok, + "env": env, + "system": system_name, + "timestamp": _beijing_now(), + } + if db_error: + payload["db_error"] = db_error + + return jsonify(payload), (200 if db_ok else 503) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 0000000..e3cf7d0 --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,164 @@ +# Pear Admin Flask - 部署指南 + +本文档面向把本项目部署到生产环境的运维 / 二次开发者。 +请结合根目录的 `README.md` 与 `docs/SECURITY.md` 一起阅读。 + +## 0. 环境要求 + +| 项目 | 版本 | +| --- | --- | +| Python | 3.8(推荐 3.11) | +| SQLite | 3.x(仅开发用) | +| MySQL | 5.7+ / 8.0(生产推荐) | +| 操作系统 | Windows / Linux 均可 | + +## 1. 克隆代码 + +```bash +git clone https://gitea.bwhome.top/bwadmin/pear-admin-flask.git +cd pear-admin-flask +``` + +> 如仓库使用自签证书导致 `SEC_E_WRONG_PRINCIPAL` 报错, +> 可临时关闭 SSL 校验:`git -c http.sslVerify=false clone `。 +> 长期建议把根证书导入到系统信任库。 + +## 2. 创建虚拟环境 + 安装依赖 + +```bash +# Linux / macOS +python -m venv venv +source venv/bin/activate + +# Windows (cmd) +python -m venv venv +venv\Scripts\activate.bat + +pip install -r requirements.txt +``` + +## 3. 配置 .env + +复制模板: + +```bash +cp .env.example .env # Linux +copy .env.example .env # Windows +``` + +按需修改(**生产模式 SECRET_KEY 必填**,参见下文): + +```dotenv +PEAR_ENV=production +SECRET_KEY=<用 python -c "import secrets; print(secrets.token_urlsafe(48))" 生成> + +# MySQL 示例 +SQLALCHEMY_DATABASE_URI=mysql+pymysql://user:pass@127.0.0.1:3306/pear?charset=utf8mb4 + +# 邮件(可选) +MAIL_SERVER=smtp.qq.com +MAIL_USE_SSL=true +MAIL_PORT=465 +MAIL_USERNAME=your_account@qq.com +MAIL_PASSWORD=your_authorization_code_here +``` + +⚠️ `.env` 文件已在 `.gitignore` 中,**不会**被提交到 git。 + +## 4. 数据库初始化 + +```bash +# 1) 生成迁移脚本 +flask db migrate +# 2) 升级到当前 head +flask db upgrade +# 3) 导入 admin 基础数据(用户 / 角色 / 菜单) +flask admin init +``` + +MySQL 用户需提前建库: + +```sql +CREATE DATABASE pear DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'pear'@'%' IDENTIFIED BY 'your_strong_password'; +GRANT ALL PRIVILEGES ON pear.* TO 'pear'@'%'; +FLUSH PRIVILEGES; +``` + +## 5. 启动 + +### 5.1 开发 + +```bash +# Linux +make run +# Windows +start.bat +``` + +### 5.2 生产 + +推荐用 `gunicorn`(`pip install gunicorn`),或反向代理到 `waitress`: + +```bash +# Linux +PEAR_ENV=production gunicorn -w 4 -b 0.0.0.0:5000 app:app + +# Windows +pip install waitress +PEAR_ENV=production waitress-serve --port=5000 app:app +``` + +### 5.3 健康检查 + +服务起来后: + +```bash +curl http://127.0.0.1:5000/healthz +# 200 OK → {"status":"ok","db_ok":true,...} +# 503 → {"status":"degraded","db_ok":false,...} +``` + +## 6. 反向代理(Nginx 示例) + +```nginx +server { + listen 80; + server_name pear.example.com; + + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 50m; + } + + location = /healthz { + proxy_pass http://127.0.0.1:5000/healthz; + access_log off; + } +} +``` + +## 7. 升级流程 + +```bash +git pull +source venv/bin/activate +pip install -r requirements.txt +flask db migrate +flask db upgrade +# 重启进程 +systemctl restart pear-admin-flask +``` + +## 8. 常见问题 + +| 现象 | 处理 | +| --- | --- | +| `[ProConfig] 生产环境必须通过环境变量 SECRET_KEY 传入强随机密钥` | 在 `.env` 或系统环境变量中设置 `SECRET_KEY`。 | +| `SEC_E_WRONG_PRINCIPAL` 克隆失败 | `git -c http.sslVerify=false clone ...` 或导入根证书。 | +| 启动后 `/` 跳转到登录页 | 正常。后台默认账号 `admin` / 密码 `123456`,**请立即修改**。 | +| 数据库表缺失 | `flask db upgrade` 后 `flask admin init`。 | diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..80e6f2f --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,89 @@ +# Pear Admin Flask - 安全指南 + +## 1. 密钥(SECRET_KEY) + +`SECRET_KEY` 用于 Flask `session` 签名、`csrf_token`、Flask-Session +等。**一旦泄露,攻击者可伪造任意管理员会话。** + +要求: +- 长度 ≥ 32 字节(推荐 48+); +- 高熵随机(`secrets.token_urlsafe(48)` / `openssl rand -base64 48`); +- **禁止**使用默认值 `pear-system-flask`; +- **禁止**把 `.env` / 含密钥的部署脚本提交到 git。 + +开发模式自动生成的密钥仅供本地调试,**重启即失效**。 + +## 2. 凭据(Mail / DB) + +所有凭据通过 `.env` 注入,代码里只放占位符。强烈建议: +- 生产数据库单独建用户,**只授必要的库权限**; +- Mail 使用**授权码**而非登录密码(QQ / 163 / Gmail 均提供); +- 定期轮换凭据,并在变更后重启服务。 + +## 3. 默认管理员账号 + +`flask admin init` 会写入默认账号 `admin / 123456`,**首次登录后必须立即修改密码**。 + +可执行: + +```bash +# 1) 登录后台 → 用户管理 → 修改 admin 密码; +# 2) 或直接 SQL: +sqlite3 pear.db "UPDATE rbac_user SET password='' WHERE username='admin';" +``` + +## 4. Cookie / Session + +`SESSION_COOKIE_HTTPONLY = True`(默认)与 `SESSION_COOKIE_SAMESITE = 'Lax'` +已经在配置中体现。生产部署在 HTTPS 下请额外设置: + +```python +SESSION_COOKIE_SECURE = True +``` + +> 若启用 Flask-Session(filesystem / redis),请把 `flask_session/` +> 目录排除在 Web 静态目录之外,并定期清理。 + +## 5. CSRF + +本项目基于 `flask_wtf`,所有写操作均有 CSRF 校验。 +前端表单务必带上 `{{ csrf_token() }}` 或请求头 `X-CSRFToken`。 + +## 6. 上传文件 + +`UPLOADED_PHOTOS_DEST = static/upload`,后缀白名单已限制为图片。 +但**用户可上传 SVG / 伪装为图片的 JS** 等——建议: + +- 上传目录关闭脚本执行(Nginx: `location ^~ /static/upload/ { ... }`); +- 后端校验真实文件类型(`python-magic`),而非仅看后缀。 + +## 7. 限流 + +`Flask-Limiter` 已挂载(`applications/extensions/init_limit.py`)。 +生产模式请按业务需求调整默认速率,避免误伤。 + +## 8. 日志 + +`LOG_LEVEL` 控制根 logger 输出等级。生产推荐 `WARN` / `ERROR`。 +**不要把 `SECRET_KEY`、用户密码、token 写入日志。** + +## 9. 依赖漏洞 + +建议在 CI 中加入 `pip-audit`: + +```bash +pip install pip-audit +pip-audit -r requirements.txt +``` + +## 10. 上线前 Checklist + +- [ ] `PEAR_ENV=production` +- [ ] `SECRET_KEY` 已替换为强随机值 +- [ ] 数据库用户仅授必要权限 +- [ ] 默认 admin 密码已修改 +- [ ] HTTPS 已上线(`SESSION_COOKIE_SECURE` 同步开启) +- [ ] `flask_session/` 目录已加入备份策略 +- [ ] `static/upload/` 已禁用脚本执行 +- [ ] `pip-audit` 跑过 +- [ ] `/healthz` 已接入负载均衡 / 容器探活 diff --git a/requirements.txt b/requirements.txt index fa93c4b..c9a635d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,4 +42,5 @@ Werkzeug==2.3.6 zipp==3.16.2 flask_wtf==1.2.1 wtforms~=3.1.1 -Flask-Session==0.5.0 \ No newline at end of file +Flask-Session==0.5.0 +python-dotenv==1.0.1 \ No newline at end of file diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..510875c --- /dev/null +++ b/start.bat @@ -0,0 +1,63 @@ +@echo off +REM ============================================================ +REM Pear Admin Flask 一键启动脚本(Windows / cmd) +REM +REM 用法: +REM start.bat 开发模式启动(默认) +REM start.bat pro 生产模式启动(要求 .env 已设 SECRET_KEY) +REM start.bat init 仅初始化数据库 + 导入 admin 数据 +REM start.bat migrate 仅做 db migrate / upgrade +REM ============================================================ +setlocal + +set "VENV_PY=venv\Scripts\python.exe" +set "APP=app.py" +set "MODE=%~1" + +if "%MODE%"=="" set "MODE=dev" + +echo [start.bat] 模式=%MODE% + +REM 1) 准备虚拟环境(首次) +if not exist "%VENV_PY%" ( + echo [start.bat] 创建虚拟环境 venv ... + python -m venv venv + if errorlevel 1 ( + echo [start.bat][错误] 创建 venv 失败,请确认系统已安装 Python 3.8+ + exit /b 1 + ) +) + +REM 2) 安装依赖 +echo [start.bat] 安装依赖 ... +"%VENV_PY%" -m pip install --upgrade pip >nul +"%VENV_PY%" -m pip install -r requirements.txt +if errorlevel 1 ( + echo [start.bat][错误] 安装依赖失败 + exit /b 1 +) + +REM 3) 数据库迁移 + admin 初始化(仅首次或有变更时执行) +if /I "%MODE%"=="init" goto do_init +if /I "%MODE%"=="migrate" goto do_migrate + +:do_init +echo [start.bat] 初始化数据库(migrate + upgrade) ... +"%VENV_PY%" -m flask db migrate +"%VENV_PY%" -m flask db upgrade +echo [start.bat] 导入 admin 基础数据 ... +"%VENV_PY%" -m flask admin init +echo [start.bat] 初始化完成。可以执行 start.bat 启动服务。 +exit /b 0 + +:do_migrate +"%VENV_PY%" -m flask db migrate +"%VENV_PY%" -m flask db upgrade +exit /b 0 + +REM 4) 启动 +echo [start.bat] 启动 Flask(PEAR_ENV=%MODE%)... +set "PEAR_ENV=%MODE%" +"%VENV_PY%" "%APP%" + +endlocal