|
|
@@ -0,0 +1,202 @@
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""验证「登录后把 StoreUser 写进 Redis,以 token 为 key,凭 token 取用户信息」。
|
|
|
+
|
|
|
+覆盖点:
|
|
|
+ 1. 登录落库:key = yoshop:store:token:{token},value 就是用户 JSON(顶层字段,无 user/store_id/is_login 包裹,无 password)
|
|
|
+ 2. TTL = 7 天
|
|
|
+ 3. /store/store.user/info 的用户信息来自缓存(篡改缓存里的 userName,接口跟着变,说明没回查数据库)
|
|
|
+ 4. 带 token 访问业务接口正常
|
|
|
+ 5. /store/store.user/renew 改资料后缓存被刷新,且不延长登录时长
|
|
|
+ 6. 登出后 key 删除,旧 token 再访问 401
|
|
|
+
|
|
|
+前置:Java 服务运行在 18080(yoshop.auth.mode=redis),库内有临时账号 camel_test_admin / 123456。
|
|
|
+"""
|
|
|
+import json
|
|
|
+import socket
|
|
|
+import sys
|
|
|
+
|
|
|
+import requests
|
|
|
+
|
|
|
+BASE = "http://127.0.0.1:18080"
|
|
|
+REDIS = ("192.168.1.177", 6379, "@@Qinys12346..")
|
|
|
+PREFIX = "yoshop:store:token:"
|
|
|
+USER_NAME = "camel_test_admin"
|
|
|
+PASSWORD = "123456"
|
|
|
+
|
|
|
+S = requests.Session()
|
|
|
+S.trust_env = False
|
|
|
+
|
|
|
+passed = failed = 0
|
|
|
+
|
|
|
+
|
|
|
+def check(name, cond, extra=""):
|
|
|
+ global passed, failed
|
|
|
+ if cond:
|
|
|
+ passed += 1
|
|
|
+ print(" [ok] %s" % name)
|
|
|
+ else:
|
|
|
+ failed += 1
|
|
|
+ print(" [FAIL] %s %s" % (name, extra))
|
|
|
+
|
|
|
+
|
|
|
+# ---------------------------------------------------------------- 最小 Redis 客户端
|
|
|
+def resp(sock, *args):
|
|
|
+ data = b"*%d\r\n" % len(args)
|
|
|
+ for a in args:
|
|
|
+ if isinstance(a, bytes):
|
|
|
+ b = a
|
|
|
+ elif isinstance(a, str):
|
|
|
+ b = a.encode()
|
|
|
+ else:
|
|
|
+ b = str(a).encode()
|
|
|
+ data += b"$%d\r\n%s\r\n" % (len(b), b)
|
|
|
+ sock.sendall(data)
|
|
|
+ return read(sock)
|
|
|
+
|
|
|
+
|
|
|
+def read(sock):
|
|
|
+ line = readline(sock)
|
|
|
+ t, rest = line[:1], line[1:]
|
|
|
+ if t in (b"+", b"-"):
|
|
|
+ return rest.decode()
|
|
|
+ if t == b":":
|
|
|
+ return int(rest)
|
|
|
+ if t == b"$":
|
|
|
+ n = int(rest)
|
|
|
+ if n == -1:
|
|
|
+ return None
|
|
|
+ buf = b""
|
|
|
+ while len(buf) < n + 2:
|
|
|
+ buf += sock.recv(n + 2 - len(buf))
|
|
|
+ return buf[:n].decode()
|
|
|
+ if t == b"*":
|
|
|
+ n = int(rest)
|
|
|
+ return None if n == -1 else [read(sock) for _ in range(n)]
|
|
|
+ return line.decode(errors="replace")
|
|
|
+
|
|
|
+
|
|
|
+def readline(sock):
|
|
|
+ buf = b""
|
|
|
+ while not buf.endswith(b"\r\n"):
|
|
|
+ c = sock.recv(1)
|
|
|
+ if not c:
|
|
|
+ return buf
|
|
|
+ buf += c
|
|
|
+ return buf[:-2]
|
|
|
+
|
|
|
+
|
|
|
+class Redis:
|
|
|
+ def __enter__(self):
|
|
|
+ self.s = socket.create_connection((REDIS[0], REDIS[1]), timeout=5)
|
|
|
+ resp(self.s, "AUTH", REDIS[2])
|
|
|
+ return self
|
|
|
+
|
|
|
+ def __exit__(self, *a):
|
|
|
+ self.s.close()
|
|
|
+
|
|
|
+ def __call__(self, *args):
|
|
|
+ return resp(self.s, *args)
|
|
|
+
|
|
|
+
|
|
|
+def login():
|
|
|
+ return S.post(BASE + "/store/passport/login",
|
|
|
+ headers={"Content-Type": "application/json"},
|
|
|
+ data=json.dumps({"username": USER_NAME, "password": PASSWORD}),
|
|
|
+ timeout=30).json()
|
|
|
+
|
|
|
+
|
|
|
+def get(path, token):
|
|
|
+ headers = {"Access-Token": token} if token else {}
|
|
|
+ return S.get(BASE + path, headers=headers, timeout=30).json()
|
|
|
+
|
|
|
+
|
|
|
+def post(path, token, body=None):
|
|
|
+ headers = {"Content-Type": "application/json"}
|
|
|
+ if token:
|
|
|
+ headers["Access-Token"] = token
|
|
|
+ return S.post(BASE + path, headers=headers, json=body or {}, timeout=30).json()
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ with Redis() as r:
|
|
|
+ print("== 1. 登录 ==")
|
|
|
+ res = login()
|
|
|
+ check("登录成功", res.get("status") == 200, json.dumps(res, ensure_ascii=False))
|
|
|
+ token = (res.get("data") or {}).get("token")
|
|
|
+ check("返回 token", isinstance(token, str) and len(token) == 32, str(token))
|
|
|
+ if not token:
|
|
|
+ return 1
|
|
|
+
|
|
|
+ key = PREFIX + token
|
|
|
+ print("== 2. 登录态落 Redis:token 即 key,value 即用户 ==")
|
|
|
+ raw = r("GET", key)
|
|
|
+ check("key = token", raw is not None, "key=%s" % key)
|
|
|
+ ttl = r("TTL", key)
|
|
|
+ check("TTL = 604800(允许 5 秒误差)", isinstance(ttl, int) and 604795 <= ttl <= 604800, "ttl=%s" % ttl)
|
|
|
+ try:
|
|
|
+ val = json.loads(raw) if raw else {}
|
|
|
+ except Exception:
|
|
|
+ val = {}
|
|
|
+ if isinstance(val, str):
|
|
|
+ val = json.loads(val)
|
|
|
+ check("value 顶层就是用户字段", val.get("storeUserId") == 10992 and val.get("userName") == USER_NAME,
|
|
|
+ json.dumps(val, ensure_ascii=False)[:200])
|
|
|
+ check("不再有 user / store_id / is_login 包裹",
|
|
|
+ not ({"user", "store_id", "is_login"} & set(val.keys())), sorted(val.keys()))
|
|
|
+ check("关键字段齐全",
|
|
|
+ all(k in val for k in ("storeUserId", "userName", "realName", "isSuper", "storeId", "createTime")),
|
|
|
+ sorted(val.keys()))
|
|
|
+ check("password 不落缓存", "password" not in val, sorted(val.keys()))
|
|
|
+
|
|
|
+ print("== 3. 用户信息取自缓存(不回查数据库)==")
|
|
|
+ info = get("/store/store.user/info", token)
|
|
|
+ user_info = (info.get("data") or {}).get("userInfo") or {}
|
|
|
+ check("info 返回 200", info.get("status") == 200, json.dumps(info, ensure_ascii=False)[:160])
|
|
|
+ check("userInfo 为当前登录用户", user_info.get("storeUserId") == 10992 and user_info.get("userName") == USER_NAME,
|
|
|
+ json.dumps(user_info, ensure_ascii=False)[:200])
|
|
|
+ check("roles.isSuper = 0", ((info.get("data") or {}).get("roles") or {}).get("isSuper") == 0,
|
|
|
+ json.dumps(info.get("data"), ensure_ascii=False)[:160])
|
|
|
+
|
|
|
+ sentinel = dict(val, userName="from_redis_only")
|
|
|
+ r("SET", key, json.dumps(sentinel, ensure_ascii=False))
|
|
|
+ hit = ((get("/store/store.user/info", token).get("data") or {}).get("userInfo") or {})
|
|
|
+ check("篡改缓存后接口跟着变(证明按 token 读缓存)", hit.get("userName") == "from_redis_only",
|
|
|
+ json.dumps(hit, ensure_ascii=False)[:160])
|
|
|
+ # 还原缓存
|
|
|
+ r("SET", key, json.dumps(val, ensure_ascii=False))
|
|
|
+ r("EXPIRE", key, 604800)
|
|
|
+
|
|
|
+ print("== 4. 带 token 访问业务接口 ==")
|
|
|
+ goods = get("/store/goods/list", token)
|
|
|
+ check("/store/goods/list 返回 200", goods.get("status") == 200, json.dumps(goods, ensure_ascii=False)[:160])
|
|
|
+
|
|
|
+ print("== 5. 改资料后缓存同步刷新,且不延长登录时长 ==")
|
|
|
+ before = r("TTL", key)
|
|
|
+ renewed = post("/store/store.user/renew", token, {"form": {"realName": "驼峰测试改"}})
|
|
|
+ check("renew 返回 200", renewed.get("status") == 200, json.dumps(renewed, ensure_ascii=False)[:160])
|
|
|
+ fresh = json.loads(r("GET", key) or "{}")
|
|
|
+ if isinstance(fresh, str):
|
|
|
+ fresh = json.loads(fresh)
|
|
|
+ check("缓存 realName 已更新", fresh.get("realName") == "驼峰测试改",
|
|
|
+ json.dumps(fresh, ensure_ascii=False)[:200])
|
|
|
+ after = r("TTL", key)
|
|
|
+ check("TTL 未被延长(沿用剩余有效期)", isinstance(after, int) and after <= before and after > before - 20,
|
|
|
+ "before=%s after=%s" % (before, after))
|
|
|
+ # 改回去,保持库内数据原样
|
|
|
+ post("/store/store.user/renew", token, {"form": {"realName": "驼峰测试"}})
|
|
|
+
|
|
|
+ print("== 6. 登出 ==")
|
|
|
+ out = post("/store/passport/logout", token)
|
|
|
+ check("登出返回 200", out.get("status") == 200, json.dumps(out, ensure_ascii=False)[:160])
|
|
|
+ check("key 已删除", r("GET", key) is None, "key=%s" % key)
|
|
|
+ check("TTL 为 -2", r("TTL", key) == -2, "ttl=%s" % r("TTL", key))
|
|
|
+ after_logout = get("/store/goods/list", token)
|
|
|
+ check("旧 token 再访问返回 401", after_logout.get("status") == 401,
|
|
|
+ json.dumps(after_logout, ensure_ascii=False)[:160])
|
|
|
+
|
|
|
+ print("\n通过 %d / 失败 %d" % (passed, failed))
|
|
|
+ return 1 if failed else 0
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ sys.exit(main())
|