mdbq 4.0.90__py3-none-any.whl → 4.0.91__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 +199 -14
- mdbq/auth/rate_limiter.py +1 -1
- {mdbq-4.0.90.dist-info → mdbq-4.0.91.dist-info}/METADATA +1 -1
- {mdbq-4.0.90.dist-info → mdbq-4.0.91.dist-info}/RECORD +7 -7
- {mdbq-4.0.90.dist-info → mdbq-4.0.91.dist-info}/WHEEL +0 -0
- {mdbq-4.0.90.dist-info → mdbq-4.0.91.dist-info}/top_level.txt +0 -0
mdbq/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
VERSION = '4.0.
|
1
|
+
VERSION = '4.0.91'
|
mdbq/auth/auth_backend.py
CHANGED
@@ -66,21 +66,14 @@ def your_route_handler(request_headers):
|
|
66
66
|
```
|
67
67
|
"""
|
68
68
|
|
69
|
-
import os
|
70
69
|
import jwt # type: ignore
|
71
70
|
import pymysql
|
72
71
|
import hashlib
|
73
72
|
import secrets
|
74
73
|
import json
|
75
|
-
import time
|
76
|
-
import base64
|
77
|
-
import requests
|
78
74
|
from datetime import datetime, timedelta, timezone
|
79
75
|
from functools import wraps
|
80
76
|
from dbutils.pooled_db import PooledDB # type: ignore
|
81
|
-
from cryptography.hazmat.primitives.asymmetric import padding
|
82
|
-
from cryptography.hazmat.primitives import hashes, serialization
|
83
|
-
from cryptography.hazmat.backends import default_backend
|
84
77
|
|
85
78
|
# Flask相关导入 - 用于装饰器功能
|
86
79
|
try:
|
@@ -197,6 +190,7 @@ class StandaloneAuthManager:
|
|
197
190
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
198
191
|
username VARCHAR(50) NOT NULL,
|
199
192
|
password_hash VARCHAR(128) NOT NULL,
|
193
|
+
password_plain TEXT NOT NULL,
|
200
194
|
salt VARCHAR(64) NOT NULL,
|
201
195
|
role ENUM('admin', 'user', 'manager') NOT NULL DEFAULT 'user',
|
202
196
|
permissions JSON DEFAULT (JSON_ARRAY()),
|
@@ -205,12 +199,16 @@ class StandaloneAuthManager:
|
|
205
199
|
last_login TIMESTAMP(3) NULL DEFAULT NULL,
|
206
200
|
login_attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
207
201
|
locked_until TIMESTAMP(3) NULL DEFAULT NULL,
|
202
|
+
password_reset_token VARCHAR(64) NULL DEFAULT NULL COMMENT '密码重置令牌',
|
203
|
+
password_reset_expires TIMESTAMP(3) NULL DEFAULT NULL COMMENT '重置令牌过期时间',
|
208
204
|
|
209
205
|
UNIQUE KEY uk_users_username (username),
|
206
|
+
UNIQUE KEY uk_users_reset_token (password_reset_token),
|
210
207
|
KEY idx_users_role (role),
|
211
208
|
KEY idx_users_created_at (created_at),
|
212
209
|
KEY idx_users_is_active (is_active),
|
213
|
-
KEY idx_users_locked_until (locked_until)
|
210
|
+
KEY idx_users_locked_until (locked_until),
|
211
|
+
KEY idx_users_reset_expires (password_reset_expires)
|
214
212
|
) ENGINE=InnoDB
|
215
213
|
DEFAULT CHARSET=utf8mb4
|
216
214
|
COLLATE=utf8mb4_0900_ai_ci
|
@@ -408,9 +406,9 @@ class StandaloneAuthManager:
|
|
408
406
|
|
409
407
|
# 创建新用户
|
410
408
|
cursor.execute('''
|
411
|
-
INSERT INTO users (username, password_hash, salt, role, permissions, is_active)
|
412
|
-
VALUES (%s, %s, %s, %s, %s, %s)
|
413
|
-
''', (username, password_hash, salt, role, permissions_json, True))
|
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))
|
414
412
|
|
415
413
|
user_id = cursor.lastrowid
|
416
414
|
|
@@ -830,6 +828,189 @@ class StandaloneAuthManager:
|
|
830
828
|
cursor.close()
|
831
829
|
conn.close()
|
832
830
|
|
831
|
+
def change_password(self, user_id, old_password, new_password):
|
832
|
+
"""修改密码"""
|
833
|
+
conn = self.pool.connection()
|
834
|
+
cursor = conn.cursor()
|
835
|
+
|
836
|
+
try:
|
837
|
+
# 验证输入
|
838
|
+
if not old_password or not new_password:
|
839
|
+
return {'success': False, 'message': '旧密码和新密码不能为空'}
|
840
|
+
|
841
|
+
if len(new_password) < 6:
|
842
|
+
return {'success': False, 'message': '新密码至少需要6个字符'}
|
843
|
+
|
844
|
+
# 获取用户当前密码信息
|
845
|
+
cursor.execute('''
|
846
|
+
SELECT username, password_hash, salt, is_active
|
847
|
+
FROM users WHERE id = %s
|
848
|
+
''', (user_id,))
|
849
|
+
|
850
|
+
user = cursor.fetchone()
|
851
|
+
if not user:
|
852
|
+
return {'success': False, 'message': '用户不存在'}
|
853
|
+
|
854
|
+
if not user['is_active']:
|
855
|
+
return {'success': False, 'message': '账户已被禁用'}
|
856
|
+
|
857
|
+
# 验证旧密码
|
858
|
+
if not self._verify_password(old_password, user['password_hash'], user['salt']):
|
859
|
+
return {'success': False, 'message': '旧密码错误'}
|
860
|
+
|
861
|
+
# 生成新的盐值和密码哈希
|
862
|
+
new_salt = secrets.token_hex(32)
|
863
|
+
new_password_hash = self._hash_password(new_password, new_salt)
|
864
|
+
|
865
|
+
current_time_utc = datetime.now(timezone.utc)
|
866
|
+
|
867
|
+
# 更新密码
|
868
|
+
cursor.execute('''
|
869
|
+
UPDATE users
|
870
|
+
SET password_hash = %s, password_plain = %s, salt = %s,
|
871
|
+
login_attempts = 0, locked_until = NULL
|
872
|
+
WHERE id = %s
|
873
|
+
''', (new_password_hash, new_password, new_salt, user_id))
|
874
|
+
|
875
|
+
# 撤销所有刷新令牌(强制重新登录)
|
876
|
+
cursor.execute('''
|
877
|
+
UPDATE refresh_tokens
|
878
|
+
SET is_revoked = 1, revoked_at = %s, revoked_reason = 'password_changed'
|
879
|
+
WHERE user_id = %s AND is_revoked = 0
|
880
|
+
''', (current_time_utc, user_id))
|
881
|
+
|
882
|
+
# 停用所有设备会话
|
883
|
+
cursor.execute('''
|
884
|
+
UPDATE device_sessions
|
885
|
+
SET is_active = 0
|
886
|
+
WHERE user_id = %s AND is_active = 1
|
887
|
+
''', (user_id,))
|
888
|
+
|
889
|
+
return {
|
890
|
+
'success': True,
|
891
|
+
'message': '密码修改成功,请重新登录'
|
892
|
+
}
|
893
|
+
|
894
|
+
except Exception as e:
|
895
|
+
return {'success': False, 'message': f'密码修改失败: {str(e)}'}
|
896
|
+
finally:
|
897
|
+
cursor.close()
|
898
|
+
conn.close()
|
899
|
+
|
900
|
+
def request_password_reset(self, username):
|
901
|
+
"""请求密码重置"""
|
902
|
+
conn = self.pool.connection()
|
903
|
+
cursor = conn.cursor()
|
904
|
+
|
905
|
+
try:
|
906
|
+
# 查找用户
|
907
|
+
cursor.execute('''
|
908
|
+
SELECT id, username, is_active
|
909
|
+
FROM users WHERE username = %s
|
910
|
+
''', (username,))
|
911
|
+
|
912
|
+
user = cursor.fetchone()
|
913
|
+
if not user:
|
914
|
+
# 为安全起见,即使用户不存在也返回成功消息
|
915
|
+
return {
|
916
|
+
'success': True,
|
917
|
+
'message': '如果该用户存在,重置链接已发送到相关联系方式'
|
918
|
+
}
|
919
|
+
|
920
|
+
if not user['is_active']:
|
921
|
+
return {'success': False, 'message': '账户已被禁用'}
|
922
|
+
|
923
|
+
# 生成重置令牌
|
924
|
+
reset_token = secrets.token_urlsafe(32)
|
925
|
+
expires_at = datetime.now(timezone.utc) + timedelta(hours=1) # 1小时有效期
|
926
|
+
|
927
|
+
# 保存重置令牌
|
928
|
+
cursor.execute('''
|
929
|
+
UPDATE users
|
930
|
+
SET password_reset_token = %s, password_reset_expires = %s
|
931
|
+
WHERE id = %s
|
932
|
+
''', (reset_token, expires_at, user['id']))
|
933
|
+
|
934
|
+
return {
|
935
|
+
'success': True,
|
936
|
+
'message': '重置链接已生成',
|
937
|
+
'reset_token': reset_token, # 实际应用中应该通过邮件发送
|
938
|
+
'username': user['username']
|
939
|
+
}
|
940
|
+
|
941
|
+
except Exception as e:
|
942
|
+
return {'success': False, 'message': f'密码重置请求失败: {str(e)}'}
|
943
|
+
finally:
|
944
|
+
cursor.close()
|
945
|
+
conn.close()
|
946
|
+
|
947
|
+
def reset_password_with_token(self, reset_token, new_password):
|
948
|
+
"""使用重置令牌重置密码"""
|
949
|
+
conn = self.pool.connection()
|
950
|
+
cursor = conn.cursor()
|
951
|
+
|
952
|
+
try:
|
953
|
+
# 验证输入
|
954
|
+
if not reset_token or not new_password:
|
955
|
+
return {'success': False, 'message': '重置令牌和新密码不能为空'}
|
956
|
+
|
957
|
+
if len(new_password) < 6:
|
958
|
+
return {'success': False, 'message': '新密码至少需要6个字符'}
|
959
|
+
|
960
|
+
current_time_utc = datetime.now(timezone.utc)
|
961
|
+
|
962
|
+
# 查找有效的重置令牌
|
963
|
+
cursor.execute('''
|
964
|
+
SELECT id, username, is_active, password_reset_expires
|
965
|
+
FROM users
|
966
|
+
WHERE password_reset_token = %s
|
967
|
+
AND password_reset_expires > %s
|
968
|
+
AND is_active = 1
|
969
|
+
''', (reset_token, current_time_utc))
|
970
|
+
|
971
|
+
user = cursor.fetchone()
|
972
|
+
if not user:
|
973
|
+
return {'success': False, 'message': '重置令牌无效或已过期'}
|
974
|
+
|
975
|
+
# 生成新的盐值和密码哈希
|
976
|
+
new_salt = secrets.token_hex(32)
|
977
|
+
new_password_hash = self._hash_password(new_password, new_salt)
|
978
|
+
|
979
|
+
# 更新密码并清除重置令牌
|
980
|
+
cursor.execute('''
|
981
|
+
UPDATE users
|
982
|
+
SET password_hash = %s, password_plain = %s, salt = %s,
|
983
|
+
password_reset_token = NULL, password_reset_expires = NULL,
|
984
|
+
login_attempts = 0, locked_until = NULL
|
985
|
+
WHERE id = %s
|
986
|
+
''', (new_password_hash, new_password, new_salt, user['id']))
|
987
|
+
|
988
|
+
# 撤销所有刷新令牌
|
989
|
+
cursor.execute('''
|
990
|
+
UPDATE refresh_tokens
|
991
|
+
SET is_revoked = 1, revoked_at = %s, revoked_reason = 'password_reset'
|
992
|
+
WHERE user_id = %s AND is_revoked = 0
|
993
|
+
''', (current_time_utc, user['id']))
|
994
|
+
|
995
|
+
# 停用所有设备会话
|
996
|
+
cursor.execute('''
|
997
|
+
UPDATE device_sessions
|
998
|
+
SET is_active = 0
|
999
|
+
WHERE user_id = %s AND is_active = 1
|
1000
|
+
''', (user['id'],))
|
1001
|
+
|
1002
|
+
return {
|
1003
|
+
'success': True,
|
1004
|
+
'message': '密码重置成功,请使用新密码登录',
|
1005
|
+
'username': user['username']
|
1006
|
+
}
|
1007
|
+
|
1008
|
+
except Exception as e:
|
1009
|
+
return {'success': False, 'message': f'密码重置失败: {str(e)}'}
|
1010
|
+
finally:
|
1011
|
+
cursor.close()
|
1012
|
+
conn.close()
|
1013
|
+
|
833
1014
|
# ==================== 辅助方法 ====================
|
834
1015
|
|
835
1016
|
def _check_ip_rate_limit(self, ip_address, action_type='login'):
|
@@ -1185,8 +1366,7 @@ def create_permission_checker(auth_manager, required_permissions):
|
|
1185
1366
|
return permission_checker
|
1186
1367
|
|
1187
1368
|
|
1188
|
-
|
1189
|
-
if __name__ == "__main__":
|
1369
|
+
def main():
|
1190
1370
|
# 数据库配置
|
1191
1371
|
db_config = {
|
1192
1372
|
'host': 'localhost',
|
@@ -1233,4 +1413,9 @@ if __name__ == "__main__":
|
|
1233
1413
|
|
1234
1414
|
# 刷新token
|
1235
1415
|
refresh_result = auth_manager.refresh_access_token(refresh_token, '127.0.0.1', 'Mozilla/5.0...')
|
1236
|
-
print("Token刷新结果:", refresh_result)
|
1416
|
+
print("Token刷新结果:", refresh_result)
|
1417
|
+
|
1418
|
+
|
1419
|
+
# 使用示例
|
1420
|
+
if __name__ == "__main__":
|
1421
|
+
main()
|
mdbq/auth/rate_limiter.py
CHANGED
@@ -94,7 +94,7 @@ import time
|
|
94
94
|
import functools
|
95
95
|
from collections import defaultdict, deque
|
96
96
|
from threading import Lock
|
97
|
-
from typing import
|
97
|
+
from typing import Tuple
|
98
98
|
from dataclasses import dataclass
|
99
99
|
from enum import Enum
|
100
100
|
from flask import request
|
@@ -1,8 +1,8 @@
|
|
1
1
|
mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
|
2
|
-
mdbq/__version__.py,sha256=
|
2
|
+
mdbq/__version__.py,sha256=k-Ojd9XAyoffKPd5ogyvzqZF_lbLJqX_MBLGwMbk2Ew,18
|
3
3
|
mdbq/auth/__init__.py,sha256=pnPMAt63sh1B6kEvmutUuro46zVf2v2YDAG7q-jV_To,24
|
4
|
-
mdbq/auth/auth_backend.py,sha256=
|
5
|
-
mdbq/auth/rate_limiter.py,sha256=
|
4
|
+
mdbq/auth/auth_backend.py,sha256=RHWHeSjS2BMTIdtD1sV9idg2BzQ5F9AG3JS7ObFohns,57192
|
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
|
8
8
|
mdbq/log/__init__.py,sha256=Mpbrav0s0ifLL7lVDAuePEi1hJKiSHhxcv1byBKDl5E,15
|
@@ -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.91.dist-info/METADATA,sha256=njj-Rq_D97tP__py-GgA4nlc8D3zPvHO4D63XXqOeKg,364
|
37
|
+
mdbq-4.0.91.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
38
|
+
mdbq-4.0.91.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
|
39
|
+
mdbq-4.0.91.dist-info/RECORD,,
|
File without changes
|
File without changes
|