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,226 @@
1
+ """
2
+ 密钥管理器 (Secret Manager)
3
+
4
+ 生产级密钥管理:
5
+ - 优先从环境变量读取(推荐与Vault/K8s Secrets/AWS Secrets Manager集成)
6
+ - 支持 .env 文件(开发环境)
7
+ - 支持 base64 编码密钥解码
8
+ - 密钥脱敏(日志中不输出明文)
9
+ - 密钥轮换支持
10
+ """
11
+
12
+ import os
13
+ import base64
14
+ import logging
15
+ import hashlib
16
+ from typing import Optional, Dict, Any
17
+ from pathlib import Path
18
+
19
+ logger = logging.getLogger("Spring.Security.Secret")
20
+
21
+ # 敏感配置键名模式
22
+ _SENSITIVE_KEY_PATTERNS = {
23
+ 'password', 'secret', 'token', 'key', 'credential',
24
+ 'api_key', 'apikey', 'access_key', 'private_key',
25
+ }
26
+
27
+
28
+ def is_sensitive_key(key: str) -> bool:
29
+ """判断配置键是否为敏感字段"""
30
+ key_lower = key.lower()
31
+ return any(pattern in key_lower for pattern in _SENSITIVE_KEY_PATTERNS)
32
+
33
+
34
+ def mask_secret(value: str, show_chars: int = 4) -> str:
35
+ """脱敏显示密钥,仅显示前后show_chars位"""
36
+ if not value or not isinstance(value, str):
37
+ return "***"
38
+ if len(value) <= show_chars * 2:
39
+ return "***"
40
+ return f"{value[:show_chars]}***{value[-show_chars:]}"
41
+
42
+
43
+ class SecretManager:
44
+ """
45
+ 密钥管理器
46
+
47
+ 密钥加载优先级:
48
+ 1. 环境变量 (SPRING_SECRETS_ 前缀或直接变量名)
49
+ 2. Docker/K8s secrets 文件 (/run/secrets/{name})
50
+ 3. .env 文件(仅开发环境,需显式启用)
51
+ """
52
+
53
+ _instance = None
54
+ _secrets: Dict[str, str] = {}
55
+ _initialized = False
56
+
57
+ def __new__(cls):
58
+ if cls._instance is None:
59
+ cls._instance = super().__new__(cls)
60
+ return cls._instance
61
+
62
+ def __init__(self):
63
+ if not self._initialized:
64
+ self._secrets = {}
65
+ self._initialized = False
66
+ self._init_from_env()
67
+
68
+ def _init_from_env(self):
69
+ """从环境变量初始化密钥"""
70
+ # 从 SPRING_SECRETS_ 前缀加载
71
+ prefix = "SPRING_SECRETS_"
72
+ for key, value in os.environ.items():
73
+ if key.startswith(prefix):
74
+ secret_name = key[len(prefix):].lower()
75
+ self._secrets[secret_name] = value
76
+ logger.debug(f"Loaded secret from env: {secret_name}={mask_secret(value)}")
77
+
78
+ # Docker/K8s secrets 文件
79
+ secrets_dir = os.getenv('SPRING_SECRETS_DIR', '/run/secrets')
80
+ secrets_path = Path(secrets_dir)
81
+ if secrets_path.exists() and secrets_path.is_dir():
82
+ for f in secrets_path.iterdir():
83
+ if f.is_file():
84
+ name = f.name.lower().replace('-', '_')
85
+ try:
86
+ self._secrets[name] = f.read_text().strip()
87
+ logger.debug(f"Loaded secret from file: {name}")
88
+ except Exception as e:
89
+ logger.warning(f"Failed to read secret file {f}: {e}")
90
+
91
+ # 开发环境:.env 文件
92
+ if os.getenv('SPRING_ENV', '').lower() in ('dev', 'development', 'local'):
93
+ env_file = Path('.env')
94
+ if env_file.exists():
95
+ try:
96
+ for line in env_file.read_text().splitlines():
97
+ line = line.strip()
98
+ if line and not line.startswith('#') and '=' in line:
99
+ k, v = line.split('=', 1)
100
+ k = k.strip()
101
+ v = v.strip().strip('"').strip("'")
102
+ if k.startswith(prefix):
103
+ secret_name = k[len(prefix):].lower()
104
+ self._secrets[secret_name] = v
105
+ logger.debug("Loaded secrets from .env file (development only)")
106
+ except Exception as e:
107
+ logger.warning(f"Failed to read .env file: {e}")
108
+
109
+ self._initialized = True
110
+
111
+ def get_secret(self, name: str, default: Optional[str] = None,
112
+ decode_base64: bool = False) -> Optional[str]:
113
+ """
114
+ 获取密钥
115
+
116
+ Args:
117
+ name: 密钥名称(不区分大小写,自动转换下划线/连字符)
118
+ default: 默认值
119
+ decode_base64: 是否base64解码
120
+
121
+ Returns:
122
+ 密钥值,不存在时返回default
123
+ """
124
+ name_normalized = name.lower().replace('-', '_')
125
+
126
+ # 1. 从已加载的secrets中查找
127
+ if name_normalized in self._secrets:
128
+ value = self._secrets[name_normalized]
129
+ return self._decode(value, decode_base64)
130
+
131
+ # 2. 直接从环境变量查找(支持DB_PASSWORD, REDIS_PASSWORD等标准命名)
132
+ env_variants = [
133
+ name.upper(),
134
+ name.upper().replace('.', '_'),
135
+ name.upper().replace('-', '_'),
136
+ ]
137
+ for env_name in env_variants:
138
+ if env_name in os.environ:
139
+ return self._decode(os.environ[env_name], decode_base64)
140
+
141
+ return default
142
+
143
+ def _decode(self, value: str, decode_base64: bool) -> str:
144
+ """解码密钥值"""
145
+ if decode_base64 and value:
146
+ try:
147
+ return base64.b64decode(value).decode('utf-8')
148
+ except Exception:
149
+ logger.warning("Failed to decode base64 secret, returning raw value")
150
+ return value
151
+
152
+ def require_secret(self, name: str, decode_base64: bool = False) -> str:
153
+ """获取必须存在的密钥,不存在则抛出异常"""
154
+ value = self.get_secret(name, decode_base64=decode_base64)
155
+ if value is None:
156
+ raise ValueError(f"Required secret '{name}' not found. "
157
+ f"Set environment variable {name.upper()} or "
158
+ f"SPRING_SECRETS_{name.upper()}")
159
+ return value
160
+
161
+ def get_database_password(self) -> str:
162
+ """获取数据库密码"""
163
+ return self.get_secret('db_password', '') or ''
164
+
165
+ def get_redis_password(self) -> str:
166
+ """获取Redis密码"""
167
+ return self.get_secret('redis_password', '') or ''
168
+
169
+ def get_rabbitmq_password(self) -> str:
170
+ """获取RabbitMQ密码"""
171
+ return self.get_secret('rabbitmq_password', '') or ''
172
+
173
+ def get_jwt_secret(self) -> str:
174
+ """获取JWT密钥"""
175
+ return self.require_secret('jwt_secret_key')
176
+
177
+ def get_nacos_password(self) -> str:
178
+ """获取Nacos密码"""
179
+ return self.get_secret('nacos_password', 'nacos') or 'nacos'
180
+
181
+ def set_secret(self, name: str, value: str) -> None:
182
+ """运行时设置密钥(用于密钥轮换)"""
183
+ name_normalized = name.lower().replace('-', '_')
184
+ self._secrets[name_normalized] = value
185
+ logger.info(f"Secret rotated: {name_normalized}")
186
+
187
+ def clear(self) -> None:
188
+ """清空所有密钥(测试用)"""
189
+ self._secrets.clear()
190
+ self._initialized = False
191
+
192
+ @classmethod
193
+ def reset(cls):
194
+ """重置单例(测试用)"""
195
+ cls._instance = None
196
+
197
+
198
+ def resolve_secret_config(config: Dict[str, Any], prefix: str = "") -> Dict[str, Any]:
199
+ """
200
+ 递归解析配置中的密钥引用
201
+
202
+ 支持 ${secret:key_name} 或 ${secret:key_name:default} 格式
203
+ """
204
+ secret_mgr = SecretManager()
205
+ import re
206
+ _SECRET_PATTERN = re.compile(r'^\$\{secret:([^}:]+)(?::(.*))?\}$')
207
+
208
+ def _resolve(value, path=""):
209
+ if isinstance(value, dict):
210
+ return {k: _resolve(v, f"{path}.{k}" if path else k) for k, v in value.items()}
211
+ if isinstance(value, list):
212
+ return [_resolve(v, f"{path}[{i}]") for i, v in enumerate(value)]
213
+ if isinstance(value, str):
214
+ match = _SECRET_PATTERN.match(value)
215
+ if match:
216
+ secret_name = match.group(1)
217
+ default_val = match.group(2)
218
+ resolved = secret_mgr.get_secret(secret_name, default=default_val)
219
+ if resolved is None:
220
+ if is_sensitive_key(path.split('.')[-1] if path else ''):
221
+ logger.warning(f"Secret '{secret_name}' not found for config '{path}'")
222
+ return default_val if default_val is not None else value
223
+ return resolved
224
+ return value
225
+
226
+ return _resolve(config)
@@ -0,0 +1,248 @@
1
+ """
2
+ Spring Security AOP 切面实现
3
+ 提供认证授权功能
4
+ """
5
+ from typing import Any, Callable, Dict, List
6
+ import functools
7
+ import inspect
8
+ from spring.security.security_context import SecurityContext, SecurityContextHolder
9
+ from spring.security.jwt_utils import jwt_utils
10
+
11
+
12
+ class SecurityError(Exception):
13
+ """Base exception carrying an HTTP status for the web adapter."""
14
+
15
+ status_code = 500
16
+
17
+
18
+ class AuthenticationError(SecurityError):
19
+ status_code = 401
20
+
21
+
22
+ class AuthorizationError(SecurityError):
23
+ status_code = 403
24
+
25
+
26
+ # ==================== @PreAuthorize 注解切面 ====================
27
+ def pre_authorize_decorator(annotation):
28
+ """
29
+ @PreAuthorize 注解切面
30
+ 支持表达式:
31
+ - hasRole('ROLE_ADMIN')
32
+ - hasAnyRole('ROLE_ADMIN', 'ROLE_USER')
33
+ - hasPermission('user:read')
34
+ - hasAnyPermission('user:read', 'user:write')
35
+ - authentication.name == 'admin'
36
+ """
37
+ def decorator(func: Callable) -> Callable:
38
+ def authorize() -> None:
39
+ expression = annotation.value
40
+ if not SecurityContextHolder.is_authenticated():
41
+ raise AuthenticationError("Authentication required")
42
+ if not _evaluate_expression(expression):
43
+ raise AuthorizationError("Access denied")
44
+
45
+ if inspect.iscoroutinefunction(func):
46
+ @functools.wraps(func)
47
+ async def async_wrapper(*args, **kwargs):
48
+ authorize()
49
+ return await func(*args, **kwargs)
50
+
51
+ return async_wrapper
52
+
53
+ @functools.wraps(func)
54
+ def wrapper(*args, **kwargs):
55
+ authorize()
56
+
57
+ return func(*args, **kwargs)
58
+
59
+ return wrapper
60
+ return decorator
61
+
62
+
63
+ def _evaluate_expression(expression: str) -> bool:
64
+ """
65
+ 评估权限表达式
66
+
67
+ Args:
68
+ expression: 权限表达式
69
+
70
+ Returns:
71
+ 是否满足条件
72
+ """
73
+ if not expression:
74
+ return True
75
+
76
+ # 处理 hasRole 表达式
77
+ if expression.startswith('hasRole('):
78
+ role = expression.replace('hasRole(', '').replace(')', '').strip().strip("'\"")
79
+ return SecurityContextHolder.has_role(role)
80
+
81
+ # 处理 hasAnyRole 表达式
82
+ if expression.startswith('hasAnyRole('):
83
+ roles_str = expression.replace('hasAnyRole(', '').replace(')', '').strip()
84
+ roles = [r.strip().strip("'\"") for r in roles_str.split(',')]
85
+ return SecurityContextHolder.has_any_role(*roles)
86
+
87
+ # 处理 hasPermission 表达式
88
+ if expression.startswith('hasPermission('):
89
+ permission = expression.replace('hasPermission(', '').replace(')', '').strip().strip("'\"")
90
+ return SecurityContextHolder.has_permission(permission)
91
+
92
+ # 处理 hasAnyPermission 表达式
93
+ if expression.startswith('hasAnyPermission('):
94
+ permissions_str = expression.replace('hasAnyPermission(', '').replace(')', '').strip()
95
+ permissions = [p.strip().strip("'\"") for p in permissions_str.split(',')]
96
+ return SecurityContextHolder.has_any_permission(*permissions)
97
+
98
+ # 处理 authentication.name == 'xxx' 表达式
99
+ if 'authentication.name' in expression:
100
+ # 提取用户名
101
+ import re
102
+ match = re.search(r"authentication\.name\s*==\s*['\"]([^'\"]+)['\"]", expression)
103
+ if match:
104
+ expected_name = match.group(1)
105
+ authentication = SecurityContextHolder.get_authentication()
106
+ if authentication:
107
+ principal = authentication.get('principal', {})
108
+ if isinstance(principal, dict):
109
+ name = principal.get('name', '')
110
+ else:
111
+ name = str(principal)
112
+ return name == expected_name
113
+
114
+ return False
115
+
116
+
117
+ # ==================== @Secured 注解切面 ====================
118
+ def secured_decorator(annotation):
119
+ """
120
+ @Secured 注解切面
121
+ 检查当前用户是否拥有指定角色中的任一角色
122
+ """
123
+ def decorator(func: Callable) -> Callable:
124
+ def authorize() -> None:
125
+ roles = annotation.value
126
+ if not SecurityContextHolder.is_authenticated():
127
+ raise AuthenticationError("Authentication required")
128
+ if not SecurityContextHolder.has_any_role(*roles):
129
+ raise AuthorizationError(f"Required role(s) {roles}")
130
+
131
+ if inspect.iscoroutinefunction(func):
132
+ @functools.wraps(func)
133
+ async def async_wrapper(*args, **kwargs):
134
+ authorize()
135
+ return await func(*args, **kwargs)
136
+
137
+ return async_wrapper
138
+
139
+ @functools.wraps(func)
140
+ def wrapper(*args, **kwargs):
141
+ authorize()
142
+
143
+ return func(*args, **kwargs)
144
+
145
+ return wrapper
146
+ return decorator
147
+
148
+
149
+ # ==================== @Authenticate 注解切面 ====================
150
+ def authenticate_decorator(annotation):
151
+ """
152
+ @Authenticate 注解切面
153
+ 从请求头中获取 Token 并验证,设置安全上下文
154
+ """
155
+ def decorator(func: Callable) -> Callable:
156
+ def prepare_context(args, kwargs):
157
+ request = kwargs.pop('_spring_request', None)
158
+ token = kwargs.get('token') or kwargs.get('authorization')
159
+
160
+ accepted_parameters = inspect.signature(func).parameters
161
+ if 'token' not in accepted_parameters:
162
+ kwargs.pop('token', None)
163
+ if 'authorization' not in accepted_parameters:
164
+ kwargs.pop('authorization', None)
165
+
166
+ candidates = list(args)
167
+ if request is not None:
168
+ candidates.append(request)
169
+ if token is None:
170
+ for candidate in candidates:
171
+ if hasattr(candidate, 'headers'):
172
+ token = candidate.headers.get('Authorization')
173
+ break
174
+
175
+ if isinstance(token, str) and token.lower().startswith('bearer '):
176
+ token = token[7:].strip()
177
+ if not token:
178
+ raise AuthenticationError("Token required")
179
+
180
+ try:
181
+ payload = jwt_utils.decode_token(token)
182
+ except Exception as exc:
183
+ raise AuthenticationError(f"Invalid token: {exc}") from exc
184
+
185
+ authentication = {
186
+ 'principal': payload.get('user_id') or payload.get('sub'),
187
+ 'credentials': token,
188
+ 'roles': payload.get('roles', []),
189
+ 'permissions': payload.get('permissions', []),
190
+ 'details': payload,
191
+ }
192
+ context = SecurityContext()
193
+ context.authentication = authentication
194
+ context.principal = authentication['principal']
195
+ context.credentials = token
196
+ context.roles = authentication['roles']
197
+ context.permissions = authentication['permissions']
198
+ return SecurityContextHolder.set_context(context)
199
+
200
+ if inspect.iscoroutinefunction(func):
201
+ @functools.wraps(func)
202
+ async def async_wrapper(*args, **kwargs):
203
+ context_token = prepare_context(args, kwargs)
204
+ try:
205
+ return await func(*args, **kwargs)
206
+ finally:
207
+ SecurityContextHolder.reset_context(context_token)
208
+
209
+ return async_wrapper
210
+
211
+ @functools.wraps(func)
212
+ def wrapper(*args, **kwargs):
213
+ context_token = prepare_context(args, kwargs)
214
+ try:
215
+ return func(*args, **kwargs)
216
+ finally:
217
+ SecurityContextHolder.reset_context(context_token)
218
+
219
+ return wrapper
220
+ return decorator
221
+
222
+
223
+ # ==================== 注解处理映射 ====================
224
+ SECURITY_ANNOTATION_DECORATORS = {
225
+ 'PreAuthorize': pre_authorize_decorator,
226
+ 'Secured': secured_decorator,
227
+ 'Authenticate': authenticate_decorator,
228
+ }
229
+
230
+
231
+ def apply_security_annotations(target: Any, method: Callable) -> Callable:
232
+ """应用所有安全注解"""
233
+ annotations = getattr(method, '__spring_annotations__', [])
234
+
235
+ wrapped = method
236
+
237
+ # Authentication must be the outermost wrapper so it runs before checks.
238
+ ordered = sorted(
239
+ annotations,
240
+ key=lambda item: 1 if type(item).__name__ == 'Authenticate' else 0,
241
+ )
242
+ for annotation in ordered:
243
+ annotation_type = type(annotation).__name__
244
+ decorator_func = SECURITY_ANNOTATION_DECORATORS.get(annotation_type)
245
+ if decorator_func:
246
+ wrapped = decorator_func(annotation)(wrapped)
247
+
248
+ return wrapped
@@ -0,0 +1,172 @@
1
+ """
2
+ 安全上下文管理
3
+ 用于存储当前用户的认证信息
4
+ """
5
+ from contextvars import ContextVar, Token
6
+ from typing import Optional, Dict, Any, List
7
+
8
+
9
+ class SecurityContext:
10
+ """安全上下文,存储当前用户信息"""
11
+
12
+ def __init__(self):
13
+ self._authentication: Optional[Dict[str, Any]] = None
14
+ self._principal: Optional[Any] = None
15
+ self._credentials: Optional[str] = None
16
+ self._roles: List[str] = []
17
+ self._permissions: List[str] = []
18
+
19
+ @property
20
+ def authentication(self) -> Optional[Dict[str, Any]]:
21
+ return self._authentication
22
+
23
+ @authentication.setter
24
+ def authentication(self, value: Optional[Dict[str, Any]]):
25
+ self._authentication = value
26
+
27
+ @property
28
+ def principal(self) -> Optional[Any]:
29
+ return self._principal
30
+
31
+ @principal.setter
32
+ def principal(self, value: Optional[Any]):
33
+ self._principal = value
34
+
35
+ @property
36
+ def credentials(self) -> Optional[str]:
37
+ return self._credentials
38
+
39
+ @credentials.setter
40
+ def credentials(self, value: Optional[str]):
41
+ self._credentials = value
42
+
43
+ @property
44
+ def roles(self) -> List[str]:
45
+ return self._roles
46
+
47
+ @roles.setter
48
+ def roles(self, value: List[str]):
49
+ self._roles = value
50
+
51
+ @property
52
+ def permissions(self) -> List[str]:
53
+ return self._permissions
54
+
55
+ @permissions.setter
56
+ def permissions(self, value: List[str]):
57
+ self._permissions = value
58
+
59
+ def is_authenticated(self) -> bool:
60
+ """判断当前用户是否已认证"""
61
+ return self._authentication is not None
62
+
63
+ def has_role(self, role: str) -> bool:
64
+ """判断当前用户是否拥有指定角色"""
65
+ return role in self._roles
66
+
67
+ def has_any_role(self, *roles: str) -> bool:
68
+ """判断当前用户是否拥有任一指定角色"""
69
+ return any(role in self._roles for role in roles)
70
+
71
+ def has_permission(self, permission: str) -> bool:
72
+ """判断当前用户是否拥有指定权限"""
73
+ return permission in self._permissions
74
+
75
+ def has_any_permission(self, *permissions: str) -> bool:
76
+ """判断当前用户是否拥有任一指定权限"""
77
+ return any(permission in self._permissions for permission in permissions)
78
+
79
+ def clear(self):
80
+ """清除安全上下文"""
81
+ self._authentication = None
82
+ self._principal = None
83
+ self._credentials = None
84
+ self._roles = []
85
+ self._permissions = []
86
+
87
+
88
+ class SecurityContextHolder:
89
+ """安全上下文持有者,隔离线程和 asyncio Task。"""
90
+
91
+ _context_var: ContextVar[Optional[SecurityContext]] = ContextVar(
92
+ 'spring_security_context', default=None
93
+ )
94
+
95
+ @classmethod
96
+ def get_context(cls) -> SecurityContext:
97
+ """获取当前执行上下文的安全上下文"""
98
+ context = cls._context_var.get()
99
+ if context is None:
100
+ context = SecurityContext()
101
+ cls._context_var.set(context)
102
+ return context
103
+
104
+ @classmethod
105
+ def set_context(cls, context: SecurityContext) -> Token:
106
+ """设置当前执行上下文并返回可用于恢复的 token"""
107
+ return cls._context_var.set(context)
108
+
109
+ @classmethod
110
+ def reset_context(cls, token: Token) -> None:
111
+ """恢复设置新上下文之前的安全上下文"""
112
+ cls._context_var.reset(token)
113
+
114
+ @classmethod
115
+ def clear_context(cls):
116
+ """清除当前执行上下文,避免身份泄漏到后续调用"""
117
+ cls._context_var.set(SecurityContext())
118
+
119
+ @classmethod
120
+ def get_authentication(cls) -> Optional[Dict[str, Any]]:
121
+ """获取当前认证信息"""
122
+ return cls.get_context().authentication
123
+
124
+ @classmethod
125
+ def set_authentication(cls, authentication: Dict[str, Any]):
126
+ """设置当前认证信息"""
127
+ context = cls.get_context()
128
+ context.authentication = authentication
129
+ context.principal = authentication.get('principal')
130
+ context.credentials = authentication.get('credentials')
131
+ context.roles = authentication.get('roles', [])
132
+ context.permissions = authentication.get('permissions', [])
133
+
134
+ @classmethod
135
+ def get_principal(cls) -> Optional[Any]:
136
+ """获取当前用户主体"""
137
+ return cls.get_context().principal
138
+
139
+ @classmethod
140
+ def get_roles(cls) -> List[str]:
141
+ """获取当前用户角色列表"""
142
+ return cls.get_context().roles
143
+
144
+ @classmethod
145
+ def get_permissions(cls) -> List[str]:
146
+ """获取当前用户权限列表"""
147
+ return cls.get_context().permissions
148
+
149
+ @classmethod
150
+ def is_authenticated(cls) -> bool:
151
+ """判断当前用户是否已认证"""
152
+ return cls.get_context().is_authenticated()
153
+
154
+ @classmethod
155
+ def has_role(cls, role: str) -> bool:
156
+ """判断当前用户是否拥有指定角色"""
157
+ return cls.get_context().has_role(role)
158
+
159
+ @classmethod
160
+ def has_any_role(cls, *roles: str) -> bool:
161
+ """判断当前用户是否拥有任一指定角色"""
162
+ return cls.get_context().has_any_role(*roles)
163
+
164
+ @classmethod
165
+ def has_permission(cls, permission: str) -> bool:
166
+ """判断当前用户是否拥有指定权限"""
167
+ return cls.get_context().has_permission(permission)
168
+
169
+ @classmethod
170
+ def has_any_permission(cls, *permissions: str) -> bool:
171
+ """判断当前用户是否拥有任一指定权限"""
172
+ return cls.get_context().has_any_permission(*permissions)
@@ -0,0 +1,45 @@
1
+ """SpringBootAI 测试切片模块(对齐 Spring Boot ``@SpringBootTest``/``@WebMvcTest``/``@DataJpaTest``)。
2
+
3
+ 模块组成(``spring.test.slicing``):
4
+ - ``SpringBootTest``:全量应用上下文,对齐 ``@SpringBootTest``。
5
+ - ``WebMvcTest``:仅 Web 切片(指定 Controller + Mock 依赖 + FastAPI ``TestClient``)。
6
+ - ``DataJpaTest``:仅数据切片(内存 SQLite + 建表 + ``PagingAndSortingRepository`` 工厂)。
7
+ - ``TestPool``:内存连接池,供数据切片复用。
8
+
9
+ 典型用法(pytest)::
10
+
11
+ from spring.test import SpringBootTest, WebMvcTest, DataJpaTest
12
+
13
+ def test_full_context():
14
+ with SpringBootTest(App, config={"app": {"name": "demo"}}) as ctx:
15
+ svc = ctx.get_bean("user_service")
16
+ ...
17
+
18
+ def test_web_layer():
19
+ with WebMvcTest(controllers=[UserController]) as mvc:
20
+ resp = mvc.get_client().get("/users")
21
+ assert resp.status_code == 200
22
+
23
+ def test_data_layer():
24
+ with DataJpaTest(entities=[User]) as jpa:
25
+ repo = jpa.repository_for(User)
26
+ repo.save(User(name="tom"))
27
+
28
+ 设计原则:复用既有 ``ApplicationContext``/``WebApplicationContext``/``DdlAutoManager``/
29
+ ``PagingAndSortingRepository``,不重复造轮子;切片提供 ``close()`` 与上下文管理器语义。
30
+
31
+ 与 Java 的差异:
32
+ - Spring Boot 切片用自动配置裁剪;本实现手动注册指定 Bean + Mock 依赖,更轻量直接。
33
+ - ``@WebMvcTest`` 自动 Mock ``@Service``/``@Repository``;本实现 Mock 构造函数依赖。
34
+ """
35
+ from .slicing import TestPool, SpringBootTest, WebMvcTest, DataJpaTest
36
+
37
+ __version__ = "1.0.0"
38
+
39
+ __all__ = [
40
+ "TestPool",
41
+ "SpringBootTest",
42
+ "WebMvcTest",
43
+ "DataJpaTest",
44
+ "__version__",
45
+ ]