huace-aigc-auth-client 1.1.21__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.21"
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,21 +78,26 @@ 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
  """
86
85
  if not self.enabled:
87
86
  return
88
87
 
88
+ # 过滤重定向请求(3xx 状态码),这些通常是框架自动处理的,不应统计
89
+ if 300 <= status_code < 400:
90
+ return
91
+
89
92
  try:
90
93
  stat_data = {
91
94
  'api_path': api_path,
92
95
  'api_method': api_method,
93
96
  'status_code': status_code,
94
97
  'response_time': response_time,
98
+ 'token': token,
95
99
  'error_message': error_message,
96
- 'request_params': request_params,
97
- 'timestamp': datetime.utcnow().isoformat()
100
+ 'request_params': request_params
98
101
  }
99
102
  self.queue.put_nowait(stat_data)
100
103
  except queue.Full:
@@ -135,19 +138,50 @@ class ApiStatsCollector:
135
138
  self._flush_buffer(buffer)
136
139
 
137
140
  def _flush_buffer(self, buffer: List[Dict[str, Any]]):
138
- """刷新缓冲区:批量提交统计数据"""
141
+ """刷新缓冲区:按token分组批量提交统计数据"""
139
142
  if not buffer:
140
143
  return
141
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
+ """
142
176
  try:
143
177
  headers = {
144
178
  'X-App-Id': self.app_id,
145
179
  'X-App-Secret': self.app_secret,
146
- 'Authorization': f'Bearer {self.token}',
180
+ 'Authorization': f'Bearer {token}',
147
181
  'Content-Type': 'application/json'
148
182
  }
149
183
 
150
- payload = {'stats': buffer}
184
+ payload = {'stats': stats}
151
185
 
152
186
  response = requests.post(
153
187
  f'{self.api_url}/stats/report/batch',
@@ -173,7 +207,6 @@ def init_api_stats_collector(
173
207
  api_url: str,
174
208
  app_id: str,
175
209
  app_secret: str,
176
- token: str,
177
210
  batch_size: int = 10,
178
211
  flush_interval: float = 5.0,
179
212
  enabled: bool = True
@@ -185,7 +218,6 @@ def init_api_stats_collector(
185
218
  api_url: 统计接口 URL
186
219
  app_id: 应用 ID
187
220
  app_secret: 应用密钥
188
- token: 用户访问令牌
189
221
  batch_size: 批量提交大小
190
222
  flush_interval: 刷新间隔(秒)
191
223
  enabled: 是否启用
@@ -198,7 +230,6 @@ def init_api_stats_collector(
198
230
  api_url=api_url,
199
231
  app_id=app_id,
200
232
  app_secret=app_secret,
201
- token=token,
202
233
  batch_size=batch_size,
203
234
  flush_interval=flush_interval,
204
235
  enabled=enabled
@@ -224,6 +255,7 @@ def collect_api_stat(
224
255
  api_method: str,
225
256
  status_code: int,
226
257
  response_time: float,
258
+ token: str,
227
259
  error_message: Optional[str] = None,
228
260
  request_params: Optional[Dict[str, Any]] = None
229
261
  ):
@@ -231,7 +263,21 @@ def collect_api_stat(
231
263
  快捷方法:收集接口统计数据
232
264
 
233
265
  使用全局收集器实例
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: 请求参数
234
276
  """
277
+ # 过滤重定向请求(3xx 状态码),这些通常是框架自动处理的,不应统计
278
+ if 300 <= status_code < 400:
279
+ return
280
+
235
281
  collector = get_api_stats_collector()
236
282
  if collector:
237
283
  collector.collect(
@@ -239,6 +285,7 @@ def collect_api_stat(
239
285
  api_method=api_method,
240
286
  status_code=status_code,
241
287
  response_time=response_time,
288
+ token=token,
242
289
  error_message=error_message,
243
290
  request_params=request_params
244
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.21
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=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,,