test_token_user.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. # -*- coding: utf-8 -*-
  2. """验证「登录后把 StoreUser 写进 Redis,以 token 为 key,凭 token 取用户信息」。
  3. 覆盖点:
  4. 1. 登录落库:key = yoshop:store:token:{token},value 就是用户 JSON(顶层字段,无 user/store_id/is_login 包裹,无 password)
  5. 2. TTL = 7 天
  6. 3. /store/store.user/info 的用户信息来自缓存(篡改缓存里的 userName,接口跟着变,说明没回查数据库)
  7. 4. 带 token 访问业务接口正常
  8. 5. /store/store.user/renew 改资料后缓存被刷新,且不延长登录时长
  9. 6. 登出后 key 删除,旧 token 再访问 401
  10. 前置:Java 服务运行在 18080(yoshop.auth.mode=redis),库内有临时账号 camel_test_admin / 123456
  11. (先跑 `python tools/seed_store_user.py add` 创建,跑完 `remove` 清理)。
  12. """
  13. import json
  14. import socket
  15. import sys
  16. import requests
  17. BASE = "http://127.0.0.1:18080"
  18. REDIS = ("192.168.1.177", 6379, "@@Qinys12346..")
  19. PREFIX = "yoshop:store:token:"
  20. USER_NAME = "camel_test_admin"
  21. PASSWORD = "123456"
  22. S = requests.Session()
  23. S.trust_env = False
  24. passed = failed = 0
  25. def check(name, cond, extra=""):
  26. global passed, failed
  27. if cond:
  28. passed += 1
  29. print(" [ok] %s" % name)
  30. else:
  31. failed += 1
  32. print(" [FAIL] %s %s" % (name, extra))
  33. # ---------------------------------------------------------------- 最小 Redis 客户端
  34. def resp(sock, *args):
  35. data = b"*%d\r\n" % len(args)
  36. for a in args:
  37. if isinstance(a, bytes):
  38. b = a
  39. elif isinstance(a, str):
  40. b = a.encode()
  41. else:
  42. b = str(a).encode()
  43. data += b"$%d\r\n%s\r\n" % (len(b), b)
  44. sock.sendall(data)
  45. return read(sock)
  46. def read(sock):
  47. line = readline(sock)
  48. t, rest = line[:1], line[1:]
  49. if t in (b"+", b"-"):
  50. return rest.decode()
  51. if t == b":":
  52. return int(rest)
  53. if t == b"$":
  54. n = int(rest)
  55. if n == -1:
  56. return None
  57. buf = b""
  58. while len(buf) < n + 2:
  59. buf += sock.recv(n + 2 - len(buf))
  60. return buf[:n].decode()
  61. if t == b"*":
  62. n = int(rest)
  63. return None if n == -1 else [read(sock) for _ in range(n)]
  64. return line.decode(errors="replace")
  65. def readline(sock):
  66. buf = b""
  67. while not buf.endswith(b"\r\n"):
  68. c = sock.recv(1)
  69. if not c:
  70. return buf
  71. buf += c
  72. return buf[:-2]
  73. class Redis:
  74. def __enter__(self):
  75. self.s = socket.create_connection((REDIS[0], REDIS[1]), timeout=5)
  76. resp(self.s, "AUTH", REDIS[2])
  77. return self
  78. def __exit__(self, *a):
  79. self.s.close()
  80. def __call__(self, *args):
  81. return resp(self.s, *args)
  82. def login():
  83. return S.post(BASE + "/store/passport/login",
  84. headers={"Content-Type": "application/json"},
  85. data=json.dumps({"username": USER_NAME, "password": PASSWORD}),
  86. timeout=30).json()
  87. def get(path, token):
  88. headers = {"Access-Token": token} if token else {}
  89. return S.get(BASE + path, headers=headers, timeout=30).json()
  90. def post(path, token, body=None):
  91. headers = {"Content-Type": "application/json"}
  92. if token:
  93. headers["Access-Token"] = token
  94. return S.post(BASE + path, headers=headers, json=body or {}, timeout=30).json()
  95. def main():
  96. with Redis() as r:
  97. print("== 1. 登录 ==")
  98. res = login()
  99. check("登录成功", res.get("status") == 200, json.dumps(res, ensure_ascii=False))
  100. token = (res.get("data") or {}).get("token")
  101. # 账号 ID 随库内自增变化,不写死(原为 10992,重建账号后会漂移)
  102. uid = (res.get("data") or {}).get("userId")
  103. check("返回 token", isinstance(token, str) and len(token) == 32, str(token))
  104. if not token:
  105. return 1
  106. key = PREFIX + token
  107. print("== 2. 登录态落 Redis:token 即 key,value 即用户 ==")
  108. raw = r("GET", key)
  109. check("key = token", raw is not None, "key=%s" % key)
  110. ttl = r("TTL", key)
  111. check("TTL = 604800(允许 5 秒误差)", isinstance(ttl, int) and 604795 <= ttl <= 604800, "ttl=%s" % ttl)
  112. try:
  113. val = json.loads(raw) if raw else {}
  114. except Exception:
  115. val = {}
  116. if isinstance(val, str):
  117. val = json.loads(val)
  118. check("value 顶层就是用户字段", val.get("storeUserId") == uid and val.get("userName") == USER_NAME,
  119. json.dumps(val, ensure_ascii=False)[:200])
  120. check("不再有 user / store_id / is_login 包裹",
  121. not ({"user", "store_id", "is_login"} & set(val.keys())), sorted(val.keys()))
  122. check("关键字段齐全",
  123. all(k in val for k in ("storeUserId", "userName", "realName", "isSuper", "storeId", "createTime")),
  124. sorted(val.keys()))
  125. check("password 不落缓存", "password" not in val, sorted(val.keys()))
  126. print("== 3. 用户信息取自缓存(不回查数据库)==")
  127. info = get("/store/store.user/info", token)
  128. user_info = (info.get("data") or {}).get("userInfo") or {}
  129. check("info 返回 200", info.get("status") == 200, json.dumps(info, ensure_ascii=False)[:160])
  130. check("userInfo 为当前登录用户", user_info.get("storeUserId") == uid and user_info.get("userName") == USER_NAME,
  131. json.dumps(user_info, ensure_ascii=False)[:200])
  132. check("roles.isSuper = 0", ((info.get("data") or {}).get("roles") or {}).get("isSuper") == 0,
  133. json.dumps(info.get("data"), ensure_ascii=False)[:160])
  134. sentinel = dict(val, userName="from_redis_only")
  135. r("SET", key, json.dumps(sentinel, ensure_ascii=False))
  136. hit = ((get("/store/store.user/info", token).get("data") or {}).get("userInfo") or {})
  137. check("篡改缓存后接口跟着变(证明按 token 读缓存)", hit.get("userName") == "from_redis_only",
  138. json.dumps(hit, ensure_ascii=False)[:160])
  139. # 还原缓存
  140. r("SET", key, json.dumps(val, ensure_ascii=False))
  141. r("EXPIRE", key, 604800)
  142. print("== 4. 带 token 访问业务接口 ==")
  143. goods = get("/store/goods/list", token)
  144. check("/store/goods/list 返回 200", goods.get("status") == 200, json.dumps(goods, ensure_ascii=False)[:160])
  145. print("== 5. 改资料后缓存同步刷新,且不延长登录时长 ==")
  146. before = r("TTL", key)
  147. renewed = post("/store/store.user/renew", token, {"form": {"realName": "驼峰测试改"}})
  148. check("renew 返回 200", renewed.get("status") == 200, json.dumps(renewed, ensure_ascii=False)[:160])
  149. fresh = json.loads(r("GET", key) or "{}")
  150. if isinstance(fresh, str):
  151. fresh = json.loads(fresh)
  152. check("缓存 realName 已更新", fresh.get("realName") == "驼峰测试改",
  153. json.dumps(fresh, ensure_ascii=False)[:200])
  154. after = r("TTL", key)
  155. check("TTL 未被延长(沿用剩余有效期)", isinstance(after, int) and after <= before and after > before - 20,
  156. "before=%s after=%s" % (before, after))
  157. # 改回去,保持库内数据原样
  158. post("/store/store.user/renew", token, {"form": {"realName": "驼峰测试"}})
  159. print("== 6. 登出 ==")
  160. out = post("/store/passport/logout", token)
  161. check("登出返回 200", out.get("status") == 200, json.dumps(out, ensure_ascii=False)[:160])
  162. check("key 已删除", r("GET", key) is None, "key=%s" % key)
  163. check("TTL 为 -2", r("TTL", key) == -2, "ttl=%s" % r("TTL", key))
  164. after_logout = get("/store/goods/list", token)
  165. check("旧 token 再访问返回 401", after_logout.get("status") == 401,
  166. json.dumps(after_logout, ensure_ascii=False)[:160])
  167. print("\n通过 %d / 失败 %d" % (passed, failed))
  168. return 1 if failed else 0
  169. if __name__ == "__main__":
  170. sys.exit(main())