skyplatform-iam 1.0.1__py3-none-any.whl → 1.0.5__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.
@@ -3,16 +3,20 @@ SkyPlatform IAM SDK 中间件模块
3
3
  """
4
4
  import logging
5
5
  from typing import Optional, Callable, Dict, Any
6
- from fastapi import Request, Response, HTTPException
6
+ from fastapi import Request, Response, HTTPException, status
7
7
  from fastapi.responses import JSONResponse
8
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
8
9
  from starlette.middleware.base import BaseHTTPMiddleware
10
+ import jwt
9
11
 
10
12
  from .config import AuthConfig
11
13
  from .connect_agenterra_iam import ConnectAgenterraIam
14
+ from .global_manager import get_global_manager
12
15
  from .exceptions import (
13
16
  AuthenticationError,
14
17
  AuthorizationError,
15
- ConfigurationError
18
+ ConfigurationError,
19
+ IAMServiceError
16
20
  )
17
21
 
18
22
  logger = logging.getLogger(__name__)
@@ -22,38 +26,91 @@ class AuthMiddleware(BaseHTTPMiddleware):
22
26
  """
23
27
  认证中间件
24
28
  自动拦截请求进行Token验证和权限检查
29
+ 支持全局实例共享和延迟初始化
25
30
  """
26
31
 
27
32
  def __init__(
28
33
  self,
29
34
  app,
30
- config: AuthConfig,
31
- skip_validation: Optional[Callable[[Request], bool]] = None
35
+ config: Optional[AuthConfig] = None,
36
+ skip_validation: Optional[Callable[[Request], bool]] = None,
37
+ use_global_manager: bool = True
32
38
  ):
33
39
  """
34
40
  初始化认证中间件
35
41
 
36
42
  Args:
37
43
  app: FastAPI应用实例
38
- config: 认证配置
44
+ config: 认证配置,如果为None且use_global_manager=True,则从全局管理器获取
39
45
  skip_validation: 自定义跳过验证的函数
46
+ use_global_manager: 是否使用全局管理器(推荐)
40
47
  """
41
48
  super().__init__(app)
42
- self.config = config
43
- self.iam_client = ConnectAgenterraIam()
49
+ self.use_global_manager = use_global_manager
44
50
  self.skip_validation = skip_validation
51
+
52
+ if use_global_manager:
53
+ # 使用全局管理器(延迟初始化)
54
+ self.config = None
55
+ self.iam_client = None
56
+ logger.info("AuthMiddleware使用全局管理器模式")
57
+ else:
58
+ # 传统模式(向后兼容)
59
+ if config is None:
60
+ raise ConfigurationError("在非全局管理器模式下,config参数不能为None")
61
+ self.config = config
62
+ self.iam_client = ConnectAgenterraIam(config=config)
63
+
64
+ # 验证配置
65
+ try:
66
+ self.config.validate_config()
67
+ except ValueError as e:
68
+ raise ConfigurationError(str(e))
69
+ logger.info("AuthMiddleware使用传统模式")
70
+
71
+ def _get_config_and_client(self):
72
+ """获取配置和客户端实例"""
73
+ if self.use_global_manager:
74
+ try:
75
+ manager = get_global_manager()
76
+ if not manager.is_initialized():
77
+ raise IAMServiceError("SkyPlatform IAM SDK未初始化,请先调用init_skyplatform_iam()")
78
+ return manager.get_config(), manager.get_client()
79
+ except Exception as e:
80
+ logger.error(f"从全局管理器获取配置和客户端失败: {str(e)}")
81
+ raise IAMServiceError(f"获取IAM配置失败: {str(e)}")
82
+ else:
83
+ return self.config, self.iam_client
45
84
 
46
- # 验证配置
85
+ def is_path_whitelisted(self, path: str) -> bool:
86
+ """
87
+ 检查路径是否在本地白名单中
88
+ """
47
89
  try:
48
- self.config.validate_config()
49
- except ValueError as e:
50
- raise ConfigurationError(str(e))
90
+ config, _ = self._get_config_and_client()
91
+ return config.is_path_whitelisted(path)
92
+ except Exception as e:
93
+ logger.error(f"检查白名单路径失败: {str(e)}")
94
+ return False
51
95
 
52
96
  async def dispatch(self, request: Request, call_next: Callable) -> Response:
53
97
  """
54
98
  中间件主要处理逻辑
