springbootAI 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. spring/__init__.py +66 -0
  2. spring/ai/__init__.py +78 -0
  3. spring/ai/advisors.py +139 -0
  4. spring/ai/annotations.py +74 -0
  5. spring/ai/autoconfig.py +481 -0
  6. spring/ai/core.py +391 -0
  7. spring/ai/etl.py +188 -0
  8. spring/ai/memory.py +109 -0
  9. spring/ai/observability.py +129 -0
  10. spring/ai/providers.py +789 -0
  11. spring/ai/resilience.py +258 -0
  12. spring/ai/tools.py +106 -0
  13. spring/ai/vectorstore.py +303 -0
  14. spring/annotations/__init__.py +188 -0
  15. spring/annotations/cache.py +126 -0
  16. spring/annotations/cloud.py +207 -0
  17. spring/annotations/conditional.py +272 -0
  18. spring/annotations/core.py +864 -0
  19. spring/annotations/messaging.py +107 -0
  20. spring/aop/__init__.py +4 -0
  21. spring/aop/cloud_aop.py +404 -0
  22. spring/aop/comprehensive_aop.py +1015 -0
  23. spring/aop/method_interceptor.py +19 -0
  24. spring/aop/proxy_factory.py +55 -0
  25. spring/cloud/__init__.py +76 -0
  26. spring/cloud/discovery.py +364 -0
  27. spring/cloud/feign.py +469 -0
  28. spring/cloud/gateway.py +452 -0
  29. spring/cloud/load_balancer.py +149 -0
  30. spring/cloud/seata.py +557 -0
  31. spring/cloud/sentinel.py +525 -0
  32. spring/cloud/tracer.py +337 -0
  33. spring/config/__init__.py +21 -0
  34. spring/config/binding.py +206 -0
  35. spring/config/config_loader.py +405 -0
  36. spring/context/__init__.py +13 -0
  37. spring/context/application_context.py +589 -0
  38. spring/context/bean_definition.py +70 -0
  39. spring/context/bean_factory.py +1052 -0
  40. spring/context/registry.py +58 -0
  41. spring/context/scanner.py +106 -0
  42. spring/core/__init__.py +3 -0
  43. spring/core/graceful_shutdown.py +196 -0
  44. spring/core/typing_utils.py +50 -0
  45. spring/csv/__init__.py +52 -0
  46. spring/csv/annotations.py +402 -0
  47. spring/csv/converters.py +69 -0
  48. spring/csv/easy_csv.py +95 -0
  49. spring/csv/exceptions.py +27 -0
  50. spring/csv/reader.py +195 -0
  51. spring/csv/writer.py +155 -0
  52. spring/data/__init__.py +54 -0
  53. spring/data/page.py +181 -0
  54. spring/data/repository.py +274 -0
  55. spring/data/specification.py +228 -0
  56. spring/datasource/__init__.py +66 -0
  57. spring/datasource/annotations.py +133 -0
  58. spring/datasource/context.py +69 -0
  59. spring/datasource/dynamic.py +148 -0
  60. spring/event/__init__.py +7 -0
  61. spring/event/publisher.py +69 -0
  62. spring/excel/__init__.py +51 -0
  63. spring/excel/annotations.py +405 -0
  64. spring/excel/converters.py +231 -0
  65. spring/excel/easy_excel.py +94 -0
  66. spring/excel/exceptions.py +31 -0
  67. spring/excel/reader.py +254 -0
  68. spring/excel/style.py +95 -0
  69. spring/excel/writer.py +197 -0
  70. spring/i18n/__init__.py +97 -0
  71. spring/i18n/accessor.py +94 -0
  72. spring/i18n/auto_config.py +177 -0
  73. spring/i18n/holder.py +106 -0
  74. spring/i18n/locale.py +152 -0
  75. spring/i18n/locale_resolver.py +367 -0
  76. spring/i18n/message_source.py +250 -0
  77. spring/i18n/middleware.py +79 -0
  78. spring/i18n/properties.py +168 -0
  79. spring/i18n/sources.py +255 -0
  80. spring/logging/__init__.py +1 -0
  81. spring/logging/loguru_logger.py +228 -0
  82. spring/main.py +378 -0
  83. spring/messaging/__init__.py +1 -0
  84. spring/messaging/rabbitmq.py +302 -0
  85. spring/monitoring/__init__.py +1 -0
  86. spring/monitoring/prometheus.py +199 -0
  87. spring/orm/__init__.py +258 -0
  88. spring/orm/database.py +222 -0
  89. spring/orm/ddl_auto.py +1217 -0
  90. spring/orm/migration.py +419 -0
  91. spring/orm/mybatis_integration.py +400 -0
  92. spring/orm/pymybatis/__init__.py +86 -0
  93. spring/orm/pymybatis/annotations/__init__.py +30 -0
  94. spring/orm/pymybatis/annotations/annotations.py +332 -0
  95. spring/orm/pymybatis/cache/__init__.py +47 -0
  96. spring/orm/pymybatis/cache/cache.py +371 -0
  97. spring/orm/pymybatis/cache/redis_cache.py +434 -0
  98. spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
  99. spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
  100. spring/orm/pymybatis/configuration.py +525 -0
  101. spring/orm/pymybatis/core/__init__.py +10 -0
  102. spring/orm/pymybatis/core/sql_session.py +1382 -0
  103. spring/orm/pymybatis/core/sql_session_factory.py +76 -0
  104. spring/orm/pymybatis/dialect/__init__.py +9 -0
  105. spring/orm/pymybatis/dialect/dialect.py +445 -0
  106. spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
  107. spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
  108. spring/orm/pymybatis/interceptor/__init__.py +31 -0
  109. spring/orm/pymybatis/interceptor/interceptor.py +427 -0
  110. spring/orm/pymybatis/mapper/__init__.py +9 -0
  111. spring/orm/pymybatis/mapper/mapper.py +540 -0
  112. spring/orm/pymybatis/metrics/__init__.py +41 -0
  113. spring/orm/pymybatis/metrics/metrics.py +595 -0
  114. spring/orm/pymybatis/pool/__init__.py +9 -0
  115. spring/orm/pymybatis/pool/connection_pool.py +711 -0
  116. spring/orm/pymybatis/security/__init__.py +19 -0
  117. spring/orm/pymybatis/security/access_control.py +415 -0
  118. spring/orm/pymybatis/security/password_encoder.py +293 -0
  119. spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
  120. spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
  121. spring/orm/pymybatis/transaction/__init__.py +9 -0
  122. spring/orm/pymybatis/transaction/transaction.py +288 -0
  123. spring/orm/pymybatis/type_handler/__init__.py +37 -0
  124. spring/orm/pymybatis/type_handler/type_handler.py +473 -0
  125. spring/orm/pymybatis/version.py +9 -0
  126. spring/orm/pymybatis/xml_parser/__init__.py +9 -0
  127. spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
  128. spring/retry/__init__.py +12 -0
  129. spring/retry/retry_annotations.py +71 -0
  130. spring/retry/retry_decorator.py +155 -0
  131. spring/scheduling/__init__.py +3 -0
  132. spring/scheduling/scheduler.py +389 -0
  133. spring/security/__init__.py +39 -0
  134. spring/security/jwt_utils.py +281 -0
  135. spring/security/replay_protection.py +206 -0
  136. spring/security/secret_manager.py +226 -0
  137. spring/security/security_aop.py +248 -0
  138. spring/security/security_context.py +172 -0
  139. spring/test/__init__.py +45 -0
  140. spring/test/slicing.py +341 -0
  141. spring/tracing/__init__.py +11 -0
  142. spring/tracing/skywalking.py +229 -0
  143. spring/tx/__init__.py +52 -0
  144. spring/tx/events.py +172 -0
  145. spring/tx/synchronization.py +143 -0
  146. spring/utils/__init__.py +5 -0
  147. spring/utils/banner.py +32 -0
  148. spring/utils/logger.py +73 -0
  149. spring/utils/redis_client.py +526 -0
  150. spring/validation/__init__.py +55 -0
  151. spring/validation/aop.py +141 -0
  152. spring/validation/constraints.py +357 -0
  153. spring/validation/exceptions.py +55 -0
  154. spring/validation/validator.py +139 -0
  155. spring/web/__init__.py +12 -0
  156. spring/web/actuator.py +319 -0
  157. spring/web/exception_handler.py +61 -0
  158. spring/web/health.py +399 -0
  159. spring/web/interceptor.py +91 -0
  160. spring/web/result.py +44 -0
  161. spring/web/swagger.py +601 -0
  162. spring/web/web_context.py +755 -0
  163. spring/websocket/__init__.py +86 -0
  164. spring/websocket/annotations.py +169 -0
  165. spring/websocket/broker.py +238 -0
  166. spring/websocket/exceptions.py +26 -0
  167. spring/websocket/handler.py +243 -0
  168. spring/websocket/router.py +526 -0
  169. spring/websocket/session.py +216 -0
  170. springbootai-1.8.0.dist-info/METADATA +2796 -0
  171. springbootai-1.8.0.dist-info/RECORD +175 -0
  172. springbootai-1.8.0.dist-info/WHEEL +5 -0
  173. springbootai-1.8.0.dist-info/entry_points.txt +2 -0
  174. springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
  175. springbootai-1.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,281 @@
