huace-aigc-auth-client 1.1.17__tar.gz → 1.1.19__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.
- {huace_aigc_auth_client-1.1.17/huace_aigc_auth_client.egg-info → huace_aigc_auth_client-1.1.19}/PKG-INFO +26 -1
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/README.md +25 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client/__init__.py +5 -1
- huace_aigc_auth_client-1.1.19/huace_aigc_auth_client/api_stats_collector.py +301 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client/legacy_adapter.py +1 -1
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client/sdk.py +117 -2
- huace_aigc_auth_client-1.1.19/huace_aigc_auth_client/user_context.py +189 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19/huace_aigc_auth_client.egg-info}/PKG-INFO +26 -1
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client.egg-info/SOURCES.txt +2 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/pyproject.toml +1 -1
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/LICENSE +0 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/MANIFEST.in +0 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/QUICK_START.txt +0 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client/webhook.py +0 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client/webhook_flask.py +0 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client.egg-info/dependency_links.txt +0 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client.egg-info/requires.txt +0 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client.egg-info/top_level.txt +0 -0
- {huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/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.
|
|
3
|
+
Version: 1.1.19
|
|
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 对象
|
{huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client/__init__.py
RENAMED
|
@@ -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.
|
|
175
|
+
__version__ = "1.1.19"
|
|
@@ -0,0 +1,301 @@
|
|
|
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
|
+
query_string: Optional[str] = None,
|
|
70
|
+
error_message: Optional[str] = None
|
|
71
|
+
):
|
|
72
|
+
"""
|
|
73
|
+
收集接口统计数据
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
api_path: 接口路径
|
|
77
|
+
api_method: 请求方法
|
|
78
|
+
status_code: 状态码
|
|
79
|
+
response_time: 响应时间(秒)
|
|
80
|
+
query_string: 查询字符串
|
|
81
|
+
error_message: 错误信息
|
|
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
|
+
'query_string': query_string,
|
|
93
|
+
'error_message': error_message,
|
|
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
|
+
query_string: Optional[str] = None,
|
|
221
|
+
error_message: Optional[str] = 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
|
+
query_string=query_string,
|
|
236
|
+
error_message=error_message
|
|
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
|
+
query_string=str(request.url.query)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return response
|
|
282
|
+
except Exception as e:
|
|
283
|
+
response_time = time.time() - start_time
|
|
284
|
+
collect_api_stat(
|
|
285
|
+
api_path=request.url.path,
|
|
286
|
+
api_method=request.method,
|
|
287
|
+
status_code=500,
|
|
288
|
+
response_time=response_time,
|
|
289
|
+
error_message=str(e)
|
|
290
|
+
)
|
|
291
|
+
raise
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
3. 应用关闭时停止:
|
|
295
|
+
|
|
296
|
+
from huace_aigc_auth_client.api_stats_collector import stop_api_stats_collector
|
|
297
|
+
|
|
298
|
+
@app.on_event("shutdown")
|
|
299
|
+
async def shutdown_event():
|
|
300
|
+
stop_api_stats_collector()
|
|
301
|
+
"""
|
|
@@ -246,7 +246,7 @@ class LegacySystemAdapter(ABC):
|
|
|
246
246
|
else:
|
|
247
247
|
auth_data["password"] = password
|
|
248
248
|
|
|
249
|
-
result =
|
|
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
|
{huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client/sdk.py
RENAMED
|
@@ -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,61 @@ 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
|
+
def _collect_stats(
|
|
598
|
+
self,
|
|
599
|
+
api_path: str,
|
|
600
|
+
api_method: str,
|
|
601
|
+
status_code: int,
|
|
602
|
+
response_time: float,
|
|
603
|
+
query_string: Optional[str] = None,
|
|
604
|
+
error_message: Optional[str] = None
|
|
605
|
+
):
|
|
606
|
+
"""收集接口统计"""
|
|
607
|
+
if not self.enable_stats or not self.stats_collector:
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
try:
|
|
611
|
+
self.stats_collector.collect(
|
|
612
|
+
api_path=api_path,
|
|
613
|
+
api_method=api_method,
|
|
614
|
+
status_code=status_code,
|
|
615
|
+
response_time=response_time,
|
|
616
|
+
query_string=query_string,
|
|
617
|
+
error_message=error_message
|
|
618
|
+
)
|
|
619
|
+
except Exception:
|
|
620
|
+
pass # 静默失败
|
|
566
621
|
|
|
567
622
|
def _should_skip(self, path: str) -> bool:
|
|
568
623
|
"""检查是否应该跳过验证"""
|
|
@@ -593,6 +648,7 @@ class AuthMiddleware:
|
|
|
593
648
|
from fastapi.responses import JSONResponse
|
|
594
649
|
|
|
595
650
|
path = request.url.path
|
|
651
|
+
start_time = time.time()
|
|
596
652
|
|
|
597
653
|
# 检查是否跳过
|
|
598
654
|
if self._should_skip(path):
|
|
@@ -604,6 +660,8 @@ class AuthMiddleware:
|
|
|
604
660
|
|
|
605
661
|
if not token:
|
|
606
662
|
logger.warning("AuthMiddleware未提供认证信息")
|
|
663
|
+
response_time = time.time() - start_time
|
|
664
|
+
self._collect_stats(path, request.method, 401, response_time, str(request.url.query), "未提供认证信息")
|
|
607
665
|
return JSONResponse(
|
|
608
666
|
status_code=401,
|
|
609
667
|
content={"code": 401, "message": "未提供认证信息", "data": None}
|
|
@@ -612,8 +670,14 @@ class AuthMiddleware:
|
|
|
612
670
|
# 验证 token
|
|
613
671
|
try:
|
|
614
672
|
user_info = self.client.get_user_info(token)
|
|
673
|
+
# 初始化统计收集器(第一次有token时)
|
|
674
|
+
if self.enable_stats and self.stats_collector is None:
|
|
675
|
+
self._init_stats_collector(token)
|
|
615
676
|
# 将用户信息存储到 request.state
|
|
616
677
|
request.state.user_info = user_info
|
|
678
|
+
# 设置上下文
|
|
679
|
+
set_current_user(dataclasses.asdict(user_info))
|
|
680
|
+
|
|
617
681
|
# 处理代理头部,确保重定向(如果有)使用正确的协议
|
|
618
682
|
forwarded_proto = request.headers.get("x-forwarded-proto")
|
|
619
683
|
if forwarded_proto:
|
|
@@ -622,11 +686,25 @@ class AuthMiddleware:
|
|
|
622
686
|
await user_info_callback(request, user_info)
|
|
623
687
|
except AigcAuthError as e:
|
|
624
688
|
logger.error(f"AuthMiddleware认证失败: {e.message}")
|
|
689
|
+
response_time = time.time() - start_time
|
|
690
|
+
self._collect_stats(path, request.method, 401, response_time, str(request.url.query), e.message)
|
|
625
691
|
return JSONResponse(
|
|
626
692
|
status_code=401,
|
|
627
693
|
content={"code": e.code, "message": e.message, "data": None}
|
|
628
694
|
)
|
|
629
|
-
|
|
695
|
+
|
|
696
|
+
# 处理请求
|
|
697
|
+
try:
|
|
698
|
+
response = await call_next(request)
|
|
699
|
+
response_time = time.time() - start_time
|
|
700
|
+
self._collect_stats(path, request.method, response.status_code, response_time, str(request.url.query))
|
|
701
|
+
return response
|
|
702
|
+
except Exception as e:
|
|
703
|
+
response_time = time.time() - start_time
|
|
704
|
+
self._collect_stats(path, request.method, 500, response_time, str(request.url.query), str(e))
|
|
705
|
+
raise
|
|
706
|
+
finally:
|
|
707
|
+
clear_current_user()
|
|
630
708
|
|
|
631
709
|
def flask_before_request(self, user_info_callback: Callable = None):
|
|
632
710
|
"""
|
|
@@ -640,6 +718,8 @@ class AuthMiddleware:
|
|
|
640
718
|
from flask import request, jsonify, g
|
|
641
719
|
|
|
642
720
|
path = request.path
|
|
721
|
+
# 记录开始时间到 g 对象
|
|
722
|
+
g.start_time = time.time()
|
|
643
723
|
|
|
644
724
|
# 检查是否跳过
|
|
645
725
|
if self._should_skip(path):
|
|
@@ -651,6 +731,8 @@ class AuthMiddleware:
|
|
|
651
731
|
|
|
652
732
|
if not token:
|
|
653
733
|
logger.warning("AuthMiddleware未提供认证信息")
|
|
734
|
+
response_time = time.time() - g.start_time
|
|
735
|
+
self._collect_stats(path, request.method, 401, response_time, request.query_string.decode(), "未提供认证信息")
|
|
654
736
|
return jsonify({
|
|
655
737
|
"code": 401,
|
|
656
738
|
"message": "未提供认证信息",
|
|
@@ -660,12 +742,19 @@ class AuthMiddleware:
|
|
|
660
742
|
# 验证 token
|
|
661
743
|
try:
|
|
662
744
|
user_info = self.client.get_user_info(token)
|
|
745
|
+
# 初始化统计收集器(第一次有token时)
|
|
746
|
+
if self.enable_stats and self.stats_collector is None:
|
|
747
|
+
self._init_stats_collector(token)
|
|
663
748
|
# 将用户信息存储到 flask.g
|
|
664
749
|
g.user_info = user_info
|
|
750
|
+
# 设置上下文
|
|
751
|
+
set_current_user(dataclasses.asdict(user_info))
|
|
665
752
|
if user_info_callback:
|
|
666
753
|
user_info_callback(request, user_info)
|
|
667
754
|
except AigcAuthError as e:
|
|
668
755
|
logger.error(f"AuthMiddleware认证失败: {e.message}")
|
|
756
|
+
response_time = time.time() - g.start_time
|
|
757
|
+
self._collect_stats(path, request.method, 401, response_time, request.query_string.decode(), e.message)
|
|
669
758
|
return jsonify({
|
|
670
759
|
"code": e.code,
|
|
671
760
|
"message": e.message,
|
|
@@ -673,6 +762,32 @@ class AuthMiddleware:
|
|
|
673
762
|
}), 401
|
|
674
763
|
|
|
675
764
|
return None
|
|
765
|
+
|
|
766
|
+
def flask_after_request(self, response):
|
|
767
|
+
"""
|
|
768
|
+
Flask after_request 处理器(用于收集响应统计)
|
|
769
|
+
|
|
770
|
+
使用方法:
|
|
771
|
+
@app.after_request
|
|
772
|
+
def after_request(response):
|
|
773
|
+
return auth_middleware.flask_after_request(response)
|
|
774
|
+
"""
|
|
775
|
+
from flask import request, g
|
|
776
|
+
|
|
777
|
+
# 清除上下文
|
|
778
|
+
clear_current_user()
|
|
779
|
+
|
|
780
|
+
if hasattr(g, 'start_time'):
|
|
781
|
+
response_time = time.time() - g.start_time
|
|
782
|
+
self._collect_stats(
|
|
783
|
+
request.path,
|
|
784
|
+
request.method,
|
|
785
|
+
response.status_code,
|
|
786
|
+
response_time,
|
|
787
|
+
request.query_string.decode()
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
return response
|
|
676
791
|
|
|
677
792
|
def get_current_user_fastapi(self, request) -> Optional[UserInfo]:
|
|
678
793
|
"""
|
|
@@ -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.
|
|
3
|
+
Version: 1.1.19
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{huace_aigc_auth_client-1.1.17 → huace_aigc_auth_client-1.1.19}/huace_aigc_auth_client/webhook.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|