Python 接口自动化:Token 过期的9种解决方案

为什么Token管理是接口自动化的痛点?

 

在进行Python接口自动化测试时,相信大家都遇到过这样的困扰:

❌ 每次跑用例前手动更新 Token

❌ 测试中途 Token 过期,用例批量失败

❌ 多环境(dev/test/prod)Token 管理混乱

❌ 登录接口变化,所有用例都要改

 

Token过期导致测试用例批量失败,已经成为接口自动化测试中最常见的"拦路虎"。

 

别担心,今天就来彻底解决这个难题。我们将会从“为什么Token需要自动刷新”、“Token 刷新的常见场景”以及“Token过期的9种解决方案”三方面进行详述。

为什么Token需要自动刷新?

在Python接口自动化测试中,Token自动刷新不是一个可有可无的"锦上添花"功能,而是确保测试稳定性和可靠性的核心机制。

 

例如:大多数基于Token的认证机制(如JWT、OAuth2)都会为Token设置一个有效期(例如30分钟、2小时等)。一旦Token过期,接口就会返回401 Unauthorized错误,导致测试用例失败。这会导致测试的连续性和稳定性,影响测试效率,也会影响自动化测试模拟用户真实行为(如用户会在Token过期后通过刷新Token或重新登录来继续操作)。

Token 刷新的常见场景

 

Token 刷新的常见场景,如下图:

image

 我们要做的,就是让程序能自动判断并处理这四种情况。

Token过期的9种解决方案

 

解决方案1:简单粗暴型 - 每次请求前重新登录

核心思路:

每次发送请求前都先调用登录接口获取新Token

代码实现:

def get_fresh_token():
    """每次请求前获取新Token"""
    login_data = {"username": "testuser", "password": "testpass"}
    resp = requests.post(f"{BASE_URL}/login", json=login_data)
    return resp.json()["data"]["access_token"]
def test_api_with_fresh_token():
    token = get_fresh_token()
    headers = {"Authorization": f"Bearer {token}"}
    response = requests.get(f"{BASE_URL}/api/data", headers=headers)
    return response.json()

优缺点分析:

优点:实现简单,保证每次都是新Token;

缺点:效率极低,增加额外请求,频繁登录可能触发风控。

 

解决方案2:前置获取型 - 用例执行前统一获取

核心思路:

在测试套件执行前一次性获取Token,所有用例共享。

代码实现:

import pytest
class TestBase:
    token = None
    @classmethod
    def setup_class(cls):
        """测试类执行前获取Token"""
        cls.token = cls.get_token_once()
    @classmethod
    def get_token_once(cls):
        login_data = {"username": "testuser", "password": "testpass"}
        resp = requests.post(f"{BASE_URL}/login", json=login_data)
        return resp.json()["data"]["access_token"]
    def test_user_info(self):
        headers = {"Authorization": f"Bearer {self.token}"}
        response = requests.get(f"{BASE_URL}/user/info", headers=headers)
        assert response.status_code == 200

优缺点分析:

优点:减少登录次数,提高效率;

缺点:长时间运行测试时Token仍会过期。

 

解决方案3:智能检测型 - 请求失败时重新登录

核心思路:

捕获401错误,自动重新登录并重试请求

代码实现:

class SmartTokenClient:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.token = None
    def login(self):
        """登录获取Token"""
        login_data = {
            "username": self.username,
            "password": self.password
        }
        resp = requests.post(f"{self.base_url}/login", json=login_data)
        self.token = resp.json()["data"]["access_token"]
        return self.token
    def request_with_retry(self, method, endpoint, **kwargs):
        """带重试机制的请求"""
        if not self.token:
            self.login()
        headers = kwargs.get('headers', {})
        headers['Authorization'] = f'Bearer {self.token}'
        kwargs['headers'] = headers
        response = requests.request(method, f"{self.base_url}{endpoint}", **kwargs)
        # 检测到Token过期,重新登录并重试
        if response.status_code == 401:
            print("检测到Token过期,重新登录...")
            self.login()
            kwargs['headers']['Authorization'] = f'Bearer {self.token}'
            response = requests.request(method, f"{self.base_url}{endpoint}", **kwargs)
        return response
# 使用示例
client = SmartTokenClient(BASE_URL, "testuser", "testpass")
response = client.request_with_retry("GET", "/api/users")

优缺点分析

优点:自动处理过期情况,用户体验好;

缺点:第一次401错误无法避免,需要重试。

 

解决方案4:主动刷新型 - 定期刷新Token

核心思路:

记录Token获取时间,在过期前主动刷新

代码实现:

