huace-aigc-auth-client 1.1.0__py3-none-any.whl → 1.1.2__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.
@@ -90,4 +90,4 @@ __all__ = [
90
90
  "create_sync_config",
91
91
  "create_default_field_mappings",
92
92
  ]
93
- __version__ = "1.1.0"
93
+ __version__ = "1.1.2"
@@ -62,7 +62,8 @@ class SyncConfig:
62
62
  # 密码处理
63
63
  password_mode: PasswordMode = PasswordMode.UNIFIED
64
64
  unified_password: str = "Abc@123456" # 统一初始密码
65
- password_mapper: Optional[Callable[[str], str]] = None # 自定义密码映射函数
65
+ password_mapper: Optional[Callable[[Dict], str]] = None # 自定义密码映射函数(接收用户数据字典)
66
+ password_is_hashed: bool = False # password_mapper 返回的是否已经是加密后的密码
66
67
 
67
68
  # Webhook 配置
68
69
  webhook_enabled: bool = False
@@ -104,10 +105,19 @@ class LegacySystemAdapter(ABC):
104
105
  - create_user: 创建用户
105
106
  - update_user: 更新用户(可选)
106
107
  - get_all_users: 获取所有用户(用于初始化同步)
108
+ - handle_webhook: 处理 webhook 通知(可选,异步方法)
107
109
  """
108
110
 
109
- def __init__(self, sync_config: SyncConfig):
111
+ def __init__(self, sync_config: SyncConfig, auth_client=None):
112
+ """
113
+ 初始化适配器
114
+
115
+ Args:
116
+ sync_config: 同步配置
117
+ auth_client: AigcAuthClient 实例(可选,用于批量同步)
118
+ """
110
119
  self.config = sync_config
120
+ self.auth_client = auth_client
111
121
 
112
122
  @abstractmethod
113
123
  def get_user_by_unique_field(self, value: Any) -> Optional[LegacyUserData]:
@@ -127,6 +137,196 @@ class LegacySystemAdapter(ABC):
127
137
  """获取所有旧系统用户(用于初始化同步)"""
128
138
  return []
129
139
 
140
+ @abstractmethod
141
+ async def get_user_by_username_async(self, username: str) -> Optional[LegacyUserData]:
142
+ """异步获取用户(子类必须实现)
143
+
144
+ Args:
145
+ username: 用户名
146
+
147
+ Returns:
148
+ Optional[LegacyUserData]: 用户数据,不存在返回 None
149
+ """
150
+ raise NotImplementedError("Subclass must implement get_user_by_username_async method")
151
+
152
+ @abstractmethod
153
+ async def _create_user_async(self, user_data: Dict[str, Any]) -> Optional[Any]:
154
+ """异步创建用户(子类必须实现)
155
+
156
+ Args:
157
+ user_data: 用户数据字典
158
+
159
+ Returns:
160
+ Optional[Any]: 创建的用户 ID 或其他标识
161
+ """
162
+ raise NotImplementedError("Subclass must implement _create_user_async method")
163
+
164
+ @abstractmethod
165
+ async def _update_user_async(self, username: str, user_data: Dict[str, Any]) -> bool:
166
+ """异步更新用户(子类必须实现)
167
+
168
+ Args:
169
+ username: 用户名
170
+ user_data: 要更新的用户数据
171
+
172
+ Returns:
173
+ bool: 更新成功返回 True
174
+ """
175
+ raise NotImplementedError("Subclass must implement _update_user_async method")
176
+
177
+ @abstractmethod
178
+ async def get_all_users_async(self) -> List[LegacyUserData]:
179
+ """获取所有用户(子类必须实现,用于批量同步)
180
+
181
+ Returns:
182
+ List[LegacyUserData]: 所有用户数据列表
183
+ """
184
+ raise NotImplementedError("Subclass must implement get_all_users_async method")
185
+
186
+ async def upsert_user_async(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
187
+ """异步创建或更新用户(存在则更新,不存在则新增)
188
+
189
+ 这是一个默认实现,子类可以选择性覆盖以优化性能。
190
+
191
+ Args:
192
+ user_data: 用户数据字典(必须包含 username)
193
+
194
+ Returns:
195
+ Dict: 操作结果 {"created": bool, "user_id": Any}
196
+ """
197
+ username = user_data.get("username")
198
+ if not username:
199
+ raise ValueError("username is required for upsert operation")
200
+
201
+ # 检查用户是否存在
202
+ existing = await self.get_user_by_username_async(username)
203
+
204
+ if existing:
205
+ # 用户存在,执行更新
206
+ await self._update_user_async(username, user_data)
207
+ return {"created": False, "user_id": existing.get("id")}
208
+ else:
209
+ # 用户不存在,执行创建
210
+ user_id = await self._create_user_async(user_data)
211
+ return {"created": True, "user_id": user_id}
212
+
213
+ async def batch_sync_to_auth(self) -> Dict[str, Any]:
214
+ """批量同步旧系统用户到 aigc-auth(默认实现)
215
+
216
+ 子类可以选择性覆盖此方法以自定义同步逻辑。
217
+
218
+ Returns:
219
+ Dict: 同步结果统计
220
+ """
221
+ if not self.auth_client:
222
+ raise ValueError("auth_client is required for batch_sync_to_auth. Please provide it in constructor.")
223
+
224
+ users = await self.get_all_users_async()
225
+
226
+ results = {
227
+ "total": len(users),
228
+ "success": 0,
229
+ "failed": 0,
230
+ "skipped": 0,
231
+ "errors": []
232
+ }
233
+
234
+ for user in users:
235
+ try:
236
+ auth_data = self.transform_legacy_to_auth(user)
237
+
238
+ # 获取密码(支持新的元组返回格式)
239
+ password_result = self.get_password_for_sync(user)
240
+ if isinstance(password_result, tuple):
241
+ password, is_hashed = password_result
242
+ else:
243
+ password, is_hashed = password_result, False
244
+
245
+ # 根据是否已加密选择不同的字段
246
+ if is_hashed:
247
+ auth_data["password_hashed"] = password
248
+ else:
249
+ auth_data["password"] = password
250
+
251
+ result = self.auth_client.sync_user_to_auth(auth_data)
252
+
253
+ if result.get("success"):
254
+ if result.get("created"):
255
+ results["success"] += 1
256
+ else:
257
+ results["skipped"] += 1
258
+ else:
259
+ results["failed"] += 1
260
+ results["errors"].append({
261
+ "user": user.get("username"),
262
+ "error": result.get("message")
263
+ })
264
+ except Exception as e:
265
+ results["failed"] += 1
266
+ results["errors"].append({
267
+ "user": user.get("username"),
268
+ "error": str(e)
269
+ })
270
+
271
+ return results
272
+
273
+ async def handle_webhook(self, event: str, data: Dict[str, Any]) -> Dict[str, Any]:
274
+ """
275
+ 处理来自 aigc-auth 的 webhook 通知(默认实现)
276
+
277
+ 子类可以选择性覆盖此方法以自定义 webhook 处理逻辑。
278
+
279
+ Args:
280
+ event: 事件类型,如 "user.created", "user.updated", "user.deleted"
281
+ data: 事件数据(用户信息)
282
+
283
+ Returns:
284
+ Dict: 处理结果
285
+ """
286
+ if event == "user.created" or event == "user.updated" or event == "user.login":
287
+ # 转换数据格式
288
+ legacy_data = self.transform_auth_to_legacy(data)
289
+
290
+ # 获取密码
291
+ password_result = self.get_password_for_sync()
292
+ if isinstance(password_result, tuple):
293
+ password, is_hashed = password_result
294
+ else:
295
+ password, is_hashed = password_result, False
296
+ legacy_data["password"] = password
297
+
298
+ # 创建或更新用户
299
+ result = await self.upsert_user_async(legacy_data)
300
+
301
+ return {
302
+ "success": True,
303
+ "message": "User created" if result["created"] else "User updated",
304
+ "created": result["created"],
305
+ "user_id": result["user_id"]
306
+ }
307
+
308
+ elif event == "user.deleted":
309
+ # 禁用用户而不是删除
310
+ username = data.get("username")
311
+ await self._update_user_async(username, {"status": "disabled"})
312
+
313
+ return {"success": True, "message": "User disabled"}
314
+
315
+ elif event == "user.init_sync_auth":
316
+ # 初始化同步:批量同步旧系统用户到 aigc-auth
317
+ if not self.auth_client:
318
+ return {"success": False, "message": "auth_client is required for init_sync_auth event. Please provide it in constructor."}
319
+
320
+ results = await self.batch_sync_to_auth()
321
+
322
+ return {
323
+ "success": True,
324
+ "message": f"Batch sync completed: {results['success']} created, {results['skipped']} skipped, {results['failed']} failed",
325
+ "results": results
326
+ }
327
+
328
+ return {"success": True, "message": "Event ignored"}
329
+
130
330
  def transform_auth_to_legacy(self, auth_user: Dict[str, Any]) -> Dict[str, Any]:
131
331
  """将 aigc-auth 用户数据转换为旧系统格式"""
132
332
  result = {}
@@ -167,15 +367,22 @@ class LegacySystemAdapter(ABC):
167
367
 
168
368
  return result
169
369
 
170
- def get_password_for_sync(self, legacy_user: Optional[LegacyUserData] = None) -> str:
171
- """获取同步时使用的密码"""
370
+ def get_password_for_sync(self, legacy_user: Optional[LegacyUserData] = None) -> tuple:
371
+ """获取同步时使用的密码
372
+
373
+ Returns:
374
+ tuple: (password, is_hashed)
375
+ - password: 密码字符串
376
+ - is_hashed: 是否已经是加密后的密码
377
+ """
172
378
  if self.config.password_mode == PasswordMode.UNIFIED:
173
- return self.config.unified_password
379
+ return (self.config.unified_password, False)
174
380
  elif self.config.password_mode == PasswordMode.CUSTOM_MAPPING:
175
381
  if self.config.password_mapper and legacy_user:
176
- return self.config.password_mapper(legacy_user.data)
177
- return self.config.unified_password
178
- return self.config.unified_password
382
+ password = self.config.password_mapper(legacy_user.data)
383
+ return (password, self.config.password_is_hashed)
384
+ return (self.config.unified_password, False)
385
+ return (self.config.unified_password, False)
179
386
 
180
387
 
181
388
  class WebhookSender:
@@ -253,78 +460,10 @@ class UserSyncService:
253
460
  self.adapter = legacy_adapter
254
461
  self.config = legacy_adapter.config
255
462
  self.webhook_sender = WebhookSender(self.config)
256
-
257
- def sync_on_login(self, auth_user_info) -> SyncResult:
258
- """
259
- 登录时同步用户到旧系统
260
463
 
