mdbq 4.0.91__py3-none-any.whl → 4.0.92__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.
- mdbq/__version__.py +1 -1
- mdbq/auth/auth_backend.py +93 -36
- {mdbq-4.0.91.dist-info → mdbq-4.0.92.dist-info}/METADATA +1 -1
- {mdbq-4.0.91.dist-info → mdbq-4.0.92.dist-info}/RECORD +6 -6
- {mdbq-4.0.91.dist-info → mdbq-4.0.92.dist-info}/WHEEL +0 -0
- {mdbq-4.0.91.dist-info → mdbq-4.0.92.dist-info}/top_level.txt +0 -0
mdbq/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
VERSION = '4.0.
|
1
|
+
VERSION = '4.0.92'
|
mdbq/auth/auth_backend.py
CHANGED
@@ -189,6 +189,7 @@ class StandaloneAuthManager:
|
|
189
189
|
CREATE TABLE IF NOT EXISTS users (
|
190
190
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
191
191
|
username VARCHAR(50) NOT NULL,
|
192
|
+
email VARCHAR(100) NOT NULL,
|
192
193
|
password_hash VARCHAR(128) NOT NULL,
|
193
194
|
password_plain TEXT NOT NULL,
|
194
195
|
salt VARCHAR(64) NOT NULL,
|
@@ -203,6 +204,7 @@ class StandaloneAuthManager:
|
|
203
204
|
password_reset_expires TIMESTAMP(3) NULL DEFAULT NULL COMMENT '重置令牌过期时间',
|
204
205
|
|
205
206
|
UNIQUE KEY uk_users_username (username),
|
207
|
+
UNIQUE KEY uk_users_email (email),
|
206
208
|
UNIQUE KEY uk_users_reset_token (password_reset_token),
|
207
209
|
KEY idx_users_role (role),
|
208
210
|
KEY idx_users_created_at (created_at),
|
@@ -372,15 +374,15 @@ class StandaloneAuthManager:
|
|
372
374
|
cursor.close()
|
373
375
|
conn.close()
|
374
376
|
|
375
|
-
def register_user(self, username, password, role='user', permissions=None):
|
377
|
+
def register_user(self, username, password, role='user', permissions=None, email=None):
|
376
378
|
"""用户注册"""
|
377
379
|
conn = self.pool.connection()
|
378
380
|
cursor = conn.cursor()
|
379
381
|
|
380
382
|
try:
|
381
383
|
# 验证输入
|
382
|
-
if not username or not password:
|
383
|
-
return {'success': False, 'message': '
|
384
|
+
if not username or not password or not email:
|
385
|
+
return {'success': False, 'message': '用户名、密码和邮箱不能为空'}
|
384
386
|
|
385
387
|
if len(username.strip()) < 3:
|
386
388
|
return {'success': False, 'message': '用户名至少需要3个字符'}
|
@@ -388,13 +390,25 @@ class StandaloneAuthManager:
|
|
388
390
|
if len(password) < 6:
|
389
391
|
return {'success': False, 'message': '密码至少需要6个字符'}
|
390
392
|
|
393
|
+
# 邮箱格式验证
|
394
|
+
import re
|
395
|
+
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
396
|
+
if not re.match(email_pattern, email):
|
397
|
+
return {'success': False, 'message': '请输入有效的邮箱地址'}
|
398
|
+
|
391
399
|
username = username.strip()
|
400
|
+
email = email.strip().lower()
|
392
401
|
|
393
402
|
# 检查用户名是否已存在
|
394
403
|
cursor.execute('SELECT id FROM users WHERE username = %s', (username,))
|
395
404
|
if cursor.fetchone():
|
396
405
|
return {'success': False, 'message': '用户名已被占用'}
|
397
406
|
|
407
|
+
# 检查邮箱是否已存在
|
408
|
+
cursor.execute('SELECT id FROM users WHERE email = %s', (email,))
|
409
|
+
if cursor.fetchone():
|
410
|
+
return {'success': False, 'message': '邮箱已被注册'}
|
411
|
+
|
398
412
|
# 生成盐值和密码哈希
|
399
413
|
salt = secrets.token_hex(32)
|
400
414
|
password_hash = self._hash_password(password, salt)
|
@@ -406,9 +420,9 @@ class StandaloneAuthManager:
|
|
406
420
|
|
407
421
|
# 创建新用户
|
408
422
|
cursor.execute('''
|
409
|
-
INSERT INTO users (username, password_hash, password_plain, salt, role, permissions, is_active)
|
410
|
-
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
411
|
-
''', (username, password_hash, password, salt, role, permissions_json, True))
|
423
|
+
INSERT INTO users (username, email, password_hash, password_plain, salt, role, permissions, is_active)
|
424
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
425
|
+
''', (username, email, password_hash, password, salt, role, permissions_json, True))
|
412
426
|
|
413
427
|
user_id = cursor.lastrowid
|
414
428
|
|
@@ -418,6 +432,7 @@ class StandaloneAuthManager:
|
|
418
432
|
'user': {
|
419
433
|
'id': user_id,
|
420
434
|
'username': username,
|
435
|
+
'email': email,
|
421
436
|
'role': role,
|
422
437
|
'permissions': permissions
|
423
438
|
}
|
@@ -429,13 +444,13 @@ class StandaloneAuthManager:
|
|
429
444
|
cursor.close()
|
430
445
|
conn.close()
|
431
446
|
|
432
|
-
def authenticate_user(self,
|
433
|
-
"""用户身份验证"""
|
447
|
+
def authenticate_user(self, username_or_email, password, ip_address=None, user_agent=None):
|
448
|
+
"""用户身份验证 - 支持用户名或邮箱登录"""
|
434
449
|
|
435
450
|
# 检查IP是否被限流
|
436
451
|
ip_check = self._check_ip_rate_limit(ip_address, 'login')
|
437
452
|
if ip_check['blocked']:
|
438
|
-
self._log_login_attempt(
|
453
|
+
self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', f'ip_blocked_{ip_check["remaining_time"]}s')
|
439
454
|
return {
|
440
455
|
'success': False,
|
441
456
|
'error': 'ip_blocked',
|
@@ -447,24 +462,26 @@ class StandaloneAuthManager:
|
|
447
462
|
cursor = conn.cursor()
|
448
463
|
|
449
464
|
try:
|
450
|
-
# 获取用户信息
|
465
|
+
# 获取用户信息 - 支持用户名或邮箱
|
451
466
|
cursor.execute('''
|
452
|
-
SELECT id, username, password_hash, salt, role, permissions,
|
467
|
+
SELECT id, username, email, password_hash, salt, role, permissions,
|
453
468
|
is_active, login_attempts, locked_until
|
454
|
-
FROM users WHERE username = %s
|
455
|
-
''', (
|
469
|
+
FROM users WHERE username = %s OR email = %s
|
470
|
+
''', (username_or_email, username_or_email))
|
456
471
|
|
457
472
|
user = cursor.fetchone()
|
458
473
|
if not user:
|
459
|
-
self._log_login_attempt(
|
474
|
+
self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', 'user_not_found')
|
460
475
|
self._record_ip_failure(ip_address, 'login')
|
461
476
|
return {
|
462
477
|
'success': False,
|
463
478
|
'error': 'invalid_credentials',
|
464
|
-
'message': '
|
479
|
+
'message': '用户名/邮箱或密码错误'
|
465
480
|
}
|
466
481
|
|
467
482
|
user_id = user['id']
|
483
|
+
username = user['username']
|
484
|
+
email = user['email']
|
468
485
|
password_hash = user['password_hash']
|
469
486
|
salt = user['salt']
|
470
487
|
role = user['role']
|
@@ -475,7 +492,7 @@ class StandaloneAuthManager:
|
|
475
492
|
|
476
493
|
# 检查账户状态
|
477
494
|
if not is_active:
|
478
|
-
self._log_login_attempt(
|
495
|
+
self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', 'account_disabled', user_id)
|
479
496
|
self._record_ip_failure(ip_address, 'login')
|
480
497
|
return {
|
481
498
|
'success': False,
|
@@ -493,7 +510,7 @@ class StandaloneAuthManager:
|
|
493
510
|
|
494
511
|
if locked_until > current_time_utc:
|
495
512
|
remaining_seconds = int((locked_until - current_time_utc).total_seconds())
|
496
|
-
self._log_login_attempt(
|
513
|
+
self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', 'account_locked', user_id)
|
497
514
|
self._record_ip_failure(ip_address, 'login')
|
498
515
|
return {
|
499
516
|
'success': False,
|
@@ -514,7 +531,7 @@ class StandaloneAuthManager:
|
|
514
531
|
UPDATE users SET login_attempts = %s, locked_until = %s WHERE id = %s
|
515
532
|
''', (login_attempts, locked_until, user_id))
|
516
533
|
|
517
|
-
self._log_login_attempt(
|
534
|
+
self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', f'password_incorrect_locked_{lockout_duration}s', user_id)
|
518
535
|
self._record_ip_failure(ip_address, 'login')
|
519
536
|
|
520
537
|
return {
|
@@ -528,14 +545,14 @@ class StandaloneAuthManager:
|
|
528
545
|
UPDATE users SET login_attempts = %s WHERE id = %s
|
529
546
|
''', (login_attempts, user_id))
|
530
547
|
|
531
|
-
self._log_login_attempt(
|
548
|
+
self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', f'password_incorrect_attempt_{login_attempts}', user_id)
|
532
549
|
self._record_ip_failure(ip_address, 'login')
|
533
550
|
|
534
551
|
remaining_attempts = self.auth_config['max_login_attempts'] - login_attempts
|
535
552
|
return {
|
536
553
|
'success': False,
|
537
554
|
'error': 'invalid_credentials',
|
538
|
-
'message': f'
|
555
|
+
'message': f'用户名/邮箱或密码错误,还可以尝试 {remaining_attempts} 次',
|
539
556
|
'remaining_attempts': remaining_attempts
|
540
557
|
}
|
541
558
|
|
@@ -545,13 +562,14 @@ class StandaloneAuthManager:
|
|
545
562
|
''', (current_time_utc, user_id))
|
546
563
|
|
547
564
|
# 记录成功登录
|
548
|
-
self._log_login_attempt(
|
565
|
+
self._log_login_attempt(username_or_email, ip_address, user_agent, 'success', None, user_id)
|
549
566
|
self._reset_ip_failures(ip_address, 'login')
|
550
567
|
|
551
568
|
return {
|
552
569
|
'success': True,
|
553
570
|
'user_id': user_id,
|
554
|
-
'username':
|
571
|
+
'username': username,
|
572
|
+
'email': email,
|
555
573
|
'role': role,
|
556
574
|
'permissions': self._safe_json_parse(permissions),
|
557
575
|
'last_login': current_time_utc.isoformat()
|
@@ -897,28 +915,32 @@ class StandaloneAuthManager:
|
|
897
915
|
cursor.close()
|
898
916
|
conn.close()
|
899
917
|
|
900
|
-
def request_password_reset(self, username):
|
901
|
-
"""请求密码重置"""
|
918
|
+
def request_password_reset(self, username, email):
|
919
|
+
"""请求密码重置 - 需要同时验证用户名和邮箱"""
|
902
920
|
conn = self.pool.connection()
|
903
921
|
cursor = conn.cursor()
|
904
922
|
|
905
923
|
try:
|
906
|
-
# 查找用户
|
924
|
+
# 查找用户 - 必须用户名和邮箱都匹配
|
907
925
|
cursor.execute('''
|
908
|
-
SELECT id, username, is_active
|
909
|
-
FROM users WHERE username = %s
|
910
|
-
''', (username,))
|
926
|
+
SELECT id, username, email, is_active
|
927
|
+
FROM users WHERE username = %s AND email = %s
|
928
|
+
''', (username, email))
|
911
929
|
|
912
930
|
user = cursor.fetchone()
|
913
931
|
if not user:
|
914
|
-
#
|
932
|
+
# 为安全起见,统一返回错误信息
|
915
933
|
return {
|
916
|
-
'success':
|
917
|
-
'message': '
|
934
|
+
'success': False,
|
935
|
+
'message': '用户名或邮箱不正确'
|
918
936
|
}
|
919
937
|
|
920
938
|
if not user['is_active']:
|
921
|
-
|
939
|
+
# 账户被禁用,也返回用户名或邮箱不正确
|
940
|
+
return {
|
941
|
+
'success': False,
|
942
|
+
'message': '用户名或邮箱不正确'
|
943
|
+
}
|
922
944
|
|
923
945
|
# 生成重置令牌
|
924
946
|
reset_token = secrets.token_urlsafe(32)
|
@@ -931,11 +953,19 @@ class StandaloneAuthManager:
|
|
931
953
|
WHERE id = %s
|
932
954
|
''', (reset_token, expires_at, user['id']))
|
933
955
|
|
956
|
+
# 邮箱脱敏处理
|
957
|
+
masked_email = self._mask_email(user['email'])
|
958
|
+
|
959
|
+
# 直接返回重置令牌给前端
|
934
960
|
return {
|
935
961
|
'success': True,
|
936
|
-
'message': '
|
937
|
-
'
|
938
|
-
|
962
|
+
'message': f'验证成功,重置令牌已生成(邮箱:{masked_email})',
|
963
|
+
'data': {
|
964
|
+
'reset_token': reset_token,
|
965
|
+
'masked_email': masked_email,
|
966
|
+
'username': user['username'],
|
967
|
+
'expires_at': expires_at.isoformat()
|
968
|
+
}
|
939
969
|
}
|
940
970
|
|
941
971
|
except Exception as e:
|
@@ -1215,6 +1245,33 @@ class StandaloneAuthManager:
|
|
1215
1245
|
return []
|
1216
1246
|
return json_str or []
|
1217
1247
|
|
1248
|
+
def _mask_email(self, email):
|
1249
|
+
"""邮箱脱敏处理"""
|
1250
|
+
if not email or '@' not in email:
|
1251
|
+
return email
|
1252
|
+
|
1253
|
+
local_part, domain_part = email.split('@', 1)
|
1254
|
+
|
1255
|
+
# 处理本地部分(@前面的部分)
|
1256
|
+
if len(local_part) <= 3:
|
1257
|
+
# 短邮箱名,只显示第一个字符
|
1258
|
+
masked_local = local_part[0] + '***'
|
1259
|
+
else:
|
1260
|
+
# 长邮箱名,显示前3个字符
|
1261
|
+
masked_local = local_part[:3] + '***'
|
1262
|
+
|
1263
|
+
# 处理域名部分
|
1264
|
+
if '.' in domain_part:
|
1265
|
+
domain_name, domain_ext = domain_part.rsplit('.', 1)
|
1266
|
+
if len(domain_name) <= 2:
|
1267
|
+
masked_domain = '***.' + domain_ext
|
1268
|
+
else:
|
1269
|
+
masked_domain = domain_name[:2] + '***.' + domain_ext
|
1270
|
+
else:
|
1271
|
+
masked_domain = '***'
|
1272
|
+
|
1273
|
+
return f"{masked_local}@{masked_domain}"
|
1274
|
+
|
1218
1275
|
|
1219
1276
|
# Flask集成装饰器
|
1220
1277
|
def require_auth(auth_manager):
|
@@ -1387,7 +1444,7 @@ def main():
|
|
1387
1444
|
auth_manager = StandaloneAuthManager(db_config, auth_config)
|
1388
1445
|
|
1389
1446
|
# 注册用户
|
1390
|
-
result = auth_manager.register_user('admin', 'password123', 'admin', ['read', 'write', 'admin'])
|
1447
|
+
result = auth_manager.register_user('admin', 'password123', 'admin', ['read', 'write', 'admin'], 'admin@example.com')
|
1391
1448
|
print("注册结果:", result)
|
1392
1449
|
|
1393
1450
|
# 用户认证
|
@@ -1,7 +1,7 @@
|
|
1
1
|
mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
|
2
|
-
mdbq/__version__.py,sha256=
|
2
|
+
mdbq/__version__.py,sha256=pEZWv3rS-jyg5fbC-o4UV_JxSAfkN0r2M3A0QEOiAhg,18
|
3
3
|
mdbq/auth/__init__.py,sha256=pnPMAt63sh1B6kEvmutUuro46zVf2v2YDAG7q-jV_To,24
|
4
|
-
mdbq/auth/auth_backend.py,sha256=
|
4
|
+
mdbq/auth/auth_backend.py,sha256=F5EPVmNRiOs3G2ZOFI9fTdW1OgOjL-yeBonvOj91jZM,59702
|
5
5
|
mdbq/auth/rate_limiter.py,sha256=e7K8pMQlZ1vm1N-c0HBH8tbAwzcmXSRiAl81JNZ369g,26192
|
6
6
|
mdbq/js/__init__.py,sha256=hpMi3_ZKwIWkzc0LnKL-SY9AS-7PYFHq0izYTgEvxjc,30
|
7
7
|
mdbq/js/jc.py,sha256=FOc6HOOTJwnoZLZmgmaE1SQo9rUnVhXmefhKMD2MlDA,13229
|
@@ -33,7 +33,7 @@ mdbq/route/routes.py,sha256=DHJg0eRNi7TKqhCHuu8ia3vdQ8cTKwrTm6mwDBtNboM,19111
|
|
33
33
|
mdbq/selenium/__init__.py,sha256=AKzeEceqZyvqn2dEDoJSzDQnbuENkJSHAlbHAD0u0ZI,10
|
34
34
|
mdbq/selenium/get_driver.py,sha256=1NTlVUE6QsyjTrVVVqTO2LOnYf578ccFWlWnvIXGtic,20903
|
35
35
|
mdbq/spider/__init__.py,sha256=RBMFXGy_jd1HXZhngB2T2XTvJqki8P_Fr-pBcwijnew,18
|
36
|
-
mdbq-4.0.
|
37
|
-
mdbq-4.0.
|
38
|
-
mdbq-4.0.
|
39
|
-
mdbq-4.0.
|
36
|
+
mdbq-4.0.92.dist-info/METADATA,sha256=CvQCEeGo3DkG2bSvOKjDqfL17JWVLN1s_n75zXP7oSs,364
|
37
|
+
mdbq-4.0.92.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
38
|
+
mdbq-4.0.92.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
|
39
|
+
mdbq-4.0.92.dist-info/RECORD,,
|
File without changes
|
File without changes
|