huace-aigc-auth-client 1.1.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.
@@ -0,0 +1,704 @@
1
+ """
2
+ AIGC Auth Python SDK
3
+
4
+ 提供以下功能:
5
+ 1. Token 验证
6
+ 2. 获取用户信息
7
+ 3. 权限检查
8
+ 4. FastAPI/Flask 请求拦截中间件
9
+ 5. 旧系统接入支持(用户同步)
10
+ """
11
+
12
+ import os
13
+ import time
14
+ import hashlib
15
+ import requests
16
+ import logging
17
+ from functools import wraps
18
+ from typing import Optional, List, Dict, Any, Callable, Tuple
19
+ from dataclasses import dataclass
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # 尝试加载 .env 文件
24
+ try:
25
+ from dotenv import load_dotenv
26
+ load_dotenv()
27
+ except ImportError:
28
+ pass
29
+
30
+
31
+ @dataclass
32
+ class UserInfo:
33
+ """用户信息"""
34
+ id: int
35
+ username: str
36
+ nickname: Optional[str] = None
37
+ email: Optional[str] = None
38
+ phone: Optional[str] = None
39
+ avatar: Optional[str] = None
40
+ roles: List[str] = None
41
+ permissions: List[str] = None
42
+ department: Optional[str] = None
43
+ company: Optional[str] = None
44
+ is_admin: Optional[bool] = None
45
+
46
+ def __post_init__(self):
47
+ if self.roles is None:
48
+ self.roles = []
49
+ if self.permissions is None:
50
+ self.permissions = []
51
+
52
+ def has_role(self, role: str) -> bool:
53
+ """检查是否拥有指定角色"""
54
+ return role in self.roles
55
+
56
+ def has_permission(self, permission: str) -> bool:
57
+ """检查是否拥有指定权限"""
58
+ return permission in self.permissions
59
+
60
+ def has_any_permission(self, permissions: List[str]) -> bool:
61
+ """检查是否拥有任意一个权限"""
62
+ return any(p in self.permissions for p in permissions)
63
+
64
+ def has_all_permissions(self, permissions: List[str]) -> bool:
65
+ """检查是否拥有所有权限"""
66
+ return all(p in self.permissions for p in permissions)
67
+
68
+
69
+ @dataclass
70
+ class TokenVerifyResult:
71
+ """Token 验证结果"""
72
+ valid: bool
73
+ user_id: Optional[str] = None
74
+ username: Optional[str] = None
75
+ expires_at: Optional[str] = None
76
+
77
+
78
+ class AigcAuthError(Exception):
79
+ """AIGC Auth SDK 异常"""
80
+ def __init__(self, code: int, message: str):
81
+ self.code = code
82
+ self.message = message
83
+ super().__init__(f"[{code}] {message}")
84
+
85
+
86
+ class AigcAuthClient:
87
+ """
88
+ AIGC Auth 客户端
89
+
90
+ 使用方法:
91
+ client = AigcAuthClient(
92
+ app_id="your_app_id",
93
+ app_secret="your_app_secret",
94
+ base_url="https://aigc-auth.huacemedia.com/api/v1" # 可选
95
+ )
96
+
97
+ # 验证 token
98
+ result = client.verify_token(token)
99
+
100
+ # 获取用户信息
101
+ user = client.get_user_info(token)
102
+
103
+ # 检查权限
104
+ results = client.check_permissions(token, ["user:read", "user:write"])
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ app_id: Optional[str] = None,
110
+ app_secret: Optional[str] = None,
111
+ base_url: Optional[str] = None,
112
+ timeout: int = 30,
113
+ cache_ttl: int = 300 # 缓存有效期(秒),默认 5 分钟
114
+ ):
115
+ """
116
+ 初始化客户端
117
+
118
+ Args:
119
+ app_id: 应用 ID,可从环境变量 AIGC_AUTH_APP_ID 读取
120
+ app_secret: 应用密钥,可从环境变量 AIGC_AUTH_APP_SECRET 读取
121
+ base_url: API 基础 URL,可从环境变量 AIGC_AUTH_BASE_URL 读取
122
+ timeout: 请求超时时间(秒)
123
+ cache_ttl: 缓存有效期(秒),默认 300 秒(5 分钟)
124
+ """
125
+ self.app_id = app_id or os.getenv("AIGC_AUTH_APP_ID")
126
+ self.app_secret = app_secret or os.getenv("AIGC_AUTH_APP_SECRET")
127
+ self.base_url = (
128
+ base_url or
129
+ os.getenv("AIGC_AUTH_BASE_URL") or
130
+ "https://aigc-auth.huacemedia.com/api/v1"
131
+ )
132
+ self.timeout = timeout
133
+ self.cache_ttl = cache_ttl
134
+
135
+ # 缓存存储: {cache_key: (data, timestamp)}
136
+ self._cache: Dict[str, Tuple[Dict, float]] = {}
137
+
138
+ if not self.app_id or not self.app_secret:
139
+ raise ValueError(
140
+ "必须提供 app_id 和 app_secret,"
141
+ "可通过参数传入或设置环境变量 AIGC_AUTH_APP_ID 和 AIGC_AUTH_APP_SECRET"
142
+ )
143
+
144
+ def _get_headers(self) -> Dict[str, str]:
145
+ """获取请求头"""
146
+ return {
147
+ "Content-Type": "application/json",
148
+ "X-App-ID": self.app_id,
149
+ "X-App-Secret": self.app_secret
150
+ }
151
+
152
+ def _generate_cache_key(self, token: str, url: str, method: str, extra_data: Dict = None) -> str:
153
+ """
154
+ 生成缓存键
155
+
156
+ Args:
157
+ token: 用户 token
158
+ url: 请求 URL
159
+ method: 请求方法
160
+ extra_data: 额外的请求参数(会被排序后拼接到 key 中)
161
+
162
+ Returns:
163
+ str: 缓存键(使用 hash 以节省内存)
164
+ """
165
+ key_string = f"{token}:{url}:{method}"
166
+
167
+ # 如果有额外参数,将其排序后拼接到 key 中
168
+ if extra_data:
169
+ # 对参数进行排序并转换为字符串
170
+ import json
171
+ sorted_data = json.dumps(extra_data, sort_keys=True, ensure_ascii=False)
172
+ key_string = f"{key_string}:{sorted_data}"
173
+
174
+ return hashlib.md5(key_string.encode()).hexdigest()
175
+
176
+ def _get_from_cache(self, cache_key: str) -> Optional[Dict]:
177
+ """
178
+ 从缓存中获取数据
179
+
180
+ Args:
181
+ cache_key: 缓存键
182
+
183
+ Returns:
184
+ Optional[Dict]: 缓存的数据,如果缓存不存在或已过期则返回 None
185
+ """
186
+ if cache_key not in self._cache:
187
+ return None
188
+
189
+ data, timestamp = self._cache[cache_key]
190
+ current_time = time.time()
191
+
192
+ # 检查缓存是否过期
193
+ if current_time - timestamp > self.cache_ttl:
194
+ # 清理过期缓存
195
+ del self._cache[cache_key]
196
+ return None
197
+
198
+ return data
199
+
200
+ def _set_cache(self, cache_key: str, data: Dict):
201
+ """
202
+ 设置缓存
203
+
204
+ Args:
205
+ cache_key: 缓存键
206
+ data: 要缓存的数据
207
+ """
208
+ self._cache[cache_key] = (data, time.time())
209
+
210
+ # 简单的缓存清理:如果缓存数量过多,清理所有过期的缓存
211
+ if len(self._cache) > 1000:
212
+ self._clean_expired_cache()
213
+
214
+ def _clean_expired_cache(self):
215
+ """清理所有过期的缓存"""
216
+ current_time = time.time()
217
+ expired_keys = [
218
+ key for key, (_, timestamp) in self._cache.items()
219
+ if current_time - timestamp > self.cache_ttl
220
+ ]
221
+ for key in expired_keys:
222
+ del self._cache[key]
223
+
224
+ def clear_cache(self):
225
+ """清空所有缓存"""
226
+ self._cache.clear()
227
+
228
+ def _request(self, method: str, endpoint: str, data: Dict = None, token: str = None) -> Dict:
229
+ """
230
+ 发送请求
231
+
232
+ Args:
233
+ method: 请求方法
234
+ endpoint: 端点路径
235
+ data: 请求数据
236
+ token: 用户 token(用于缓存键)
237
+
238
+ Returns:
239
+ Dict: 响应数据
240
+ """
241
+ url = f"{self.base_url}/sdk{endpoint}"
242
+
243
+ # 如果提供了 token,尝试从缓存中获取
244
+ if token:
245
+ # 从 data 中提取 token 之外的参数作为额外的缓存键信息
246
+ extra_data = None
247
+ if data:
248
+ extra_data = {k: v for k, v in data.items() if k != 'token'}
249
+
250
+ cache_key = self._generate_cache_key(token, url, method, extra_data)
251
+ cached_data = self._get_from_cache(cache_key)
252
+ if cached_data is not None:
253
+ return cached_data
254
+
255
+ try:
256
+ response = requests.request(
257
+ method=method,
258
+ url=url,
259
+ json=data,
260
+ headers=self._get_headers(),
261
+ timeout=self.timeout
262
+ )
263
+ response.raise_for_status()
264
+ result = response.json()
265
+
266
+ if result.get("code") != 0:
267
+ raise AigcAuthError(
268
+ result.get("code", -1),
269
+ result.get("message", "未知错误")
270
+ )
271
+
272
+ response_data = result.get("data", {})
273
+
274
+ # 如果请求成功且提供了 token,缓存响应数据
275
+ if token:
276
+ self._set_cache(cache_key, response_data)
277
+
278
+ return response_data
279
+
280
+ except requests.exceptions.RequestException as e:
281
+ raise AigcAuthError(-1, f"请求失败: {str(e)}")
282
+
283
+ def verify_token(self, token: str) -> TokenVerifyResult:
284
+ """
285
+ 验证 Token
286
+
287
+ Args:
288
+ token: 用户的 access_token
289
+
290
+ Returns:
291
+ TokenVerifyResult: 验证结果
292
+ """
293
+ data = self._request("POST", "/token/verify", {"token": token}, token=token)
294
+ return TokenVerifyResult(
295
+ valid=data.get("valid", False),
296
+ user_id=data.get("userId"),
297
+ username=data.get("username"),
298
+ expires_at=data.get("expiresAt")
299
+ )
300
+
301
+ def get_user_info(self, token: str) -> UserInfo:
302
+ """
303
+ 获取用户信息
304
+
305
+ Args:
306
+ token: 用户的 access_token
307
+
308
+ Returns:
309
+ UserInfo: 用户信息
310
+
311
+ Raises:
312
+ AigcAuthError: 当 token 无效或用户不存在时
313
+ """
314
+ data = self._request("POST", "/user/info", {"token": token}, token=token)
315
+ return UserInfo(
316
+ id=data.get("id"),
317
+ username=data.get("username"),
318
+ nickname=data.get("nickname"),
319
+ email=data.get("email"),
320
+ phone=data.get("phone"),
321
+ avatar=data.get("avatar"),
322
+ roles=data.get("roles", []),
323
+ permissions=data.get("permissions", []),
324
+ department=data.get("department"),
325
+ company=data.get("company"),
326
+ is_admin=data.get("is_admin")
327
+ )
328
+
329
+ def check_permissions(
330
+ self,
331
+ token: str,
332
+ permission_codes: List[str]
333
+ ) -> Dict[str, bool]:
334
+ """
335
+ 批量检查权限
336
+
337
+ Args:
338
+ token: 用户的 access_token
339
+ permission_codes: 权限代码列表
340
+
341
+ Returns:
342
+ Dict[str, bool]: 权限检查结果,key 为权限代码,value 为是否拥有
343
+ """
344
+ data = self._request("POST", "/permission/check", {
345
+ "token": token,
346
+ "permissionCodes": permission_codes
347
+ }, token=token)
348
+ return data.get("results", {})
349
+
350
+ def get_user_info_from_header(self, authorization: str) -> Optional[UserInfo]:
351
+ """
352
+ 从 Authorization header 获取用户信息
353
+
354
+ Args:
355
+ authorization: Authorization header 的值,格式为 "Bearer {token}"
356
+
357
+ Returns:
358
+ UserInfo: 用户信息,如果验证失败返回 None
359
+ """
360
+ if not authorization:
361
+ return None
362
+
363
+ if not authorization.startswith("Bearer "):
364
+ return None
365
+
366
+ token = authorization[7:] # 移除 "Bearer " 前缀
367
+
368
+ try:
369
+ return self.get_user_info(token)
370
+ except AigcAuthError:
371
+ return None
372
+
373
+ # ============ 用户同步相关方法 ============
374
+
375
+ def sync_user_to_auth(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
376
+ """
377
+ 同步用户到 aigc-auth(用于旧系统初始化同步)
378
+
379
+ Args:
380
+ user_data: 用户数据,必须包含 username 和 password
381
+
382
+ Returns:
383
+ Dict: 同步结果
384
+ - success: bool 是否成功
385
+ - created: bool 是否新建(False 表示已存在)
386
+ - user_id: int 用户ID
387
+ - message: str 消息
388
+ """
389
+ return self._request("POST", "/sync/user", user_data)
390
+
391
+ def batch_sync_users_to_auth(self, users: List[Dict[str, Any]]) -> Dict[str, Any]:
392
+ """
393
+ 批量同步用户到 aigc-auth
394
+
395
+ Args:
396
+ users: 用户数据列表
397
+
398
+ Returns:
399
+ Dict: 批量同步结果
400
+ - total: int 总数
401
+ - success: int 成功数
402
+ - failed: int 失败数
403
+ - skipped: int 跳过数(已存在)
404
+ - errors: List[Dict] 错误详情
405
+ """
406
+ return self._request("POST", "/sync/batch", {"users": users})
407
+
408
+ def register_webhook(self, webhook_url: str, events: List[str], secret: Optional[str] = None) -> Dict[str, Any]:
409
+ """
410
+ 注册 webhook 接收增量用户
411
+
412
+ Args:
413
+ webhook_url: webhook 接收地址
414
+ events: 订阅的事件列表,如 ["user.created", "user.updated"]
415
+ secret: webhook 签名密钥
416
+
417
+ Returns:
418
+ Dict: 注册结果
419
+ - webhook_id: str webhook ID
420
+ - success: bool 是否成功
421
+ """
422
+ return self._request("POST", "/webhook/register", {
423
+ "url": webhook_url,
424
+ "events": events,
425
+ "secret": secret
426
+ })
427
+
428
+ def unregister_webhook(self, webhook_id: str) -> Dict[str, Any]:
429
+ """
430
+ 注销 webhook
431
+
432
+ Args:
433
+ webhook_id: webhook ID
434
+
435
+ Returns:
436
+ Dict: 注销结果
437
+ """
438
+ return self._request("POST", "/webhook/unregister", {"webhookId": webhook_id})
439
+
440
+
441
+ def require_auth(
442
+ client: AigcAuthClient,
443
+ permissions: List[str] = None,
444
+ any_permission: bool = False
445
+ ):
446
+ """
447
+ FastAPI 路由装饰器,要求用户登录
448
+
449
+ Args:
450
+ client: AigcAuthClient 实例
451
+ permissions: 需要的权限列表(可选)
452
+ any_permission: 是否只需要任意一个权限,默认需要全部
453
+
454
+ 使用方法:
455
+ @app.get("/protected")
456
+ @require_auth(client)
457
+ def protected_route(user_info: UserInfo):
458
+ return {"user": user_info.username}
459
+
460
+ @app.get("/admin")
461
+ @require_auth(client, permissions=["admin:access"])
462
+ def admin_route(user_info: UserInfo):
463
+ return {"admin": True}
464
+ """
465
+ def decorator(func: Callable):
466
+ @wraps(func)
467
+ def wrapper(*args, **kwargs):
468
+ # 尝试从 FastAPI 获取 request
469
+ request = kwargs.get("request")
470
+ if request is None:
471
+ for arg in args:
472
+ if hasattr(arg, "headers"):
473
+ request = arg
474
+ break
475
+
476
+ if request is None:
477
+ raise AigcAuthError(401, "无法获取请求对象")
478
+
479
+ authorization = request.headers.get("Authorization")
480
+ user_info = client.get_user_info_from_header(authorization)
481
+
482
+ if user_info is None:
483
+ raise AigcAuthError(401, "未登录或 Token 已过期")
484
+
485
+ # 检查权限
486
+ if permissions:
487
+ if any_permission:
488
+ if not user_info.has_any_permission(permissions):
489
+ raise AigcAuthError(403, "权限不足")
490
+ else:
491
+ if not user_info.has_all_permissions(permissions):
492
+ raise AigcAuthError(403, "权限不足")
493
+
494
+ # 将用户信息注入到 kwargs
495
+ kwargs["user_info"] = user_info
496
+ return func(*args, **kwargs)
497
+
498
+ return wrapper
499
+ return decorator
500
+
501
+
502
+ class AuthMiddleware:
503
+ """
504
+ 通用认证中间件
505
+
506
+ 支持 FastAPI 和 Flask
507
+
508
+ FastAPI 使用方法:
509
+ from fastapi import FastAPI, Request
510
+ from sdk import AigcAuthClient, AuthMiddleware
511
+
512
+ app = FastAPI()
513
+ client = AigcAuthClient(app_id="xxx", app_secret="xxx")
514
+ auth_middleware = AuthMiddleware(client)
515
+
516
+ @app.middleware("http")
517
+ async def auth_middleware_handler(request: Request, call_next):
518
+ return await auth_middleware.fastapi_middleware(request, call_next)
519
+
520
+ Flask 使用方法:
521
+ from flask import Flask
522
+ from sdk import AigcAuthClient, AuthMiddleware
523
+
524
+ app = Flask(__name__)
525
+ client = AigcAuthClient(app_id="xxx", app_secret="xxx")
526
+ auth_middleware = AuthMiddleware(client)
527
+
528
+ @app.before_request
529
+ def before_request():
530
+ return auth_middleware.flask_before_request()
531
+ """
532
+
533
+ def __init__(
534
+ self,
535
+ client: AigcAuthClient,
536
+ exclude_paths: List[str] = None,
537
+ exclude_prefixes: List[str] = None
538
+ ):
539
+ """
540
+ 初始化中间件
541
+
542
+ Args:
543
+ client: AigcAuthClient 实例
544
+ exclude_paths: 排除的路径列表(精确匹配)
545
+ exclude_prefixes: 排除的路径前缀列表
546
+ """
547
+ self.client = client
548
+ self.exclude_paths = exclude_paths or []
549
+ self.exclude_prefixes = exclude_prefixes or []
550
+
551
+ def _should_skip(self, path: str) -> bool:
552
+ """检查是否应该跳过验证"""
553
+ if path in self.exclude_paths:
554
+ return True
555
+ for prefix in self.exclude_prefixes:
556
+ if path.startswith(prefix):
557
+ return True
558
+ return False
559
+
560
+ def _extract_token(self, authorization: str) -> Optional[str]:
561
+ """从 Authorization header 提取 token"""
562
+ if not authorization:
563
+ return None
564
+ if not authorization.startswith("Bearer "):
565
+ return None
566
+ return authorization[7:]
567
+
568
+ async def fastapi_middleware(self, request, call_next):
569
+ """
570
+ FastAPI 中间件
571
+
572
+ 使用方法:
573
+ @app.middleware("http")
574
+ async def auth(request: Request, call_next):
575
+ return await auth_middleware.fastapi_middleware(request, call_next)
576
+ """
577
+ from fastapi.responses import JSONResponse
578
+
579
+ path = request.url.path
580
+
581
+ # 检查是否跳过
582
+ if self._should_skip(path):
583
+ return await call_next(request)
584
+
585
+ # 获取 Authorization header
586
+ authorization = request.headers.get("Authorization")
587
+ token = self._extract_token(authorization)
588
+
589
+ if not token:
590
+ return JSONResponse(
591
+ status_code=401,
592
+ content={"code": 401, "message": "未提供认证信息", "data": None}
593
+ )
594
+
595
+ # 验证 token
596
+ try:
597
+ user_info = self.client.get_user_info(token)
598
+ # 将用户信息存储到 request.state
599
+ request.state.user_info = user_info
600
+ # 处理代理头部,确保重定向(如果有)使用正确的协议
601
+ forwarded_proto = request.headers.get("x-forwarded-proto")
602
+ if forwarded_proto:
603
+ request.scope["scheme"] = forwarded_proto
604
+ except AigcAuthError as e:
605
+ return JSONResponse(
606
+ status_code=401,
607
+ content={"code": e.code, "message": e.message, "data": None}
608
+ )
609
+
610
+ return await call_next(request)
611
+
612
+ def flask_before_request(self):
613
+ """
614
+ Flask before_request 处理器
615
+
616
+ 使用方法:
617
+ @app.before_request
618
+ def before_request():
619
+ return auth_middleware.flask_before_request()
620
+ """
621
+ from flask import request, jsonify, g
622
+
623
+ path = request.path
624
+
625
+ # 检查是否跳过
626
+ if self._should_skip(path):
627
+ return None
628
+
629
+ # 获取 Authorization header
630
+ authorization = request.headers.get("Authorization")
631
+ token = self._extract_token(authorization)
632
+
633
+ if not token:
634
+ return jsonify({
635
+ "code": 401,
636
+ "message": "未提供认证信息",
637
+ "data": None
638
+ }), 401
639
+
640
+ # 验证 token
641
+ try:
642
+ user_info = self.client.get_user_info(token)
643
+ # 将用户信息存储到 flask.g
644
+ g.user_info = user_info
645
+ except AigcAuthError as e:
646
+ return jsonify({
647
+ "code": e.code,
648
+ "message": e.message,
649
+ "data": None
650
+ }), 401
651
+
652
+ return None
653
+
654
+ def get_current_user_fastapi(self, request) -> Optional[UserInfo]:
655
+ """
656
+ FastAPI 中获取当前用户
657
+
658
+ Args:
659
+ request: FastAPI Request 对象
660
+
661
+ Returns:
662
+ UserInfo: 用户信息,如果未登录返回 None
663
+ """
664
+ return getattr(request.state, "user_info", None)
665
+
666
+ def get_current_user_flask(self) -> Optional[UserInfo]:
667
+ """
668
+ Flask 中获取当前用户
669
+
670
+ Returns:
671
+ UserInfo: 用户信息,如果未登录返回 None
672
+ """
673
+ from flask import g
674
+ return getattr(g, "user_info", None)
675
+
676
+
677
+ # 便捷函数:创建 FastAPI 依赖
678
+ def create_fastapi_auth_dependency(client: AigcAuthClient):
679
+ """
680
+ 创建 FastAPI 认证依赖
681
+
682
+ 使用方法:
683
+ from fastapi import Depends
684
+ from sdk import AigcAuthClient, create_fastapi_auth_dependency
685
+
686
+ client = AigcAuthClient(app_id="xxx", app_secret="xxx")
687
+ get_current_user = create_fastapi_auth_dependency(client)
688
+
689
+ @app.get("/me")
690
+ async def get_me(user: UserInfo = Depends(get_current_user)):
691
+ return {"username": user.username}
692
+ """
693
+ from fastapi import Request, HTTPException
694
+
695
+ async def get_current_user(request: Request) -> UserInfo:
696
+ authorization = request.headers.get("Authorization")
697
+ user_info = client.get_user_info_from_header(authorization)
698
+
699
+ if user_info is None:
700
+ raise HTTPException(status_code=401, detail="未登录或 Token 已过期")
701
+
702
+ return user_info
703
+
704
+ return get_current_user