import time
class ProactiveTokenClient:
    def __init__(self, base_url, username, password, token_ttl=1800):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.token_ttl = token_ttl  # Token有效期,默认30分钟
        self.token = None
        self.token_time = 0
    def get_token(self):
        """获取Token,如果快过期则主动刷新"""
        current_time = time.time()
        # 没有Token或Token即将过期(提前5分钟刷新)
        if not self.token or (current_time - self.token_time) > (self.token_ttl - 300):
            self.refresh_token()
        return self.token
    def refresh_token(self):
        """刷新Token"""
        login_data = {
            "username": self.username, 
            "password": self.password
        }
        resp = requests.post(f"{self.base_url}/login", json=login_data)
        self.token = resp.json()["data"]["access_token"]
        self.token_time = time.time()
        print(f"Token已刷新: {self.token[:20]}...")
    def get(self, endpoint, **kwargs):
        """GET请求"""
        token = self.get_token()
        headers = kwargs.get('headers', {})
        headers['Authorization'] = f'Bearer {token}'
        kwargs['headers'] = headers
        return requests.get(f"{self.base_url}{endpoint}", **kwargs)

优缺点分析:

优点:避免401错误,用户体验最佳;

缺点:需要知道Token准确有效期,实现稍复杂。

 

解决方案5:双Token轮换型 - Access Token + Refresh Token

核心思路:

使用Refresh Token静默刷新Access Token,用户无感知

代码实现:

class DualTokenClient:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.access_token = None
        self.refresh_token = None
        self.setup_tokens()
    def setup_tokens(self):
        """初始获取双Token"""
        login_data = {
            "username": self.username,
            "password": self.password
        }
        resp = requests.post(f"{self.base_url}/login", json=login_data)
        token_data = resp.json()["data"]
        self.access_token = token_data["access_token"]
        self.refresh_token = token_data["refresh_token"]
    def refresh_access_token(self):
        """使用Refresh Token刷新Access Token"""
        refresh_data = {
            "refresh_token": self.refresh_token
        }
        try:
            resp = requests.post(f"{self.base_url}/refresh", json=refresh_data)
            if resp.status_code == 200:
                token_data = resp.json()["data"]
                self.access_token = token_data["access_token"]
                # 有些系统会返回新的refresh_token
                if "refresh_token" in token_data:
                    self.refresh_token = token_data["refresh_token"]
                return True
        except Exception as e:
            print(f"刷新Token失败: {e}")
        # 刷新失败,重新登录
        self.setup_tokens()
        return False
    def request(self, method, endpoint, max_retry=1, **kwargs):
        """支持Token自动刷新的请求"""
        for attempt in range(max_retry + 1):
            headers = kwargs.get('headers', {})
            headers['Authorization'] = f'Bearer {self.access_token}'
            kwargs['headers'] = headers
            response = requests.request(method, f"{self.base_url}{endpoint}", **kwargs)
            if response.status_code != 401:
                return response
            if attempt < max_retry:
                print("Access Token过期,尝试刷新...")
                if self.refresh_access_token():
                    continue
        return response

优缺点分析:

优点:用户体验极佳,安全性高;

缺点:需要服务端支持双Token机制。

 

解决方案6:装饰器模式 - 无侵入式Token管理

核心思路:

使用装饰器自动为请求方法添加Token管理功能。

代码实现

def auto_token_refresh(func):
    """自动Token刷新装饰器"""
    def wrapper(self, *args, **kwargs):
        # 确保有有效Token
        self.ensure_valid_token()
        # 执行原函数
        result = func(self, *args, **kwargs)
        # 如果返回401,刷新Token后重试
        if hasattr(result, 'status_code') and result.status_code == 401:
            print("检测到401错误,自动刷新Token并重试...")
            self.refresh_token()
            result = func(self, *args, **kwargs)
        return result
    return wrapper
class DecoratorTokenClient:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.token = None
    def login(self):
        """登录获取Token"""
        login_data = {
            "username": self.username,
            "password": self.password
        }
        resp = requests.post(f"{self.base_url}/login", json=login_data)
        self.token = resp.json()["data"]["access_token"]
        return self.token
    def refresh_token(self):
        """刷新Token"""
        return self.login()
    def ensure_valid_token(self):
        """确保有有效的Token"""
        if not self.token:
            self.login()
    @auto_token_refresh
    def get_users(self):
        """获取用户列表"""
        headers = {"Authorization": f"Bearer {self.token}"}
        return requests.get(f"{self.base_url}/users", headers=headers)
    @auto_token_refresh  
    def create_order(self, order_data):
        """创建订单"""
        headers = {"Authorization": f"Bearer {self.token}"}
        return requests.post(f"{self.base_url}/orders", json=order_data, headers=headers)
# 使用示例
client = DecoratorTokenClient(BASE_URL, "testuser", "testpass")
response = client.get_users()  # 自动处理Token

优缺点分析:

