huace-aigc-auth-client 1.1.18__tar.gz → 1.1.20__tar.gz

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 (19) hide show
  1. {huace_aigc_auth_client-1.1.18/huace_aigc_auth_client.egg-info → huace_aigc_auth_client-1.1.20}/PKG-INFO +26 -1
  2. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/README.md +25 -0
  3. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/huace_aigc_auth_client/__init__.py +5 -1
  4. huace_aigc_auth_client-1.1.20/huace_aigc_auth_client/api_stats_collector.py +300 -0
  5. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/huace_aigc_auth_client/sdk.py +214 -2
  6. huace_aigc_auth_client-1.1.20/huace_aigc_auth_client/user_context.py +189 -0
  7. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20/huace_aigc_auth_client.egg-info}/PKG-INFO +26 -1
  8. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/huace_aigc_auth_client.egg-info/SOURCES.txt +2 -0
  9. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/pyproject.toml +1 -1
  10. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/LICENSE +0 -0
  11. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/MANIFEST.in +0 -0
  12. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/QUICK_START.txt +0 -0
  13. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/huace_aigc_auth_client/legacy_adapter.py +0 -0
  14. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/huace_aigc_auth_client/webhook.py +0 -0
  15. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/huace_aigc_auth_client/webhook_flask.py +0 -0
  16. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/huace_aigc_auth_client.egg-info/dependency_links.txt +0 -0
  17. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/huace_aigc_auth_client.egg-info/requires.txt +0 -0
  18. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/huace_aigc_auth_client.egg-info/top_level.txt +0 -0
  19. {huace_aigc_auth_client-1.1.18 → huace_aigc_auth_client-1.1.20}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: huace-aigc-auth-client
3
- Version: 1.1.18
3
+ Version: 1.1.20
4
4
  Summary: 华策AIGC Auth Client - 提供 Token 验证、用户信息获取、权限检查、旧系统接入等功能
5
5
  Author-email: Huace <support@huace.com>
6
6
  License: MIT
@@ -272,6 +272,31 @@ def admin_only():
272
272
  return jsonify({"message": "欢迎管理员"})
273
273
  ```
274
274
 
275
+ ## 上下文管理
276
+
277
+ SDK 提供了上下文管理功能,可以在任意位置(Service、Utility 等)获取当前请求的用户信息,无需层层传递参数。
278
+ 支持 Flask 和 FastAPI,兼容同步和异步环境。
279
+
280
+ 注意:使用此功能前必须先注册 `AuthMiddleware`。
281
+
282
+ ```python
283
+ from huace_aigc_auth_client import get_current_user
284
+
285
+ def do_something_logic():
286
+ # 获取当前用户信息(返回字典,非 UserInfo 对象)
287
+ user = get_current_user()
288
+
289
+ if user:
290
+ print(f"当前操作用户ID: {user['id']}")
291
+ print(f"当前操作用户名: {user['username']}")
292
+
293
+ # 也可以获取 app_id 等信息(如果未过滤)
294
+ if 'app_id' in user:
295
+ print(f"应用ID: {user['app_id']}")
296
+ else:
297
+ print("无用户上下文")
298
+ ```
299
+
275
300
  ## API 参考
276
301
 
277
302
  ### UserInfo 对象
@@ -247,6 +247,31 @@ def admin_only():
247
247
  return jsonify({"message": "欢迎管理员"})
248
248
  ```
249
249
 
250
+ ## 上下文管理
251
+
252
+ SDK 提供了上下文管理功能,可以在任意位置(Service、Utility 等)获取当前请求的用户信息,无需层层传递参数。
253
+ 支持 Flask 和 FastAPI,兼容同步和异步环境。
254
+
255
+ 注意:使用此功能前必须先注册 `AuthMiddleware`。
256
+
257
+ ```python
258
+ from huace_aigc_auth_client import get_current_user
259
+
260
+ def do_something_logic():
261
+ # 获取当前用户信息(返回字典,非 UserInfo 对象)
262
+ user = get_current_user()
263
+
264
+ if user:
265
+ print(f"当前操作用户ID: {user['id']}")
266
+ print(f"当前操作用户名: {user['username']}")
267
+
268
+ # 也可以获取 app_id 等信息(如果未过滤)
269
+ if 'app_id' in user:
270
+ print(f"应用ID: {user['app_id']}")
271
+ else:
272
+ print("无用户上下文")
273
+ ```
274
+
250
275
  ## API 参考