261
- 当用户通过 aigc-auth 登录成功后调用,
262
- 如果旧系统没有该用户则自动创建。
263
-
264
- Args:
265
- auth_user_info: aigc-auth 返回的 UserInfo 对象
266
-
267
- Returns:
268
- SyncResult: 同步结果
269
- """
270
- if not self.config.enabled:
271
- return SyncResult(success=True, message="Sync disabled")
272
-
273
- if self.config.direction == SyncDirection.LEGACY_TO_AUTH:
274
- return SyncResult(success=True, message="Direction is legacy_to_auth, skip")
275
-
276
- try:
277
- # 获取唯一标识值
278
- unique_value = getattr(auth_user_info, self.config.unique_field, None)
279
- if not unique_value:
280
- return SyncResult(
281
- success=False,
282
- message=f"Unique field '{self.config.unique_field}' not found in auth user"
283
- )
284
-
285
- # 检查旧系统是否已有该用户
286
- legacy_user = self.adapter.get_user_by_unique_field(unique_value)
287
-
288
- if legacy_user:
289
- # 用户已存在,可选更新
290
- if self.config.direction == SyncDirection.BIDIRECTIONAL:
291
- auth_data = self._user_info_to_dict(auth_user_info)
292
- legacy_data = self.adapter.transform_auth_to_legacy(auth_data)
293
- self.adapter.update_user(unique_value, legacy_data)
294
-
295
- return SyncResult(
296
- success=True,
297
- auth_user_id=auth_user_info.id,
298
- legacy_user_id=legacy_user.get("id"),
299
- message="User already exists in legacy system"
300
- )
301
-
302
- # 创建新用户
303
- auth_data = self._user_info_to_dict(auth_user_info)
304
- legacy_data = self.adapter.transform_auth_to_legacy(auth_data)
305
-
306
- # 添加密码
307
- legacy_data["password"] = self.adapter.get_password_for_sync()
308
-
309
- legacy_user_id = self.adapter.create_user(legacy_data)
310
-
311
- if legacy_user_id:
312
- return SyncResult(
313
- success=True,
314
- auth_user_id=auth_user_info.id,
315
- legacy_user_id=legacy_user_id,
316
- message="User synced to legacy system"
317
- )
318
- else:
319
- return SyncResult(
320
- success=False,
321
- auth_user_id=auth_user_info.id,
322
- message="Failed to create user in legacy system"
323
- )
324
-
325
- except Exception as e:
326
- logger.exception("Error syncing user on login")
327
- return SyncResult(success=False, message=str(e), errors=[str(e)])
464
+ # 确保 adapter 也持有 auth_client 引用
465
+ if not self.adapter.auth_client:
466
+ self.adapter.auth_client = auth_client
328
467
 
329
468
  def _user_info_to_dict(self, user_info) -> Dict[str, Any]:
330
469
  """将 UserInfo 对象转换为字典"""
@@ -339,74 +478,59 @@ class UserSyncService:
339
478
  "permissions": user_info.permissions,
340
479
  "department": user_info.department,
341
480
  "company": user_info.company,
342
- "is_admin": user_info.is_admin
481
+ "is_admin": user_info.is_admin,
482
+ "status": user_info.status,
343
483
  }
344
484
 
345
- def batch_sync_to_auth(
346
- self,
347
- users: List[LegacyUserData],
348
- on_progress: Optional[Callable[[int, int], None]] = None
349
- ) -> Dict[str, Any]:
485
+ async def sync_on_login_async(self, auth_user_info) -> Dict[str, Any]:
350
486
  """