55
99
  """
56
100
  try:
101
+ # 获取请求路径
102
+ api_path = request.url.path
103
+
104
+ # 首先检查路径是否在本地白名单中
105
+ if self.is_path_whitelisted(api_path):
106
+ logger.info(f"路径 {api_path} 在本地白名单中,跳过认证直接允许访问")
107
+ # 设置白名单标识
108
+ request.state.user = None
109
+ request.state.authenticated = False
110
+ request.state.is_whitelist = True
111
+ # 直接调用下一个处理器
112
+ response = await call_next(request)
113
+ return response
57
114
 
58
115
  # 提取Token(可能为空,白名单接口不需要token)
59
116
  token = self._extract_token(request)
@@ -84,7 +141,6 @@ class AuthMiddleware(BaseHTTPMiddleware):
84
141
  return response
85
142
 
86
143
  except HTTPException as e:
87
- # FastAPI HTTPException直接返回
88
144
  return self._create_error_response(
89
145
  status_code=e.status_code,
90
146
  message=str(e.detail),
@@ -117,23 +173,31 @@ class AuthMiddleware(BaseHTTPMiddleware):
117
173
  """
118
174
  从请求中提取Token
119
175
  """
120
- # 从Authorization头提取
121
- auth_header = request.headers.get(self.config.token_header)
122
- if auth_header and auth_header.startswith(self.config.token_prefix):
123
- return auth_header[len(self.config.token_prefix):].strip()
176
+ try:
177
+ config, _ = self._get_config_and_client()
178
+
179
+ # 从Authorization头提取
180
+ auth_header = request.headers.get(config.token_header)
181
+ if auth_header and auth_header.startswith(config.token_prefix):
182
+ return auth_header[len(config.token_prefix):].strip()
124
183
 
125
- # 从查询参数提取(备选方案)
126
- token = request.query_params.get("token")
127
- if token:
128
- return token
184
+ # 从查询参数提取(备选方案)
185
+ token = request.query_params.get("token")
186
+ if token:
187
+ return token
129
188
 
130
- return None
189
+ return None
190
+ except Exception as e:
191
+ logger.error(f"提取Token失败: {str(e)}")
192
+ return None
131
193
 
132
194
  async def _verify_token_and_permission(self, request: Request, token: Optional[str]) -> Optional[Dict[str, Any]]:
133
195
  """
134
196
  验证Token和权限
