huace-aigc-auth-client 1.1.15__py3-none-any.whl → 1.1.21__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.
@@ -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.15"
175
+ __version__ = "1.1.21"
@@ -0,0 +1,307 @@
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_id: str,
20
+ app_secret: str,
21
+ token: str,
22
+ batch_size: int = 10,
23
+ flush_interval: float = 5.0,
24
+ enabled: bool = True
25
+ ):
26
+ """
27
+ 初始化统计收集器
28
+
29
+ Args:
30
+ api_url: 统计接口 URL(如:http://auth.example.com/api/sdk/stats/report/batch)
31
+ app_id: 应用 ID
32
+ app_secret: 应用密钥
33
+ token: 用户访问令牌
34
+ batch_size: 批量提交大小
35
+ flush_interval: 刷新间隔(秒)
36
+ enabled: 是否启用
37
+ """
38
+ self.api_url = api_url.rstrip('/')
39
+ self.app_id = app_id
40
+ self.app_secret = app_secret
41
+ self.token = token
42
+ self.batch_size = batch_size
43
+ self.flush_interval = flush_interval
44
+ self.enabled = enabled
45
+
46
+ self.queue = queue.Queue()
47
+ self.running = True
48
+ self.worker_thread = None
49
+
50
+ if self.enabled:
51
+ self.start()
52
+
53
+ def start(self):
54
+ """启动工作线程"""
55
+ if self.worker_thread is None or not self.worker_thread.is_alive():
56
+ self.running = True
57
+ self.worker_thread = threading.Thread(target=self._worker, daemon=True)
58
+ self.worker_thread.start()
59
+
60
+ def stop(self):
61
+ """停止工作线程"""
62
+ self.running = False
63
+ if self.worker_thread:
64
+ self.worker_thread.join(timeout=10)
65
+
66
+ def collect(
67
+ self,
68
+ api_path: str,
69
+ api_method: str,
70
+ status_code: int,
71
+ response_time: float,
72
+ error_message: Optional[str] = None,
73
+ request_params: Optional[Dict[str, Any]] = None
74
+ ):
75
+ """
76
+ 收集接口统计数据
77
+
78
+ Args:
79
+ api_path: 接口路径
80
+ api_method: 请求方法
81
+ status_code: 状态码
82
+ response_time: 响应时间(秒)
83
+ error_message: 错误信息
84
+ request_params: 请求参数(包含 headers, query_params, view_params, request_body, form_params)
85
+ """
86
+ if not self.enabled:
87
+ return
88
+
89
+ try:
90
+ stat_data = {
91
+ 'api_path': api_path,
92
+ 'api_method': api_method,
93
+ 'status_code': status_code,
94
+ 'response_time': response_time,
95
+ 'error_message': error_message,
96
+ 'request_params': request_params,
97
+ 'timestamp': datetime.utcnow().isoformat()
98
+ }
99
+ self.queue.put_nowait(stat_data)
100
+ except queue.Full:
101
+ pass # 队列满了,丢弃数据
102
+ except Exception:
103
+ pass # 静默失败,不影响主流程
104
+
105
+ def _worker(self):
106
+ """后台工作线程:批量提交统计数据"""
107
+ buffer = []
108
+ last_flush_time = time.time()
109
+
110
+ while self.running:
111
+ try:
112
+ # 尝试从队列获取数据
113
+ try:
114
+ stat_data = self.queue.get(timeout=1.0)
115
+ buffer.append(stat_data)
116
+ except queue.Empty:
117
+ pass
118
+
119
+ current_time = time.time()
120
+ should_flush = (
121
+ len(buffer) >= self.batch_size or
122
+ (buffer and (current_time - last_flush_time) >= self.flush_interval)
123
+ )
124
+
125
+ if should_flush:
126
+ self._flush_buffer(buffer)
127
+ buffer = []
128
+ last_flush_time = current_time
129
+
130
+ except Exception:
131
+ pass # 静默失败
132
+
133
+ # 停止前刷新剩余数据
134
+ if buffer:
135
+ self._flush_buffer(buffer)
136
+
137
+ def _flush_buffer(self, buffer: List[Dict[str, Any]]):
138
+ """刷新缓冲区:批量提交统计数据"""
139
+ if not buffer:
140
+ return
141
+
142
+ try:
143
+ headers = {
144
+ 'X-App-Id': self.app_id,
145
+ 'X-App-Secret': self.app_secret,
146
+ 'Authorization': f'Bearer {self.token}',
147
+ 'Content-Type': 'application/json'
148
+ }
149
+
150
+ payload = {'stats': buffer}
151
+
152
+ response = requests.post(
153
+ f'{self.api_url}/stats/report/batch',
154
+ json=payload,
155
+ headers=headers,
156
+ timeout=5
157
+ )
158
+
159
+ # 静默失败,不抛出异常
160
+ if response.status_code != 200:
161
+ pass
162
+
163
+ except Exception:
164
+ pass # 静默失败,不影响主流程
165
+
166
+
167
+ # ============ 全局实例 ============
168
+
169
+ _global_collector: Optional[ApiStatsCollector] = None
170
+
171
+
172
+ def init_api_stats_collector(
173
+ api_url: str,
174
+ app_id: str,
175
+ app_secret: str,
176
+ token: str,
177
+ batch_size: int = 10,
178
+ flush_interval: float = 5.0,
179
+ enabled: bool = True
180
+ ) -> ApiStatsCollector:
181
+ """
182
+ 初始化全局统计收集器
183
+
184
+ Args:
185
+ api_url: 统计接口 URL
186
+ app_id: 应用 ID
187
+ app_secret: 应用密钥
188
+ token: 用户访问令牌
189
+ batch_size: 批量提交大小
190
+ flush_interval: 刷新间隔(秒)
191
+ enabled: 是否启用
192
+
193
+ Returns:
194
+ 统计收集器实例
195
+ """
196
+ global _global_collector
197
+ _global_collector = ApiStatsCollector(
198
+ api_url=api_url,
199
+ app_id=app_id,
200
+ app_secret=app_secret,
201
+ token=token,
202
+ batch_size=batch_size,
203
+ flush_interval=flush_interval,
204
+ enabled=enabled
205
+ )
206
+ return _global_collector
207
+
208
+
209
+ def get_api_stats_collector() -> Optional[ApiStatsCollector]:
210
+ """获取全局统计收集器实例"""
211
+ return _global_collector
212
+
213
+
214
+ def stop_api_stats_collector():
215
+ """停止全局统计收集器"""
216
+ global _global_collector
217
+ if _global_collector:
218
+ _global_collector.stop()
219
+ _global_collector = None
220
+
221
+
222
+ def collect_api_stat(
223
+ api_path: str,
224
+ api_method: str,
225
+ status_code: int,
226
+ response_time: float,
227
+ error_message: Optional[str] = None,
228
+ request_params: Optional[Dict[str, Any]] = None
229
+ ):
230
+ """
231
+ 快捷方法:收集接口统计数据
232
+
233
+ 使用全局收集器实例
234
+ """
235
+ collector = get_api_stats_collector()
236
+ if collector:
237
+ collector.collect(
238
+ api_path=api_path,
239
+ api_method=api_method,
240
+ status_code=status_code,
241
+ response_time=response_time,
242
+ error_message=error_message,
243
+ request_params=request_params
244
+ )
245
+
246
+
247
+ # ============ 使用示例 ============
248
+ """
249
+ 使用示例:
250
+
251
+ 1. 应用启动时初始化:
252
+
253
+ from huace_aigc_auth_client.api_stats_collector import init_api_stats_collector
254
+
255
+ # 在应用启动时初始化
256
+ init_api_stats_collector(
257
+ api_url='http://auth.example.com/api/sdk',
258
+ app_secret='your-app-secret',
259
+ token='user-access-token',
260
+ batch_size=10,
261
+ flush_interval=5.0,
262
+ enabled=True
263
+ )
264
+
265
+
266
+ 2. 在拦截器中使用:
267
+
268
+ from huace_aigc_auth_client.api_stats_collector import collect_api_stat
269
+ import time
270
+
271
+ @app.middleware("http")
272
+ async def monitor_middleware(request: Request, call_next):
273
+ start_time = time.time()
274
+
275
+ try:
276
+ response = await call_next(request)
277
+ response_time = time.time() - start_time
278
+
279
+ # 收集统计
280
+ collect_api_stat(
281
+ api_path=request.url.path,
282
+ api_method=request.method,
283
+ status_code=response.status_code,
284
+ response_time=response_time
285
+ )
286
+
287
+ return response
288
+ except Exception as e:
289
+ response_time = time.time() - start_time
290
+ collect_api_stat(
291
+ api_path=request.url.path,
292
+ api_method=request.method,
293
+ status_code=500,
294
+ response_time=response_time,
295
+ error_message=str(e)
296
+ )
297
+ raise
298
+
299
+
300
+ 3. 应用关闭时停止:
301
+
302
+ from huace_aigc_auth_client.api_stats_collector import stop_api_stats_collector
303
+
304
+ @app.on_event("shutdown")
305
+ async def shutdown_event():
306
+ stop_api_stats_collector()
307
+ """
@@ -246,7 +246,7 @@ class LegacySystemAdapter(ABC):
246
246
  else:
