huace-aigc-auth-client 1.1.22__py3-none-any.whl → 1.1.23__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.
@@ -172,4 +172,4 @@ __all__ = [
172
172
  # 用户上下文
173
173
  "get_current_user",
174
174
  ]
175
- __version__ = "1.1.22"
175
+ __version__ = "1.1.23"
@@ -18,8 +18,7 @@ class ApiStatsCollector:
18
18
  api_url: str,
19
19
  app_id: str,
20
20
  app_secret: str,
21
- token: str,
22
- batch_size: int = 10,
21
+ batch_size: int = 50,
23
22
  flush_interval: float = 5.0,
24
23
  enabled: bool = True
25
24
  ):
@@ -30,7 +29,6 @@ class ApiStatsCollector:
30
29
  api_url: 统计接口 URL(如:http://auth.example.com/api/sdk/stats/report/batch)
31
30
  app_id: 应用 ID
32
31
  app_secret: 应用密钥
33
- token: 用户访问令牌
34
32
  batch_size: 批量提交大小
35
33
  flush_interval: 刷新间隔(秒)
36
34
  enabled: 是否启用
@@ -38,7 +36,6 @@ class ApiStatsCollector:
38
36
  self.api_url = api_url.rstrip('/')
39
37
  self.app_id = app_id
40
38
  self.app_secret = app_secret
41
- self.token = token
42
39
  self.batch_size = batch_size
43
40
  self.flush_interval = flush_interval
44
41
  self.enabled = enabled
@@ -69,6 +66,7 @@ class ApiStatsCollector:
69
66
  api_method: str,
70
67
  status_code: int,
71
68
  response_time: float,
69
+ token: str,
72
70
  error_message: Optional[str] = None,
73
71
  request_params: Optional[Dict[str, Any]] = None
74
72
  ):
@@ -80,6 +78,7 @@ class ApiStatsCollector:
80
78
  api_method: 请求方法
81
79
  status_code: 状态码
82
80
  response_time: 响应时间(秒)
81
+ token: 用户访问令牌
83
82
  error_message: 错误信息
84
83
  request_params: 请求参数(包含 headers, query_params, view_params, request_body, form_params)