1
+ """
2
+ JWT 工具类
3
+ 提供 Token 生成、验证、解析等功能
4
+ """
5
+ import jwt
6
+ import time
7
+ import uuid
8
+ import functools
9
+ from typing import Optional, Dict, Any
10
+
11
+
12
+ class _InstanceOrDefaultMethod:
13
+ """Bind to an explicit instance, or to the configured module singleton."""
14
+
15
+ def __init__(self, func):
16
+ self.func = func
17
+ functools.update_wrapper(self, func)
18
+
19
+ def __get__(self, instance, owner):
20
+ target = instance
21
+ if target is None:
22
+ target = globals().get('jwt_utils')
23
+ if target is None:
24
+ target = owner()
25
+ return self.func.__get__(target, owner)
26
+
27
+
28
+ class JwtUtils:
29
+ """JWT工具类"""
30
+
31
+ ALLOWED_ALGORITHMS = {'HS256', 'HS384', 'HS512'}
32
+
33
+ def __init__(self, secret_key: str = None, algorithm: str = 'HS256'):
34
+ self.configure(secret_key=secret_key, algorithm=algorithm)
35
+
36
+ def configure(
37
+ self,
38
+ secret_key: Optional[str] = None,
39
+ algorithm: str = 'HS256',
40
+ issuer: Optional[str] = None,
41
+ audience: Optional[str] = None,
42
+ leeway: int = 0,
43
+ ) -> None:
44
+ normalized_algorithm = str(algorithm).upper()
45
+ if normalized_algorithm not in self.ALLOWED_ALGORITHMS:
46
+ raise ValueError(f"不允许的 JWT 算法: {normalized_algorithm}")
47
+ self.secret_key = secret_key or 'spring-python-secret-key-change-in-production'
48
+ self.algorithm = normalized_algorithm
49
+ self.issuer = issuer
50
+ self.audience = audience
51
+ self.leeway = max(0, int(leeway))
52
+
53
+ @_InstanceOrDefaultMethod
54
+ def generate_token(self, payload: Dict[str, Any], expires_in: int = 3600) -> str:
55
+ """
56
+ 生成 JWT Token
57
+
58
+ Args:
59
+ payload: 载荷数据
60
+ expires_in: 过期时间(秒),默认1小时
61
+
62
+ Returns:
63
+ JWT Token字符串
64
+ """
65
+ if expires_in <= 0:
66
+ raise ValueError("expires_in 必须大于 0")
67
+ now = int(time.time())
68
+ token_payload = {
69
+ **payload,
70
+ 'exp': now + expires_in,
71
+ 'iat': now,
72
+ 'jti': str(uuid.uuid4()),
73
+ 'token_type': 'access',
74
+ }
75
+ if self.issuer:
76
+ token_payload['iss'] = self.issuer
77
+ if self.audience:
78
+ token_payload['aud'] = self.audience
79
+
80
+ return jwt.encode(token_payload, self.secret_key, algorithm=self.algorithm)
81
+
82
+ @_InstanceOrDefaultMethod
83
+ def generate_refresh_token(self, payload: Dict[str, Any], expires_in: int = 86400) -> str:
84
+ """
85
+ 生成刷新 Token
86
+
87
+ Args:
88
+ payload: 载荷数据
89
+ expires_in: 过期时间(秒),默认24小时
90
+
91
+ Returns:
92
+ 刷新 Token 字符串
93
+ """
94
+ token = self.generate_token(payload, expires_in)
95
+ decoded = jwt.decode(token, options={'verify_signature': False})
96
+ decoded['token_type'] = 'refresh'
97
+ return jwt.encode(decoded, self.secret_key, algorithm=self.algorithm)
98
+
99
+ @_InstanceOrDefaultMethod
100
+ def validate_token(self, token: str) -> bool:
101
+ """
102
+ 验证 Token 是否有效
103
+
104
+ Args:
105
+ token: JWT Token
106
+
107
+ Returns:
108
+ 是否有效
109
+ """
110
+ try:
111
+ self.decode_token(token)
112
+ return True
113
+ except (jwt.PyJWTError, ValueError, TypeError):
114
+ return False
115
+
116
+ @_InstanceOrDefaultMethod
117
+ def verify_token(self, token: str) -> Dict[str, Any]:
118
+ """验证并返回 Token 载荷;无效 Token 会抛出解码异常。"""
119
+ return self.decode_token(token)
120
+
121
+ @_InstanceOrDefaultMethod
122
+ def decode_token(self, token: str) -> Dict[str, Any]:
123
+ """
124
+ 解码并验证 Token
125
+
126
+ Args:
127
+ token: JWT Token
128
+
129
+ Returns:
130
+ 解码后的载荷数据
131
+
132
+ Raises:
133
+ jwt.ExpiredSignatureError: Token已过期
134
+ jwt.InvalidTokenError: Token无效
135
+ """
136
+ options = {
137
+ 'require': ['exp', 'iat', 'jti', 'token_type'],
138
+ 'verify_aud': self.audience is not None,
139
+ 'verify_iss': self.issuer is not None,
140
+ }
141
+ return jwt.decode(
142
+ token,
143
+ self.secret_key,
144
+ algorithms=[self.algorithm],
145
+ audience=self.audience,
146
+ issuer=self.issuer,
147
+ leeway=self.leeway,
148
+ options=options,
149
+ )
150
+
151
+ @_InstanceOrDefaultMethod
152
+ def get_payload(self, token: str) -> Dict[str, Any]:
153
+ """
154
+ 获取 Token 载荷(不验证签名)
155
+
156
+ Args:
157
+ token: JWT Token
158
+
159
+ Returns:
160
+ 载荷数据
161
+ """
162
+ return jwt.decode(token, options={'verify_signature': False})
163
+
164
+ @_InstanceOrDefaultMethod
165
+ def is_expired(self, token: str) -> bool:
166
+ """
167
+ 检查 Token 是否已过期
168
+
169
+ Args:
170
+ token: JWT Token
171
+
172
+ Returns:
173
+ 是否已过期
174
+ """
175
+ try:
176
+ payload = self.get_payload(token)
177
+ exp = payload.get('exp', 0)
178
+ return time.time() > exp
179
+ except (jwt.PyJWTError, ValueError, TypeError):
180
+ return True
181
+
182
+ @_InstanceOrDefaultMethod
183
+ def refresh_token(self, refresh_token: str, expires_in: int = 3600) -> str:
184
+ """
185
+ 使用刷新 Token 生成新的访问 Token
186
+
187
+ Args:
188
+ refresh_token: 刷新 Token
189
+ expires_in: 新 Token 过期时间(秒)
190
+
191
+ Returns:
192
+ 新的访问 Token
193
+ """
194
+ payload = self.decode_token(refresh_token)
195
+ if payload.get('token_type') != 'refresh':
196
+ raise jwt.InvalidTokenError("访问令牌不能用于刷新")
197
+
198
+ # 移除过期时间相关字段
199
+ payload.pop('exp', None)
200
+ payload.pop('iat', None)
201
+ payload.pop('jti', None)
202
+ payload.pop('token_type', None)
203
+
204
+ return self.generate_token(payload, expires_in)
205
+
206
+ @_InstanceOrDefaultMethod
207
+ def extract_user_id(self, token: str) -> Optional[str]:
208
+ """
209
+ 从 Token 中提取用户ID
210
+
211
+ Args:
212
+ token: JWT Token
213
+
214
+ Returns:
215
+ 用户ID
216
+ """
217
+ try:
218
+ payload = self.decode_token(token)
219
+ return payload.get('user_id') or payload.get('sub')
220
+ except (jwt.PyJWTError, ValueError, TypeError):
221
+ return None
222
+
223
+ @_InstanceOrDefaultMethod
224
+ def extract_roles(self, token: str) -> list:
225
+ """
226
+ 从 Token 中提取角色列表
227
+
228
+ Args:
229
+ token: JWT Token
230
+
231
+ Returns:
232
+ 角色列表
233
+ """
234
+ try:
235
+ payload = self.decode_token(token)
236
+ roles = payload.get('roles', [])
237
+ if isinstance(roles, str):
238
+ return [roles]
239
+ return roles
240
+ except (jwt.PyJWTError, ValueError, TypeError):
241
+ return []
242
+
243
+ @_InstanceOrDefaultMethod
244
+ def extract_permissions(self, token: str) -> list:
245
+ """
246
+ 从 Token 中提取权限列表
247
+
248
+ Args:
249
+ token: JWT Token
250
+
251
+ Returns:
252
+ 权限列表
253
+ """
254
+ try:
255
+ payload = self.decode_token(token)
256
+ permissions = payload.get('permissions', [])
257
+ if isinstance(permissions, str):
258
+ return [permissions]
259
+ return permissions
260
+ except (jwt.PyJWTError, ValueError, TypeError):
261
+ return []
262
+
263
+
264
+ # 创建全局 JWT 工具实例
265
+ jwt_utils = JwtUtils()
266
+
267
+
268
+ def init_jwt(config: dict) -> None:
269
+ """
270
+ 初始化 JWT 配置
271
+
272
+ Args:
273
+ config: JWT配置字典,包含secret_key等
274
+ """
275
+ jwt_utils.configure(
276
+ secret_key=config.get('secret_key', 'spring-python-secret-key-change-in-production'),
277
+ algorithm=config.get('algorithm', 'HS256'),
278
+ issuer=config.get('issuer'),
279
+ audience=config.get('audience'),
280
+ leeway=config.get('leeway', 0),
281
+ )
@@ -0,0 +1,206 @@
1
+ """
2
+ 重放攻击防护 (Replay Attack Protection)
3
+
4
+ 防护机制:
5
+ - Nonce + Timestamp 双重校验
6
+ - 时间戳窗口 (默认5分钟)
7
+ - Nonce 去重缓存 (Redis或内存)
8
+ - 请求签名验证
9
+ """
10
+
11
+ import time
12
+ import hashlib
13
+ import hmac
14
+ import logging
15
+ import threading
16
+ from typing import Optional, Dict, Set, Tuple
17
+ from collections import OrderedDict
18
+
19
+ logger = logging.getLogger("Spring.Security.Replay")
20
+
21
+ # 默认时间戳窗口(秒)
22
+ DEFAULT_TIMESTAMP_WINDOW = 300
23
+ # Nonce缓存最大容量(LRU)
24
+ DEFAULT_NONCE_CACHE_SIZE = 100000
25
+ # Nonce过期清理间隔(秒)
26
+ NONCE_CLEANUP_INTERVAL = 60
27
+
28
+
29
+ class NonceCache:
30
+ """线程安全的LRU Nonce缓存"""
31
+
32
+ def __init__(self, max_size: int = DEFAULT_NONCE_CACHE_SIZE, ttl: int = DEFAULT_TIMESTAMP_WINDOW):
33
+ self._cache: "OrderedDict[str, float]" = OrderedDict()
34
+ self._max_size = max_size
35
+ self._ttl = ttl
36
+ self._lock = threading.RLock()
37
+ self._last_cleanup = time.monotonic()
38
+
39
+ def _cleanup_expired(self):
40
+ """清理过期的nonce"""
41
+ now = time.time()
42
+ if time.monotonic() - self._last_cleanup < NONCE_CLEANUP_INTERVAL:
43
+ # 即使到了清理间隔,也只清理部分避免阻塞
44
+ expired_keys = []
45
+ for k, ts in list(self._cache.items()):
46
+ if now - ts > self._ttl:
47
+ expired_keys.append(k)
48
+ else:
49
+ break # OrderedDict按插入顺序,前面过期后面也应该过期
50
+ for k in expired_keys:
51
+ self._cache.pop(k, None)
52
+ self._last_cleanup = time.monotonic()
53
+
54
+ def check_and_add(self, nonce: str) -> bool:
55
+ """
56
+ 检查nonce是否已存在,不存在则添加
57
+
58
+ Returns:
59
+ True 如果nonce有效(未重复),False 如果重复
60
+ """
61
+ with self._lock:
62
+ self._cleanup_expired()
63
+
64
+ if nonce in self._cache:
65
+ return False
66
+
67
+ # 容量控制
68
+ while len(self._cache) >= self._max_size:
69
+ self._cache.popitem(last=False)
70
+
71
+ self._cache[nonce] = time.time()
72
+ return True
73
+
74
+
75
+ class RedisNonceCache:
76
+ """基于Redis的分布式Nonce缓存"""
77
+
78
+ def __init__(self, redis_client, key_prefix: str = "springpy:nonce:", ttl: int = DEFAULT_TIMESTAMP_WINDOW):
79
+ self._redis = redis_client
80
+ self._prefix = key_prefix
81
+ self._ttl = ttl
82
+
83
+ def check_and_add(self, nonce: str) -> bool:
84
+ key = f"{self._prefix}{nonce}"
85
+ try:
86
+ # SET NX: 仅在key不存在时设置,成功返回True(nonce有效)
87
+ return bool(self._redis.set(key, '1', ex=self._ttl, nx=True))
88
+ except Exception as e:
89
+ logger.warning(f"Redis nonce check failed, falling back to allow: {e}")
90
+ return True # Redis故障时降级为允许(由timestamp校验兜底)
91
+
92
+
93
+ class ReplayProtection:
94
+ """
95
+ 重放攻击保护器
96
+
97
+ Usage:
98
+ protector = ReplayProtection(secret_key="your-secret")
99
+ is_valid, reason = protector.validate_request(
100
+ timestamp=request.headers.get("X-Timestamp"),
101
+ nonce=request.headers.get("X-Nonce"),
102
+ signature=request.headers.get("X-Signature"),
103
+ body=request.body,
104
+ )
105
+ """
106
+
107
+ def __init__(self, secret_key: str,
108
+ timestamp_window: int = DEFAULT_TIMESTAMP_WINDOW,
109
+ nonce_cache=None,
110
+ redis_client=None):
111
+ self.secret_key = secret_key.encode('utf-8')
112
+ self.timestamp_window = timestamp_window
113
+
114
+ if nonce_cache:
115
+ self.nonce_cache = nonce_cache
116
+ elif redis_client:
117
+ self.nonce_cache = RedisNonceCache(redis_client, ttl=timestamp_window)
118
+ else:
119
+ self.nonce_cache = NonceCache(ttl=timestamp_window)
120
+
121
+ def generate_signature(self, timestamp: str, nonce: str, body: str = "",
122
+ method: str = "", path: str = "") -> str:
123
+ """
124
+ 生成请求签名
125
+
126
+ 签名字符串: METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY_SHA256
127
+ """
128
+ body_hash = hashlib.sha256(body.encode('utf-8') if isinstance(body, str) else body).hexdigest()
129
+ message = f"{method.upper()}\n{path}\n{timestamp}\n{nonce}\n{body_hash}"
130
+ return hmac.new(self.secret_key, message.encode('utf-8'), hashlib.sha256).hexdigest()
131
+
132
+ def validate_request(self, timestamp: str, nonce: str,
133
+ signature: str = "", body: str = "",
134
+ method: str = "", path: str = "") -> Tuple[bool, str]:
135
+ """
136
+ 验证请求是否为重放攻击
137
+
138
+ Args:
139
+ timestamp: 请求时间戳(毫秒或秒,自动检测)
140
+ nonce: 唯一请求标识
141
+ signature: 请求签名(可选)
142
+ body: 请求体(用于签名验证)
143
+ method: HTTP方法
144
+ path: 请求路径
145
+
146
+ Returns:
147
+ (is_valid, reason)
148
+ """
149
+ # 1. 验证时间戳
150
+ try:
151
+ ts = int(timestamp)
152
+ # 自动检测毫秒/秒
153
+ if ts > 1e12:
154
+ ts = ts / 1000
155
+ now = time.time()
156
+ if abs(now - ts) > self.timestamp_window:
157
+ return False, f"Timestamp expired: window={self.timestamp_window}s, diff={abs(now - ts):.1f}s"
158
+ except (ValueError, TypeError):
159
+ return False, "Invalid timestamp format"
160
+
161
+ # 2. 验证Nonce
162
+ if not nonce or len(nonce) < 8:
163
+ return False, "Invalid or missing nonce (minimum 8 characters)"
164
+
165
+ if not self.nonce_cache.check_and_add(nonce):
166
+ logger.warning(f"Replay attack detected: duplicate nonce {nonce[:8]}***")
167
+ return False, "Duplicate nonce detected (possible replay attack)"
168
+
169
+ # 3. 验证签名(如果提供)
170
+ if signature:
171
+ expected_sig = self.generate_signature(str(timestamp), nonce, body, method, path)
172
+ if not hmac.compare_digest(expected_sig, signature):
173
+ return False, "Invalid signature"
174
+
175
+ return True, "OK"
176
+
177
+ def validate_headers(self, headers: Dict[str, str], body: str = "",
178
+ method: str = "", path: str = "") -> Tuple[bool, str]:
179
+ """从HTTP头验证请求"""
180
+ return self.validate_request(
181
+ timestamp=headers.get("x-timestamp", headers.get("X-Timestamp", "")),
182
+ nonce=headers.get("x-nonce", headers.get("X-Nonce", "")),
183
+ signature=headers.get("x-signature", headers.get("X-Signature", "")),
184
+ body=body,
185
+ method=method,
186
+ path=path,
187
+ )
188
+
189
+
190
+ def create_replay_protection(secret_key: str = None, redis_client=None,
191
+ **kwargs) -> Optional[ReplayProtection]:
192
+ """
193
+ 工厂方法创建重放保护器
194
+
195
+ 如果没有secret_key则返回None(不启用重放防护)
196
+ """
197
+ if not secret_key:
198
+ # 尝试从配置获取
199
+ try:
200
+ from spring.security.secret_manager import SecretManager
201
+ secret_key = SecretManager().get_jwt_secret()
202
+ except Exception:
203
+ return None
204
+ if not secret_key:
205
+ return None
206
+ return ReplayProtection(secret_key, redis_client=redis_client, **kwargs)