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.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- 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)
|
spring/test/__init__.py
ADDED
|
@@ -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
|
+
]
|