85
84
  """
@@ -96,6 +95,7 @@ class ApiStatsCollector:
96
95
  'api_method': api_method,
97
96
  'status_code': status_code,
98
97
  'response_time': response_time,
98
+ 'token': token,
99
99
  'error_message': error_message,
100
100
  'request_params': request_params
101
101
  }
@@ -138,19 +138,50 @@ class ApiStatsCollector:
138
138
  self._flush_buffer(buffer)
139
139
 
140
140
  def _flush_buffer(self, buffer: List[Dict[str, Any]]):
141
- """刷新缓冲区:批量提交统计数据"""
141
+ """刷新缓冲区:按token分组批量提交统计数据"""
142
142
  if not buffer:
143
143
  return
144
144
 
145
+ try:
146
+ # 按token分组
147
+ token_groups: Dict[str, List[Dict[str, Any]]] = {}
148
+ for stat in buffer:
149
+ token = stat.get('token')
150
+ if not token:
151
+ continue
152
+
153
+ if token not in token_groups:
154
+ token_groups[token] = []
155
+
156
+ # 移除token字段后添加到分组
157
+ stat_copy = stat.copy()
158
+ stat_copy.pop('token', None)
159
+ token_groups[token].append(stat_copy)
160
+
161
+ # 对每个token分组分别提交
162
+ for token, stats in token_groups.items():
163
+ self._submit_stats(token, stats)
164
+
165
+ except Exception:
166
+ pass # 静默失败,不影响主流程
167
+
168
+ def _submit_stats(self, token: str, stats: List[Dict[str, Any]]):
169
+ """
170
+ 提交统计数据到服务端
171
+
172
+ Args:
173
+ token: 用户访问令牌
174
+ stats: 统计数据列表(已移除token字段)
175
+ """
145
176
  try:
146
177
  headers = {
147
178
  'X-App-Id': self.app_id,
148
179
  'X-App-Secret': self.app_secret,
149
- 'Authorization': f'Bearer {self.token}',
180
+ 'Authorization': f'Bearer {token}',
150
181
  'Content-Type': 'application/json'
151
182
  }
152
183
 
153
- payload = {'stats': buffer}
184
+ payload = {'stats': stats}
154
185
 
155
186
  response = requests.post(
156
187
  f'{self.api_url}/stats/report/batch',
@@ -176,7 +207,6 @@ def init_api_stats_collector(
176
207
  api_url: str,
177
208
  app_id: str,
178
209
  app_secret: str,
179
- token: str,
180
210
  batch_size: int = 10,
181
211
  flush_interval: float = 5.0,
182
212
  enabled: bool = True
@@ -188,7 +218,6 @@ def init_api_stats_collector(
188
218
  api_url: 统计接口 URL
189
219
  app_id: 应用 ID
190
220
  app_secret: 应用密钥
191
- token: 用户访问令牌
192
221
  batch_size: 批量提交大小
193
222
  flush_interval: 刷新间隔(秒)
194
223
  enabled: 是否启用
@@ -201,7 +230,6 @@ def init_api_stats_collector(
201
230
  api_url=api_url,
202
231
  app_id=app_id,
203
232
  app_secret=app_secret,
204
- token=token,
205
233
  batch_size=batch_size,
206
234
  flush_interval=flush_interval,
207
235
  enabled=enabled
@@ -227,6 +255,7 @@ def collect_api_stat(
227
255
  api_method: str,
228
256
  status_code: int,
229
257
  response_time: float,
258
+ token: str,
230
259
  error_message: Optional[str] = None,
231
260
  request_params: Optional[Dict[str, Any]] = None
232
261
  ):
@@ -235,6 +264,15 @@ def collect_api_stat(
235
264
 
236
265
  使用全局收集器实例
237
266
  注意:会自动过滤 3xx 重定向状态码的请求
267
+
268
+ Args:
269
+ api_path: 接口路径
270
+ api_method: 请求方法
271
+ status_code: 状态码
272
+ response_time: 响应时间(秒)
273
+ token: 用户访问令牌
274
+ error_message: 错误信息
275
+ request_params: 请求参数
238
276
  """
239
277
  # 过滤重定向请求(3xx 状态码),这些通常是框架自动处理的,不应统计
240
278
  if 300 <= status_code < 400:
@@ -247,6 +285,7 @@ def collect_api_stat(
247
285
  api_method=api_method,
248
286
  status_code=status_code,
249
287
  response_time=response_time,
288
+ token=token,
250
289
  error_message=error_message,
251
290
  request_params=request_params
252
291
  )
@@ -570,24 +570,28 @@ class AuthMiddleware:
570
570
  self.exclude_paths = exclude_paths or []
571
571
  self.exclude_prefixes = exclude_prefixes or []
572
572
  self.enable_stats = enable_stats
573
- self.stats_collector = None
574
573
 
575
- # 如果启用统计,设置统计接口 URL(默认使用 client 的 base_url)
574
+ # 如果启用统计,初始化全局统计收集器
576
575
  if self.enable_stats:
577
576
  self.stats_api_url = stats_api_url or f"{self.client.base_url}/sdk"
577
+ self._init_global_stats_collector()
578
578
 
579
- def _init_stats_collector(self, token: str):
580
- """初始化统计收集器(延迟初始化)"""
581
- if not self.enable_stats or self.stats_collector is not None:
579
+ def _init_global_stats_collector(self):
580
+ """初始化全局统计收集器(只初始化一次)"""
581
+ if not self.enable_stats:
582
582
  return
583
583
 
584
584
  try:
