mdbq 4.0.102__py3-none-any.whl → 4.0.104__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.
Potentially problematic release.
This version of mdbq might be problematic. Click here for more details.
- mdbq/__version__.py +1 -1
- mdbq/auth/auth_backend.py +159 -25
- {mdbq-4.0.102.dist-info → mdbq-4.0.104.dist-info}/METADATA +1 -1
- {mdbq-4.0.102.dist-info → mdbq-4.0.104.dist-info}/RECORD +6 -6
- {mdbq-4.0.102.dist-info → mdbq-4.0.104.dist-info}/WHEEL +0 -0
- {mdbq-4.0.102.dist-info → mdbq-4.0.104.dist-info}/top_level.txt +0 -0
mdbq/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = '4.0.
|
|
1
|
+
VERSION = '4.0.104'
|
mdbq/auth/auth_backend.py
CHANGED
|
@@ -147,8 +147,8 @@ class StandaloneAuthManager:
|
|
|
147
147
|
ping=1,
|
|
148
148
|
charset='utf8mb4',
|
|
149
149
|
cursorclass=pymysql.cursors.DictCursor,
|
|
150
|
-
autocommit=True
|
|
151
|
-
|
|
150
|
+
autocommit=True # 启用自动提交,避免锁冲突
|
|
151
|
+
# 使用系统本地时区,不强制设置UTC
|
|
152
152
|
)
|
|
153
153
|
|
|
154
154
|
except Exception as e:
|
|
@@ -515,15 +515,15 @@ class StandaloneAuthManager:
|
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
# 检查账户锁定状态
|
|
518
|
-
|
|
518
|
+
current_time = datetime.now()
|
|
519
519
|
if locked_until:
|
|
520
520
|
if locked_until.tzinfo is None:
|
|
521
521
|
locked_until = locked_until.replace(tzinfo=timezone.utc)
|
|
522
522
|
elif locked_until.tzinfo != timezone.utc:
|
|
523
523
|
locked_until = locked_until.astimezone(timezone.utc)
|
|
524
524
|
|
|
525
|
-
if locked_until >
|
|
526
|
-
remaining_seconds = int((locked_until -
|
|
525
|
+
if locked_until > current_time:
|
|
526
|
+
remaining_seconds = int((locked_until - current_time).total_seconds())
|
|
527
527
|
self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', 'account_locked', user_id)
|
|
528
528
|
self._record_ip_failure(ip_address, 'login')
|
|
529
529
|
return {
|
|
@@ -540,7 +540,7 @@ class StandaloneAuthManager:
|
|
|
540
540
|
|
|
541
541
|
if login_attempts >= self.auth_config['max_login_attempts']:
|
|
542
542
|
lockout_duration = self.auth_config['lockout_duration']
|
|
543
|
-
locked_until =
|
|
543
|
+
locked_until = current_time + timedelta(seconds=lockout_duration)
|
|
544
544
|
cursor.execute('''
|
|
545
545
|
UPDATE users SET login_attempts = %s, locked_until = %s WHERE id = %s
|
|
546
546
|
''', (login_attempts, locked_until, user_id))
|
|
@@ -573,7 +573,7 @@ class StandaloneAuthManager:
|
|
|
573
573
|
# 登录成功,重置尝试次数
|
|
574
574
|
cursor.execute('''
|
|
575
575
|
UPDATE users SET login_attempts = 0, locked_until = NULL, last_login = %s WHERE id = %s
|
|
576
|
-
''', (
|
|
576
|
+
''', (current_time, user_id))
|
|
577
577
|
|
|
578
578
|
# 记录成功登录
|
|
579
579
|
self._log_login_attempt(username_or_email, ip_address, user_agent, 'success', None, user_id)
|
|
@@ -586,7 +586,7 @@ class StandaloneAuthManager:
|
|
|
586
586
|
'email': email,
|
|
587
587
|
'role': role,
|
|
588
588
|
'permissions': self._safe_json_parse(permissions),
|
|
589
|
-
'last_login':
|
|
589
|
+
'last_login': current_time.isoformat()
|
|
590
590
|
}
|
|
591
591
|
|
|
592
592
|
finally:
|
|
@@ -595,16 +595,16 @@ class StandaloneAuthManager:
|
|
|
595
595
|
|
|
596
596
|
def generate_access_token(self, user_info):
|
|
597
597
|
"""生成访问令牌"""
|
|
598
|
-
|
|
599
|
-
|
|
598
|
+
now = datetime.now()
|
|
599
|
+
exp = now + timedelta(seconds=self.auth_config['access_token_expires'])
|
|
600
600
|
|
|
601
601
|
payload = {
|
|
602
602
|
'user_id': user_info['user_id'],
|
|
603
603
|
'username': user_info['username'],
|
|
604
604
|
'role': user_info['role'],
|
|
605
605
|
'permissions': user_info['permissions'],
|
|
606
|
-
'iat': int(
|
|
607
|
-
'exp': int(
|
|
606
|
+
'iat': int(now.timestamp()),
|
|
607
|
+
'exp': int(exp.timestamp()),
|
|
608
608
|
'type': 'access'
|
|
609
609
|
}
|
|
610
610
|
|
|
@@ -646,7 +646,7 @@ class StandaloneAuthManager:
|
|
|
646
646
|
cursor = conn.cursor()
|
|
647
647
|
|
|
648
648
|
try:
|
|
649
|
-
|
|
649
|
+
current_time = datetime.now()
|
|
650
650
|
|
|
651
651
|
# 软删除:将该用户在该域名下的相同设备指纹的所有活跃会话标记为非活跃
|
|
652
652
|
cursor.execute('''
|
|
@@ -702,7 +702,7 @@ class StandaloneAuthManager:
|
|
|
702
702
|
full_device_info.get('timezone_offset'),
|
|
703
703
|
full_device_info.get('language'),
|
|
704
704
|
full_device_info.get('hardware_concurrency'),
|
|
705
|
-
ip_address, ip_address, user_agent,
|
|
705
|
+
ip_address, ip_address, user_agent, current_time))
|
|
706
706
|
|
|
707
707
|
device_session_id = cursor.lastrowid
|
|
708
708
|
|
|
@@ -716,9 +716,9 @@ class StandaloneAuthManager:
|
|
|
716
716
|
"""生成刷新令牌"""
|
|
717
717
|
token = secrets.token_urlsafe(64)
|
|
718
718
|
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
|
719
|
-
|
|
720
|
-
expires_at =
|
|
721
|
-
absolute_expires_at =
|
|
719
|
+
current_time = datetime.now()
|
|
720
|
+
expires_at = current_time + timedelta(seconds=self.auth_config['refresh_token_expires'])
|
|
721
|
+
absolute_expires_at = current_time + timedelta(days=self.auth_config['absolute_refresh_expires_days'])
|
|
722
722
|
|
|
723
723
|
conn = self.pool.connection()
|
|
724
724
|
cursor = conn.cursor()
|
|
@@ -737,7 +737,7 @@ class StandaloneAuthManager:
|
|
|
737
737
|
rotation_count, max_rotations, created_at
|
|
738
738
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
739
739
|
''', (token_hash, token, user_info['user_id'], device_session_id, expires_at,
|
|
740
|
-
absolute_expires_at, 0, self.auth_config['max_refresh_rotations'],
|
|
740
|
+
absolute_expires_at, 0, self.auth_config['max_refresh_rotations'], current_time))
|
|
741
741
|
|
|
742
742
|
return token
|
|
743
743
|
|
|
@@ -758,8 +758,7 @@ class StandaloneAuthManager:
|
|
|
758
758
|
|
|
759
759
|
try:
|
|
760
760
|
# 验证刷新令牌
|
|
761
|
-
|
|
762
|
-
current_time_naive = current_time_utc.replace(tzinfo=None)
|
|
761
|
+
current_time = datetime.now()
|
|
763
762
|
|
|
764
763
|
cursor.execute('''
|
|
765
764
|
SELECT rt.user_id, rt.device_session_id, rt.expires_at, rt.absolute_expires_at,
|
|
@@ -775,7 +774,7 @@ class StandaloneAuthManager:
|
|
|
775
774
|
AND rt.rotation_count < rt.max_rotations
|
|
776
775
|
AND rt.is_revoked = 0
|
|
777
776
|
AND ds.is_active = 1
|
|
778
|
-
''', (token_hash,
|
|
777
|
+
''', (token_hash, current_time, current_time))
|
|
779
778
|
|
|
780
779
|
result = cursor.fetchone()
|
|
781
780
|
|
|
@@ -787,7 +786,7 @@ class StandaloneAuthManager:
|
|
|
787
786
|
UPDATE device_sessions
|
|
788
787
|
SET last_activity = %s
|
|
789
788
|
WHERE id = %s
|
|
790
|
-
''', (
|
|
789
|
+
''', (current_time, result['device_session_id']))
|
|
791
790
|
|
|
792
791
|
# 生成新的tokens
|
|
793
792
|
user_info = {
|
|
@@ -803,7 +802,7 @@ class StandaloneAuthManager:
|
|
|
803
802
|
# 生成新的refresh token(轮换)
|
|
804
803
|
new_token = secrets.token_urlsafe(64)
|
|
805
804
|
new_token_hash = hashlib.sha256(new_token.encode()).hexdigest()
|
|
806
|
-
token_expires_at =
|
|
805
|
+
token_expires_at = current_time + timedelta(seconds=self.auth_config['refresh_token_expires'])
|
|
807
806
|
|
|
808
807
|
# 删除旧token并插入新token
|
|
809
808
|
cursor.execute('''
|
|
@@ -818,7 +817,7 @@ class StandaloneAuthManager:
|
|
|
818
817
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
819
818
|
''', (new_token_hash, new_token, result['user_id'], result['device_session_id'],
|
|
820
819
|
token_expires_at, result['absolute_expires_at'],
|
|
821
|
-
result['rotation_count'] + 1, result['max_rotations'],
|
|
820
|
+
result['rotation_count'] + 1, result['max_rotations'], current_time))
|
|
822
821
|
|
|
823
822
|
return {
|
|
824
823
|
'access_token': access_token,
|
|
@@ -1013,7 +1012,7 @@ class StandaloneAuthManager:
|
|
|
1013
1012
|
|
|
1014
1013
|
# 生成重置令牌
|
|
1015
1014
|
reset_token = secrets.token_urlsafe(32)
|
|
1016
|
-
expires_at = datetime.now(
|
|
1015
|
+
expires_at = datetime.now() + timedelta(hours=1) # 1小时有效期
|
|
1017
1016
|
|
|
1018
1017
|
# 保存重置令牌
|
|
1019
1018
|
cursor.execute('''
|
|
@@ -1429,6 +1428,7 @@ class StandaloneAuthManager:
|
|
|
1429
1428
|
|
|
1430
1429
|
def logout_device(self, user_id, device_id=None, ip_address=None, user_agent=None, device_info=None, login_domain=''):
|
|
1431
1430
|
"""
|
|
1431
|
+
【已废弃】
|
|
1432
1432
|
登出设备(支持两种识别方式)
|
|
1433
1433
|
|
|
1434
1434
|
Args:
|
|
@@ -1522,6 +1522,140 @@ class StandaloneAuthManager:
|
|
|
1522
1522
|
cursor.close()
|
|
1523
1523
|
conn.close()
|
|
1524
1524
|
|
|
1525
|
+
def logout_current_device(self, user_id, access_token=None):
|
|
1526
|
+
"""
|
|
1527
|
+
退出当前设备(通过token识别)
|
|
1528
|
+
|
|
1529
|
+
Args:
|
|
1530
|
+
user_id: 用户ID
|
|
1531
|
+
access_token: 当前的访问令牌(用于识别设备会话)
|
|
1532
|
+
|
|
1533
|
+
Returns:
|
|
1534
|
+
dict: 登出结果
|
|
1535
|
+
"""
|
|
1536
|
+
conn = self.pool.connection()
|
|
1537
|
+
cursor = conn.cursor()
|
|
1538
|
+
|
|
1539
|
+
try:
|
|
1540
|
+
current_time_utc = datetime.now(timezone.utc)
|
|
1541
|
+
|
|
1542
|
+
if access_token:
|
|
1543
|
+
# 方式1:通过access_token解析出设备会话信息
|
|
1544
|
+
try:
|
|
1545
|
+
payload = self.verify_access_token(access_token)
|
|
1546
|
+
if not payload:
|
|
1547
|
+
return {'success': False, 'message': '无效的访问令牌'}
|
|
1548
|
+
|
|
1549
|
+
# 查找该用户最近活跃的设备(作为当前设备的近似)
|
|
1550
|
+
cursor.execute('''
|
|
1551
|
+
SELECT ds.id, ds.device_name, rt.id as token_id
|
|
1552
|
+
FROM device_sessions ds
|
|
1553
|
+
LEFT JOIN refresh_tokens rt ON ds.id = rt.device_session_id AND rt.is_revoked = 0
|
|
1554
|
+
WHERE ds.user_id = %s AND ds.is_active = 1
|
|
1555
|
+
ORDER BY ds.last_activity DESC
|
|
1556
|
+
LIMIT 1
|
|
1557
|
+
''', (user_id,))
|
|
1558
|
+
|
|
1559
|
+
except Exception:
|
|
1560
|
+
# token解析失败,使用备用方案
|
|
1561
|
+
cursor.execute('''
|
|
1562
|
+
SELECT id, device_name FROM device_sessions
|
|
1563
|
+
WHERE user_id = %s AND is_active = 1
|
|
1564
|
+
ORDER BY last_activity DESC
|
|
1565
|
+
LIMIT 1
|
|
1566
|
+
''', (user_id,))
|
|
1567
|
+
else:
|
|
1568
|
+
# 方式2:没有token时,登出最新活跃的设备
|
|
1569
|
+
cursor.execute('''
|
|
1570
|
+
SELECT id, device_name FROM device_sessions
|
|
1571
|
+
WHERE user_id = %s AND is_active = 1
|
|
1572
|
+
ORDER BY last_activity DESC
|
|
1573
|
+
LIMIT 1
|
|
1574
|
+
''', (user_id,))
|
|
1575
|
+
|
|
1576
|
+
device = cursor.fetchone()
|
|
1577
|
+
|
|
1578
|
+
if not device:
|
|
1579
|
+
return {'success': False, 'message': '没有找到活跃的设备会话'}
|
|
1580
|
+
|
|
1581
|
+
device_session_id = device['id']
|
|
1582
|
+
device_name = device['device_name']
|
|
1583
|
+
|
|
1584
|
+
# 撤销该设备的刷新令牌
|
|
1585
|
+
cursor.execute('''
|
|
1586
|
+
UPDATE refresh_tokens
|
|
1587
|
+
SET is_revoked = 1, revoked_at = %s, revoked_reason = 'current_device_logout'
|
|
1588
|
+
WHERE device_session_id = %s AND is_revoked = 0
|
|
1589
|
+
''', (current_time_utc, device_session_id))
|
|
1590
|
+
|
|
1591
|
+
# 停用设备会话
|
|
1592
|
+
cursor.execute('''
|
|
1593
|
+
UPDATE device_sessions
|
|
1594
|
+
SET is_active = 0
|
|
1595
|
+
WHERE id = %s
|
|
1596
|
+
''', (device_session_id,))
|
|
1597
|
+
|
|
1598
|
+
return {'success': True, 'message': f'已成功退出当前设备 "{device_name}"'}
|
|
1599
|
+
|
|
1600
|
+
except Exception as e:
|
|
1601
|
+
return {'success': False, 'message': f'退出当前设备失败: {str(e)}'}
|
|
1602
|
+
finally:
|
|
1603
|
+
cursor.close()
|
|
1604
|
+
conn.close()
|
|
1605
|
+
|
|
1606
|
+
def logout_specific_device(self, user_id, device_id):
|
|
1607
|
+
"""
|
|
1608
|
+
退出指定设备(通过device_id)
|
|
1609
|
+
|
|
1610
|
+
Args:
|
|
1611
|
+
user_id: 用户ID
|
|
1612
|
+
device_id: 要退出的设备ID
|
|
1613
|
+
|
|
1614
|
+
Returns:
|
|
1615
|
+
dict: 登出结果
|
|
1616
|
+
"""
|
|
1617
|
+
conn = self.pool.connection()
|
|
1618
|
+
cursor = conn.cursor()
|
|
1619
|
+
|
|
1620
|
+
try:
|
|
1621
|
+
current_time_utc = datetime.now(timezone.utc)
|
|
1622
|
+
|
|
1623
|
+
# 通过device_id查找设备,确保属于该用户
|
|
1624
|
+
cursor.execute('''
|
|
1625
|
+
SELECT id, device_name FROM device_sessions
|
|
1626
|
+
WHERE user_id = %s AND device_id = %s AND is_active = 1
|
|
1627
|
+
''', (user_id, device_id))
|
|
1628
|
+
|
|
1629
|
+
device = cursor.fetchone()
|
|
1630
|
+
|
|
1631
|
+
if not device:
|
|
1632
|
+
return {'success': False, 'message': '设备不存在或已退出'}
|
|
1633
|
+
|
|
1634
|
+
device_session_id = device['id']
|
|
1635
|
+
device_name = device['device_name']
|
|
1636
|
+
|
|
1637
|
+
# 撤销该设备的刷新令牌
|
|
1638
|
+
cursor.execute('''
|
|
1639
|
+
UPDATE refresh_tokens
|
|
1640
|
+
SET is_revoked = 1, revoked_at = %s, revoked_reason = 'specific_device_logout'
|
|
1641
|
+
WHERE device_session_id = %s AND is_revoked = 0
|
|
1642
|
+
''', (current_time_utc, device_session_id))
|
|
1643
|
+
|
|
1644
|
+
# 停用设备会话
|
|
1645
|
+
cursor.execute('''
|
|
1646
|
+
UPDATE device_sessions
|
|
1647
|
+
SET is_active = 0
|
|
1648
|
+
WHERE id = %s
|
|
1649
|
+
''', (device_session_id,))
|
|
1650
|
+
|
|
1651
|
+
return {'success': True, 'message': f'已成功退出设备 "{device_name}"'}
|
|
1652
|
+
|
|
1653
|
+
except Exception as e:
|
|
1654
|
+
return {'success': False, 'message': f'退出指定设备失败: {str(e)}'}
|
|
1655
|
+
finally:
|
|
1656
|
+
cursor.close()
|
|
1657
|
+
conn.close()
|
|
1658
|
+
|
|
1525
1659
|
def get_user_profile_stats(self, user_id):
|
|
1526
1660
|
"""获取用户资料统计信息"""
|
|
1527
1661
|
conn = self.pool.connection()
|
|
@@ -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=kxyPyk2WXu3DvipWC0icSn0MFXbrNZr1qZab1hWurG8,19
|
|
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=P3kaQqnT1TdZEHMeR5_UDqGzlx1v-7BWdsD3Uu7YYzo,86787
|
|
5
5
|
mdbq/auth/rate_limiter.py,sha256=1m_Paxp8pDNpmyoFGRpFMVOJpbmeIvfVcfiQ2oH72qM,32850
|
|
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=QVGfTvDgu0CpcKCvk1ra74H8uojgqTLUav1fnVAqLEA,29433
|
|
|
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.104.dist-info/METADATA,sha256=EwEeO0P8TpW-CyVkRnnxB1FNSswUPgLik7H1jV8P4po,365
|
|
37
|
+
mdbq-4.0.104.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
38
|
+
mdbq-4.0.104.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
|
|
39
|
+
mdbq-4.0.104.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|