251
276
 
252
277
  ### UserInfo 对象
@@ -54,6 +54,8 @@ from .sdk import (
54
54
  create_fastapi_auth_dependency
55
55
  )
56
56
 
57
+ from .user_context import get_current_user
58
+
57
59
  from .legacy_adapter import (
58
60
  LegacySystemAdapter,
59
61
  LegacyUserData,
@@ -167,5 +169,7 @@ __all__ = [
167
169
  "register_flask_webhook_routes",
168
170
  # Logger 设置
169
171
  "setLogger",
172
+ # 用户上下文
173
+ "get_current_user",
170
174
  ]
171
- __version__ = "1.1.18"
175
+ __version__ = "1.1.20"
@@ -0,0 +1,300 @@
1
+ """
2
+ SDK 客户端接口监控模块
3
+ 提供异步队列提交接口统计数据到服务端
4
+ """
5
+ import time
6
+ import queue
7
+ import threading
8
+ import requests
9
+ from typing import Optional, Dict, Any, List
10
+ from datetime import datetime
11
+
12
+
13
+ class ApiStatsCollector:
14
+ """接口统计收集器(异步队列方式)"""
15
+
16
+ def __init__(
17
+ self,
18
+ api_url: str,
19
+ app_secret: str,
20
+ token: str,
21
+ batch_size: int = 10,
22
+ flush_interval: float = 5.0,
23
+ enabled: bool = True
24
+ ):
25
+ """
26
+ 初始化统计收集器
27
+
28
+ Args:
29
+ api_url: 统计接口 URL(如:http://auth.example.com/api/sdk/stats/report/batch)
30
+ app_secret: 应用密钥
31
+ token: 用户访问令牌
32
+ batch_size: 批量提交大小
33
+ flush_interval: 刷新间隔(秒)
34
+ enabled: 是否启用
35
+ """
36
+ self.api_url = api_url.rstrip('/')
37
+ self.app_secret = app_secret
38
+ self.token = token
39
+ self.batch_size = batch_size
40
+ self.flush_interval = flush_interval
41
+ self.enabled = enabled
42
+
43
+ self.queue = queue.Queue()
44
+ self.running = True
45
+ self.worker_thread = None
46
+
47
+ if self.enabled:
48
+ self.start()
49
+
50
+ def start(self):
51
+ """启动工作线程"""
52
+ if self.worker_thread is None or not self.worker_thread.is_alive():
53
+ self.running = True
54
+ self.worker_thread = threading.Thread(target=self._worker, daemon=True)
55
+ self.worker_thread.start()
56
+
57
+ def stop(self):
58
+ """停止工作线程"""
59
+ self.running = False
60
+ if self.worker_thread:
61
+ self.worker_thread.join(timeout=10)
62
+
63
+ def collect(
64
+ self,
65
+ api_path: str,
66
+ api_method: str,
67
+ status_code: int,
68
+ response_time: float,
69
+ error_message: Optional[str] = None,
70
+ request_params: Optional[Dict[str, Any]] = None
71
+ ):
72
+ """
73
+ 收集接口统计数据
74
+
75
+ Args:
76
+ api_path: 接口路径
77
+ api_method: 请求方法
78
+ status_code: 状态码
79
+ response_time: 响应时间(秒)
80
+ error_message: 错误信息
81
+ request_params: 请求参数(包含 headers, query_params, view_params, request_body, form_params)
82
+ """
83
+ if not self.enabled:
84
+ return
85
+
86
+ try:
87
+ stat_data = {
88
+ 'api_path': api_path,
89
+ 'api_method': api_method,
90
+ 'status_code': status_code,
91
+ 'response_time': response_time,
92
+ 'error_message': error_message,
93
+ 'request_params': request_params,
94
+ 'timestamp': datetime.utcnow().isoformat()
95
+ }
96
+ self.queue.put_nowait(stat_data)
97
+ except queue.Full:
98
+ pass # 队列满了,丢弃数据
99
+ except Exception:
100
+ pass # 静默失败,不影响主流程
101
+
102
+ def _worker(self):
103
+ """后台工作线程:批量提交统计数据"""
104
+ buffer = []
105
+ last_flush_time = time.time()
106
+
107
+ while self.running:
108
+ try:
109
+ # 尝试从队列获取数据
110
+ try:
111
+ stat_data = self.queue.get(timeout=1.0)
112
+ buffer.append(stat_data)
113
+ except queue.Empty:
114
+ pass
115
+
116
+ current_time = time.time()
117
+ should_flush = (
118
+ len(buffer) >= self.batch_size or
119
+ (buffer and (current_time - last_flush_time) >= self.flush_interval)
120
+ )
121
+
122
+ if should_flush:
123
+ self._flush_buffer(buffer)
124
+ buffer = []
125
+ last_flush_time = current_time
126
+
127
+ except Exception:
128
+ pass # 静默失败
129
+
130
+ # 停止前刷新剩余数据
131
+ if buffer:
132
+ self._flush_buffer(buffer)
133
+
134
+ def _flush_buffer(self, buffer: List[Dict[str, Any]]):
135
+ """刷新缓冲区:批量提交统计数据"""
136
+ if not buffer:
137
+ return
138
+
139
+ try:
140
+ headers = {
141
+ 'X-App-Secret': self.app_secret,
142
+ 'Authorization': f'Bearer {self.token}',
143
+ 'Content-Type': 'application/json'
144
+ }
145
+
146
+ payload = {'stats': buffer}
147
+
148
+ response = requests.post(
149
+ f'{self.api_url}/stats/report/batch',
150
+ json=payload,
151
+ headers=headers,
152
+ timeout=5
153
+ )
154
+
155
+ # 静默失败,不抛出异常
156
+ if response.status_code != 200:
157
+ pass
158
+
159
+ except Exception:
160
+ pass # 静默失败,不影响主流程
161
+
162
+
163
+ # ============ 全局实例 ============
164
+
165
+ _global_collector: Optional[ApiStatsCollector] = None
166
+
167
+
168
+ def init_api_stats_collector(
169
+ api_url: str,
170
+ app_secret: str,
171
+ token: str,
172
+ batch_size: int = 10,
173
+ flush_interval: float = 5.0,
174
+ enabled: bool = True
175
+ ) -> ApiStatsCollector:
176
+ """
177
+ 初始化全局统计收集器
178
+
179
+ Args:
180
+ api_url: 统计接口 URL
181
+ app_secret: 应用密钥
182
+ token: 用户访问令牌
183
+ batch_size: 批量提交大小
184
+ flush_interval: 刷新间隔(秒)
185
+ enabled: 是否启用
186
+
187
+ Returns:
188
+ 统计收集器实例
189
+ """
190
+ global _global_collector
191
+ _global_collector = ApiStatsCollector(
192
+ api_url=api_url,
193
+ app_secret=app_secret,
194
+ token=token,
195
+ batch_size=batch_size,
196
+ flush_interval=flush_interval,
197
+ enabled=enabled
198
+ )
199
+ return _global_collector
200
+
201
+
202
+ def get_api_stats_collector() -> Optional[ApiStatsCollector]:
203
+ """获取全局统计收集器实例"""
204
+ return _global_collector
205
+
206
+
207
+ def stop_api_stats_collector():
208
+ """停止全局统计收集器"""
209
+ global _global_collector
210
+ if _global_collector:
211
+ _global_collector.stop()
212
+ _global_collector = None
213
+
214
+
215
+ def collect_api_stat(
216
+ api_path: str,
217
+ api_method: str,
218
+ status_code: int,
219
+ response_time: float,
220
+ error_message: Optional[str] = None,
221
+ request_params: Optional[Dict[str, Any]] = None
222
+ ):
223
+ """
224
+ 快捷方法:收集接口统计数据
225
+
226
+ 使用全局收集器实例
227
+ """
228
+ collector = get_api_stats_collector()
229
+ if collector:
230
+ collector.collect(
231
+ api_path=api_path,
232
+ api_method=api_method,
233
+ status_code=status_code,
234
+ response_time=response_time,
235
+ error_message=error_message,
236
+ request_params=request_params
237
+ )
238
+
239
+
240
+ # ============ 使用示例 ============
241
+ """
242
+ 使用示例:
243
+
244
+ 1. 应用启动时初始化:
245
+
246
+ from huace_aigc_auth_client.api_stats_collector import init_api_stats_collector
247
+
248
+ # 在应用启动时初始化
249
+ init_api_stats_collector(
250
+ api_url='http://auth.example.com/api/sdk',
251
+ app_secret='your-app-secret',
252
+ token='user-access-token',
253
+ batch_size=10,
254
+ flush_interval=5.0,
255
+ enabled=True
256
+ )
257
+
258
+
259
+ 2. 在拦截器中使用:
260
+
261
+ from huace_aigc_auth_client.api_stats_collector import collect_api_stat
262
+ import time
263
+
264
+ @app.middleware("http")
265
+ async def monitor_middleware(request: Request, call_next):
266
+ start_time = time.time()
267
+
268
+ try:
269
+ response = await call_next(request)
270
+ response_time = time.time() - start_time
271
+
272
+ # 收集统计
273
+ collect_api_stat(
274
+ api_path=request.url.path,
275
+ api_method=request.method,
276
+ status_code=response.status_code,
277
+ response_time=response_time
278
+ )
279
+
280
+ return response
281
+ except Exception as e:
282
+ response_time = time.time() - start_time
283
+ collect_api_stat(
284
+ api_path=request.url.path,
285
+ api_method=request.method,
286
+ status_code=500,
287
+ response_time=response_time,
288
+ error_message=str(e)
289
+ )
290
+ raise
291
+
292
+
293
+ 3. 应用关闭时停止:
294
+
295
+ from huace_aigc_auth_client.api_stats_collector import stop_api_stats_collector
296
+
297
+ @app.on_event("shutdown")
298
+ async def shutdown_event():
299
+ stop_api_stats_collector()
300
+ """
@@ -14,9 +14,11 @@ import time
14
14
  import hashlib
15
15
  import requests
16
16
  import logging
17
+ import dataclasses
17
18
  from functools import wraps
18
19
  from typing import Optional, List, Dict, Any, Callable, Tuple
19
20
  from dataclasses import dataclass
21
+ from .user_context import set_current_user, clear_current_user
20
22
 
21
23
  logger = logging.getLogger(__name__)
22
24
  def setLogger(log):
@@ -550,7 +552,9 @@ class AuthMiddleware:
550
552
  self,
551
553
  client: AigcAuthClient,
552
554
  exclude_paths: List[str] = None,
553
- exclude_prefixes: List[str] = None
555
+ exclude_prefixes: List[str] = None,
556
+ enable_stats: bool = True,
557
+ stats_api_url: Optional[str] = None
554
558
  ):