585
- from .api_stats_collector import init_api_stats_collector
586
- self.stats_collector = init_api_stats_collector(
585
+ from .api_stats_collector import init_api_stats_collector, get_api_stats_collector
586
+
587
+ # 检查是否已经初始化
588
+ if get_api_stats_collector() is not None:
589
+ return
590
+
591
+ init_api_stats_collector(
587
592
  api_url=self.stats_api_url,
588
593
  app_id=self.client.app_id,
589
594
  app_secret=self.client.app_secret,
590
- token=token,
591
595
  batch_size=10,
592
596
  flush_interval=5.0,
593
597
  enabled=True
@@ -615,21 +619,31 @@ class AuthMiddleware:
615
619
  "form_params": None
616
620
  }
617
621
 
618
- # 获取请求体(JSON 或文本)
622
+ # 获取 content-type
623
+ content_type = request.headers.get('Content-Type', '').lower()
624
+
625
+ # 获取请求体(使用白名单方式,只处理已知安全的内容类型)
619
626
  if request.is_json:
620
627
  try:
621
628
  params["request_body"] = request.get_json(silent=True)
622
629
  except Exception:
623
630
  pass
624
- elif request.data:
631
+ elif 'application/x-www-form-urlencoded' in content_type:
632
+ # 表单数据
633
+ if request.form:
634
+ params["form_params"] = request.form.to_dict(flat=False)
635
+ elif 'multipart/form-data' in content_type:
636
+ # 文件上传请求,不读取 request.data,避免损坏文件流
637
+ # 只收集表单字段(非文件字段)
638
+ if request.form:
639
+ params["form_params"] = request.form.to_dict(flat=False)
640
+ elif content_type.startswith('text/') and request.data and len(request.data) < 10240:
641
+ # 只读取文本类型的请求体,且限制大小避免内存问题
625
642
  try:
626
643
  params["request_body"] = request.data.decode('utf-8')
627
644
  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)
645
+ pass
646
+ # 其他类型(如 application/octet-stream 等二进制流)直接跳过,不读取
633
647
 
634
648
  return params
635
649
  except Exception as e:
@@ -656,28 +670,38 @@ class AuthMiddleware:
656
670
  "form_params": None
657
671
  }
658
672
 
659
- # 获取请求体
660
- content_type = request.headers.get("content-type", "")
673
+ # 获取请求体(使用白名单方式,只处理已知安全的内容类型)
674
+ content_type = request.headers.get("content-type", "").lower()
661
675
 
662
676
  if "application/json" in content_type:
663
677
  try:
664
678
  params["request_body"] = await request.json()
665
679
  except Exception:
666
680
  pass
667
- elif "application/x-www-form-urlencoded" in content_type or "multipart/form-data" in content_type:
681
+ elif "application/x-www-form-urlencoded" in content_type:
668
682
  try:
669
683
  form = await request.form()
670
684
  params["form_params"] = {k: v for k, v in form.items()}
671
685
  except Exception:
672
686
  pass
673
- else:
674
- # 尝试读取原始body
687
+ elif "multipart/form-data" in content_type:
688
+ # 文件上传请求,不读取 body,避免损坏文件流
689
+ # 只收集表单字段(非文件字段)
690
+ try:
691
+ form = await request.form()
692
+ # 只收集非文件字段(通过检查是否有filename属性判断是否为文件)
693
+ params["form_params"] = {k: v for k, v in form.items() if not hasattr(v, 'filename')}
694
+ except Exception:
695
+ pass
696
+ elif content_type.startswith('text/'):
697
+ # 只读取文本类型的请求体,且限制大小避免内存问题
675
698
  try:
676
699
  body = await request.body()
677
- if body:
700
+ if body and len(body) < 10240: # 限制10KB避免内存问题
678
701
  params["request_body"] = body.decode('utf-8')
679
702
  except Exception:
680
703
  pass
704
+ # 其他类型(如 application/octet-stream 等二进制流)直接跳过,不读取
681
705
 
682
706
  return params
683
707
  except Exception as e:
@@ -690,22 +714,27 @@ class AuthMiddleware:
690
714
  api_method: str,
691
715
  status_code: int,
692
716
  response_time: float,
717
+ token: str,
693
718
  error_message: Optional[str] = None,
694
719
  request_params: Optional[Dict[str, Any]] = None
695
720
  ):
696
721
  """收集接口统计"""
697
- if not self.enable_stats or not self.stats_collector:
722
+ if not self.enable_stats:
698
723
  return
699
724
 
700
725
  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