351
- 批量同步旧系统用户到 aigc-auth
487
+ 登录时异步同步用户到旧系统
488
+
489
+ 当用户通过 aigc-auth 登录成功后调用,
490
+ 如果旧系统没有该用户则自动创建。
352
491
 
353
492
  Args:
354
- users: 旧系统用户列表
355
- on_progress: 进度回调函数 (current, total)
493
+ auth_user_info: aigc-auth 返回的 UserInfo 对象
356
494
 
357
495
  Returns:
358
- Dict: 同步结果统计
496
+ Dict: 同步结果
359
497
  """
360
- results = {
361
- "total": len(users),
362
- "success": 0,
363
- "failed": 0,
364
- "skipped": 0,
365
- "errors": []
366
- }
498
+ if not self.config.enabled:
499
+ return {"success": True, "message": "Sync disabled"}
367
500
 
368
- for i, user in enumerate(users):
369
- try:
370
- auth_data = self.adapter.transform_legacy_to_auth(user)
371
-
372
- # 添加密码
373
- auth_data["password"] = self.adapter.get_password_for_sync(user)
374
-
375
- # 调用 SDK 同步接口
376
- result = self.auth_client.sync_user_to_auth(auth_data)
377
-
378
- if result.get("success"):
379
- if result.get("created"):
380
- results["success"] += 1
381
- else:
382
- results["skipped"] += 1
383
- else:
384
- results["failed"] += 1
385
- results["errors"].append({
386
- "user": user.get(self.config.unique_field),
387
- "error": result.get("message")
388
- })
389
-
390
- except Exception as e:
391
- results["failed"] += 1
392
- results["errors"].append({
393
- "user": user.get(self.config.unique_field),
394
- "error": str(e)
395
- })
396
-
397
- if on_progress:
398
- on_progress(i + 1, len(users))
501
+ # 转换用户数据
502
+ auth_data = self._user_info_to_dict(auth_user_info)
503
+ legacy_data = self.adapter.transform_auth_to_legacy(auth_data)
399
504
 
400
- return results
401
-
402
- def send_user_created_webhook(self, auth_user_data: Dict[str, Any]) -> bool:
403
- """发送用户创建 webhook"""
404
- return self.webhook_sender.send("user.created", auth_user_data)
505
+ # 获取密码(支持新的元组返回格式)
506
+ password_result = self.adapter.get_password_for_sync()
507
+ if isinstance(password_result, tuple):
508
+ password, is_hashed = password_result
509
+ else:
510
+ password, is_hashed = password_result, False
511
+ legacy_data["password"] = password
512
+
513
+ # 使用 upsert 方法(存在则更新,不存在则创建)
514
+ result = await self.adapter.upsert_user_async(legacy_data)
515
+
516
+ return {
517
+ "success": True,
518
+ "auth_user_id": auth_user_info.id,
519
+ "legacy_user_id": result["user_id"],
520
+ "created": result["created"],
521
+ "message": "User created" if result["created"] else "User updated"
522
+ }
405
523
 
406
- def send_user_updated_webhook(self, auth_user_data: Dict[str, Any]) -> bool:
407
- """发送用户更新 webhook"""
408
- return self.webhook_sender.send("user.updated", auth_user_data)
409
-
524
+ async def batch_sync_to_auth_async(self) -> Dict[str, Any]:
525
+ """
526
+ 异步批量同步旧系统用户到 aigc-auth
527
+
528
+ 直接委托给 adapter 的默认实现
529
+
530
+ Returns:
531
+ Dict: 同步结果统计
532
+ """
533
+ return await self.adapter.batch_sync_to_auth()
410
534
 
411
535
  # ============ 预设字段映射 ============
412
536
 
@@ -42,6 +42,7 @@ class UserInfo:
42
42
  department: Optional[str] = None
43
43
  company: Optional[str] = None
44
44
  is_admin: Optional[bool] = None
45
+ status: Optional[str] = None
45
46
 
46
47
  def __post_init__(self):
47
48
  if self.roles is None:
@@ -91,7 +92,7 @@ class AigcAuthClient:
91
92
  client = AigcAuthClient(
92
93
  app_id="your_app_id",
93
94
  app_secret="your_app_secret",
94
- base_url="https://aigc-auth.huacemedia.com/api/v1" # 可选
95
+ base_url="https://aigc-auth.huacemedia.com/aigc-auth/api/v1" # 可选
95
96
  )
96
97
 
97
98
  # 验证 token
@@ -127,7 +128,7 @@ class AigcAuthClient:
127
128
  self.base_url = (
128
129
  base_url or
129
130
  os.getenv("AIGC_AUTH_BASE_URL") or
130
- "https://aigc-auth.huacemedia.com/api/v1"
131
+ "https://aigc-auth.huacemedia.com/aigc-auth/api/v1"
131
132
  )
132
133
  self.timeout = timeout
133
134
  self.cache_ttl = cache_ttl
@@ -225,7 +226,7 @@ class AigcAuthClient:
225
226
  """清空所有缓存"""
226
227
  self._cache.clear()
227
228
 
228
- def _request(self, method: str, endpoint: str, data: Dict = None, token: str = None) -> Dict:
229
+ def _request(self, method: str, endpoint: str, data: Dict = None, token: str = None, headers: Dict[str, str] = None) -> Dict:
229
230
  """
