mock_sso_proxy.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. """本地单点登录代理 mock(需求2 开发联调用,stdlib 零依赖)。
  2. 模拟真实单点登录环境的两个 REST 对端:
  3. 代理服务(本脚本 8080):GET /middle/token/get.json → 颁发单点登录 token
  4. 用户服务(本脚本 8800):POST /auth/user/getUserBasicInfo → 校验 Bearer token 并返回用户基本信息
  5. 响应信封与真实环境一致:{"msg": "成功", "code": 200, "data": {...}},code=200 为成功。
  6. 真实环境中两个服务地址不同(代理 127.0.0.1:8080、用户服务 x.x.x.x:8800),
  7. mock 用同一进程监听两个端口。
  8. 用户与登录态来源(优先级从高到低):
  9. 1. 共享状态文件 ../mock-sso-service/sso-state.json —— 由 mock-sso-service
  10. 管理界面驱动(模拟登录/退出实时生效,本脚本每次请求重读);
  11. 2. 文件不存在时回落本文件写死的 USERS 实例(单独启动本脚本的旧用法不变)。
  12. 用法:python tools/mock-sso-proxy/mock_sso_proxy.py
  13. """
  14. from __future__ import annotations
  15. import json
  16. from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
  17. from pathlib import Path
  18. # 共享状态文件:mock-sso-service 拥有并原子写入,本脚本只读
  19. STATE_FILE = Path(__file__).resolve().parent.parent / "mock-sso-service" / "sso-state.json"
  20. # 回落实例用户:token → 用户基本信息(对端不提供机构字段,机构归属由本系统设密环节用户自选)
  21. USERS = {
  22. "mock-sso-token-zhangsan": {
  23. "userId": "9001",
  24. "username": "zhangsan",
  25. "realName": "张三",
  26. "sex": "1",
  27. "deptId": "100",
  28. "createTime": "2019-03-20T15:04:10.000+0000",
  29. "cardId": "",
  30. },
  31. "mock-sso-token-lisi": {
  32. "userId": "9002",
  33. "username": "lisi",
  34. "realName": "李四",
  35. "sex": "0",
  36. "deptId": "101",
  37. "createTime": "2020-06-01T09:00:00.000+0000",
  38. "cardId": "",
  39. },
  40. }
  41. # 回落模式下代理当前颁发的 token(真实代理由大系统登录态写入;mock 固定取第一个实例用户)
  42. CURRENT_TOKEN = next(iter(USERS))
  43. PROXY_PORT = 8080
  44. USER_SERVICE_PORT = 8800
  45. def _ok(data: dict) -> dict:
  46. return {"msg": "成功", "code": 200, "data": data}
  47. def _fail(code: int, msg: str) -> dict:
  48. return {"msg": msg, "code": code, "data": None}
  49. def _load_shared_state():
  50. """读共享状态文件;返回 (token→用户, 当前颁发token) 或 None(文件缺失/损坏 → 回落内置实例)。"""
  51. try:
  52. state = json.loads(STATE_FILE.read_text(encoding="utf-8"))
  53. except (OSError, ValueError):
  54. return None
  55. by_token = {}
  56. for user in state.get("users", []):
  57. token = user.get("accessToken")
  58. if token:
  59. by_token[token] = user
  60. current = None
  61. for user in state.get("users", []):
  62. if user.get("userId") == state.get("currentUserId") and user.get("accessToken"):
  63. current = user["accessToken"]
  64. break
  65. return by_token, current
  66. class Handler(BaseHTTPRequestHandler):
  67. """两端口共用处理器:按端口与路径分发。"""
  68. server_version = "MockSsoProxy/0.3"
  69. def log_message(self, fmt, *args): # noqa: A003 - 保留 stdlib 签名
  70. print(f"[mock-sso] {self.address_string()} {fmt % args}")
  71. def _write(self, payload: dict, status: int = 200) -> None:
  72. body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
  73. self.send_response(status)
  74. self.send_header("Content-Type", "application/json; charset=utf-8")
  75. self.send_header("Content-Length", str(len(body)))
  76. self.end_headers()
  77. self.wfile.write(body)
  78. # ----------------------------------------------------------- 代理(8080)
  79. def do_GET(self) -> None: # noqa: N802 - stdlib 固定命名
  80. if self.server.server_port == PROXY_PORT and self.path == "/middle/token/get.json":
  81. shared = _load_shared_state()
  82. if shared is not None:
  83. _, current = shared
  84. if current is None:
  85. # 门户未登录:信封格式不变(DMS 网关按 code!=200 转"未返回有效令牌")
  86. self._write(_fail(401, "门户未登录"))
  87. return
  88. self._write(_ok({
  89. "accessToken": current,
  90. "tokenType": "Bearer",
  91. "refreshToken": shared[0][current].get("refreshToken") or f"{current}-refresh",
  92. }))
  93. return
  94. self._write(_ok({
  95. "accessToken": CURRENT_TOKEN,
  96. "tokenType": "Bearer",
  97. "refreshToken": f"{CURRENT_TOKEN}-refresh",
  98. }))
  99. return
  100. self._write(_fail(404, "not found"), status=404)
  101. # ------------------------------------------------------- 用户服务(8800)
  102. def do_POST(self) -> None: # noqa: N802 - stdlib 固定命名
  103. if self.server.server_port != USER_SERVICE_PORT or self.path != "/auth/user/getUserBasicInfo":
  104. self._write(_fail(404, "not found"), status=404)
  105. return
  106. token = self.headers.get("Authorization", "").removeprefix("Bearer ").strip()
  107. shared = _load_shared_state()
  108. user = shared[0].get(token) if shared else USERS.get(token)
  109. if user is None:
  110. self._write(_fail(401, "token非法或已过期"))
  111. return
  112. # 状态文件用户行携带 token 管理字段,对外只输出对端契约内的基本信息
  113. basic = {k: user[k] for k in
  114. ("userId", "username", "realName", "sex", "deptId", "createTime", "cardId")}
  115. self._write(_ok({
  116. "basicUserInfo": basic,
  117. "expendUserInfo": {"officePhone": "", "phone": ""},
  118. }))
  119. def main() -> None:
  120. proxy = ThreadingHTTPServer(("127.0.0.1", PROXY_PORT), Handler)
  121. user_service = ThreadingHTTPServer(("127.0.0.1", USER_SERVICE_PORT), Handler)
  122. import threading
  123. threading.Thread(target=user_service.serve_forever, daemon=True).start()
  124. shared = _load_shared_state()
  125. if shared is not None:
  126. _, current = shared
  127. print(f"[mock-sso] 状态来源 共享文件 {STATE_FILE}")
  128. print(f"[mock-sso] 当前登录态 {'token=' + current if current else '门户未登录(模拟登录请在管理界面操作)'}")
  129. else:
  130. print(f"[mock-sso] 状态来源 内置实例(未找到共享文件,可启动 mock-sso-service 接管用户与登录态)")
  131. print(f"[mock-sso] 当前实例用户:{USERS[CURRENT_TOKEN]['realName']}"
  132. f"(token={CURRENT_TOKEN})")
  133. print(f"[mock-sso] 代理服务 http://127.0.0.1:{PROXY_PORT}/middle/token/get.json")
  134. print(f"[mock-sso] 用户服务 http://127.0.0.1:{USER_SERVICE_PORT}/auth/user/getUserBasicInfo")
  135. print("[mock-sso] Ctrl+C 停止")
  136. try:
  137. proxy.serve_forever()
  138. except KeyboardInterrupt:
  139. pass
  140. if __name__ == "__main__":
  141. main()