- )
726
+ from .api_stats_collector import get_api_stats_collector
727
+ collector = get_api_stats_collector()
728
+ if collector:
729
+ collector.collect(
730
+ api_path=api_path,
731
+ api_method=api_method,
732
+ status_code=status_code,
733
+ response_time=response_time,
734
+ token=token,
735
+ error_message=error_message,
736
+ request_params=request_params
737
+ )
709
738
  except Exception:
710
739
  pass # 静默失败
711
740
 
@@ -736,6 +765,11 @@ class AuthMiddleware:
736
765
  return await auth_middleware.fastapi_middleware(request, call_next)
737
766
  """
738
767
  from fastapi.responses import JSONResponse
768
+
769
+ # 处理代理头部,确保重定向(如果有)使用正确的协议
770
+ forwarded_proto = request.headers.get("x-forwarded-proto")
771
+ if forwarded_proto:
772
+ request.scope["scheme"] = forwarded_proto
739
773
 
740
774
  path = request.url.path
741
775
  start_time = time.time()
@@ -754,7 +788,7 @@ class AuthMiddleware:
754
788
  if not token:
755
789
  logger.warning("AuthMiddleware未提供认证信息")
756
790
  response_time = time.time() - start_time
757
- self._collect_stats(path, request.method, 401, response_time, "未提供认证信息", request_params)
791
+ self._collect_stats(path, request.method, 401, response_time, "", "未提供认证信息", request_params)
758
792
  return JSONResponse(
759
793
  status_code=401,
760
794
  content={"code": 401, "message": "未提供认证信息", "data": None}
@@ -763,24 +797,16 @@ class AuthMiddleware:
763
797
  # 验证 token
764
798
  try:
765
799
  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)
769
800
  # 将用户信息存储到 request.state
770
801
  request.state.user_info = user_info
771
802
  # 设置上下文
772
803
  set_current_user(dataclasses.asdict(user_info))
773
-
774
- # 处理代理头部,确保重定向(如果有)使用正确的协议
775
- forwarded_proto = request.headers.get("x-forwarded-proto")
776
- if forwarded_proto:
777
- request.scope["scheme"] = forwarded_proto
778
804
  if user_info_callback:
779
805
  await user_info_callback(request, user_info)
780
806
  except AigcAuthError as e:
781
807
  logger.error(f"AuthMiddleware认证失败: {e.message}")
782
808
  response_time = time.time() - start_time
783
- self._collect_stats(path, request.method, 401, response_time, e.message, request_params)
809
+ self._collect_stats(path, request.method, 401, response_time, token, e.message, request_params)
784
810
  return JSONResponse(
785
811
  status_code=401,
786
812
  content={"code": e.code, "message": e.message, "data": None}
@@ -790,11 +816,11 @@ class AuthMiddleware:
790
816
  try:
791
817
  response = await call_next(request)
792
818
  response_time = time.time() - start_time
793
- self._collect_stats(path, request.method, response.status_code, response_time, None, request_params)
819
+ self._collect_stats(path, request.method, response.status_code, response_time, token, None, request_params)
794
820
  return response
795
821
  except Exception as e:
796
822
  response_time = time.time() - start_time
797
- self._collect_stats(path, request.method, 500, response_time, str(e), request_params)
823
+ self._collect_stats(path, request.method, 500, response_time, token, str(e), request_params)
798
824
  raise
799
825
  finally:
800
826
  clear_current_user()
@@ -828,7 +854,7 @@ class AuthMiddleware:
828
854
  if not token:
829
855
  logger.warning("AuthMiddleware未提供认证信息")
830
856
  response_time = time.time() - g.start_time
831
- self._collect_stats(path, request.method, 401, response_time, "未提供认证信息", g.request_params)
857
+ self._collect_stats(path, request.method, 401, response_time, "", "未提供认证信息", g.request_params)
832
858
  return jsonify({
833
859
  "code": 401,
834
860
  "message": "未提供认证信息",
@@ -838,9 +864,6 @@ class AuthMiddleware:
838
864
  # 验证 token
839
865
  try:
840
866
  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)
844
867
  # 将用户信息存储到 flask.g
845
868
  g.user_info = user_info
846
869
  # 设置上下文
@@ -850,7 +873,7 @@ class AuthMiddleware:
850
873
  except AigcAuthError as e:
851
874
  logger.error(f"AuthMiddleware认证失败: {e.message}")
852
875
  response_time = time.time() - g.start_time
853
- self._collect_stats(path, request.method, 401, response_time, e.message, g.request_params)
876
+ self._collect_stats(path, request.method, 401, response_time, token, e.message, g.request_params)
854
877
  return jsonify({
855
878
  "code": e.code,
856
879
  "message": e.message,
@@ -876,11 +899,15 @@ class AuthMiddleware:
876
899
  if hasattr(g, 'start_time'):
877
900
  response_time = time.time() - g.start_time
878
901
  request_params = getattr(g, 'request_params', None)
902
+ # 从 request 的 Authorization header 获取 token
903
+ authorization = request.headers.get("Authorization")
904
+ token = self._extract_token(authorization) or ""
879
905
  self._collect_stats(
880
906
  request.path,
881
907
  request.method,
882
908
  response.status_code,
883
909
  response_time,
910
+ token,
884
911
  None,
885
912
  request_params
886
913
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: huace-aigc-auth-client
3
- Version: 1.1.22
3
+ Version: 1.1.23
4
4
  Summary: 华策AIGC Auth Client - 提供 Token 验证、用户信息获取、权限检查、旧系统接入等功能
5
5
  Author-email: Huace <support@huace.com>
6
6
  License: MIT
@@ -0,0 +1,12 @@
1
+ huace_aigc_auth_client/__init__.py,sha256=qGDEDd6vL6JEkT-4RVvVHmVsryEfg4L4r_LS1CGGs1Y,4630
2
+ huace_aigc_auth_client/api_stats_collector.py,sha256=UCUu2CHWsJyiAM58-XLQtfxWK0fjZ8TTdw90gv_Gx04,10347
3
+ huace_aigc_auth_client/legacy_adapter.py,sha256=TVCBAKejE2z2HQFsEwDW8LMiaIkXNfz3Mxv6_E-UJFY,24102
4
+ huace_aigc_auth_client/sdk.py,sha256=rc5tQnr3c_PyQlUC7q3gnfTeJVnlSgBkcrCwGvMDq8Q,33050
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.23.dist-info/licenses/LICENSE,sha256=z7dgC7KljhBLNvKjN15391nMj3aLt0gbud8-Yf1F8EQ,1063
9
+ huace_aigc_auth_client-1.1.23.dist-info/METADATA,sha256=ElYEsPFjJBo6sKLbn5qz9j_ZhgwdGVgwZYJ6SxIjEXQ,23629
10
+ huace_aigc_auth_client-1.1.23.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ huace_aigc_auth_client-1.1.23.dist-info/top_level.txt,sha256=kbv0nQ6PQ0JVneWPH7O2AbtlJnP7AjvFJ6JjM6ZEBxo,23
12
+ huace_aigc_auth_client-1.1.23.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- huace_aigc_auth_client/__init__.py,sha256=ci3wft3OSN-HynwYWyHtgyECiMx9c7TGtkzmALxf6jQ,4630
2
- huace_aigc_auth_client/api_stats_collector.py,sha256=gVtbcmLv83KdE4jwXTqPaeuKZMz8ET7QdlbSqdQ_KNY,9023
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.22.dist-info/licenses/LICENSE,sha256=z7dgC7KljhBLNvKjN15391nMj3aLt0gbud8-Yf1F8EQ,1063
9
- huace_aigc_auth_client-1.1.22.dist-info/METADATA,sha256=o2KeTOZF1PSqR4_SgX_iD_wD3dR2DLJARnwl0Vqk5j8,23629
10
- huace_aigc_auth_client-1.1.22.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
- huace_aigc_auth_client-1.1.22.dist-info/top_level.txt,sha256=kbv0nQ6PQ0JVneWPH7O2AbtlJnP7AjvFJ6JjM6ZEBxo,23
12
- huace_aigc_auth_client-1.1.22.dist-info/RECORD,,