One-Line Python Decorator to Auto‑Encrypt, Sign, and Secure API Requests
Learn how to replace repetitive manual signing, encryption, timestamp, and nonce handling in API tests with a single Python @secure_request decorator that automatically encrypts specified fields, adds security parameters, generates signatures, and sends the request, improving maintainability and reducing errors.
Background
Modern API automation testing often requires digital signatures, encryption of sensitive parameters, and inclusion of timestamp and nonce to prevent replay attacks. Manually constructing these elements for each test case leads to duplicated code, high error risk, and difficult maintenance.
Typical Pain Points
Repeated logic for signing, encrypting, and adding security fields in every test.
Algorithm changes require modifications across all test cases.
Easy to miss fields such as nonce or timestamp.
Solution: @secure_request Decorator
The decorator abstracts all security concerns. By decorating a function that returns a plain‑text parameter dictionary, the decorator automatically encrypts configured fields, injects timestamp and nonce, generates a signature, and sends the HTTP request.
@secure_request(
encrypt_fields=["phone", "id_card"],
secret_key="my_secret_2026",
sign_algorithm="md5"
)
def create_user(name: str, phone: str):
return {"name": name, "phone": phone, "action": "register"}
# Test case
resp = create_user("Alice", "13800138000") # Encryption, signing, and request are automatic
assert resp.json()["code"] == 200Core Implementation
import os, time, random, hashlib, json, functools
from typing import List, Dict, Any, Optional
from Crypto.Cipher import AES # requires pycryptodome
from Crypto.Util.Padding import pad
import base64, requests
def random_str(length: int = 8) -> str:
chars = "abcdefghijklmnopqrstuvwxyz0123456789"
return ''.join(random.choice(chars) for _ in range(length))
def aes_encrypt(data: str, key: str) -> str:
key_bytes = key.encode('utf-8')[:16].ljust(16, b'\0')
cipher = AES.new(key_bytes, AES.MODE_ECB)
padded_data = pad(data.encode('utf-8'), AES.block_size)
encrypted = cipher.encrypt(padded_data)
return base64.b64encode(encrypted).decode('utf-8')
def secure_request(
url: str,
method: str = "POST",
encrypt_fields: Optional[List[str]] = None,
secret_key: str = os.getenv("API_SECRET_KEY", ""),
sign_algorithm: str = "md5",
timeout: int = 10
):
"""Decorator that adds encryption, signature, timestamp, and nonce to API requests.
:param url: Full endpoint URL
:param method: HTTP method
:param encrypt_fields: List of field names to encrypt
:param secret_key: Signing key
:param sign_algorithm: "md5" or "sha256"
:param timeout: Request timeout in seconds
"""
encrypt_fields = encrypt_fields or []
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
raw_params = func(*args, **kwargs)
if not isinstance(raw_params, dict):
raise ValueError("Decorated function must return a dict")
params = raw_params.copy()
params["timestamp"] = str(int(time.time()))
params["nonce"] = random_str()
for field in encrypt_fields:
if field in params:
params[field] = aes_encrypt(str(params[field]), secret_key)
sign_items = sorted(params.items())
sign_str = "&".join([f"{k}={v}" for k, v in sign_items]) + f"&key={secret_key}"
if sign_algorithm == "md5":
sign = hashlib.md5(sign_str.encode()).hexdigest()
elif sign_algorithm == "sha256":
sign = hashlib.sha256(sign_str.encode()).hexdigest()
else:
raise ValueError(f"Unsupported sign algorithm: {sign_algorithm}")
params["sign"] = sign
if method.upper() == "GET":
resp = requests.get(url, params=params, timeout=timeout)
else:
resp = requests.post(url, json=params, timeout=timeout)
return resp
return wrapper
return decoratorUsage Example
@secure_request(
url="https://api.example.com/v1/user",
method="POST",
encrypt_fields=["phone", "id_card"],
secret_key="MySuperSecretKey2026!",
sign_algorithm="sha256"
)
def register_user(name: str, phone: str, id_card: str):
"""Return plain parameters; decorator handles security automatically"""
return {"name": name, "phone": phone, "id_card": id_card, "channel": "auto_test"}
# Test case
resp = register_user("张三", "13800138000", "11010119900307XXXX")
assert resp.status_code == 200
assert resp.json()["success"] is TrueIn the actual request body, phone and id_card appear as Base64‑encoded AES ciphertexts, together with timestamp, nonce, and the generated sign.
Security Enhancement Suggestions
Support Chinese national algorithms SM2/SM4 by replacing aes_encrypt with a gmssl implementation.
Use dynamic secret‑key management (e.g., fetch from Vault or KMS) instead of hard‑coding.
Enforce replay‑attack protection by validating timestamp within an acceptable window and ensuring nonce uniqueness.
Require HTTPS URLs; raise a warning if the endpoint does not start with https://.
Integration with Test Frameworks
Provide a pytest fixture to inject secret_key per environment.
Attach sanitized request parameters to Allure reports for auditability.
Make the decorator compatible with existing mock utilities to skip security logic in mock mode.
Why This Decorator Is a Powerful Tool for Secure Testing
Compared with the traditional approach of manually writing encryption and signing logic in each test case, the decorator offers a one‑line configuration that automatically handles all security steps, eliminates code duplication, reduces the risk of missing fields, and isolates business logic from security concerns.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
