reset_weak_password_111111.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. #!/usr/bin/env python3
  2. """将 sys_user 中密码明文等价于 111111 的账号统一改为 Zkxw@123。
  3. 因 bcrypt 每次盐不同,无法用 SQL WHERE password='111111' 命中,必须在应用侧校验。
  4. 连接方式(任选其一,优先顺序见下方):
  5. 1) --database-url / 环境变量 DATABASE_URL
  6. 2) --db-host + --db-user + --db-password + --db-name(可选 --db-port)
  7. 3) --env-file / 环境变量 ENV_FILE(默认 backend/.env)
  8. 用法示例(在 backend 目录):
  9. # 显式指定库(推荐测试/生产,避免误连本地 .env)
  10. python scripts/reset_weak_password_111111.py \\
  11. --database-url 'mysql+pymysql://user:pass@host:3306/dbname' --dry-run
  12. python scripts/reset_weak_password_111111.py \\
  13. --db-host 1.2.3.4 --db-user root --db-password 'xxx' --db-name zkxw --dry-run
  14. # 使用指定环境文件
  15. python scripts/reset_weak_password_111111.py --env-file ./.env.test --dry-run
  16. # 正式写入(去掉 --dry-run)
  17. python scripts/reset_weak_password_111111.py --database-url '...'
  18. # 生产后端容器:脚本可放任意路径,容器内已有 DATABASE_URL 与 /app
  19. docker exec <backend容器> python /tmp/reset_weak_password_111111.py --dry-run
  20. 范围:del_flag=0(含停用账号 status!=1)。
  21. """
  22. from __future__ import annotations
  23. import argparse
  24. import os
  25. import re
  26. import sys
  27. from datetime import datetime
  28. from pathlib import Path
  29. from urllib.parse import quote_plus
  30. WEAK_PASSWORD = "111111"
  31. NEW_PASSWORD = "Zkxw@123"
  32. def _candidate_dirs(start: Path):
  33. current = start.resolve()
  34. if current.is_file():
  35. current = current.parent
  36. while True:
  37. yield current
  38. if current.parent == current:
  39. break
  40. current = current.parent
  41. def _has_app_package(directory: Path) -> bool:
  42. return (directory / "app" / "__init__.py").is_file()
  43. def _find_backend_root() -> Path:
  44. """定位含 app 包的目录,不依赖脚本放在 backend/scripts 下。"""
  45. seen = set()
  46. starts = [Path(__file__), Path.cwd(), Path("/app")]
  47. for start in starts:
  48. if not start.exists():
  49. continue
  50. for directory in _candidate_dirs(start):
  51. key = str(directory)
  52. if key in seen:
  53. continue
  54. seen.add(key)
  55. if _has_app_package(directory):
  56. return directory
  57. raise SystemExit(
  58. "找不到后端 app 包。请在后端容器内执行,或在含 app 目录的环境中运行本脚本。"
  59. )
  60. def _mask_database_url(url: str) -> str:
  61. return re.sub(r":([^:@/]+)@", r":***@", url or "")
  62. def _build_url_from_parts(
  63. *,
  64. host: str,
  65. user: str,
  66. password: str,
  67. name: str,
  68. port: int,
  69. ) -> str:
  70. return (
  71. f"mysql+pymysql://{quote_plus(user)}:{quote_plus(password)}"
  72. f"@{host}:{port}/{name}"
  73. )
  74. def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
  75. parser = argparse.ArgumentParser(
  76. description="重置弱密码 111111 → Zkxw@123(支持变量指定目标库)"
  77. )
  78. parser.add_argument(
  79. "--dry-run",
  80. action="store_true",
  81. help="只列出将修改的账号,不写库",
  82. )
  83. parser.add_argument(
  84. "--env-file",
  85. default=None,
  86. help="环境文件路径(等同 ENV_FILE);未指定 database-url/分项时使用",
  87. )
  88. parser.add_argument(
  89. "--database-url",
  90. default=None,
  91. help="完整 SQLAlchemy URL(等同 DATABASE_URL),优先于 env-file",
  92. )
  93. parser.add_argument("--db-host", default=None, help="MySQL 主机")
  94. parser.add_argument("--db-port", type=int, default=3306, help="MySQL 端口,默认 3306")
  95. parser.add_argument("--db-user", default=None, help="MySQL 用户")
  96. parser.add_argument("--db-password", default=None, help="MySQL 密码")
  97. parser.add_argument("--db-name", default=None, help="数据库名")
  98. return parser.parse_args(argv)
  99. def _resolve_connection(args: argparse.Namespace) -> tuple[str, str]:
  100. """返回 (来源说明, DATABASE_URL)。在 import app 之前调用并写入 os.environ。"""
  101. parts = (args.db_host, args.db_user, args.db_password, args.db_name)
  102. if any(parts) and not all(p is not None and str(p) != "" for p in parts):
  103. raise SystemExit(
  104. "使用分项连接时须同时提供: --db-host --db-user --db-password --db-name"
  105. )
  106. if all(p is not None and str(p) != "" for p in parts):
  107. url = _build_url_from_parts(
  108. host=args.db_host,
  109. user=args.db_user,
  110. password=args.db_password,
  111. name=args.db_name,
  112. port=args.db_port or 3306,
  113. )
  114. os.environ["DATABASE_URL"] = url
  115. return ("cli --db-*", url)
  116. if args.database_url:
  117. url = args.database_url.strip()
  118. os.environ["DATABASE_URL"] = url
  119. return ("cli --database-url", url)
  120. env_url = (os.environ.get("DATABASE_URL") or "").strip()
  121. if env_url:
  122. return ("env DATABASE_URL", env_url)
  123. env_file = args.env_file or os.environ.get("ENV_FILE") or str(_find_backend_root() / ".env")
  124. env_file_path = Path(env_file).expanduser()
  125. if not env_file_path.is_absolute():
  126. env_file_path = (Path.cwd() / env_file_path).resolve()
  127. if not env_file_path.is_file():
  128. raise SystemExit(
  129. f"环境文件不存在: {env_file_path}\n"
  130. "请改用 --database-url 或 --db-host/--db-user/--db-password/--db-name"
  131. )
  132. os.environ["ENV_FILE"] = str(env_file_path)
  133. # DATABASE_URL 由 Settings 从该文件加载;此处仅记录来源
  134. return (f"env-file {env_file_path}", "")
  135. def main(argv: list[str] | None = None) -> int:
  136. args = _parse_args(argv)
  137. source, early_url = _resolve_connection(args)
  138. sys.path.insert(0, str(_find_backend_root()))
  139. # 必须在设置 ENV_FILE / DATABASE_URL 之后再 import,否则 Settings 已冻结
  140. from sqlalchemy import create_engine
  141. from sqlalchemy.orm import sessionmaker
  142. from app.core.config import settings
  143. from app.core.security import get_password_hash, verify_password
  144. from app.models.models import SysUser
  145. database_url = (os.environ.get("DATABASE_URL") or "").strip() or settings.DATABASE_URL
  146. if early_url:
  147. database_url = early_url
  148. print(f"目标库来源: {source}")
  149. print(f"DATABASE_URL: {_mask_database_url(database_url)}")
  150. engine = create_engine(
  151. database_url,
  152. pool_pre_ping=True,
  153. pool_recycle=3600,
  154. echo=False,
  155. )
  156. Session = sessionmaker(autocommit=False, autoflush=False, expire_on_commit=False, bind=engine)
  157. db = Session()
  158. try:
  159. rows = (
  160. db.query(SysUser)
  161. .filter(SysUser.del_flag == 0)
  162. .order_by(SysUser.create_time.desc())
  163. .all()
  164. )
  165. hits = []
  166. for u in rows:
  167. pwd = getattr(u, "password", None) or ""
  168. if verify_password(WEAK_PASSWORD, pwd):
  169. hits.append(u)
  170. if not hits:
  171. print("未发现密码为 111111 的用户(del_flag=0)。")
  172. return 0
  173. print(f"命中 {len(hits)} 个弱密码用户:")
  174. for u in hits:
  175. print(
  176. f" id={u.id} user_no={u.user_no!r} status={u.status!r} "
  177. f"tenant_no={u.tenant_no!r}"
  178. )
  179. if args.dry_run:
  180. print("dry-run:未写入数据库。去掉 --dry-run 后执行正式更新。")
  181. return 0
  182. new_hash = get_password_hash(NEW_PASSWORD)
  183. now = datetime.now()
  184. for u in hits:
  185. u.password = new_hash
  186. u.update_time = now
  187. db.commit()
  188. print(f"已更新 {len(hits)} 个用户密码为强密码(明文 Zkxw@123)。请通知相关用户。")
  189. return 0
  190. except Exception as exc:
  191. db.rollback()
  192. print(f"执行失败: {exc}", file=sys.stderr)
  193. return 1
  194. finally:
  195. db.close()
  196. engine.dispose()
  197. if __name__ == "__main__":
  198. raise SystemExit(main())