247
247
  auth_data["password"] = password
248
248
 
249
- result = await self.auth_client.sync_user_to_auth(auth_data)
249
+ result = self.auth_client.sync_user_to_auth(auth_data)
250
250
  logger.info(f"Sync result for user {legacy_user.get('username')}: {result}")
251
251
 
252
252
  return result
@@ -297,7 +297,8 @@ class LegacySystemAdapter(ABC):
297
297
  "error": result.get("message")
298
298
  })
299
299
  except Exception as e:
300
- logger.error(f"Error syncing user {user.get('username')}: {e}")
300
+ import traceback
301
+ logger.error(f"Error syncing user {user.get('username')}: {e}\n{traceback.format_exc()}")
301
302
  results["failed"] += 1
302
303
  results["errors"].append({
303
304
  "user": user.get("username"),
@@ -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):
@@ -95,7 +97,7 @@ class AigcAuthClient:
95
97
  client = AigcAuthClient(
96
98
  app_id="your_app_id",
97
99
  app_secret="your_app_secret",
98
- base_url="https://aigc-auth.huacemedia.com/aigc-auth/api/v1"
100
+ base_url="后端 API 鉴权地址"
99
101
  )
100
102
 
101
103
  # 验证 token
@@ -130,8 +132,7 @@ class AigcAuthClient:
130
132
  self.app_secret = app_secret or os.getenv("AIGC_AUTH_APP_SECRET")
131
133
  self.base_url = (
132
134
  base_url or
133
- os.getenv("AIGC_AUTH_BASE_URL") or
134
- "https://aigc-auth.huacemedia.com/aigc-auth/api/v1"
135
+ os.getenv("AIGC_AUTH_BASE_URL")
135
136
  )
136
137
  self.timeout = timeout
137
138
  self.cache_ttl = cache_ttl
@@ -551,7 +552,9 @@ class AuthMiddleware:
551
552
  self,
552
553
  client: AigcAuthClient,
553
554
  exclude_paths: List[str] = None,
554
- exclude_prefixes: List[str] = None
555
+ exclude_prefixes: List[str] = None,
556
+ enable_stats: bool = True,
557
+ stats_api_url: Optional[str] = None
555
558
  ):