135
197
  """
136
198
  try:
199
+ config, iam_client = self._get_config_and_client()
200
+
137
201
  # 获取请求信息
138
202
  api_path = request.url.path
139
203
  method = request.method
@@ -143,7 +207,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
143
207
  server_sk = request.headers.get("SERVER-SK", "")
144
208
 
145
209
  # 调用IAM验证接口(即使token为空也要调用,因为可能是白名单接口)
146
- user_info = self.iam_client.verify_token(
210
+ user_info = iam_client.verify_token(
147
211
  token=token or "", # 如果token为None,传递空字符串
148
212
  api=api_path,
149
213
  method=method,
@@ -158,8 +222,12 @@ class AuthMiddleware(BaseHTTPMiddleware):
158
222
  raise
159
223
  except Exception as e:
160
224
  logger.error(f"Token验证异常: {str(e)}")
161
- if self.config.enable_debug:
162
- logger.exception("详细异常信息:")
225
+ try:
226
+ config, _ = self._get_config_and_client()
227
+ if config.enable_debug:
228
+ logger.exception("详细异常信息:")
229
+ except:
230
+ pass
163
231
  return None
164
232
 
165
233
  def _create_error_response(
@@ -184,3 +252,274 @@ class AuthMiddleware(BaseHTTPMiddleware):
184
252
  status_code=status_code,
185
253
  content=error_data
186
254
  )
255
+
256
+
257
+ class AuthService:
258
+ """
259
+ 认证服务类
260
+ 提供依赖注入式的认证功能
261
+ """
262
+
263
+ def __init__(self, auth_config: AuthConfig):
264
+ if auth_config is None:
265
+ raise ValueError("auth_config参数不能为None,必须传入AuthConfig配置对象")
266
+ self.security = HTTPBearer(auto_error=False)
267
+ self.iam_client = ConnectAgenterraIam(config=auth_config)
268
+ self.auth_config = auth_config
269
+
270
+ def is_path_whitelisted(self, path: str) -> bool:
271
+ """
272
+ 检查路径是否在白名单中
273
+ """
274
+ if not self.auth_config:
275
+ return False
276
+ return self.auth_config.is_path_whitelisted(path)
277
+
278
+ async def verify_token(self, request: Request):
279
+ """验证token和权限"""
280
+ # 通过token, server_ak, server_sk判断是否有权限
281
+ api_path = request.url.path
282
+
283
+ # 首先检查路径是否在白名单中
284
+ if self.is_path_whitelisted(api_path):
285
+ logger.info(f"路径 {api_path} 在白名单中,跳过IAM鉴权")
286
+ return True
287
+
288
+ credentials: HTTPAuthorizationCredentials = await self.security(request)
289
+ method = request.method
290
+
291
+ server_ak = request.headers.get("SERVER-AK", "")
292
+ server_sk = request.headers.get("SERVER-SK", "")
293
+
294
+ token = ""
295
+ if credentials is not None:
296
+ token = credentials.credentials
297
+ user_info_by_iam = self.iam_client.verify_token(token, api_path, method, server_ak, server_sk)
298
+ if user_info_by_iam:
299
+ return True
300
+ return False
301
+
302
+ async def get_current_user(self, request: Request) -> Optional[Dict]:
303
+ """获取当前用户信息"""
304
+ try:
305
+ # 直接调用verify_token方法进行token验证
306
+ if not await self.verify_token(request):
307
+ return None
308
+
309
+ # 获取token用于后续用户信息获取
310
+ credentials: HTTPAuthorizationCredentials = await self.security(request)
311
+ if not credentials:
312
+ return None
313
+
314
+ token = credentials.credentials
315
+
316
+ # 直接解析JWT token获取payload
317
+ payload = self.decode_jwt_token(token)
318
+ if not payload:
319
+ logger.error("JWT token解析失败")
320
+ return None
321
+
322
+ # 从payload中提取用户信息
323
+ iam_user_id = payload.get("sub") # JWT标准中用户ID存储在sub字段
324
+ username = None
325
+
326
+ # 解析新的凭证信息结构
327
+ all_credentials = payload.get("all_credentials", [])
328
+ total_credentials = payload.get("total_credentials", 0)
329
+
330
+ # 从all_credentials中提取username(向后兼容)
331
+ for cred in all_credentials:
332
+ if cred.get("type") == "username":
333
+ username = cred.get("value")
334
+ break
335
+
336
+ # 向后兼容性:如果没有all_credentials,尝试从payload的其他字段构建
337
+ if not all_credentials:
338
+ credentials_list = []
339
+ # 检查payload中是否有直接的username字段
340
+ if payload.get("username"):
341
+ username = payload.get("username")
342
+ credentials_list.append({"type": "username", "value": username})
343
+ if payload.get("email"):
344
+ credentials_list.append({"type": "email", "value": payload.get("email")})
345
+ if payload.get("phone"):
346
+ credentials_list.append({"type": "phone", "value": payload.get("phone")})
347
+ all_credentials = credentials_list
348
+ total_credentials = len(credentials_list)
349
+
350
+ if not username:
351
+ return None
352
+
353
+ # 构建用户信息字典
354
+ user_info = {
355
+ "id": iam_user_id,
356
+ "username": username,
357
+ "all_credentials": all_credentials,
358
+ "total_credentials": total_credentials,
359
+ "microservice": payload.get("microservice") # 添加微服务信息
360
+ }
361
+
362
+ # 向后兼容:添加传统字段映射
363
+ for cred in all_credentials:
364
+ if cred.get("type") == "email":
365
+ user_info["email"] = cred.get("value")
366
+ elif cred.get("type") == "phone":
367
+ user_info["phone"] = cred.get("value")
368
+ elif cred.get("type") == "username" and not user_info.get("username"):
369
+ user_info["username"] = cred.get("value")
370
+
371
+ # 统计凭证类型分布
372
+ cred_types = [cred.get("type") for cred in all_credentials]
373
+ cred_type_count = {cred_type: cred_types.count(cred_type) for cred_type in set(cred_types)}
374
+
375
+ logger.info(
376
+ f"用户认证成功: user_id={iam_user_id}, username={username}, 凭证数量={total_credentials}, 凭证类型分布={cred_type_count}")
377
+ logger.debug(f"JWT payload: {payload}")
378
+
379
+ # 将用户信息添加到请求状态中
380
+ request.state.user = user_info
381
+ return user_info
382
+
383
+ except HTTPException as e:
384
+ logger.error(f"获取当前用户信息失败: {str(e)}")
385
+ # 重新抛出HTTP异常(403权限不足)
386
+ return None
387
+ except Exception as e:
388
+ logger.error(f"获取当前用户信息失败: {str(e)}")
389
+ return None
390
+
391
+ async def require_auth(self, request: Request) -> Dict:
392
+ """要求用户必须登录"""
393
+ try:
394
+ user_info = await self.get_current_user(request)
395
+ if not user_info:
396
+ raise HTTPException(
397
+ status_code=status.HTTP_401_UNAUTHORIZED,
398
+ detail="需要登录认证",
399
+ headers={"WWW-Authenticate": "Bearer"},
400
+ )
401
+ return user_info
402
+ except HTTPException:
403
+ # 重新抛出HTTP异常(可能是403权限不足或401未认证)
404
+ raise
405
+
406
+ async def optional_auth(self, request: Request) -> Optional[Dict]:
407
+ """可选的用户认证(不强制要求登录)"""
408
+ try:
409
+ return await self.get_current_user(request)
410
+ except HTTPException:
411
+ # 对于可选认证,如果是403权限不足,仍然抛出异常
412
+ # 如果是401未认证,返回None
413
+ raise
414
+
415
+ def decode_jwt_token(self, token: str) -> Optional[Dict]:
416
+ """直接解析JWT token获取payload"""
417
+ try:
418
+ # 不验证签名,只解析payload(因为token已经通过verify_token验证过)
419
+ decoded_payload = jwt.decode(token, options={"verify_signature": False})
420
+ logger.debug(f"JWT token解析成功: {decoded_payload}")
421
+ return decoded_payload
422
+ except jwt.InvalidTokenError as e:
423
+ logger.error(f"JWT token解析失败: {str(e)}")
424
+ return None
425
+ except Exception as e:
426
+ logger.error(f"JWT token解析异常: {str(e)}")
427
+ return None
428
+
429
+
430
+ # 全局认证服务实例(延迟初始化)
431
+ auth_service = None
432
+
433
+
434
+ def setup_auth_middleware(auth_config: AuthConfig) -> None:
435
+ """
436
+ 设置认证中间件配置(向后兼容)
437
+
438
+ Args:
439
+ auth_config: 认证配置实例,包含白名单路径等配置
440
+
441
+ Deprecated:
442
+ 请使用 init_skyplatform_iam() 替代
443
+ """
444
+ global auth_service
445
+ auth_service = AuthService(auth_config)
446
+ logger.warning("setup_auth_middleware()已废弃,请使用init_skyplatform_iam()替代")
447
+ logger.info(f"认证中间件已配置,白名单路径数量: {len(auth_config.get_whitelist_paths())}")
448
+
449
+
450
+ def create_auth_middleware(
451
+ app,
452
+ config: Optional[AuthConfig] = None,
453
+ use_global_manager: bool = True
454
+ ) -> AuthMiddleware:
455
+ """
456
+ 创建认证中间件实例
457
+
458
+ Args:
459
+ app: FastAPI应用实例
460
+ config: 认证配置,如果为None且use_global_manager=True,则从全局管理器获取
461
+ use_global_manager: 是否使用全局管理器(推荐)
462
+
463
+ Returns:
464
+ AuthMiddleware: 认证中间件实例
465
+
466
+ Example:
467
+ # 使用全局管理器(推荐)
468
+ middleware = create_auth_middleware(app)
469
+
470
+ # 传统模式(向后兼容)
471
+ middleware = create_auth_middleware(app, config, use_global_manager=False)
472
+ """
473
+ return AuthMiddleware(app, config, use_global_manager=use_global_manager)
474
+
475
+
476
+ # 便捷的依赖函数
477
+ async def get_current_user(request: Request) -> Dict:
478
+ """
479
+ 获取当前用户的依赖函数
480
+ 优先使用全局管理器,向后兼容传统模式
481
+ """
482
+ try:
483
+ # 尝试使用全局管理器
484
+ manager = get_global_manager()
485
+ if manager.is_initialized():
486
+ user_info = await manager.get_current_user_info(request)
487
+ if user_info is None:
488
+ raise HTTPException(
489
+ status_code=status.HTTP_401_UNAUTHORIZED,
490
+ detail="需要登录认证",
491
+ headers={"WWW-Authenticate": "Bearer"},
492
+ )
493
+ return user_info
494
+ except IAMServiceError:
495
+ pass # 全局管理器未初始化,尝试传统模式
496
+
497
+ # 传统模式(向后兼容)
498
+ if auth_service is None:
499
+ raise HTTPException(
500
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
501
+ detail="认证服务未初始化,请先调用init_skyplatform_iam()或setup_auth_middleware()函数进行配置"
502
+ )
503
+ return await auth_service.require_auth(request)
504
+
505
+
506
+ async def get_optional_user(request: Request) -> Optional[Dict]:
507
+ """
508
+ 获取可选当前用户的依赖函数
509
+ 优先使用全局管理器,向后兼容传统模式
510
+ """
511
+ try:
512
+ # 尝试使用全局管理器
513
+ manager = get_global_manager()
514
+ if manager.is_initialized():
515
+ return await manager.get_current_user_info(request)
516
+ except IAMServiceError:
517
+ pass # 全局管理器未初始化,尝试传统模式
518
+
519
+ # 传统模式(向后兼容)
520
+ if auth_service is None:
521
+ raise HTTPException(
522
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
523
+ detail="认证服务未初始化,请先调用init_skyplatform_iam()或setup_auth_middleware()函数进行配置"
524
+ )
525
+ return await auth_service.optional_auth(request)