优点:代码简洁,无侵入性,易于维护;

缺点:需要为每个方法添加装饰器。

 

解决方案7:会话保持型 - 使用requests.Session。

核心思路:

利用Session对象保持登录状态,自动处理Cookie。

代码实现:

class SessionTokenClient:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.session = requests.Session()
        self.is_logged_in = False
    def login(self):
        """登录并保持会话"""
        login_data = {
            "username": self.username,
            "password": self.password
        }
        response = self.session.post(f"{self.base_url}/login", json=login_data)
        if response.status_code == 200:
            self.is_logged_in = True
            # 如果是Token认证,可能需要手动设置Header
            token = response.json()["data"]["access_token"]
            self.session.headers.update({"Authorization": f"Bearer {token}"})
        return response
    def ensure_login(self):
        """确保处于登录状态"""
        if not self.is_logged_in:
            self.login()
    def get(self, endpoint, **kwargs):
        """GET请求"""
        self.ensure_login()
        response = self.session.get(f"{self.base_url}{endpoint}", **kwargs)
        # 检查是否因会话过期而失败
        if response.status_code == 401:
            print("会话过期,重新登录...")
            self.login()
            response = self.session.get(f"{self.base_url}{endpoint}", **kwargs)
        return response
    def post(self, endpoint, **kwargs):
        """POST请求"""
        self.ensure_login()
        response = self.session.post(f"{self.base_url}{endpoint}", **kwargs)
        if response.status_code == 401:
            print("会话过期,重新登录...")
            self.login()
            response = self.session.post(f"{self.base_url}{endpoint}", **kwargs)
        return response
# 使用示例
client = SessionTokenClient(BASE_URL, "testuser", "testpass")
response = client.get("/api/users")  # 自动处理会话

优缺点分析:

优点:自动处理Cookie,适合基于会话的系统;

缺点:不适合纯Token认证的系统。

 

解决方案8:工厂模式 - 多环境Token管理

核心思路:

使用工厂模式管理不同环境的Token,支持动态切换。

代码实现:

from abc import ABC, abstractmethod
class TokenManager(ABC):
    """Token管理器抽象类"""
    @abstractmethod
    def get_token(self) -> str:
        pass
    @abstractmethod
    def refresh_token(self) -> bool:
        pass
class DevTokenManager(TokenManager):
    """开发环境Token管理器"""
    def get_token(self) -> str:
        # 开发环境可能返回固定Token或mock Token
        return "dev_mock_token"
    def refresh_token(self) -> bool:
        return True
class TestTokenManager(TokenManager):
    """测试环境Token管理器"""
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.token = None
    def get_token(self) -> str:
        if not self.token:
            self.refresh_token()
        return self.token
    def refresh_token(self) -> bool:
        login_data = {
            "username": self.username,
            "password": self.password
        }
        resp = requests.post(f"{self.base_url}/login", json=login_data)
        self.token = resp.json()["data"]["access_token"]
        return True
class ProdTokenManager(TokenManager):
    """生产环境Token管理器 - 更严格的安全控制"""
    def get_token(self) -> str:
        # 生产环境可能需要更复杂的认证流程
        raise Exception("生产环境请使用正式的认证流程")
    def refresh_token(self) -> bool:
        raise Exception("生产环境请使用正式的认证流程")
class TokenManagerFactory:
    """Token管理器工厂"""
    @staticmethod
    def create_manager(env: str, **kwargs) -> TokenManager:
        if env == "dev":
            return DevTokenManager()
        elif env == "test":
            return TestTokenManager(kwargs['base_url'], kwargs['username'], kwargs['password'])
        elif env == "prod":
            return ProdTokenManager()
        else:
            raise ValueError(f"不支持的环境: {env}")
# 使用示例
token_manager = TokenManagerFactory.create_manager(
    "test", 
    base_url="https://test.api.com",
    username="testuser", 
    password="testpass"
)
token = token_manager.get_token()
headers = {"Authorization": f"Bearer {token}"}
response = requests.get("https://test.api.com/api/data", headers=headers)

优缺点分析:

优点:支持多环境,扩展性强,符合开闭原则;

缺点:实现相对复杂,过度设计对于简单项目。

 

解决方案9:终极方案 - 封装支持自动刷新的AuthClient。

核心思路:

设计一个 AuthHttpClient 类,具备以下能力:

  • 自动登录获取 Token;

  • 请求时自动携带 Token;

  • 检测 401 错误,自动刷新;

  • 刷新失败则重新登录;

  • Token 持久化(文件或内存);

  • 线程安全,支持并发。

代码实现:

