reset_weak_password_111111.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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. 范围:del_flag=0(含停用账号 status!=1)。
  19. """
  20. from __future__ import annotations
  21. import argparse
  22. import os
  23. import re
  24. import sys
  25. from datetime import datetime
  26. from pathlib import Path
  27. from urllib.parse import quote_plus
  28. WEAK_PASSWORD = "111111"
  29. NEW_PASSWORD = "Zkxw@123"
  30. BACKEND_ROOT = Path(__file__).resolve().parent.parent
  31. def _mask_database_url(url: str) -> str:
  32. return re.sub(r":([^:@/]+)@", r":***@", url or "")
  33. def _build_url_from_parts(
  34. *,
  35. host: str,
  36. user: str,
  37. password: str,
  38. name: str,
  39. port: int,
  40. ) -> str:
  41. return (
  42. f"mysql+pymysql://{quote_plus(user)}:{quote_plus(password)}"
  43. f"@{host}:{port}/{name}"
  44. )
  45. def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
  46. parser = argparse.ArgumentParser(
  47. description="重置弱密码 111111 → Zkxw@123(支持变量指定目标库)"
  48. )
  49. parser.add_argument(
  50. "--dry-run",
  51. action="store_true",
  52. help="只列出将修改的账号,不写库",
  53. )
  54. parser.add_argument(
  55. "--env-file",
  56. default=None,
  57. help="环境文件路径(等同 ENV_FILE);未指定 database-url/分项时使用",
  58. )
  59. parser.add_argument(
  60. "--database-url",
  61. default=None,
  62. help="完整 SQLAlchemy URL(等同 DATABASE_URL),优先于 env-file",
  63. )
  64. parser.add_argument("--db-host", default=None, help="MySQL 主机")
  65. parser.add_argument("--db-port", type=int, default=3306, help="MySQL 端口,默认 3306")
  66. parser.add_argument("--db-user", default=None, help="MySQL 用户")
  67. parser.add_argument("--db-password", default=None, help="MySQL 密码")
  68. parser.add_argument("--db-name", default=None, help="数据库名")
  69. return parser.parse_args(argv)
  70. def _resolve_connection(args: argparse.Namespace) -> tuple[str, str]:
  71. """返回 (来源说明, DATABASE_URL)。在 import app 之前调用并写入 os.environ。"""
  72. parts = (args.db_host, args.db_user, args.db_password, args.db_name)
  73. if any(parts) and not all(p is not None and str(p) != "" for p in parts):
  74. raise SystemExit(
  75. "使用分项连接时须同时提供: --db-host --db-user --db-password --db-name"
  76. )
  77. if all(p is not None and str(p) != "" for p in parts):
  78. url = _build_url_from_parts(
  79. host=args.db_host,
  80. user=args.db_user,
  81. password=args.db_password,
  82. name=args.db_name,
  83. port=args.db_port or 3306,
  84. )
  85. os.environ["DATABASE_URL"] = url
  86. return ("cli --db-*", url)
  87. if args.database_url:
  88. url = args.database_url.strip()
  89. os.environ["DATABASE_URL"] = url
  90. return ("cli --database-url", url)
  91. env_url = (os.environ.get("DATABASE_URL") or "").strip()
  92. if env_url:
  93. return ("env DATABASE_URL", env_url)
  94. env_file = args.env_file or os.environ.get("ENV_FILE") or str(BACKEND_ROOT / ".env")
  95. env_file_path = Path(env_file).expanduser()
  96. if not env_file_path.is_absolute():
  97. env_file_path = (Path.cwd() / env_file_path).resolve()
  98. if not env_file_path.is_file():
  99. raise SystemExit(
  100. f"环境文件不存在: {env_file_path}\n"
  101. "请改用 --database-url 或 --db-host/--db-user/--db-password/--db-name"
  102. )
  103. os.environ["ENV_FILE"] = str(env_file_path)
  104. # DATABASE_URL 由 Settings 从该文件加载;此处仅记录来源
  105. return (f"env-file {env_file_path}", "")
  106. def main(argv: list[str] | None = None) -> int:
  107. args = _parse_args(argv)
  108. source, early_url = _resolve_connection(args)
  109. sys.path.insert(0, str(BACKEND_ROOT))
  110. # 必须在设置 ENV_FILE / DATABASE_URL 之后再 import,否则 Settings 已冻结
  111. from sqlalchemy import create_engine
  112. from sqlalchemy.orm import sessionmaker
  113. from app.core.config import settings
  114. from app.core.security import get_password_hash, verify_password
  115. from app.models.models import SysUser
  116. database_url = (os.environ.get("DATABASE_URL") or "").strip() or settings.DATABASE_URL
  117. if early_url:
  118. database_url = early_url
  119. print(f"目标库来源: {source}")
  120. print(f"DATABASE_URL: {_mask_database_url(database_url)}")
  121. engine = create_engine(
  122. database_url,
  123. pool_pre_ping=True,
  124. pool_recycle=3600,
  125. echo=False,
  126. )
  127. Session = sessionmaker(autocommit=False, autoflush=False, expire_on_commit=False, bind=engine)
  128. db = Session()
  129. try:
  130. rows = (
  131. db.query(SysUser)
  132. .filter(SysUser.del_flag == 0)
  133. .order_by(SysUser.create_time.desc())
  134. .all()
  135. )
  136. hits = []
  137. for u in rows:
  138. pwd = getattr(u, "password", None) or ""
  139. if verify_password(WEAK_PASSWORD, pwd):
  140. hits.append(u)
  141. if not hits:
  142. print("未发现密码为 111111 的用户(del_flag=0)。")
  143. return 0
  144. print(f"命中 {len(hits)} 个弱密码用户:")
  145. for u in hits:
  146. print(
  147. f" id={u.id} user_no={u.user_no!r} status={u.status!r} "
  148. f"tenant_no={u.tenant_no!r}"
  149. )
  150. if args.dry_run:
  151. print("dry-run:未写入数据库。去掉 --dry-run 后执行正式更新。")
  152. return 0
  153. new_hash = get_password_hash(NEW_PASSWORD)
  154. now = datetime.now()
  155. for u in hits:
  156. u.password = new_hash
  157. u.update_time = now
  158. db.commit()
  159. print(f"已更新 {len(hits)} 个用户密码为强密码(明文 Zkxw@123)。请通知相关用户。")
  160. return 0
  161. except Exception as exc:
  162. db.rollback()
  163. print(f"执行失败: {exc}", file=sys.stderr)
  164. return 1
  165. finally:
  166. db.close()
  167. engine.dispose()
  168. if __name__ == "__main__":
  169. raise SystemExit(main())