555
559
  """
556
560
  初始化中间件
@@ -559,10 +563,150 @@ class AuthMiddleware:
559
563
  client: AigcAuthClient 实例
560
564
  exclude_paths: 排除的路径列表(精确匹配)
561
565
  exclude_prefixes: 排除的路径前缀列表
566
+ enable_stats: 是否启用接口统计(默认启用)
567
+ stats_api_url: 统计接口 URL(可选,默认使用 client.base_url/sdk)
562
568
  """
563
569
  self.client = client
564
570
  self.exclude_paths = exclude_paths or []
565
571
  self.exclude_prefixes = exclude_prefixes or []
572
+ self.enable_stats = enable_stats
573
+ self.stats_collector = None
574
+
575
+ # 如果启用统计,设置统计接口 URL(默认使用 client 的 base_url)
576
+ if self.enable_stats:
577
+ self.stats_api_url = stats_api_url or f"{self.client.base_url}/sdk"
578
+
579
+ def _init_stats_collector(self, token: str):
580
+ """初始化统计收集器(延迟初始化)"""
581
+ if not self.enable_stats or self.stats_collector is not None:
582
+ return
583
+
584
+ try:
585
+ from .api_stats_collector import init_api_stats_collector
586
+ self.stats_collector = init_api_stats_collector(
587
+ api_url=self.stats_api_url,
588
+ app_secret=self.client.app_secret,
589
+ token=token,
590
+ batch_size=10,
591
+ flush_interval=5.0,
592
+ enabled=True
593
+ )
594
+ except Exception as e:
595
+ logger.warning(f"初始化统计收集器失败: {e}")
596
+
597
+ @staticmethod
598
+ def _collect_flask_request_params(request) -> Dict[str, Any]:
599
+ """
600
+ 收集 Flask 请求的所有参数
601
+
602
+ Args:
603
+ request: Flask request 对象
604
+
605
+ Returns:
606
+ 包含 headers, query_params, view_params, request_body, form_params 的字典
607
+ """
608
+ try:
609
+ params = {
610
+ "headers": dict(request.headers),
611
+ "query_params": request.args.to_dict(flat=False),
612
+ "view_params": request.view_args or {},
613
+ "request_body": None,
614
+ "form_params": None
615
+ }
616
+
617
+ # 获取请求体(JSON 或文本)
618
+ if request.is_json:
619
+ try:
620
+ params["request_body"] = request.get_json(silent=True)
621
+ except Exception:
622
+ pass
623
+ elif request.data:
624
+ try:
625
+ params["request_body"] = request.data.decode('utf-8')
626
+ except Exception:
627
+ params["request_body"] = str(request.data)
628
+
629
+ # 获取表单数据
630
+ if request.form:
631
+ params["form_params"] = request.form.to_dict(flat=False)
632
+
633
+ return params
634
+ except Exception as e:
635
+ logger.warning(f"收集Flask请求参数失败: {e}")
636
+ return {}
637
+
638
+ @staticmethod
639
+ async def _collect_fastapi_request_params(request) -> Dict[str, Any]:
640
+ """
641
+ 收集 FastAPI 请求的所有参数
642
+
643
+ Args:
644
+ request: FastAPI Request 对象
645
+
646
+ Returns:
647
+ 包含 headers, query_params, view_params, request_body, form_params 的字典
648
+ """
649
+ try:
650
+ params = {
651
+ "headers": dict(request.headers),
652
+ "query_params": dict(request.query_params),
653
+ "view_params": dict(request.path_params),
654
+ "request_body": None,
655
+ "form_params": None
656
+ }
657
+
658
+ # 获取请求体
659
+ content_type = request.headers.get("content-type", "")
660
+
661
+ if "application/json" in content_type:
662
+ try:
663
+ params["request_body"] = await request.json()
664
+ except Exception:
665
+ pass
666
+ elif "application/x-www-form-urlencoded" in content_type or "multipart/form-data" in content_type:
667
+ try:
668
+ form = await request.form()
669
+ params["form_params"] = {k: v for k, v in form.items()}
670
+ except Exception:
671
+ pass
672
+ else:
673
+ # 尝试读取原始body
674
+ try:
675
+ body = await request.body()
676
+ if body:
677
+ params["request_body"] = body.decode('utf-8')
678
+ except Exception:
679
+ pass
680
+
681
+ return params
682
+ except Exception as e:
683
+ logger.warning(f"收集FastAPI请求参数失败: {e}")
684
+ return {}
685
+
686
+ def _collect_stats(
687
+ self,
688
+ api_path: str,
689
+ api_method: str,
690
+ status_code: int,
691
+ response_time: float,
692
+ error_message: Optional[str] = None,
693
+ request_params: Optional[Dict[str, Any]] = None
694
+ ):
695
+ """收集接口统计"""
696
+ if not self.enable_stats or not self.stats_collector:
697
+ return
698
+
699
+ try:
700
+ self.stats_collector.collect(
701
+ api_path=api_path,
702
+ api_method=api_method,
703
+ status_code=status_code,
704
+ response_time=response_time,
705
+ error_message=error_message,
706
+ request_params=request_params
707
+ )
708
+ except Exception:
709
+ pass # 静默失败
566
710
 