230
231
  发送请求
231
232
 
@@ -234,6 +235,7 @@ class AigcAuthClient:
234
235
  endpoint: 端点路径
235
236
  data: 请求数据
236
237
  token: 用户 token(用于缓存键)
238
+ headers: 自定义请求头(会与默认 headers 合并,自定义的优先)
237
239
 
238
240
  Returns:
239
241
  Dict: 响应数据
@@ -253,11 +255,16 @@ class AigcAuthClient:
253
255
  return cached_data
254
256
 
255
257
  try:
258
+ # 合并 headers:默认 headers + 自定义 headers(自定义的优先)
259
+ request_headers = self._get_headers()
260
+ if headers:
261
+ request_headers.update(headers)
262
+
256
263
  response = requests.request(
257
264
  method=method,
258
265
  url=url,
259
266
  json=data,
260
- headers=self._get_headers(),
267
+ headers=request_headers,
261
268
  timeout=self.timeout
262
269
  )
263
270
  response.raise_for_status()
@@ -323,7 +330,8 @@ class AigcAuthClient:
323
330
  permissions=data.get("permissions", []),
324
331
  department=data.get("department"),
325
332
  company=data.get("company"),
326
- is_admin=data.get("is_admin")
333
+ is_admin=data.get("is_admin"),
334
+ status=data.get("status")
327
335
  )
