mdbq 4.0.122__tar.gz → 4.0.124__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.
Potentially problematic release.
This version of mdbq might be problematic. Click here for more details.
- {mdbq-4.0.122 → mdbq-4.0.124}/PKG-INFO +2 -2
- mdbq-4.0.124/mdbq/__version__.py +1 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/auth/auth_backend.py +106 -58
- mdbq-4.0.124/mdbq/auth/crypto.py +738 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq.egg-info/PKG-INFO +2 -2
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq.egg-info/SOURCES.txt +1 -0
- mdbq-4.0.122/mdbq/__version__.py +0 -1
- {mdbq-4.0.122 → mdbq-4.0.124}/README.txt +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/auth/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/auth/rate_limiter.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/js/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/js/jc.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/log/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/log/mylogger.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/myconf/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/myconf/myconf.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/mysql/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/mysql/deduplicator.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/mysql/mysql.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/mysql/s_query.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/mysql/unique_.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/mysql/uploader.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/other/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/other/download_sku_picture.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/other/error_handler.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/other/otk.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/other/pov_city.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/other/ua_sj.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/pbix/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/pbix/pbix_refresh.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/pbix/refresh_all.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/redis/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/redis/getredis.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/redis/redis_cache.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/route/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/route/analytics.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/route/monitor.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/route/routes.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/selenium/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/selenium/get_driver.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq/spider/__init__.py +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq.egg-info/dependency_links.txt +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/mdbq.egg-info/top_level.txt +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/setup.cfg +0 -0
- {mdbq-4.0.122 → mdbq-4.0.124}/setup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = '4.0.124'
|
|
@@ -15,62 +15,15 @@
|
|
|
15
15
|
- 装饰器功能: 需要Flask (pip install flask)
|
|
16
16
|
- 其他依赖: pymysql, PyJWT, cryptography, dbutils
|
|
17
17
|
|
|
18
|
-
使用方法:
|
|
19
|
-
|
|
20
|
-
1. 基础认证功能(无需Flask):
|
|
21
|
-
```python
|
|
22
|
-
# 初始化认证管理器
|
|
23
|
-
auth_manager = StandaloneAuthManager({
|
|
24
|
-
'host': 'localhost',
|
|
25
|
-
'port': 3306,
|
|
26
|
-
'user': 'root',
|
|
27
|
-
'password': 'password',
|
|
28
|
-
'database': 'auth_db'
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
# 用户认证
|
|
32
|
-
result = auth_manager.authenticate_user('username', 'password', '192.168.1.1', 'Mozilla/5.0...')
|
|
33
|
-
|
|
34
|
-
# 生成tokens
|
|
35
|
-
if result['success']:
|
|
36
|
-
device_session_id, device_id, device_name = auth_manager.create_or_update_device_session(
|
|
37
|
-
result['user_id'], '192.168.1.1', 'Mozilla/5.0...'
|
|
38
|
-
)
|
|
39
|
-
access_token = auth_manager.generate_access_token(result)
|
|
40
|
-
refresh_token = auth_manager.generate_refresh_token(result, device_session_id)
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
2. Flask装饰器使用(需要Flask):
|
|
44
|
-
```python
|
|
45
|
-
from flask import Flask, request
|
|
46
|
-
app = Flask(__name__)
|
|
47
|
-
|
|
48
|
-
@app.route('/api/protected')
|
|
49
|
-
@require_auth(auth_manager)
|
|
50
|
-
def protected_route():
|
|
51
|
-
return {'user': request.current_user['username']}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
3. 框架无关的中间件使用:
|
|
55
|
-
```python
|
|
56
|
-
# 创建认证中间件
|
|
57
|
-
auth_middleware = create_auth_middleware(auth_manager)
|
|
58
|
-
|
|
59
|
-
# 在任何框架中使用
|
|
60
|
-
def your_route_handler(request_headers):
|
|
61
|
-
user = auth_middleware(request_headers.get('Authorization'))
|
|
62
|
-
if user:
|
|
63
|
-
return {'message': f'Hello {user["username"]}'}
|
|
64
|
-
else:
|
|
65
|
-
return {'error': 'Unauthorized'}, 401
|
|
66
|
-
```
|
|
67
18
|
"""
|
|
68
19
|
|
|
20
|
+
from tkinter import N
|
|
69
21
|
import jwt # type: ignore
|
|
70
22
|
import pymysql
|
|
71
23
|
import hashlib
|
|
72
24
|
import secrets
|
|
73
25
|
import json
|
|
26
|
+
import re
|
|
74
27
|
from datetime import datetime, timedelta, timezone
|
|
75
28
|
from functools import wraps
|
|
76
29
|
from dbutils.pooled_db import PooledDB # type: ignore
|
|
@@ -130,7 +83,7 @@ class StandaloneAuthManager:
|
|
|
130
83
|
def _init_mysql_pool(self):
|
|
131
84
|
"""初始化MySQL连接池"""
|
|
132
85
|
try:
|
|
133
|
-
#
|
|
86
|
+
# 创建数据库
|
|
134
87
|
self._create_database_if_not_exists()
|
|
135
88
|
|
|
136
89
|
self.pool = PooledDB(
|
|
@@ -460,17 +413,14 @@ class StandaloneAuthManager:
|
|
|
460
413
|
if not re.match(email_pattern, email):
|
|
461
414
|
return {'success': False, 'message': '请输入有效的邮箱地址'}
|
|
462
415
|
|
|
463
|
-
username = username.strip()
|
|
464
|
-
email = email.strip().lower()
|
|
465
|
-
|
|
466
416
|
# 检查用户名是否已存在
|
|
467
|
-
|
|
468
|
-
if
|
|
417
|
+
username_exists = self.check_username_exists(username)
|
|
418
|
+
if username_exists.get('exists'):
|
|
469
419
|
return {'success': False, 'message': '用户名已被占用'}
|
|
470
420
|
|
|
471
421
|
# 检查邮箱是否已存在
|
|
472
|
-
|
|
473
|
-
if
|
|
422
|
+
email_exists = self.check_email_exists(email)
|
|
423
|
+
if email_exists.get('exists'):
|
|
474
424
|
return {'success': False, 'message': '邮箱已被注册'}
|
|
475
425
|
|
|
476
426
|
# 生成盐值和密码哈希
|
|
@@ -507,7 +457,102 @@ class StandaloneAuthManager:
|
|
|
507
457
|
finally:
|
|
508
458
|
cursor.close()
|
|
509
459
|
conn.close()
|
|
460
|
+
|
|
461
|
+
def check_username_exists(self, username):
|
|
462
|
+
"""
|
|
463
|
+
专门检查用户名是否存在
|
|
464
|
+
"""
|
|
465
|
+
if not username or not isinstance(username, str):
|
|
466
|
+
return {
|
|
467
|
+
'exists': False,
|
|
468
|
+
'message': '请提供有效的用户名',
|
|
469
|
+
'user_info': None
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
username = username.strip()
|
|
473
|
+
if not username:
|
|
474
|
+
return {
|
|
475
|
+
'exists': False,
|
|
476
|
+
'message': '用户名不能为空'
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# 验证用户名格式(3-50个字符,支持字母、数字、下划线、中文)
|
|
480
|
+
if len(username) < 3 or len(username) > 50:
|
|
481
|
+
return {
|
|
482
|
+
'exists': False,
|
|
483
|
+
'message': '用户名长度应在3-50个字符之间'
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
conn = self.pool.connection()
|
|
487
|
+
cursor = conn.cursor()
|
|
488
|
+
try:
|
|
489
|
+
cursor.execute('SELECT id FROM users WHERE username = %s', (username,))
|
|
490
|
+
if cursor.fetchone():
|
|
491
|
+
return {
|
|
492
|
+
'exists': True,
|
|
493
|
+
'message': '用户名已被占用'
|
|
494
|
+
}
|
|
495
|
+
except Exception as e:
|
|
496
|
+
return {
|
|
497
|
+
'exists': False,
|
|
498
|
+
'message': '检查用户名时发生错误'
|
|
499
|
+
}
|
|
500
|
+
finally:
|
|
501
|
+
cursor.close()
|
|
502
|
+
conn.close()
|
|
503
|
+
return {
|
|
504
|
+
'exists': False,
|
|
505
|
+
'message': '用户名可以使用'
|
|
506
|
+
}
|
|
510
507
|
|
|
508
|
+
def check_email_exists(self, email):
|
|
509
|
+
"""
|
|
510
|
+
专门检查邮箱是否存在
|
|
511
|
+
"""
|
|
512
|
+
if not email or not isinstance(email, str):
|
|
513
|
+
return {
|
|
514
|
+
'exists': False,
|
|
515
|
+
'message': '请提供有效的邮箱地址',
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
email = email.strip().lower()
|
|
519
|
+
if not email:
|
|
520
|
+
return {
|
|
521
|
+
'exists': False,
|
|
522
|
+
'message': '邮箱地址不能为空'
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# 验证邮箱格式
|
|
526
|
+
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
527
|
+
if not re.match(email_pattern, email):
|
|
528
|
+
return {
|
|
529
|
+
'exists': False,
|
|
530
|
+
'message': '邮箱格式不正确'
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
conn = self.pool.connection()
|
|
534
|
+
cursor = conn.cursor()
|
|
535
|
+
|
|
536
|
+
try:
|
|
537
|
+
cursor.execute('SELECT id FROM users WHERE email = %s', (email,))
|
|
538
|
+
if cursor.fetchone():
|
|
539
|
+
return {
|
|
540
|
+
'exists': True,
|
|
541
|
+
'message': '邮箱已被注册'
|
|
542
|
+
}
|
|
543
|
+
except Exception as e:
|
|
544
|
+
return {
|
|
545
|
+
'exists': False,
|
|
546
|
+
'message': '检查邮箱时发生错误'
|
|
547
|
+
}
|
|
548
|
+
finally:
|
|
549
|
+
cursor.close()
|
|
550
|
+
conn.close()
|
|
551
|
+
return {
|
|
552
|
+
'exists': False,
|
|
553
|
+
'message': '邮箱可以使用'
|
|
554
|
+
}
|
|
555
|
+
|
|
511
556
|
def authenticate_user(self, username_or_email, password, ip_address=None, user_agent=None):
|
|
512
557
|
"""用户身份验证 - 支持用户名或邮箱登录"""
|
|
513
558
|
|
|
@@ -658,6 +703,9 @@ class StandaloneAuthManager:
|
|
|
658
703
|
"""验证访问令牌"""
|
|
659
704
|
try:
|
|
660
705
|
payload = jwt.decode(token, self.auth_config['secret_key'], algorithms=[self.auth_config['algorithm']])
|
|
706
|
+
|
|
707
|
+
if not payload:
|
|
708
|
+
return None
|
|
661
709
|
|
|
662
710
|
if payload.get('type') != 'access':
|
|
663
711
|
return None
|
|
@@ -729,7 +777,7 @@ class StandaloneAuthManager:
|
|
|
729
777
|
for old_session in old_sessions:
|
|
730
778
|
self._revoke_device_session(cursor, old_session['id'], 'device_limit')
|
|
731
779
|
|
|
732
|
-
#
|
|
780
|
+
# 创建新设备会话
|
|
733
781
|
cursor.execute('''
|
|
734
782
|
INSERT INTO device_sessions (
|
|
735
783
|
user_id, device_id, device_fingerprint, login_domain, session_version, device_name, device_type,
|
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
通用加密解密管理器模块
|
|
4
|
+
|
|
5
|
+
功能特性:
|
|
6
|
+
- RSA-OAEP + AES-GCM 混合加密
|
|
7
|
+
- dpflask风格设备指纹验证
|
|
8
|
+
- 时间窗口验证
|
|
9
|
+
- PEM格式密钥处理
|
|
10
|
+
- Redis防重放攻击(可选)
|
|
11
|
+
|
|
12
|
+
使用场景:
|
|
13
|
+
- 敏感数据传输加密
|
|
14
|
+
- 用户认证系统
|
|
15
|
+
- API安全通信
|
|
16
|
+
- 设备身份验证
|
|
17
|
+
|
|
18
|
+
依赖说明:
|
|
19
|
+
- cryptography: RSA和AES加密算法
|
|
20
|
+
- hashlib: 哈希计算(Python标准库)
|
|
21
|
+
- json: JSON处理(Python标准库)
|
|
22
|
+
- base64: Base64编码(Python标准库)
|
|
23
|
+
- redis: 防重放攻击(可选)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import json
|
|
28
|
+
import base64
|
|
29
|
+
import time
|
|
30
|
+
import hashlib
|
|
31
|
+
import hmac
|
|
32
|
+
import threading
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from enum import Enum
|
|
35
|
+
from typing import Dict, Any, Optional, Union, List, Protocol, runtime_checkable
|
|
36
|
+
|
|
37
|
+
# 加密相关导入
|
|
38
|
+
from cryptography.hazmat.primitives import serialization, hashes
|
|
39
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
40
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
41
|
+
from cryptography.hazmat.backends import default_backend
|
|
42
|
+
from mdbq.myconf import myconf # type: ignore
|
|
43
|
+
from mdbq.log import mylogger
|
|
44
|
+
|
|
45
|
+
dir_path = os.path.expanduser("~")
|
|
46
|
+
config_file = os.path.join(dir_path, 'spd.txt')
|
|
47
|
+
parser = myconf.ConfigParser()
|
|
48
|
+
logger = mylogger.MyLogger(
|
|
49
|
+
logging_mode='file',
|
|
50
|
+
log_level='info',
|
|
51
|
+
log_format='json',
|
|
52
|
+
max_log_size=50,
|
|
53
|
+
backup_count=5,
|
|
54
|
+
enable_async=False, # 是否启用异步日志
|
|
55
|
+
sample_rate=1, # 采样DEBUG/INFO日志
|
|
56
|
+
sensitive_fields=[], # 敏感字段过滤
|
|
57
|
+
enable_metrics=False, # 是否启用性能指标
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ==================== 枚举和常量定义 ====================
|
|
62
|
+
|
|
63
|
+
class ValidationResult(Enum):
|
|
64
|
+
"""验证结果枚举"""
|
|
65
|
+
SUCCESS = "success"
|
|
66
|
+
DECRYPT_FAILED = "decrypt_failed"
|
|
67
|
+
TIMESTAMP_INVALID = "timestamp_invalid"
|
|
68
|
+
FINGERPRINT_MISMATCH = "fingerprint_mismatch"
|
|
69
|
+
NONCE_REUSED = "nonce_reused"
|
|
70
|
+
INVALID_PAYLOAD = "invalid_payload"
|
|
71
|
+
KEY_LOAD_FAILED = "key_load_failed"
|
|
72
|
+
REDIS_ERROR = "redis_error"
|
|
73
|
+
|
|
74
|
+
# ==================== 配置类 ====================
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class CryptoConfig:
|
|
78
|
+
"""加密配置类"""
|
|
79
|
+
# 密钥配置
|
|
80
|
+
key_dir_path: str = field(default_factory=lambda: os.path.expanduser("~"))
|
|
81
|
+
public_key_filename: str = 'public_key' # 公钥文件名
|
|
82
|
+
private_key_filename: str = 'private_key' # 私钥文件名
|
|
83
|
+
keys_subdir: str = 'dpsk_keys' # 密钥文件夹名
|
|
84
|
+
|
|
85
|
+
# 验证配置
|
|
86
|
+
time_window_seconds: int = 300 # 时间窗口,300秒
|
|
87
|
+
enable_nonce_check: bool = True # 是否启用nonce防重放攻击
|
|
88
|
+
nonce_expire_seconds: int = 600 # 600秒后过期
|
|
89
|
+
nonce_redis_prefix: str = "api_identity_check" # 存储的键名
|
|
90
|
+
|
|
91
|
+
# 缓存配置
|
|
92
|
+
enable_key_cache: bool = True # 是否启用密钥缓存
|
|
93
|
+
key_cache_ttl_seconds: int = 3600 # 密钥缓存过期时间
|
|
94
|
+
|
|
95
|
+
def __post_init__(self):
|
|
96
|
+
"""配置验证"""
|
|
97
|
+
if self.time_window_seconds <= 0:
|
|
98
|
+
raise ValueError("time_window_seconds must be positive")
|
|
99
|
+
if self.nonce_expire_seconds <= 0:
|
|
100
|
+
raise ValueError("nonce_expire_seconds must be positive")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ==================== 结果类 ====================
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class AuthResult:
|
|
107
|
+
"""认证结果类"""
|
|
108
|
+
success: bool
|
|
109
|
+
result_code: ValidationResult
|
|
110
|
+
payload: Optional[Dict[str, Any]] = None
|
|
111
|
+
error_details: Optional[str] = None
|
|
112
|
+
execution_time_ms: Optional[float] = None
|
|
113
|
+
debug_info: Optional[Dict[str, Any]] = None
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def success_result(cls, payload: Dict[str, Any], execution_time_ms: float = None) -> 'AuthResult':
|
|
117
|
+
"""创建成功结果"""
|
|
118
|
+
return cls(
|
|
119
|
+
success=True,
|
|
120
|
+
result_code=ValidationResult.SUCCESS,
|
|
121
|
+
payload=payload,
|
|
122
|
+
execution_time_ms=execution_time_ms
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def failure_result(cls, result_code: ValidationResult, error_details: str = None,
|
|
127
|
+
execution_time_ms: float = None, debug_info: Dict[str, Any] = None) -> 'AuthResult':
|
|
128
|
+
"""创建失败结果"""
|
|
129
|
+
return cls(
|
|
130
|
+
success=False,
|
|
131
|
+
result_code=result_code,
|
|
132
|
+
error_details=error_details,
|
|
133
|
+
execution_time_ms=execution_time_ms,
|
|
134
|
+
debug_info=debug_info
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ==================== 插件接口 ====================
|
|
139
|
+
|
|
140
|
+
@runtime_checkable
|
|
141
|
+
class FingerprintValidator(Protocol):
|
|
142
|
+
"""设备指纹验证器接口"""
|
|
143
|
+
|
|
144
|
+
def validate(self, payload: Dict[str, Any], logger_func: callable = None) -> bool:
|
|
145
|
+
"""验证设备指纹"""
|
|
146
|
+
...
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class DpflaskFingerprintValidator:
|
|
150
|
+
"""dpflask风格设备指纹验证器"""
|
|
151
|
+
|
|
152
|
+
def validate(self, payload: Dict[str, Any], logger_func: callable = None) -> bool:
|
|
153
|
+
"""验证设备指纹"""
|
|
154
|
+
try:
|
|
155
|
+
client_data = payload.get('deviceData', {})
|
|
156
|
+
client_hash = payload.get('deviceHash', '')
|
|
157
|
+
|
|
158
|
+
if not client_hash:
|
|
159
|
+
if logger_func:
|
|
160
|
+
logger_func("设备指纹哈希为空")
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def deep_sort(obj):
|
|
164
|
+
if isinstance(obj, dict):
|
|
165
|
+
return {k: deep_sort(v) for k, v in sorted(obj.items(), key=lambda x: x)}
|
|
166
|
+
if isinstance(obj, list):
|
|
167
|
+
return sorted((deep_sort(x) for x in obj),
|
|
168
|
+
key=lambda x: (isinstance(x, (int, float)), x))
|
|
169
|
+
return obj
|
|
170
|
+
|
|
171
|
+
# 严格类型转换保证与前端一致
|
|
172
|
+
standardized_data = deep_sort({
|
|
173
|
+
"userAgent": str(client_data.get("userAgent", "")),
|
|
174
|
+
"platform": str(client_data.get("platform", "")),
|
|
175
|
+
"languages": sorted([str(x) for x in client_data.get("languages", [])]),
|
|
176
|
+
"hardwareConcurrency": int(client_data.get("hardwareConcurrency", 0)),
|
|
177
|
+
"screenProps": {
|
|
178
|
+
"width": int(client_data.get("screenProps", {}).get("width", 0)),
|
|
179
|
+
"height": int(client_data.get("screenProps", {}).get("height", 0)),
|
|
180
|
+
"colorDepth": int(client_data.get("screenProps", {}).get("colorDepth", 0))
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
# JSON序列化
|
|
185
|
+
class CompactJSONEncoder(json.JSONEncoder):
|
|
186
|
+
def encode(self, obj):
|
|
187
|
+
return json.dumps(obj, separators=(',', ':'), sort_keys=False)
|
|
188
|
+
|
|
189
|
+
data_str = CompactJSONEncoder().encode(standardized_data)
|
|
190
|
+
|
|
191
|
+
# SHA512哈希
|
|
192
|
+
sha512 = hashlib.sha512()
|
|
193
|
+
sha512.update(data_str.encode('utf-8'))
|
|
194
|
+
server_hash_str = sha512.hexdigest()
|
|
195
|
+
|
|
196
|
+
# 常量时间比较
|
|
197
|
+
if not hmac.compare_digest(server_hash_str.encode(), client_hash.encode()):
|
|
198
|
+
if logger_func:
|
|
199
|
+
logger_func(f"设备指纹不匹配: 期望长度={len(server_hash_str)}, 实际长度={len(client_hash)}")
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
# 硬件验证
|
|
203
|
+
hw_concurrency = standardized_data["hardwareConcurrency"]
|
|
204
|
+
if not (1 <= hw_concurrency <= 128 and isinstance(hw_concurrency, int)):
|
|
205
|
+
if logger_func:
|
|
206
|
+
logger_func(f"硬件并发数异常: {hw_concurrency}")
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
screen = standardized_data["screenProps"]
|
|
210
|
+
if not all(isinstance(v, int) and v > 0 for v in [screen["width"], screen["height"], screen["colorDepth"]]):
|
|
211
|
+
if logger_func:
|
|
212
|
+
logger_func(f"屏幕属性异常: {screen}")
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
if logger_func:
|
|
219
|
+
logger_func(f"设备指纹验证异常: {str(e)}")
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class CryptoManager:
|
|
224
|
+
"""
|
|
225
|
+
通用加密解密管理器
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
def __init__(self,
|
|
229
|
+
config: CryptoConfig,
|
|
230
|
+
redis_client=None,
|
|
231
|
+
fingerprint_validator: Optional[FingerprintValidator] = None):
|
|
232
|
+
"""
|
|
233
|
+
初始化加密管理器
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
config: 加密配置对象(必需)
|
|
237
|
+
logger: 日志记录器,None时不使用日志
|
|
238
|
+
redis_client: Redis客户端,用于nonce防重放攻击
|
|
239
|
+
fingerprint_validator: 设备指纹验证器,None时使用默认dpflask验证器
|
|
240
|
+
"""
|
|
241
|
+
self.config = config
|
|
242
|
+
self.redis_client = redis_client
|
|
243
|
+
self.enable_nonce_check = self.config.enable_nonce_check and redis_client is not None
|
|
244
|
+
self.fingerprint_validator = fingerprint_validator or DpflaskFingerprintValidator()
|
|
245
|
+
|
|
246
|
+
self._init_redis()
|
|
247
|
+
# 密钥缓存初始化
|
|
248
|
+
self._init_cache()
|
|
249
|
+
|
|
250
|
+
# 验证环境
|
|
251
|
+
self._validate_environment()
|
|
252
|
+
|
|
253
|
+
logger.debug(f"CryptoManager初始化完成: keys_dir={self.keys_directory}, nonce_enabled={self.enable_nonce_check}")
|
|
254
|
+
|
|
255
|
+
def _init_redis(self):
|
|
256
|
+
"""初始化Redis"""
|
|
257
|
+
if not self.redis_client:
|
|
258
|
+
redis_password = parser.get_value(file_path=config_file, section='redis', key='password', value_type=str) # redis 使用本地数据,全部机子相同
|
|
259
|
+
# Redis连接配置, 创建连接池
|
|
260
|
+
import redis
|
|
261
|
+
redis_pool = redis.ConnectionPool(
|
|
262
|
+
host='127.0.0.1',
|
|
263
|
+
port=6379,
|
|
264
|
+
db=0,
|
|
265
|
+
password=redis_password,
|
|
266
|
+
max_connections=3, # 连接池最大连接数
|
|
267
|
+
socket_timeout=5, # 操作超时时间(秒)
|
|
268
|
+
socket_connect_timeout=3, # 连接超时时间(秒)
|
|
269
|
+
health_check_interval=30, # 健康检查间隔(秒)
|
|
270
|
+
decode_responses=False, # 保持二进制数据格式
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# 通过连接池获取Redis实例,这个实例用于密钥服务
|
|
274
|
+
self.redis_client = redis.Redis(connection_pool=redis_pool)
|
|
275
|
+
|
|
276
|
+
def _init_cache(self) -> None:
|
|
277
|
+
"""初始化密钥缓存"""
|
|
278
|
+
self._key_cache_lock = threading.RLock()
|
|
279
|
+
self._public_key_cache = None
|
|
280
|
+
self._private_key_cache = None
|
|
281
|
+
self._cache_timestamp = 0
|
|
282
|
+
|
|
283
|
+
# 构建密钥目录路径
|
|
284
|
+
self.keys_directory = os.path.join(self.config.key_dir_path, self.config.keys_subdir)
|
|
285
|
+
|
|
286
|
+
def _validate_environment(self) -> None:
|
|
287
|
+
"""验证运行环境"""
|
|
288
|
+
# 验证密钥目录
|
|
289
|
+
if not os.path.exists(self.keys_directory):
|
|
290
|
+
error_msg = f"密钥目录不存在: {self.keys_directory}"
|
|
291
|
+
logger.error(error_msg)
|
|
292
|
+
raise FileNotFoundError(error_msg)
|
|
293
|
+
|
|
294
|
+
# ==================== 缓存管理方法 ====================
|
|
295
|
+
|
|
296
|
+
def _is_cache_valid(self) -> bool:
|
|
297
|
+
"""检查缓存是否有效"""
|
|
298
|
+
if not self.config.enable_key_cache:
|
|
299
|
+
return False
|
|
300
|
+
current_time = time.time()
|
|
301
|
+
return (self._cache_timestamp > 0 and
|
|
302
|
+
current_time - self._cache_timestamp < self.config.key_cache_ttl_seconds)
|
|
303
|
+
|
|
304
|
+
def clear_cache(self) -> None:
|
|
305
|
+
"""清除密钥缓存"""
|
|
306
|
+
with self._key_cache_lock:
|
|
307
|
+
self._public_key_cache = None
|
|
308
|
+
self._private_key_cache = None
|
|
309
|
+
self._cache_timestamp = 0
|
|
310
|
+
logger.debug("密钥缓存已清除")
|
|
311
|
+
|
|
312
|
+
# ==================== 密钥加载方法 ====================
|
|
313
|
+
|
|
314
|
+
def _load_public_key_from_file(self) -> Optional[str]:
|
|
315
|
+
"""从文件加载公钥"""
|
|
316
|
+
try:
|
|
317
|
+
key_path = os.path.join(self.keys_directory, f'{self.config.public_key_filename}.pem')
|
|
318
|
+
|
|
319
|
+
if not os.path.exists(key_path):
|
|
320
|
+
raise FileNotFoundError(f"公钥文件不存在: {key_path}")
|
|
321
|
+
|
|
322
|
+
with open(key_path, "rb") as key_file:
|
|
323
|
+
public_key = serialization.load_pem_public_key(
|
|
324
|
+
key_file.read(),
|
|
325
|
+
backend=default_backend()
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# 转换为 PEM 格式字节
|
|
329
|
+
public_pem = public_key.public_bytes(
|
|
330
|
+
encoding=serialization.Encoding.PEM,
|
|
331
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return public_pem.decode("utf-8")
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
logger.error(f"公钥加载失败: {str(e)}")
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
def _load_private_key_from_file(self):
|
|
341
|
+
"""从文件加载私钥"""
|
|
342
|
+
try:
|
|
343
|
+
key_path = os.path.join(self.keys_directory, f'{self.config.private_key_filename}.pem')
|
|
344
|
+
|
|
345
|
+
if not os.path.exists(key_path):
|
|
346
|
+
raise FileNotFoundError(f"私钥文件不存在: {key_path}")
|
|
347
|
+
|
|
348
|
+
with open(key_path, "rb") as key_file:
|
|
349
|
+
private_key = serialization.load_pem_private_key(
|
|
350
|
+
key_file.read(),
|
|
351
|
+
password=None,
|
|
352
|
+
backend=default_backend()
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return private_key
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.error(f"私钥加载失败: {str(e)}")
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
# ==================== 公共API方法 ====================
|
|
362
|
+
|
|
363
|
+
def get_public_key(self) -> Optional[str]:
|
|
364
|
+
"""
|
|
365
|
+
获取PEM格式的公钥字符串(支持缓存)
|
|
366
|
+
"""
|
|
367
|
+
with self._key_cache_lock:
|
|
368
|
+
# 检查缓存
|
|
369
|
+
if self._is_cache_valid() and self._public_key_cache is not None:
|
|
370
|
+
logger.debug("使用缓存的公钥")
|
|
371
|
+
return self._public_key_cache
|
|
372
|
+
|
|
373
|
+
# 从文件加载
|
|
374
|
+
public_key = self._load_public_key_from_file()
|
|
375
|
+
|
|
376
|
+
# 更新缓存
|
|
377
|
+
if public_key and self.config.enable_key_cache:
|
|
378
|
+
self._public_key_cache = public_key
|
|
379
|
+
self._cache_timestamp = time.time()
|
|
380
|
+
logger.debug("公钥已缓存")
|
|
381
|
+
|
|
382
|
+
return public_key
|
|
383
|
+
|
|
384
|
+
def get_keys_info(self) -> Dict[str, Any]:
|
|
385
|
+
"""
|
|
386
|
+
获取密钥文件信息
|
|
387
|
+
"""
|
|
388
|
+
public_key_path = os.path.join(self.keys_directory, f'{self.config.public_key_filename}.pem')
|
|
389
|
+
private_key_path = os.path.join(self.keys_directory, f'{self.config.private_key_filename}.pem')
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
'keys_directory': self.keys_directory,
|
|
393
|
+
'public_key_path': public_key_path,
|
|
394
|
+
'private_key_path': private_key_path,
|
|
395
|
+
'public_key_exists': os.path.exists(public_key_path),
|
|
396
|
+
'private_key_exists': os.path.exists(private_key_path),
|
|
397
|
+
'public_key_size': os.path.getsize(public_key_path) if os.path.exists(public_key_path) else 0,
|
|
398
|
+
'private_key_size': os.path.getsize(private_key_path) if os.path.exists(private_key_path) else 0,
|
|
399
|
+
'cache_enabled': self.config.enable_key_cache,
|
|
400
|
+
'cache_ttl_seconds': self.config.key_cache_ttl_seconds,
|
|
401
|
+
'nonce_enabled': self.enable_nonce_check
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# ==================== 核心加解密方法 ====================
|
|
405
|
+
|
|
406
|
+
def decrypt_payload(self, encrypted_token: str) -> Optional[Dict[str, Any]]:
|
|
407
|
+
"""
|
|
408
|
+
解密载荷数据
|
|
409
|
+
"""
|
|
410
|
+
try:
|
|
411
|
+
if not encrypted_token:
|
|
412
|
+
logger.warning("加密令牌为空")
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
# 获取私钥(使用缓存)
|
|
416
|
+
with self._key_cache_lock:
|
|
417
|
+
if self._is_cache_valid() and self._private_key_cache is not None:
|
|
418
|
+
logger.debug("使用缓存的私钥")
|
|
419
|
+
private_key = self._private_key_cache
|
|
420
|
+
else:
|
|
421
|
+
private_key = self._load_private_key_from_file()
|
|
422
|
+
if not private_key:
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
# 更新缓存
|
|
426
|
+
if self.config.enable_key_cache:
|
|
427
|
+
self._private_key_cache = private_key
|
|
428
|
+
if self._cache_timestamp == 0: # 首次缓存时间戳
|
|
429
|
+
self._cache_timestamp = time.time()
|
|
430
|
+
logger.debug("私钥已缓存")
|
|
431
|
+
|
|
432
|
+
# 解析加密令牌
|
|
433
|
+
try:
|
|
434
|
+
token_data = json.loads(base64.b64decode(encrypted_token).decode())
|
|
435
|
+
except (json.JSONDecodeError, ValueError, base64.binascii.Error) as e:
|
|
436
|
+
logger.warning(f"令牌格式错误: {type(e).__name__}")
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
# 验证令牌格式
|
|
440
|
+
required_fields = ['key', 'iv', 'ciphertext', 'tag']
|
|
441
|
+
missing_fields = [field for field in required_fields if field not in token_data]
|
|
442
|
+
if missing_fields:
|
|
443
|
+
logger.warning(f"令牌缺少必需字段: {missing_fields}")
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
# 解码各个组件
|
|
447
|
+
try:
|
|
448
|
+
encrypted_key = base64.b64decode(token_data['key'])
|
|
449
|
+
iv = base64.b64decode(token_data['iv'])
|
|
450
|
+
ciphertext = base64.b64decode(token_data['ciphertext'])
|
|
451
|
+
tag = base64.b64decode(token_data['tag'])
|
|
452
|
+
except (ValueError, base64.binascii.Error) as e:
|
|
453
|
+
logger.warning(f"令牌组件Base64解码失败: {type(e).__name__}")
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
# 使用RSA-OAEP解密AES密钥
|
|
457
|
+
try:
|
|
458
|
+
raw_key = private_key.decrypt(
|
|
459
|
+
encrypted_key,
|
|
460
|
+
padding.OAEP(
|
|
461
|
+
mgf=padding.MGF1(algorithm=hashes.SHA512()),
|
|
462
|
+
algorithm=hashes.SHA512(),
|
|
463
|
+
label=None
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
except Exception as e:
|
|
467
|
+
logger.warning(f"RSA解密失败: {type(e).__name__}")
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
# 使用AES-GCM解密载荷数据
|
|
471
|
+
try:
|
|
472
|
+
aesgcm = AESGCM(raw_key)
|
|
473
|
+
decrypted = aesgcm.decrypt(iv, ciphertext + tag, None)
|
|
474
|
+
except Exception as e:
|
|
475
|
+
logger.warning(f"AES-GCM解密失败: {type(e).__name__}")
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
# 解析JSON载荷
|
|
479
|
+
try:
|
|
480
|
+
payload = json.loads(decrypted.decode())
|
|
481
|
+
logger.debug("载荷解密成功")
|
|
482
|
+
return payload
|
|
483
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
484
|
+
logger.warning(f"载荷JSON解析失败: {type(e).__name__}")
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
except Exception as e:
|
|
488
|
+
logger.error(f"解密载荷异常: {str(e)}")
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
def validate_payload(self, payload: Dict[str, Any], token: Optional[str] = None) -> bool:
|
|
492
|
+
"""
|
|
493
|
+
验证载荷数据的完整性和有效性
|
|
494
|
+
"""
|
|
495
|
+
try:
|
|
496
|
+
if not isinstance(payload, dict):
|
|
497
|
+
return False
|
|
498
|
+
|
|
499
|
+
# 1. 时间窗口验证
|
|
500
|
+
current_time = int(time.time())
|
|
501
|
+
payload_timestamp = payload.get('timestamp', 0)
|
|
502
|
+
|
|
503
|
+
if not isinstance(payload_timestamp, (int, float)):
|
|
504
|
+
return False
|
|
505
|
+
|
|
506
|
+
time_diff = abs(current_time - int(payload_timestamp))
|
|
507
|
+
if time_diff > self.config.time_window_seconds:
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
# 2. 设备指纹验证
|
|
511
|
+
def log_fingerprint_error(msg):
|
|
512
|
+
logger.warning(f"设备指纹验证: {msg}")
|
|
513
|
+
|
|
514
|
+
if not self.fingerprint_validator.validate(payload, log_fingerprint_error):
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
# 3. nonce验证
|
|
518
|
+
if self.enable_nonce_check and token:
|
|
519
|
+
nonce = payload.get('nonce')
|
|
520
|
+
if not nonce:
|
|
521
|
+
return False
|
|
522
|
+
|
|
523
|
+
if self._is_nonce_used(nonce, token):
|
|
524
|
+
return False
|
|
525
|
+
|
|
526
|
+
logger.debug("载荷验证通过")
|
|
527
|
+
return True
|
|
528
|
+
|
|
529
|
+
except Exception as e:
|
|
530
|
+
logger.error(f"验证载荷异常: {str(e)}")
|
|
531
|
+
return False
|
|
532
|
+
|
|
533
|
+
def _is_nonce_used(self, nonce: str, token: str) -> bool:
|
|
534
|
+
"""
|
|
535
|
+
防重放攻击检查
|
|
536
|
+
"""
|
|
537
|
+
if not self.enable_nonce_check or not self.redis_client:
|
|
538
|
+
return False
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
redis_key = f"{self.config.nonce_redis_prefix}:{nonce}"
|
|
542
|
+
# 原子操作:设置键值并设置过期时间
|
|
543
|
+
result = self.redis_client.set(
|
|
544
|
+
name=redis_key,
|
|
545
|
+
value=token,
|
|
546
|
+
ex=self.config.nonce_expire_seconds,
|
|
547
|
+
nx=True # 仅当键不存在时设置
|
|
548
|
+
)
|
|
549
|
+
is_used = not bool(result) # False表示设置成功(未使用过)
|
|
550
|
+
if is_used:
|
|
551
|
+
logger.warning(f"nonce重复使用: {nonce[:8]}...")
|
|
552
|
+
return is_used
|
|
553
|
+
except Exception as e:
|
|
554
|
+
logger.error(f"nonce检查异常: {str(e)}")
|
|
555
|
+
return True # 异常时认为已使用,拒绝请求
|
|
556
|
+
|
|
557
|
+
# ==================== 高级认证方法 ====================
|
|
558
|
+
|
|
559
|
+
def get_private_key(self, token: str) -> bool:
|
|
560
|
+
"""
|
|
561
|
+
加载私钥并解密令牌进行校检(dpflask兼容方法)
|
|
562
|
+
这是dpflask风格的一体化验证方法:解密+验证一步完成
|
|
563
|
+
"""
|
|
564
|
+
result = self.authenticate_token_detailed(token)
|
|
565
|
+
return result.success
|
|
566
|
+
|
|
567
|
+
def authenticate_token(self, token: str) -> Dict[str, Any]:
|
|
568
|
+
"""
|
|
569
|
+
认证令牌并返回详细结果(向后兼容)
|
|
570
|
+
"""
|
|
571
|
+
result = self.authenticate_token_detailed(token)
|
|
572
|
+
return {
|
|
573
|
+
'success': result.success,
|
|
574
|
+
'error': result.result_code.value if not result.success else None,
|
|
575
|
+
'message': result.error_details or ('认证成功' if result.success else '认证失败'),
|
|
576
|
+
'payload': result.payload
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
def authenticate_token_detailed(self, token: str) -> AuthResult:
|
|
580
|
+
"""
|
|
581
|
+
认证令牌并返回标准化结果对象
|
|
582
|
+
"""
|
|
583
|
+
start_time = time.time()
|
|
584
|
+
|
|
585
|
+
try:
|
|
586
|
+
if not token:
|
|
587
|
+
return AuthResult.failure_result(
|
|
588
|
+
ValidationResult.INVALID_PAYLOAD,
|
|
589
|
+
"令牌为空",
|
|
590
|
+
(time.time() - start_time) * 1000
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# 1. 解密载荷
|
|
594
|
+
payload = self.decrypt_payload(token)
|
|
595
|
+
if not payload:
|
|
596
|
+
return AuthResult.failure_result(
|
|
597
|
+
ValidationResult.DECRYPT_FAILED,
|
|
598
|
+
"令牌解密失败",
|
|
599
|
+
(time.time() - start_time) * 1000
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# 2. 验证载荷
|
|
603
|
+
if not self.validate_payload(payload, token):
|
|
604
|
+
# 根据验证失败的具体原因返回不同的结果码
|
|
605
|
+
current_time = int(time.time())
|
|
606
|
+
payload_timestamp = payload.get('timestamp', 0)
|
|
607
|
+
|
|
608
|
+
if abs(current_time - int(payload_timestamp)) > self.config.time_window_seconds:
|
|
609
|
+
return AuthResult.failure_result(
|
|
610
|
+
ValidationResult.TIMESTAMP_INVALID,
|
|
611
|
+
"时间窗口验证失败",
|
|
612
|
+
(time.time() - start_time) * 1000
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# 检查nonce
|
|
616
|
+
if self.enable_nonce_check and token:
|
|
617
|
+
nonce = payload.get('nonce')
|
|
618
|
+
if nonce and self._is_nonce_used(nonce, token):
|
|
619
|
+
return AuthResult.failure_result(
|
|
620
|
+
ValidationResult.NONCE_REUSED,
|
|
621
|
+
"nonce重复使用",
|
|
622
|
+
(time.time() - start_time) * 1000
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# 其他情况(设备指纹验证失败等)
|
|
626
|
+
return AuthResult.failure_result(
|
|
627
|
+
ValidationResult.FINGERPRINT_MISMATCH,
|
|
628
|
+
"设备指纹验证失败",
|
|
629
|
+
(time.time() - start_time) * 1000
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
# 3. 验证成功
|
|
633
|
+
execution_time = (time.time() - start_time) * 1000
|
|
634
|
+
logger.debug(f"令牌认证成功,耗时: {execution_time:.2f}ms")
|
|
635
|
+
|
|
636
|
+
return AuthResult.success_result(payload, execution_time)
|
|
637
|
+
|
|
638
|
+
except Exception as e:
|
|
639
|
+
execution_time = (time.time() - start_time) * 1000
|
|
640
|
+
logger.error(f"令牌认证异常: {str(e)}")
|
|
641
|
+
|
|
642
|
+
return AuthResult.failure_result(
|
|
643
|
+
ValidationResult.DECRYPT_FAILED,
|
|
644
|
+
f"认证异常: {str(e)}",
|
|
645
|
+
execution_time,
|
|
646
|
+
{"exception_type": type(e).__name__}
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
def get_keys_info(self) -> Dict[str, Any]:
|
|
650
|
+
"""
|
|
651
|
+
获取密钥文件信息
|
|
652
|
+
"""
|
|
653
|
+
public_key_path = os.path.join(self.keys_directory, f'{self.config.public_key_filename}.pem')
|
|
654
|
+
private_key_path = os.path.join(self.keys_directory, f'{self.config.private_key_filename}.pem')
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
'keys_directory': self.keys_directory,
|
|
658
|
+
'public_key_path': public_key_path,
|
|
659
|
+
'private_key_path': private_key_path,
|
|
660
|
+
'public_key_exists': os.path.exists(public_key_path),
|
|
661
|
+
'private_key_exists': os.path.exists(private_key_path),
|
|
662
|
+
'public_key_size': os.path.getsize(public_key_path) if os.path.exists(public_key_path) else 0,
|
|
663
|
+
'private_key_size': os.path.getsize(private_key_path) if os.path.exists(private_key_path) else 0,
|
|
664
|
+
'cache_enabled': self.config.enable_key_cache,
|
|
665
|
+
'cache_ttl_seconds': self.config.key_cache_ttl_seconds,
|
|
666
|
+
'nonce_enabled': self.enable_nonce_check
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class CryptoHelper:
|
|
671
|
+
"""
|
|
672
|
+
加密工具辅助类
|
|
673
|
+
"""
|
|
674
|
+
|
|
675
|
+
@staticmethod
|
|
676
|
+
def generate_secure_token(length: int = 32) -> str:
|
|
677
|
+
"""
|
|
678
|
+
生成安全的随机令牌
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
length (int): 令牌长度,默认32字节
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
str: 十六进制格式的随机令牌
|
|
685
|
+
"""
|
|
686
|
+
import secrets
|
|
687
|
+
return secrets.token_hex(length)
|
|
688
|
+
|
|
689
|
+
@staticmethod
|
|
690
|
+
def hash_data(data: Union[str, bytes], algorithm: str = 'sha256') -> str:
|
|
691
|
+
"""
|
|
692
|
+
对数据进行哈希处理
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
data (str|bytes): 要哈希的数据
|
|
696
|
+
algorithm (str): 哈希算法,支持 'md5', 'sha1', 'sha256', 'sha512'
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
str: 十六进制格式的哈希值
|
|
700
|
+
"""
|
|
701
|
+
if isinstance(data, str):
|
|
702
|
+
data = data.encode('utf-8')
|
|
703
|
+
|
|
704
|
+
hash_func = getattr(hashlib, algorithm.lower())()
|
|
705
|
+
hash_func.update(data)
|
|
706
|
+
return hash_func.hexdigest()
|
|
707
|
+
|
|
708
|
+
@staticmethod
|
|
709
|
+
def encode_base64(data: Union[str, bytes]) -> str:
|
|
710
|
+
"""
|
|
711
|
+
Base64编码
|
|
712
|
+
|
|
713
|
+
Args:
|
|
714
|
+
data (str|bytes): 要编码的数据
|
|
715
|
+
|
|
716
|
+
Returns:
|
|
717
|
+
str: Base64编码后的字符串
|
|
718
|
+
"""
|
|
719
|
+
if isinstance(data, str):
|
|
720
|
+
data = data.encode('utf-8')
|
|
721
|
+
return base64.b64encode(data).decode('utf-8')
|
|
722
|
+
|
|
723
|
+
@staticmethod
|
|
724
|
+
def decode_base64(encoded_data: str) -> bytes:
|
|
725
|
+
"""
|
|
726
|
+
Base64解码
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
encoded_data (str): Base64编码的字符串
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
bytes: 解码后的字节数据
|
|
733
|
+
"""
|
|
734
|
+
return base64.b64decode(encoded_data)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
if __name__ == "__main__":
|
|
738
|
+
pass
|
mdbq-4.0.122/mdbq/__version__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = '4.0.122'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|