在接口自动化测试中,Session会话保持是一个经常被忽视但极其重要的技术点。我见过太多测试工程师直接用requests库发请求,每次请求都是独立的,完全忽略了服务端会话状态的管理。实际上,现代Web应用中超过70%的接口都需要依赖会话状态,比如用户登录态、购物车数据、多步骤流程等。
上周我刚帮一个电商团队排查过这个问题:他们的自动化测试脚本单独跑登录接口能成功,但后续查询订单总是返回401。原因很简单——没有保持会话,服务端根本不认识第二次请求是谁发来的。这种问题在实际项目中太常见了,今天我就用实战案例带大家彻底掌握Session的正确用法。
很多新手会把Cookie和Session混为一谈。简单来说:
当客户端第一次访问服务端时,服务端会:
Python的requests.Session()实际上是一个高级的会话管理器,它会自动:
python复制import requests
# 错误示范:每次都是独立请求
requests.get('https://api.example.com/login')
requests.get('https://api.example.com/profile') # 无会话状态
# 正确做法
session = requests.Session()
session.get('https://api.example.com/login') # 自动保存Cookie
session.get('https://api.example.com/profile') # 保持会话
我们以典型电商流程为例:
python复制class TestEcommerce:
def setup_class(self):
self.session = requests.Session()
self.base_url = "https://api.shop.com/v1"
def test_login(self):
resp = self.session.post(
f"{self.base_url}/login",
json={"username": "test", "password": "123456"}
)
assert resp.status_code == 200
assert "auth_token" in resp.cookies # 关键点:检查Cookie
def test_add_to_cart(self):
# 注意:这里不需要再传token,Session自动维护
resp = self.session.post(
f"{self.base_url}/cart",
json={"product_id": 1001, "quantity": 2}
)
assert resp.json()["success"] is True
生产环境Session通常有有效期,需要特殊处理:
python复制def test_session_expiry():
session = requests.Session()
# 第一次登录成功
login_resp = session.post("/login", {...})
# 模拟会话过期(根据实际业务调整等待时间)
time.sleep(3600)
# 再次请求
cart_resp = session.post("/cart", {...})
if cart_resp.status_code == 401:
# 自动重新登录
new_login = session.post("/login", {...})
# 重试原请求
cart_resp = session.post("/cart", {...})
assert cart_resp.status_code == 200
当需要测试不同用户角色时:
python复制def test_multi_user():
# 管理员会话
admin_session = requests.Session()
admin_session.post("/login", json={"role": "admin"})
# 普通用户会话
user_session = requests.Session()
user_session.post("/login", json={"role": "user"})
# 并行操作
admin_resp = admin_session.get("/admin/dashboard")
user_resp = user_session.get("/user/profile")
assert admin_resp.json()["permission"] == "all"
assert user_resp.json()["permission"] == "basic"
安全测试中的重要场景:
python复制def test_session_hijacking():
# 正常登录
legal_session = requests.Session()
legal_session.post("/login", json={"user": "legal"})
# 攻击者获取到Session ID后
illegal_session = requests.Session()
illegal_session.cookies.update({"sessionid": "窃取的ID"})
resp = illegal_session.get("/user/data")
# 断言系统应有防护措施
assert resp.status_code == 403
assert "Invalid session" in resp.text
现象:请求返回Set-Cookie但后续请求未携带
排查步骤:
session.cookies.get_dict()调试python复制# 调试示例
session = requests.Session()
resp = session.get("https://example.com/login")
print(session.cookies.get_dict()) # 查看当前Cookies
现象:服务端会话丢失或混乱
解决方案:
python复制@pytest.fixture
def clean_session():
# 每个测试用例使用全新会话
session = requests.Session()
yield session
session.close() # 显式清理
python复制from requests.adapters import HTTPAdapter
session = requests.Session()
# 配置连接池
adapter = HTTPAdapter(
pool_connections=10, # 连接池数量
pool_maxsize=50, # 最大连接数
max_retries=3 # 重试次数
)
session.mount("https://", adapter)
对于高频测试场景:
python复制# 使用cachetools实现会话缓存
from cachetools import TTLCache
session_cache = TTLCache(maxsize=100, ttl=3600)
def get_cached_session(user):
if user not in session_cache:
session = requests.Session()
session.login(...)
session_cache[user] = session
return session_cache[user]
当测试需要跨机器执行时:
python复制# Redis共享会话方案
import redis
import pickle
def save_session_to_redis(session, key):
r = redis.Redis()
r.set(key, pickle.dumps(session.cookies.get_dict()))
def load_session_from_redis(key):
session = requests.Session()
cookies = pickle.loads(r.get(key))
session.cookies.update(cookies)
return session
python复制# 自定义请求钩子记录会话活动
def session_monitor(resp, *args, **kwargs):
log_data = {
"timestamp": datetime.now(),
"url": resp.url,
"cookies": dict(resp.cookies),
"status": resp.status_code
}
logging.info(json.dumps(log_data))
session = requests.Session()
session.hooks["response"].append(session_monitor)
现代API常用模式:
python复制# JWT会话处理示例
session = requests.Session()
# 登录获取JWT
login_resp = session.post("/auth", json={"user": "test"})
token = login_resp.json()["token"]
# 后续请求携带Authorization头
session.headers.update({"Authorization": f"Bearer {token}"})
对于完全无状态的API:
python复制# 使用请求签名替代会话
def sign_request(method, url, body):
nonce = str(uuid.uuid4())
timestamp = int(time.time())
signature = hmac.new(
SECRET_KEY,
f"{method}{url}{body}{nonce}{timestamp}".encode(),
hashlib.sha256
).hexdigest()
return {
"X-Auth-Nonce": nonce,
"X-Auth-Timestamp": timestamp,
"X-Auth-Signature": signature
}
# 每个请求独立签名
headers = sign_request("POST", "/api/order", json.dumps(order_data))
requests.post("/api/order", json=order_data, headers=headers)