567
711
  def _should_skip(self, path: str) -> bool:
568
712
  """检查是否应该跳过验证"""
@@ -593,6 +737,10 @@ class AuthMiddleware:
593
737
  from fastapi.responses import JSONResponse
594
738
 
595
739
  path = request.url.path
740
+ start_time = time.time()
741
+
742
+ # 收集请求参数
743
+ request_params = await self._collect_fastapi_request_params(request) if self.enable_stats else None
596
744
 
597
745
  # 检查是否跳过
598
746
  if self._should_skip(path):
@@ -604,6 +752,8 @@ class AuthMiddleware:
604
752
 
605
753
  if not token:
606
754
  logger.warning("AuthMiddleware未提供认证信息")
755
+ response_time = time.time() - start_time
756
+ self._collect_stats(path, request.method, 401, response_time, "未提供认证信息", request_params)
607
757
  return JSONResponse(
608
758
  status_code=401,
609
759
  content={"code": 401, "message": "未提供认证信息", "data": None}
@@ -612,8 +762,14 @@ class AuthMiddleware:
612
762
  # 验证 token
613
763
  try:
614
764
  user_info = self.client.get_user_info(token)
765
+ # 初始化统计收集器(第一次有token时)
766
+ if self.enable_stats and self.stats_collector is None:
767
+ self._init_stats_collector(token)
615
768
  # 将用户信息存储到 request.state
616
769
  request.state.user_info = user_info
770
+ # 设置上下文
771
+ set_current_user(dataclasses.asdict(user_info))
772
+
617
773
  # 处理代理头部,确保重定向(如果有)使用正确的协议
618
774
  forwarded_proto = request.headers.get("x-forwarded-proto")
619
775
  if forwarded_proto:
@@ -622,11 +778,25 @@ class AuthMiddleware:
622
778
  await user_info_callback(request, user_info)
623
779
  except AigcAuthError as e:
624
780
  logger.error(f"AuthMiddleware认证失败: {e.message}")
781
+ response_time = time.time() - start_time
782
+ self._collect_stats(path, request.method, 401, response_time, e.message, request_params)
625
783
  return JSONResponse(
626
784
  status_code=401,
627
785
  content={"code": e.code, "message": e.message, "data": None}
628
786
  )
629
- return await call_next(request)
787
+
788
+ # 处理请求
789
+ try:
790
+ response = await call_next(request)
791
+ response_time = time.time() - start_time
792
+ self._collect_stats(path, request.method, response.status_code, response_time, None, request_params)
793
+ return response
794
+ except Exception as e:
795
+ response_time = time.time() - start_time
796
+ self._collect_stats(path, request.method, 500, response_time, str(e), request_params)
797
+ raise
798
+ finally:
799
+ clear_current_user()
630
800
 
631
801
  def flask_before_request(self, user_info_callback: Callable = None):
632
802
  """
@@ -640,6 +810,11 @@ class AuthMiddleware:
640
810
  from flask import request, jsonify, g
641
811
 
642
812
  path = request.path
813
+ # 记录开始时间到 g 对象
814
+ g.start_time = time.time()
815
+
816
+ # 收集请求参数
817
+ g.request_params = self._collect_flask_request_params(request) if self.enable_stats else None
643
818
 
644
819
  # 检查是否跳过
645
820
  if self._should_skip(path):
@@ -651,6 +826,8 @@ class AuthMiddleware:
651
826
 
652
827
  if not token:
653
828
  logger.warning("AuthMiddleware未提供认证信息")
829
+ response_time = time.time() - g.start_time
830
+ self._collect_stats(path, request.method, 401, response_time, "未提供认证信息", g.request_params)
654
831
  return jsonify({
655
832
  "code": 401,
656
833
  "message": "未提供认证信息",
@@ -660,12 +837,19 @@ class AuthMiddleware:
660
837
  # 验证 token
661
838
  try:
662
839
  user_info = self.client.get_user_info(token)
840
+ # 初始化统计收集器(第一次有token时)
841
+ if self.enable_stats and self.stats_collector is None:
842
+ self._init_stats_collector(token)
663
843
  # 将用户信息存储到 flask.g
664
844
  g.user_info = user_info
845
+ # 设置上下文
846
+ set_current_user(dataclasses.asdict(user_info))
665
847
  if user_info_callback:
666
848
  user_info_callback(request, user_info)
667
849
  except AigcAuthError as e:
668
850
  logger.error(f"AuthMiddleware认证失败: {e.message}")
851
+ response_time = time.time() - g.start_time
852
+ self._collect_stats(path, request.method, 401, response_time, e.message, g.request_params)
669
853
  return jsonify({
670
854
  "code": e.code,
671
855
  "message": e.message,
@@ -673,6 +857,34 @@ class AuthMiddleware:
673
857
  }), 401
674
858
 
675
859
  return None
860
+
861
+ def flask_after_request(self, response):
862
+ """
863
+ Flask after_request 处理器(用于收集响应统计)
864
+
865
+ 使用方法:
866
+ @app.after_request
867
+ def after_request(response):
868
+ return auth_middleware.flask_after_request(response)
869
+ """
870
+ from flask import request, g
871
+
872
+ # 清除上下文
873
+ clear_current_user()
874
+
875
+ if hasattr(g, 'start_time'):
876
+ response_time = time.time() - g.start_time
877
+ request_params = getattr(g, 'request_params', None)
878
+ self._collect_stats(
879
+ request.path,
880
+ request.method,
881
+ response.status_code,
882
+ response_time,
883
+ None,
884
+ request_params
885
+ )
886
+
887
+ return response
676
888
 
677
889
  def get_current_user_fastapi(self, request) -> Optional[UserInfo]:
678
890
  """
@@ -0,0 +1,189 @@
1
+ """
2
+ 用户上下文管理模块
3
+ 使用 threading.local() 和 contextvars.ContextVar 统一管理用户信息
4
+ 兼容同步和异步场景
5
+ """
6
+ import threading
7
+ from contextvars import ContextVar
8
+ from typing import Optional, Dict, Any
9
+
10
+
11
+ # 使用 threading.local() 存储同步上下文的用户信息(兼容多线程)
12
+ _local = threading.local()
13
+
14
+ # 使用 contextvars.ContextVar 存储异步上下文的用户信息(兼容异步任务)
15
+ _async_user_info: ContextVar[Optional[Dict[str, Any]]] = ContextVar('user_info', default=None)
16
+
17
+
18
+ def set_current_user(user_info: Dict[str, Any]):
19
+ """
20
+ 设置当前上下文的用户信息(同时支持同步和异步)
21
+
22
+ Args:
23
+ user_info: 用户信息字典,包含以下字段:
24
+ - user_id: 用户ID
25
+ - username: 用户名
26
+ - app_id: 应用ID
27
+ - app_code: 应用代码
28
+ - token: 访问令牌(可选)
29
+ - roles: 角色列表(可选)
30
+ - permissions: 权限列表(可选)
31
+ - is_admin: 是否管理员(可选)
32
+ """
33
+ # 同时设置两个上下文,确保兼容性
34
+ _local.user_info = user_info
35
+ _async_user_info.set(user_info)
36
+
37
+
38
+ def get_current_user() -> Optional[Dict[str, Any]]:
39
+ """
40
+ 获取当前上下文的用户信息(同时支持同步和异步)
41
+
42
+ Returns:
43
+ 用户信息字典,如果未设置则返回 None
44
+ """
45
+ # 优先从 async context 获取(异步场景)
46
+ async_info = _async_user_info.get(None)
47
+ if async_info is not None:
48
+ return async_info
49
+
50
+ # 从 threading.local 获取(同步场景)
51
+ return getattr(_local, 'user_info', None)
52
+
53
+
54
+ def get_current_user_id() -> Optional[int]:
55
+ """获取当前用户ID"""
56
+ user_info = get_current_user()
57
+ return user_info.get('user_id') if user_info else None
58
+
59
+
60
+ def get_current_username() -> Optional[str]:
61
+ """获取当前用户名"""
62
+ user_info = get_current_user()
63
+ return user_info.get('username') if user_info else None
64
+
65
+
66
+ def get_current_app_id() -> Optional[int]:
67
+ """获取当前应用ID"""
68
+ user_info = get_current_user()
69
+ return user_info.get('app_id') if user_info else None
70
+
71
+
72
+ def get_current_app_code() -> Optional[str]:
73
+ """获取当前应用代码"""
74
+ user_info = get_current_user()
75
+ return user_info.get('app_code') if user_info else None
76
+
77
+
78
+ def is_current_user_admin() -> bool:
79
+ """判断当前用户是否为管理员"""
80
+ user_info = get_current_user()
81
+ return user_info.get('is_admin', False) if user_info else False
82
+
83
+
84
+ def clear_current_user():
85
+ """清理当前上下文的用户信息"""
86
+ if hasattr(_local, 'user_info'):
87
+ delattr(_local, 'user_info')
88
+ # ContextVar 不需要手动清理,会自动管理
89
+
90
+
91
+ # ============ 请求上下文管理(可选的额外信息) ============
92
+
93
+ _async_request_context: ContextVar[Optional[Dict[str, Any]]] = ContextVar('request_context', default=None)
94
+
95
+
96
+ def set_request_context(**kwargs):
97
+ """
98
+ 设置请求上下文信息
99
+
100
+ 可以存储以下信息:
101
+ - ip_address: 客户端IP
102
+ - user_agent: User Agent
103
+ - request_id: 请求ID
104
+ - trace_id: 追踪ID
105
+ """
106
+ # 同时设置两个上下文
107
+ _local.request_context = kwargs
108
+ _async_request_context.set(kwargs)
109
+
110
+
111
+ def get_request_context() -> Optional[Dict[str, Any]]:
112
+ """获取请求上下文信息"""
113
+ # 优先从 async context 获取
114
+ async_ctx = _async_request_context.get(None)
115
+ if async_ctx is not None:
116
+ return async_ctx
117
+
118
+ return getattr(_local, 'request_context', None)
119
+
120
+
121
+ def get_client_ip() -> Optional[str]:
122
+ """获取客户端IP"""
123
+ ctx = get_request_context()
124
+ return ctx.get('ip_address') if ctx else None
125
+
126
+
127
+ def clear_request_context():
128
+ """清理请求上下文"""
129
+ if hasattr(_local, 'request_context'):
130
+ delattr(_local, 'request_context')
131
+
132
+
133
+ # ============ 使用示例 ============
134
+ """
135
+ 使用示例:
136
+
137
+ 1. 在 FastAPI 中间件中设置用户信息:
138
+
139
+ from app.utils.user_context import set_current_user, clear_current_user
140
+
141
+ @app.middleware("http")
142
+ async def auth_middleware(request: Request, call_next):
143
+ # 验证 token 并获取用户信息
144
+ user_info = await verify_token_and_get_user(request)
145
+ if user_info:
146
+ set_current_user({
147
+ 'user_id': user_info.id,
148
+ 'username': user_info.username,
149
+ 'app_id': user_info.app_id,
150
+ 'app_code': user_info.app.code,
151
+ 'is_admin': user_info.is_admin
152
+ })
153
+
154
+ try:
155
+ response = await call_next(request)
156
+ return response
157
+ finally:
158
+ clear_current_user()
159
+
160
+
161
+ 2. 在业务代码中使用:
162
+
163
+ from app.utils.user_context import get_current_user_id, get_current_username
164
+
165
+ async def some_business_logic():
166
+ user_id = get_current_user_id()
167
+ username = get_current_username()
168
+
169
+ if user_id:
170
+ logger.info(f"用户 {username}({user_id}) 执行了操作")
171
+
172
+
173
+ 3. 在装饰器中使用:
174
+
175
+ from functools import wraps
176
+ from app.utils.user_context import is_current_user_admin
177
+
178
+ def require_admin(func):
179
+ @wraps(func)
180
+ async def wrapper(*args, **kwargs):
181
+ if not is_current_user_admin():
182
+ raise HTTPException(status_code=403, detail="需要管理员权限")
183
+ return await func(*args, **kwargs)
184
+ return wrapper
185
+
186
+ @require_admin
187
+ async def admin_only_endpoint():
188
+ pass
189
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: huace-aigc-auth-client
3
- Version: 1.1.18
3
+ Version: 1.1.20
4
4
  Summary: 华策AIGC Auth Client - 提供 Token 验证、用户信息获取、权限检查、旧系统接入等功能
5
5
  Author-email: Huace <support@huace.com>
6
6
  License: MIT
@@ -272,6 +272,31 @@ def admin_only():
272
272
  return jsonify({"message": "欢迎管理员"})
273
273
  ```
274
274
 
275
+ ## 上下文管理
276
+
277
+ SDK 提供了上下文管理功能,可以在任意位置(Service、Utility 等)获取当前请求的用户信息,无需层层传递参数。
278
+ 支持 Flask 和 FastAPI,兼容同步和异步环境。
279
+
280
+ 注意:使用此功能前必须先注册 `AuthMiddleware`。
281
+
282
+ ```python
283
+ from huace_aigc_auth_client import get_current_user
284
+
285
+ def do_something_logic():
286
+ # 获取当前用户信息(返回字典,非 UserInfo 对象)
287
+ user = get_current_user()
288
+
289
+ if user:
290
+ print(f"当前操作用户ID: {user['id']}")
291
+ print(f"当前操作用户名: {user['username']}")
292
+
293
+ # 也可以获取 app_id 等信息(如果未过滤)
294
+ if 'app_id' in user:
295
+ print(f"应用ID: {user['app_id']}")
296
+ else:
297
+ print("无用户上下文")
298
+ ```
299
+
275
300
  ## API 参考
276
301
 
277
302
  ### UserInfo 对象
@@ -4,8 +4,10 @@ QUICK_START.txt
4
4
  README.md
5
5
  pyproject.toml
6
6
  huace_aigc_auth_client/__init__.py
7
+ huace_aigc_auth_client/api_stats_collector.py
7
8
  huace_aigc_auth_client/legacy_adapter.py
8
9
  huace_aigc_auth_client/sdk.py
10
+ huace_aigc_auth_client/user_context.py
9
11
  huace_aigc_auth_client/webhook.py
10
12
  huace_aigc_auth_client/webhook_flask.py
11
13
  huace_aigc_auth_client.egg-info/PKG-INFO
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "huace-aigc-auth-client"
7
- version = "1.1.18"
7
+ version = "1.1.20"
8
8
  description = "华策AIGC Auth Client - 提供 Token 验证、用户信息获取、权限检查、旧系统接入等功能"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.7"