556
559
  """
557
560
  初始化中间件
@@ -560,10 +563,151 @@ class AuthMiddleware:
560
563
  client: AigcAuthClient 实例
561
564
  exclude_paths: 排除的路径列表(精确匹配)
562
565
  exclude_prefixes: 排除的路径前缀列表
566
+ enable_stats: 是否启用接口统计(默认启用)
567
+ stats_api_url: 统计接口 URL(可选,默认使用 client.base_url/sdk)
563
568
  """
564
569
  self.client = client
565
570
  self.exclude_paths = exclude_paths or []
566
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_id=self.client.app_id,
589
+ app_secret=self.client.app_secret,
590
+ token=token,
591
+ batch_size=10,
592
+ flush_interval=5.0,
593
+ enabled=True
594
+ )
595
+ except Exception as e:
596
+ logger.warning(f"初始化统计收集器失败: {e}")
597
+
598
+ @staticmethod
599
+ def _collect_flask_request_params(request) -> Dict[str, Any]:
600
+ """
601
+ 收集 Flask 请求的所有参数
602
+
603
+ Args:
604
+ request: Flask request 对象
605
+
606
+ Returns:
607
+ 包含 headers, query_params, view_params, request_body, form_params 的字典
608
+ """
609
+ try:
610
+ params = {
611
+ "headers": dict(request.headers),
612
+ "query_params": request.args.to_dict(flat=False),
613
+ "view_params": request.view_args or {},
614
+ "request_body": None,
615
+ "form_params": None
616
+ }
617
+
618
+ # 获取请求体(JSON 或文本)
619
+ if request.is_json:
620
+ try:
621
+ params["request_body"] = request.get_json(silent=True)
622
+ except Exception:
623
+ pass
624
+ elif request.data:
625
+ try:
626
+ params["request_body"] = request.data.decode('utf-8')
627
+ except Exception:
628
+ params["request_body"] = str(request.data)
629
+
630
+ # 获取表单数据
631
+ if request.form:
632
+ params["form_params"] = request.form.to_dict(flat=False)
633
+
634
+ return params
635
+ except Exception as e:
636
+ logger.warning(f"收集Flask请求参数失败: {e}")
637
+ return {}
638
+
639
+ @staticmethod
640
+ async def _collect_fastapi_request_params(request) -> Dict[str, Any]:
641
+ """
642
+ 收集 FastAPI 请求的所有参数
643
+
644
+ Args:
645
+ request: FastAPI Request 对象
646
+
647
+ Returns:
648
+ 包含 headers, query_params, view_params, request_body, form_params 的字典
649
+ """
650
+ try:
651
+ params = {
652
+ "headers": dict(request.headers),
653
+ "query_params": dict(request.query_params),
654
+ "view_params": dict(request.path_params),
655
+ "request_body": None,
656
+ "form_params": None
657
+ }
658
+
659
+ # 获取请求体
660
+ content_type = request.headers.get("content-type", "")
661
+
662
+ if "application/json" in content_type:
663
+ try:
664
+ params["request_body"] = await request.json()
665
+ except Exception:
666
+ pass
667
+ elif "application/x-www-form-urlencoded" in content_type or "multipart/form-data" in content_type:
668
+ try:
669
+ form = await request.form()
670
+ params["form_params"] = {k: v for k, v in form.items()}
671
+ except Exception:
672
+ pass
673
+ else:
674
+ # 尝试读取原始body
675
+ try:
676
+ body = await request.body()
677
+ if body:
678
+ params["request_body"] = body.decode('utf-8')
679
+ except Exception:
680
+ pass
681
+
682
+ return params
683
+ except Exception as e:
684
+ logger.warning(f"收集FastAPI请求参数失败: {e}")
685
+ return {}
686
+
687
+ def _collect_stats(
688
+ self,
689
+ api_path: str,
690
+ api_method: str,
691
+ status_code: int,
692
+ response_time: float,
693
+ error_message: Optional[str] = None,
694
+ request_params: Optional[Dict[str, Any]] = None
695
+ ):
696
+ """收集接口统计"""
697
+ if not self.enable_stats or not self.stats_collector:
698
+ return
699
+
700
+ try:
701
+ self.stats_collector.collect(
702
+ api_path=api_path,
703
+ api_method=api_method,
704
+ status_code=status_code,
705
+ response_time=response_time,
706
+ error_message=error_message,
707
+ request_params=request_params
708
+ )
709
+ except Exception:
710
+ pass # 静默失败
567
711
 