328
336
 
329
337
  def check_permissions(
@@ -372,12 +380,15 @@ class AigcAuthClient:
372
380
 
373
381
  # ============ 用户同步相关方法 ============
374
382
 
375
- def sync_user_to_auth(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
383
+ def sync_user_to_auth(self, user_data: Dict[str, Any], headers: Dict[str, str] = None) -> Dict[str, Any]:
376
384
  """
377
385
  同步用户到 aigc-auth(用于旧系统初始化同步)
378
386
 
379
387
  Args:
380
- user_data: 用户数据,必须包含 username password
388
+ user_data: 用户数据,必须包含 username,以及 password 或 password_hashed 二选一
389
+ - password: 明文密码
390
+ - password_hashed: 已加密的密码哈希(bcrypt 格式)
391
+ headers: 自定义请求头(可选)
381
392
 
382
393
  Returns:
383
394
  Dict: 同步结果
@@ -386,14 +397,15 @@ class AigcAuthClient:
386
397
  - user_id: int 用户ID
387
398
  - message: str 消息
388
399
  """
389
- return self._request("POST", "/sync/user", user_data)
400
+ return self._request("POST", "/sync/user", user_data, headers=headers)
390
401
 
391
- def batch_sync_users_to_auth(self, users: List[Dict[str, Any]]) -> Dict[str, Any]:
402
+ def batch_sync_users_to_auth(self, users: List[Dict[str, Any]], headers: Dict[str, str] = None) -> Dict[str, Any]:
392
403
  """
393
404
  批量同步用户到 aigc-auth
394
405
 
395
406
  Args:
396
- users: 用户数据列表
407
+ users: 用户数据列表,每个用户必须包含 username,以及 password 或 password_hashed 二选一
408
+ headers: 自定义请求头(可选)
397
409
 
398
410
  Returns:
399
411
  Dict: 批量同步结果
@@ -403,7 +415,7 @@ class AigcAuthClient:
403
415
  - skipped: int 跳过数(已存在)
404
416
  - errors: List[Dict] 错误详情
405
417
  """
406
- return self._request("POST", "/sync/batch", {"users": users})
418
+ return self._request("POST", "/sync/batch", {"users": users}, headers=headers)
407
419
 
408
420
  def register_webhook(self, webhook_url: str, events: List[str], secret: Optional[str] = None) -> Dict[str, Any]:
409
421
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: huace-aigc-auth-client
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: 华策AIGC Auth Client - 提供 Token 验证、用户信息获取、权限检查、旧系统接入等功能
5
5
  Author-email: Huace <support@huace.com>
6
6
  License: MIT
@@ -46,7 +46,7 @@ AIGC_AUTH_APP_ID=your_app_id
46
46
  AIGC_AUTH_APP_SECRET=your_app_secret
47
47
 
48
48
  # 可选:鉴权服务地址(默认为生产环境)
49
- AIGC_AUTH_BASE_URL=https://aigc-auth.huacemedia.com/api/v1
49
+ AIGC_AUTH_BASE_URL=https://aigc-auth.huacemedia.com/aigc-auth/api/v1
50
50
  ```
51
51
 
52
52
  如需通过 Nginx 代理:
@@ -75,7 +75,7 @@ client = AigcAuthClient()
75
75
  client = AigcAuthClient(
76
76
  app_id="your_app_id",
77
77
  app_secret="your_app_secret",
78
- base_url="https://aigc-auth.huacemedia.com/api/v1"
78
+ base_url="https://aigc-auth.huacemedia.com/aigc-auth/api/v1"
79
79
  )
80
80
  ```
81
81
 
@@ -108,11 +108,11 @@ try:
108
108
  print(f"角色: {user.roles}")
109
109
  print(f"权限: {user.permissions}")
110
110
 
111
- # 检查角色
112
- if user.has_role("admin"):
111
+ # 检查角色(在 Auth 里面配置)
112
+ if user.has_role("admin") or user.is_admin:
113
113
  print("是管理员")
114
114
 
115
- # 检查权限
115
+ # 检查权限(在 Auth 里面配置)
116
116
  if user.has_permission("user:write"):
117
117
  print("有用户写权限")
118
118
 
@@ -353,7 +353,7 @@ except AigcAuthError as e:
353
353
  # .env 文件
354
354
  AIGC_AUTH_APP_ID=your_app_id
355
355
  AIGC_AUTH_APP_SECRET=your_app_secret
356
- AIGC_AUTH_BASE_URL=https://aigc-auth.huacemedia.com/api/v1
356
+ AIGC_AUTH_BASE_URL=https://aigc-auth.huacemedia.com/aigc-auth/api/v1
357
357
 
358
358
  # 同步配置
359
359
  AIGC_AUTH_SYNC_ENABLED=true
@@ -525,7 +525,20 @@ async def init_sync():
525
525
 
526
526
  for user in users:
527
527
  auth_data = adapter.transform_legacy_to_auth(user)
528
- auth_data["password"] = sync_config.unified_password
528
+
529
+ # 获取密码(支持元组返回格式)
530
+ password_result = adapter.get_password_for_sync(user)
531
+ if isinstance(password_result, tuple):
532
+ password, is_hashed = password_result
533
+ else:
534
+ password, is_hashed = password_result, False
535
+
536
+ # 根据是否已加密选择不同的字段
537
+ if is_hashed:
538
+ auth_data["password_hashed"] = password # 直接传递已加密密码
539
+ else:
540
+ auth_data["password"] = password # 传递明文密码,服务端会加密
541
+
529
542
  client.sync_user_to_auth(auth_data)
530
543
 
531
544
  print(f"同步完成:{len(users)} 个用户")
@@ -538,28 +551,42 @@ asyncio.run(init_sync())
538
551
 
539
552
  | 模式 | 说明 | 适用场景 |
540
553
  |------|------|----------|
541
- | `UNIFIED` | 统一初始密码 | 推荐,安全性高 |
554
+ | `UNIFIED` | 统一初始密码 | 新系统接入,安全性高 |
542
555
  | `CUSTOM_MAPPING` | 自定义映射函数 | 需要保留原密码时 |
556
+ | `CUSTOM_MAPPING` + `password_is_hashed=True` | 直接同步已加密密码 | **推荐**,两系统使用相同加密方式 (bcrypt) |
543
557
 
544
558
  ```python
545
559
  from huace_aigc_auth_client import PasswordMode, create_sync_config
546
560
 
547
- # 统一密码模式(推荐)
561
+ # 统一密码模式
548
562
  sync_config = create_sync_config(
549
563
  password_mode=PasswordMode.UNIFIED,
550
564
  unified_password="Abc@123456"
551
565
  )
552
566
 
553
- # 自定义映射模式
567
+ # 自定义映射模式(返回明文密码)
554
568
  def password_mapper(user_data):
555
- return user_data.get("hashed_password", "default")
569
+ return user_data.get("password", "default")
556
570
 
557
571
  sync_config = create_sync_config(
558
572
  password_mode=PasswordMode.CUSTOM_MAPPING,
559
573
  password_mapper=password_mapper
560
574
  )
575
+
576
+ # 直接同步已加密密码(推荐,适用于两系统使用相同的 bcrypt 加密)
577
+ def hashed_password_mapper(user_data):
578
+ return user_data.get("hashed_password", "")
579
+
580
+ sync_config = create_sync_config(
581
+ password_mode=PasswordMode.CUSTOM_MAPPING,
582
+ password_mapper=hashed_password_mapper,
583
+ password_is_hashed=True # 标记返回的是已加密密码
584
+ )
561
585
  ```
562
586
 
587
+ > **注意**:当 `password_is_hashed=True` 时,SDK 会使用 `password_hashed` 字段传递到服务端,
588
+ > 服务端会直接使用该哈希值而不再重新加密。
589
+
563
590
  ### 同步方向配置
564
591
 
565
592
  ```python
@@ -611,6 +638,45 @@ from huace_aigc_auth_client import (
611
638
  )
612
639
  ```
613
640
 
641
+ ---
642
+
643
+ ## API 变更日志
644
+
645
+ ### v1.1.0 (2026-01-13)
646
+
647
+ #### 新增功能
648
+
649
+ 1. **支持直接同步已加密密码**
650
+ - `SyncConfig` 新增 `password_is_hashed` 参数
651
+ - `SyncUserRequest` 新增 `password_hashed` 字段
652
+ - 当两个系统使用相同的密码加密方式(bcrypt)时,可直接同步已加密密码
653
+
654
+ 2. **`_request` 方法支持自定义 headers**
655
+ - `sync_user_to_auth(user_data, headers=None)` 支持传入自定义请求头
656
+ - `batch_sync_users_to_auth(users, headers=None)` 支持传入自定义请求头
657
+
658
+ 3. **`get_password_for_sync` 返回格式变更**
659
+ - 旧版:返回 `str`(密码字符串)
660
+ - 新版:返回 `tuple[str, bool]`(密码字符串, 是否已加密)
661
+
662
+ #### 使用示例
663
+
664
+ ```python
665
+ # 直接同步已加密密码
666
+ sync_config = create_sync_config(
667
+ password_mode=PasswordMode.CUSTOM_MAPPING,
668
+ password_mapper=lambda user: user.get("hashed_password"),
669
+ password_is_hashed=True, # 新增参数
670
+ )
671
+
672
+ # SDK 调用
673
+ client.sync_user_to_auth({
674
+ "username": "test",
675
+ "password_hashed": "$2b$12$xxxxx...", # 直接传递 bcrypt 哈希值
676
+ "email": "test@example.com"
677
+ })
678
+ ```
679
+
614
680
  ## 许可证
615
681
 
616
682
  MIT License
@@ -0,0 +1,8 @@
1
+ huace_aigc_auth_client/__init__.py,sha256=XUdOpgiTYKRsmyhCuyH2rY7Ps6Sjsic8xA9nJF71r8U,2163
2
+ huace_aigc_auth_client/legacy_adapter.py,sha256=HRhcoOaTwfHT4F2sfr2JQ3Q5_yd8tngMbycgWjMOGRQ,20966
3
+ huace_aigc_auth_client/sdk.py,sha256=46kkNDmWcce4BkSAT9zn5n8XdURDns3YtXIIfe8TGDU,22453
4
+ huace_aigc_auth_client-1.1.2.dist-info/licenses/LICENSE,sha256=z7dgC7KljhBLNvKjN15391nMj3aLt0gbud8-Yf1F8EQ,1063
5
+ huace_aigc_auth_client-1.1.2.dist-info/METADATA,sha256=gA3CPCKM9-E8bnfeivTTeHasqJd--_JSv3FtjZ0IQ_g,20084
6
+ huace_aigc_auth_client-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ huace_aigc_auth_client-1.1.2.dist-info/top_level.txt,sha256=kbv0nQ6PQ0JVneWPH7O2AbtlJnP7AjvFJ6JjM6ZEBxo,23
8
+ huace_aigc_auth_client-1.1.2.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- huace_aigc_auth_client/__init__.py,sha256=6b5KUzvQaGgjNPeO3AdzJm9kqFAPfLQNNulkYqFadQc,2163
2
- huace_aigc_auth_client/legacy_adapter.py,sha256=dSEqROe6UK-NEcxC53aG-zEOXQgRRSV-Li128vvRWhw,16149
3
- huace_aigc_auth_client/sdk.py,sha256=obiLellLFoct79ZTJKWcjE_eAE-d4zzO8ObZE7KLoAE,21573
4
- huace_aigc_auth_client-1.1.0.dist-info/licenses/LICENSE,sha256=z7dgC7KljhBLNvKjN15391nMj3aLt0gbud8-Yf1F8EQ,1063
5
- huace_aigc_auth_client-1.1.0.dist-info/METADATA,sha256=E-kmBqpoTUqAW8pvg_1rHFrUn0hvLUn9fi94xwHWP2o,17641
6
- huace_aigc_auth_client-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- huace_aigc_auth_client-1.1.0.dist-info/top_level.txt,sha256=kbv0nQ6PQ0JVneWPH7O2AbtlJnP7AjvFJ6JjM6ZEBxo,23
8
- huace_aigc_auth_client-1.1.0.dist-info/RECORD,,