refactor(backend): 重命名后端项目为 gym-manage-api,修改包名为 cn.novalon.gym.manage
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
JWT安全测试套件
|
||||
|
||||
测试范围:
|
||||
1. JWT Token生成与验证
|
||||
2. Token过期处理
|
||||
3. Token签名验证
|
||||
4. Token刷新机制
|
||||
5. 密钥安全性验证
|
||||
|
||||
作者: 张翔
|
||||
日期: 2026-04-01
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from api.auth_api import AuthAPI
|
||||
from api.user_api import UserAPI
|
||||
from config.settings import settings
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.asyncio
|
||||
class TestJWTSecurity:
|
||||
"""JWT安全测试类"""
|
||||
|
||||
async def test_jwt_token_structure(self, authenticated_client):
|
||||
"""
|
||||
SEC-JWT-01: JWT Token结构验证
|
||||
|
||||
验证点:
|
||||
1. Token包含正确的Header
|
||||
2. Token包含正确的Payload
|
||||
3. Token使用正确的签名算法
|
||||
"""
|
||||
auth_api = AuthAPI(authenticated_client)
|
||||
|
||||
login_response = await auth_api.login("admin", "admin123")
|
||||
assert login_response.status_code == 200
|
||||
|
||||
token = login_response.json().get("token")
|
||||
assert token is not None, "未获取到Token"
|
||||
|
||||
decoded = jwt.decode(token, options={"verify_signature": False})
|
||||
|
||||
assert "sub" in decoded, "Token缺少subject声明"
|
||||
assert "exp" in decoded, "Token缺少过期时间"
|
||||
assert "iat" in decoded, "Token缺少签发时间"
|
||||
assert "userId" in decoded or "user_id" in decoded, "Token缺少用户ID"
|
||||
|
||||
async def test_jwt_token_expiration(self, authenticated_client):
|
||||
"""
|
||||
SEC-JWT-02: JWT Token过期验证
|
||||
|
||||
验证点:
|
||||
1. Token有过期时间
|
||||
2. 过期时间在合理范围内
|
||||
3. 过期Token无法使用
|
||||
"""
|
||||
auth_api = AuthAPI(authenticated_client)
|
||||
|
||||
login_response = await auth_api.login("admin", "admin123")
|
||||
token = login_response.json().get("token")
|
||||
|
||||
decoded = jwt.decode(token, options={"verify_signature": False})
|
||||
|
||||
exp_time = datetime.fromtimestamp(decoded["exp"])
|
||||
iat_time = datetime.fromtimestamp(decoded["iat"])
|
||||
|
||||
time_diff = (exp_time - iat_time).total_seconds()
|
||||
|
||||
assert time_diff > 0, "Token过期时间无效"
|
||||
assert time_diff <= 86400, "Token有效期不应超过24小时"
|
||||
|
||||
async def test_jwt_signature_verification(self, authenticated_client):
|
||||
"""
|
||||
SEC-JWT-03: JWT签名验证
|
||||
|
||||
验证点:
|
||||
1. 篡改的Token被拒绝
|
||||
2. 无效签名的Token被拒绝
|
||||
"""
|
||||
auth_api = AuthAPI(authenticated_client)
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
login_response = await auth_api.login("admin", "admin123")
|
||||
valid_token = login_response.json().get("token")
|
||||
|
||||
tampered_token = valid_token[:-5] + "XXXXX"
|
||||
|
||||
client_with_tampered = authenticated_client.__class__(
|
||||
base_url=settings.API_BASE_URL,
|
||||
headers={"Authorization": f"Bearer {tampered_token}"}
|
||||
)
|
||||
|
||||
user_api_tampered = UserAPI(client_with_tampered)
|
||||
response = await user_api_tampered.get_users_by_page()
|
||||
|
||||
assert response.status_code in [401, 403], "篡改的Token应该被拒绝"
|
||||
|
||||
async def test_jwt_algorithm_security(self, authenticated_client):
|
||||
"""
|
||||
SEC-JWT-04: JWT算法安全验证
|
||||
|
||||
验证点:
|
||||
1. 不允许使用none算法
|
||||
2. 不允许算法混淆攻击
|
||||
"""
|
||||
auth_api = AuthAPI(authenticated_client)
|
||||
|
||||
login_response = await auth_api.login("admin", "admin123")
|
||||
token = login_response.json().get("token")
|
||||
|
||||
header = jwt.get_unverified_header(token)
|
||||
|
||||
assert header["alg"] != "none", "不应允许none算法"
|
||||
assert header["alg"] in ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"], \
|
||||
"应使用安全的签名算法"
|
||||
|
||||
async def test_jwt_claims_validation(self, authenticated_client):
|
||||
"""
|
||||
SEC-JWT-05: JWT声明验证
|
||||
|
||||
验证点:
|
||||
1. 必要的声明存在
|
||||
2. 声明值有效
|
||||
"""
|
||||
auth_api = AuthAPI(authenticated_client)
|
||||
|
||||
login_response = await auth_api.login("admin", "admin123")
|
||||
token = login_response.json().get("token")
|
||||
|
||||
decoded = jwt.decode(token, options={"verify_signature": False})
|
||||
|
||||
required_claims = ["sub", "exp", "iat"]
|
||||
for claim in required_claims:
|
||||
assert claim in decoded, f"Token缺少必要声明: {claim}"
|
||||
|
||||
assert decoded["sub"] == "admin", "Subject应该是用户名"
|
||||
assert decoded["exp"] > time.time(), "Token不应已过期"
|
||||
|
||||
async def test_jwt_token_in_validation(self, authenticated_client):
|
||||
"""
|
||||
SEC-JWT-06: 无效Token验证
|
||||
|
||||
验证点:
|
||||
1. 空Token被拒绝
|
||||
2. 格式错误的Token被拒绝
|
||||
3. 过期Token被拒绝
|
||||
"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
invalid_tokens = [
|
||||
"",
|
||||
"invalid.token.format",
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid",
|
||||
None
|
||||
]
|
||||
|
||||
for invalid_token in invalid_tokens:
|
||||
if invalid_token is None:
|
||||
continue
|
||||
|
||||
client_with_invalid = authenticated_client.__class__(
|
||||
base_url=settings.API_BASE_URL,
|
||||
headers={"Authorization": f"Bearer {invalid_token}"}
|
||||
)
|
||||
|
||||
user_api_invalid = UserAPI(client_with_invalid)
|
||||
response = await user_api_invalid.get_users_by_page()
|
||||
|
||||
assert response.status_code in [401, 403, 400], \
|
||||
f"无效Token '{invalid_token}' 应该被拒绝"
|
||||
|
||||
async def test_jwt_token_refresh_mechanism(self, authenticated_client):
|
||||
"""
|
||||
SEC-JWT-07: Token刷新机制验证
|
||||
|
||||
验证点:
|
||||
1. 支持Token刷新
|
||||
2. 刷新后生成新Token
|
||||
3. 旧Token失效或共存
|
||||
"""
|
||||
auth_api = AuthAPI(authenticated_client)
|
||||
|
||||
login_response = await auth_api.login("admin", "admin123")
|
||||
original_token = login_response.json().get("token")
|
||||
|
||||
try:
|
||||
refresh_response = await auth_api.refresh_token(original_token)
|
||||
|
||||
if refresh_response.status_code == 200:
|
||||
new_token = refresh_response.json().get("token")
|
||||
assert new_token is not None, "刷新应返回新Token"
|
||||
assert new_token != original_token, "新Token应不同于原Token"
|
||||
else:
|
||||
pytest.skip("系统未实现Token刷新机制")
|
||||
except Exception as e:
|
||||
pytest.skip(f"Token刷新机制测试跳过: {str(e)}")
|
||||
|
||||
async def test_jwt_key_strength(self, authenticated_client):
|
||||
"""
|
||||
SEC-JWT-08: JWT密钥强度验证
|
||||
|
||||
验证点:
|
||||
1. 密钥长度足够
|
||||
2. 密钥不是弱密钥
|
||||
"""
|
||||
auth_api = AuthAPI(authenticated_client)
|
||||
|
||||
login_response = await auth_api.login("admin", "admin123")
|
||||
token = login_response.json().get("token")
|
||||
|
||||
header = jwt.get_unverified_header(token)
|
||||
|
||||
if header["alg"].startswith("HS"):
|
||||
weak_secrets = [
|
||||
"secret", "password", "123456", "admin",
|
||||
"your-256-bit-secret", "your-secret-key"
|
||||
]
|
||||
|
||||
for weak_secret in weak_secrets:
|
||||
try:
|
||||
jwt.decode(token, weak_secret, algorithms=[header["alg"]])
|
||||
pytest.fail(f"使用了弱密钥: {weak_secret}")
|
||||
except jwt.InvalidSignatureError:
|
||||
pass
|
||||
|
||||
async def test_jwt_user_impersonation_prevention(self, authenticated_client):
|
||||
"""
|
||||
SEC-JWT-09: 用户伪装防护验证
|
||||
|
||||
验证点:
|
||||
1. 无法通过修改Token伪装其他用户
|
||||
2. 用户ID与Token绑定
|
||||
"""
|
||||
auth_api = AuthAPI(authenticated_client)
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
login_response = await auth_api.login("admin", "admin123")
|
||||
token = login_response.json().get("token")
|
||||
|
||||
decoded = jwt.decode(token, options={"verify_signature": False})
|
||||
|
||||
users_response = await user_api.get_users_by_page()
|
||||
assert users_response.status_code == 200
|
||||
|
||||
users = users_response.json().get("content", [])
|
||||
other_user = next((u for u in users if u.get("username") != "admin"), None)
|
||||
|
||||
if other_user:
|
||||
client_with_admin_token = authenticated_client.__class__(
|
||||
base_url=settings.API_BASE_URL,
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
|
||||
user_api_admin = UserAPI(client_with_admin_token)
|
||||
response = await user_api_admin.get_user_by_id(other_user["id"])
|
||||
|
||||
assert response.status_code in [200, 403], "应正确处理跨用户访问"
|
||||
Reference in New Issue
Block a user