import requests
import json
import os
import time
from typing import Optional, Dict, Any
# =======================
# ?? 认证客户端(支持自动刷新)
# =======================
class AuthHttpClient:
    def __init__(
        self,
        base_url: str,
        login_url: str = "/auth/login",
        refresh_url: str = "/auth/refresh",
        username: str = "",
        password: str = "",
        token_file: str = "cache/token.json"
    ):
        self.base_url = base_url.rstrip("/")
        self.login_url = login_url
        self.refresh_url = refresh_url
        self.username = username
        self.password = password
        self.token_file = token_file
        self.session = requests.Session()
        # 确保缓存目录存在
        os.makedirs(os.path.dirname(token_file), exist_ok=True)
    def _save_token(self, token: str, refresh_token: str, expires_in: int):
        """保存 Token 到本地文件"""
        data = {
            "access_token": token,
            "refresh_token": refresh_token,
            "expires_at": time.time() + expires_in - 60,  # 提前60秒过期
            "username": self.username
        }
        with open(self.token_file, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)
    def _load_token(self) -> Optional[Dict[str, Any]]:
        """从文件加载 Token"""
        if not os.path.exists(self.token_file):
            return None
        try:
            with open(self.token_file, "r", encoding="utf-8") as f:
                return json.load(f)
        except:
            return None
    def _is_token_expired(self) -> bool:
        """检查 Token 是否过期"""
        token_data = self._load_token()
        if not token_data:
            return True
        return time.time() >= token_data.get("expires_at", 0)
    def login(self) -> bool:
        """登录并获取 Token"""
        try:
            resp = self.session.post(
                f"{self.base_url}{self.login_url}",
                json={"username": self.username, "password": self.password}
            )
            if resp.status_code == 200:
                data = resp.json()
                token = data["data"]["access_token"]
                refresh_token = data["data"]["refresh_token"]
                expires_in = data["data"].get("expires_in", 1800)  # 默认30分钟
                self._save_token(token, refresh_token, expires_in)
                self.session.headers.update({"Authorization": f"Bearer {token}"})
                print(f"? 登录成功,Token 已保存")
                return True
            else:
                print(f"? 登录失败: {resp.text}")
                return False
        except Exception as e:
            print(f"? 登录异常: {e}")
            return False
    def refresh_token(self) -> bool:
        """刷新 Token"""
        token_data = self._load_token()
        if not token_data:
            return False
        try:
            resp = self.session.post(
                f"{self.base_url}{self.refresh_url}",
                json={"refresh_token": token_data["refresh_token"]}
            )
            if resp.status_code == 200:
                data = resp.json()
                new_token = data["data"]["access_token"]
                new_refresh = data["data"]["refresh_token"]
                expires_in = data["data"].get("expires_in", 1800)
                self._save_token(new_token, new_refresh, expires_in)
                self.session.headers.update({"Authorization": f"Bearer {new_token}"})
                print(f"?? Token 刷新成功")
                return True
            else:
                print(f"? Token 刷新失败: {resp.text}")
                return False
        except Exception as e:
            print(f"? 刷新异常: {e}")
            return False
    def ensure_auth(self) -> bool:
        """确保当前有有效 Token(自动登录或刷新)"""
        if self._is_token_expired():
            print("?? Token 已过期,尝试刷新...")
            if not self.refresh_token():
                print("?? 刷新失败,正在重新登录...")
                return self.login()
        else:
            # 加载有效 Token
            token_data = self._load_token()
            self.session.headers.update({"Authorization": f"Bearer {token_data['access_token']}"})
            print("?? 使用现有 Token")
        return True
    def request(self, method: str, url: str, **kwargs) -> requests.Response:
        """重写 request,自动处理认证"""
        full_url = self.base_url + ("" if url.startswith("/") else "/") + url
        # 确保认证有效
        if not self.ensure_auth():
            raise Exception("认证失败,无法继续请求")
        try:
            resp = self.session.request(method, full_url, **kwargs)
            # 如果返回 401,可能是刷新失败,尝试重新登录
            if resp.status_code == 401:
                print("?? 收到 401,尝试重新登录...")
                self.login()
                # 重新发送请求
                resp = self.session.request(method, full_url, **kwargs)
            return resp
        except Exception as e:
            print(f"? 请求异常: {e}")
            raise
    # 便捷方法
    def get(self, url, **kwargs):
        return self.request("GET", url, **kwargs)
    def post(self, url, **kwargs):
        return self.request("POST", url, **kwargs)
    def put(self, url, **kwargs):
        return self.request("PUT", url, **kwargs)
    def delete(self, url, **kwargs):
        return self.request("DELETE", url, **kwargs)

优缺点分析:

这个方案融合了前面所有方案的优点,提供了最完善、最健壮的Token自动刷新机制。

posted @ 2025-11-07 15:33  溺水的小金鱼  阅读(1)  评论(0)    收藏  举报