568
712
  def _should_skip(self, path: str) -> bool:
569
713
  """检查是否应该跳过验证"""
@@ -594,6 +738,10 @@ class AuthMiddleware:
594
738
  from fastapi.responses import JSONResponse
595
739
 
596
740
  path = request.url.path
741
+ start_time = time.time()
742
+
743
+ # 收集请求参数
744
+ request_params = await self._collect_fastapi_request_params(request) if self.enable_stats else None
597
745
 
598
746
  # 检查是否跳过
599
747
  if self._should_skip(path):
@@ -605,6 +753,8 @@ class AuthMiddleware:
605
753
 
606
754
  if not token:
607
755
  logger.warning("AuthMiddleware未提供认证信息")
756
+ response_time = time.time() - start_time
757
+ self._collect_stats(path, request.method, 401, response_time, "未提供认证信息", request_params)
608
758
  return JSONResponse(
609
759
  status_code=401,
610
760
  content={"code": 401, "message": "未提供认证信息", "data": None}
@@ -613,8 +763,14 @@ class AuthMiddleware:
613
763
  # 验证 token
614
764
  try:
615
765
  user_info = self.client.get_user_info(token)
766
+ # 初始化统计收集器(第一次有token时)
767
+ if self.enable_stats and self.stats_collector is None:
768
+ self._init_stats_collector(token)
616
769
  # 将用户信息存储到 request.state
617
770
  request.state.user_info = user_info
771
+ # 设置上下文
772
+ set_current_user(dataclasses.asdict(user_info))
773
+
618
774
  # 处理代理头部,确保重定向(如果有)使用正确的协议
619
775
  forwarded_proto = request.headers.get("x-forwarded-proto")
620
776
  if forwarded_proto:
@@ -623,11 +779,25 @@ class AuthMiddleware:
623
779
  await user_info_callback(request, user_info)
624
780
  except AigcAuthError as e:
625
781
  logger.error(f"AuthMiddleware认证失败: {e.message}")
782
+ response_time = time.time() - start_time
783
+ self._collect_stats(path, request.method, 401, response_time, e.message, request_params)
626
784
  return JSONResponse(
627
785
  status_code=401,
628
786
  content={"code": e.code, "message": e.message, "data": None}
629
787
  )
630
- return await call_next(request)
788
+
789
+ # 处理请求
790
+ try:
791
+ response = await call_next(request)
792
+ response_time = time.time() - start_time
793
+ self._collect_stats(path, request.method, response.status_code, response_time, None, request_params)
794
+ return response
795
+ except Exception as e:
796
+ response_time = time.time() - start_time
797
+ self._collect_stats(path, request.method, 500, response_time, str(e), request_params)
798
+ raise
799
+ finally:
800
+ clear_current_user()
631
801
 
632
802
  def flask_before_request(self, user_info_callback: Callable = None):
633
803
  """
@@ -641,6 +811,11 @@ class AuthMiddleware:
641
811
  from flask import request, jsonify, g
642
812
 
643
813
  path = request.path
814
+ # 记录开始时间到 g 对象
815
+ g.start_time = time.time()
816
+
817
+ # 收集请求参数
818
+ g.request_params = self._collect_flask_request_params(request) if self.enable_stats else None
644
819
 
645
820
  # 检查是否跳过
646
821
  if self._should_skip(path):
@@ -652,6 +827,8 @@ class AuthMiddleware:
652
827
 
653
828
  if not token:
654
829
  logger.warning("AuthMiddleware未提供认证信息")
830
+ response_time = time.time() - g.start_time
831
+ self._collect_stats(path, request.method, 401, response_time, "未提供认证信息", g.request_params)
655
832
  return jsonify({
656
833
  "code": 401,
657
834
  "message": "未提供认证信息",
@@ -661,12 +838,19 @@ class AuthMiddleware:
661
838
  # 验证 token
662
839
  try:
663
840
  user_info = self.client.get_user_info(token)
841
+ # 初始化统计收集器(第一次有token时)
842
+ if self.enable_stats and self.stats_collector is None:
843
+ self._init_stats_collector(token)
664
844
  # 将用户信息存储到 flask.g
665
845
  g.user_info = user_info
846
+ # 设置上下文
847
+ set_current_user(dataclasses.asdict(user_info))
666
848
  if user_info_callback:
667
849
  user_info_callback(request, user_info)
668
850
  except AigcAuthError as e:
669
851
  logger.error(f"AuthMiddleware认证失败: {e.message}")
852
+ response_time = time.time() - g.start_time
853
+ self._collect_stats(path, request.method, 401, response_time, e.message, g.request_params)
670
854
  return jsonify({
671
855
  "code": e.code,
672
856
  "message": e.message,
@@ -674,6 +858,34 @@ class AuthMiddleware:
674
858
  }), 401
675
859
 
676
860
  return None
861
+
862
+ def flask_after_request(self, response):
863
+ """
864
+ Flask after_request 处理器(用于收集响应统计)
865
+
866
+ 使用方法:
867
+ @app.after_request
868
+ def after_request(response):
869
+ return auth_middleware.flask_after_request(response)
870
+ """
871
+ from flask import request, g
872
+
873
+ # 清除上下文
874
+ clear_current_user()
875
+
876
+ if hasattr(g, 'start_time'):
877
+ response_time = time.time() - g.start_time
878
+ request_params = getattr(g, 'request_params', None)
879
+ self._collect_stats(
880
+ request.path,
881
+ request.method,
882
+ response.status_code,
883
+ response_time,
884
+ None,
885
+ request_params
886
+ )
887
+
888
+ return response
677
889
 
678
890
  def get_current_user_fastapi(self, request) -> Optional[UserInfo]:
679
891
  """
@@ -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.15
3
+ Version: 1.1.21
4
4
  Summary: 华策AIGC Auth Client - 提供 Token 验证、用户信息获取、权限检查、旧系统接入等功能
5
5
  Author-email: Huace <support@huace.com>
6
6
  License: MIT
@@ -44,8 +44,8 @@ pip install huace-aigc-auth-client
44
44
  # 必填:应用 ID 和密钥(在鉴权中心创建应用后获取)
45
45
  AIGC_AUTH_APP_ID=your_app_id
46
46
  AIGC_AUTH_APP_SECRET=your_app_secret
47
- # 鉴权服务地址(默认为生产环境)- 测试环境鉴权地址:http://auth.aigc-test.huacemedia.com/aigc-auth/api/v1
48
- AIGC_AUTH_BASE_URL=https://aigc-auth.huacemedia.com/aigc-auth/api/v1
47
+ # 鉴权服务地址(默认为生产环境)
48
+ AIGC_AUTH_BASE_URL=your-auth-api-url-prefix
49
49
  ```
50
50
 
51
51
  如需通过 Nginx 代理:
@@ -74,7 +74,7 @@ client = AigcAuthClient()
74
74
  client = AigcAuthClient(
75
75
  app_id="your_app_id",
76
76
  app_secret="your_app_secret",
77
- base_url="https://aigc-auth.huacemedia.com/aigc-auth/api/v1"
77
+ base_url="your-auth-api-url-prefix"
78
78
  )
79
79
  ```
80
80
 
@@ -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 对象
@@ -352,7 +377,7 @@ except AigcAuthError as e:
352
377
  # .env 文件
353
378
  AIGC_AUTH_APP_ID=your_app_id
354
379
  AIGC_AUTH_APP_SECRET=your_app_secret
355
- AIGC_AUTH_BASE_URL=https://aigc-auth.huacemedia.com/aigc-auth/api/v1
380
+ AIGC_AUTH_BASE_URL=your-auth-api-url-prefix
356
381
 
357
382
  # 同步配置
358
383
  AIGC_AUTH_SYNC_ENABLED=true
@@ -0,0 +1,12 @@
1
+ huace_aigc_auth_client/__init__.py,sha256=E7sfAphd04F3WFwxnwnPvIUpIW7XQ36Anc4qvsuEo74,4630
2
+ huace_aigc_auth_client/api_stats_collector.py,sha256=5qsxHjsx7rELsO4PSmrDYxXAWuJBfB0HrOI-PqTV52I,8698
3
+ huace_aigc_auth_client/legacy_adapter.py,sha256=TVCBAKejE2z2HQFsEwDW8LMiaIkXNfz3Mxv6_E-UJFY,24102
4
+ huace_aigc_auth_client/sdk.py,sha256=03OzVIhawoBXWNfq8AUfdUEPLlQMrBhfsNSYANOrnwg,31346
5
+ huace_aigc_auth_client/user_context.py,sha256=KzevYLsLv1hv8rlvRw83FT-HugeoBJSJ1Pi56iLWyTE,5592
6
+ huace_aigc_auth_client/webhook.py,sha256=XQZYEbMoqIdqZWCGSTcedeDKJpDbUVSq5g08g-6Qucg,4124
7
+ huace_aigc_auth_client/webhook_flask.py,sha256=Iosu4dBtRhQZM_ytn-bn82MpVsyOiV28FBnt7Tfh31U,7225
8
+ huace_aigc_auth_client-1.1.21.dist-info/licenses/LICENSE,sha256=z7dgC7KljhBLNvKjN15391nMj3aLt0gbud8-Yf1F8EQ,1063
9
+ huace_aigc_auth_client-1.1.21.dist-info/METADATA,sha256=NNnIyfDXLdjYBv4Xr63AioZ8FSCv26RevMBA8VEUijg,23629
10
+ huace_aigc_auth_client-1.1.21.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ huace_aigc_auth_client-1.1.21.dist-info/top_level.txt,sha256=kbv0nQ6PQ0JVneWPH7O2AbtlJnP7AjvFJ6JjM6ZEBxo,23
12
+ huace_aigc_auth_client-1.1.21.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- huace_aigc_auth_client/__init__.py,sha256=KYoil3pbho69Cs0nEPJz2Bli1L8tJ9NjDDBCOU9vWT0,4540
2
- huace_aigc_auth_client/legacy_adapter.py,sha256=f1_xH6huiPdCnJx1ko-2_kWTHjWKLBK2zyPXqnPiEaY,24049
3
- huace_aigc_auth_client/sdk.py,sha256=ypClZfQm4Ux4db8XDP51I5Cuk1Uc9F2VPgECpXXphkQ,23272
4
- huace_aigc_auth_client/webhook.py,sha256=XQZYEbMoqIdqZWCGSTcedeDKJpDbUVSq5g08g-6Qucg,4124
5
- huace_aigc_auth_client/webhook_flask.py,sha256=Iosu4dBtRhQZM_ytn-bn82MpVsyOiV28FBnt7Tfh31U,7225
6
- huace_aigc_auth_client-1.1.15.dist-info/licenses/LICENSE,sha256=z7dgC7KljhBLNvKjN15391nMj3aLt0gbud8-Yf1F8EQ,1063
7
- huace_aigc_auth_client-1.1.15.dist-info/METADATA,sha256=lUBiUG93imjVSuLQBO4GVUZY-DW5xE8xvWwObY5KrGM,22971
8
- huace_aigc_auth_client-1.1.15.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
9
- huace_aigc_auth_client-1.1.15.dist-info/top_level.txt,sha256=kbv0nQ6PQ0JVneWPH7O2AbtlJnP7AjvFJ6JjM6ZEBxo,23
10
- huace_aigc_auth_client-1.1.15.dist-info/RECORD,,