mdbq 4.0.87__py3-none-any.whl → 4.0.89__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/__init__.py +4 -0
- mdbq/auth/auth_backend.py +1236 -0
- mdbq/js/__init__.py +1 -1
- mdbq/route/__init__.py +3 -1
- {mdbq-4.0.87.dist-info → mdbq-4.0.89.dist-info}/METADATA +1 -1
- {mdbq-4.0.87.dist-info → mdbq-4.0.89.dist-info}/RECORD +9 -7
- {mdbq-4.0.87.dist-info → mdbq-4.0.89.dist-info}/WHEEL +0 -0
- {mdbq-4.0.87.dist-info → mdbq-4.0.89.dist-info}/top_level.txt +0 -0
mdbq/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
VERSION = '4.0.
|
1
|
+
VERSION = '4.0.89'
|
mdbq/auth/__init__.py
ADDED
@@ -0,0 +1,1236 @@
|
|
1
|
+
"""
|
2
|
+
独立用户认证系统 - 后端核心库
|
3
|
+
|
4
|
+
功能特性:
|
5
|
+
- 基于JWT的双令牌认证机制
|
6
|
+
- MySQL数据库支持
|
7
|
+
- 多设备会话管理
|
8
|
+
- IP限流和安全防护
|
9
|
+
- 用户注册和权限管理
|
10
|
+
- 密码加密和设备指纹验证
|
11
|
+
- 连接池和性能优化
|
12
|
+
|
13
|
+
依赖说明:
|
14
|
+
- 核心功能: 无需Flask,可独立使用
|
15
|
+
- 装饰器功能: 需要Flask (pip install flask)
|
16
|
+
- 其他依赖: pymysql, PyJWT, cryptography, dbutils
|
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
|
+
"""
|
68
|
+
|
69
|
+
import os
|
70
|
+
import jwt # type: ignore
|
71
|
+
import pymysql
|
72
|
+
import hashlib
|
73
|
+
import secrets
|
74
|
+
import json
|
75
|
+
import time
|
76
|
+
import base64
|
77
|
+
import requests
|
78
|
+
from datetime import datetime, timedelta, timezone
|
79
|
+
from functools import wraps
|
80
|
+
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
|
+
|
85
|
+
# Flask相关导入 - 用于装饰器功能
|
86
|
+
try:
|
87
|
+
from flask import request
|
88
|
+
FLASK_AVAILABLE = True
|
89
|
+
except ImportError:
|
90
|
+
FLASK_AVAILABLE = False
|
91
|
+
request = None
|
92
|
+
|
93
|
+
|
94
|
+
class StandaloneAuthManager:
|
95
|
+
"""独立的身份验证管理器"""
|
96
|
+
|
97
|
+
def __init__(self, db_config, auth_config=None):
|
98
|
+
"""
|
99
|
+
初始化认证管理器
|
100
|
+
|
101
|
+
Args:
|
102
|
+
db_config (dict): 数据库配置
|
103
|
+
{
|
104
|
+
'host': 'localhost',
|
105
|
+
'port': 3306,
|
106
|
+
'user': 'root',
|
107
|
+
'password': 'password',
|
108
|
+
'database': 'auth_db'
|
109
|
+
}
|
110
|
+
auth_config (dict): 认证配置,可选
|
111
|
+
"""
|
112
|
+
self.db_config = db_config
|
113
|
+
self.db_name = db_config.get('database', 'standalone_auth')
|
114
|
+
|
115
|
+
# 默认认证配置
|
116
|
+
self.auth_config = {
|
117
|
+
'secret_key': auth_config.get('secret_key', secrets.token_hex(32)) if auth_config else secrets.token_hex(32),
|
118
|
+
'algorithm': 'HS256',
|
119
|
+
'access_token_expires': 30 * 60, # 30分钟
|
120
|
+
'refresh_token_expires': 7 * 24 * 60 * 60, # 7天
|
121
|
+
'absolute_refresh_expires_days': 30, # 30天绝对过期
|
122
|
+
'max_refresh_rotations': 100, # 最大轮换次数
|
123
|
+
'session_expires_hours': 24, # 会话过期时间
|
124
|
+
'max_login_attempts': 5, # 最大登录尝试次数
|
125
|
+
'lockout_duration': 15 * 60, # 锁定时长
|
126
|
+
'max_concurrent_devices': 10, # 最大并发设备数
|
127
|
+
'device_session_expires_days': 30, # 设备会话过期时间
|
128
|
+
'ip_max_attempts': 10, # IP最大尝试次数
|
129
|
+
'ip_window_minutes': 30, # IP限制时间窗口
|
130
|
+
'ip_lockout_duration': 60 * 60, # IP锁定时长
|
131
|
+
**(auth_config or {})
|
132
|
+
}
|
133
|
+
|
134
|
+
self._init_mysql_pool()
|
135
|
+
self.init_database()
|
136
|
+
|
137
|
+
def _init_mysql_pool(self):
|
138
|
+
"""初始化MySQL连接池"""
|
139
|
+
try:
|
140
|
+
# 先创建数据库(如果不存在)
|
141
|
+
self._create_database_if_not_exists()
|
142
|
+
|
143
|
+
self.pool = PooledDB(
|
144
|
+
creator=pymysql,
|
145
|
+
maxconnections=20, # 最大连接数
|
146
|
+
mincached=5, # 初始化空闲连接数
|
147
|
+
maxcached=10, # 空闲连接最大缓存数
|
148
|
+
blocking=True,
|
149
|
+
host=self.db_config['host'],
|
150
|
+
port=int(self.db_config['port']),
|
151
|
+
user=self.db_config['user'],
|
152
|
+
password=self.db_config['password'],
|
153
|
+
database=self.db_name,
|
154
|
+
ping=1,
|
155
|
+
charset='utf8mb4',
|
156
|
+
cursorclass=pymysql.cursors.DictCursor,
|
157
|
+
autocommit=True, # 启用自动提交,避免锁冲突
|
158
|
+
init_command="SET time_zone = '+00:00'"
|
159
|
+
)
|
160
|
+
|
161
|
+
except Exception as e:
|
162
|
+
print(f"MySQL连接池初始化失败: {e}")
|
163
|
+
raise
|
164
|
+
|
165
|
+
def _create_database_if_not_exists(self):
|
166
|
+
"""创建数据库(如果不存在)"""
|
167
|
+
try:
|
168
|
+
# 不指定数据库的连接
|
169
|
+
conn = pymysql.connect(
|
170
|
+
host=self.db_config['host'],
|
171
|
+
port=int(self.db_config['port']),
|
172
|
+
user=self.db_config['user'],
|
173
|
+
password=self.db_config['password'],
|
174
|
+
charset='utf8mb4'
|
175
|
+
)
|
176
|
+
cursor = conn.cursor()
|
177
|
+
|
178
|
+
# 创建数据库
|
179
|
+
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {self.db_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci")
|
180
|
+
|
181
|
+
cursor.close()
|
182
|
+
conn.close()
|
183
|
+
|
184
|
+
except Exception as e:
|
185
|
+
print(f"创建数据库失败: {e}")
|
186
|
+
raise
|
187
|
+
|
188
|
+
def init_database(self):
|
189
|
+
"""初始化数据库表"""
|
190
|
+
conn = self.pool.connection()
|
191
|
+
cursor = conn.cursor()
|
192
|
+
|
193
|
+
try:
|
194
|
+
# 用户表
|
195
|
+
cursor.execute('''
|
196
|
+
CREATE TABLE IF NOT EXISTS users (
|
197
|
+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
198
|
+
username VARCHAR(50) NOT NULL,
|
199
|
+
password_hash VARCHAR(128) NOT NULL,
|
200
|
+
salt VARCHAR(64) NOT NULL,
|
201
|
+
role ENUM('admin', 'user', 'manager') NOT NULL DEFAULT 'user',
|
202
|
+
permissions JSON DEFAULT (JSON_ARRAY()),
|
203
|
+
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
204
|
+
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
205
|
+
last_login TIMESTAMP(3) NULL DEFAULT NULL,
|
206
|
+
login_attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
207
|
+
locked_until TIMESTAMP(3) NULL DEFAULT NULL,
|
208
|
+
|
209
|
+
UNIQUE KEY uk_users_username (username),
|
210
|
+
KEY idx_users_role (role),
|
211
|
+
KEY idx_users_created_at (created_at),
|
212
|
+
KEY idx_users_is_active (is_active),
|
213
|
+
KEY idx_users_locked_until (locked_until)
|
214
|
+
) ENGINE=InnoDB
|
215
|
+
DEFAULT CHARSET=utf8mb4
|
216
|
+
COLLATE=utf8mb4_0900_ai_ci
|
217
|
+
''')
|
218
|
+
|
219
|
+
# 设备会话表
|
220
|
+
cursor.execute('''
|
221
|
+
CREATE TABLE IF NOT EXISTS device_sessions (
|
222
|
+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
223
|
+
user_id BIGINT UNSIGNED NOT NULL,
|
224
|
+
device_id VARCHAR(64) CHARACTER SET ascii NOT NULL,
|
225
|
+
device_fingerprint VARCHAR(128) CHARACTER SET ascii NOT NULL,
|
226
|
+
device_name VARCHAR(100) NOT NULL DEFAULT 'Unknown Device',
|
227
|
+
device_type ENUM('mobile', 'desktop', 'tablet', 'unknown') NOT NULL DEFAULT 'unknown',
|
228
|
+
platform VARCHAR(50) DEFAULT NULL,
|
229
|
+
browser VARCHAR(50) DEFAULT NULL,
|
230
|
+
ip_address VARCHAR(45) NOT NULL,
|
231
|
+
user_agent TEXT NOT NULL,
|
232
|
+
last_activity TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
233
|
+
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
234
|
+
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
235
|
+
|
236
|
+
UNIQUE KEY uk_device_sessions_device_id (device_id),
|
237
|
+
KEY idx_device_sessions_user_id (user_id),
|
238
|
+
KEY idx_device_sessions_user_device (user_id, device_id),
|
239
|
+
KEY idx_device_sessions_last_activity (last_activity),
|
240
|
+
KEY idx_device_sessions_is_active (is_active),
|
241
|
+
KEY idx_device_sessions_fingerprint (device_fingerprint),
|
242
|
+
|
243
|
+
CONSTRAINT fk_device_sessions_user_id
|
244
|
+
FOREIGN KEY (user_id)
|
245
|
+
REFERENCES users (id)
|
246
|
+
ON DELETE CASCADE
|
247
|
+
ON UPDATE CASCADE
|
248
|
+
) ENGINE=InnoDB
|
249
|
+
DEFAULT CHARSET=utf8mb4
|
250
|
+
COLLATE=utf8mb4_0900_ai_ci
|
251
|
+
''')
|
252
|
+
|
253
|
+
# 刷新令牌表
|
254
|
+
cursor.execute('''
|
255
|
+
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
256
|
+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
257
|
+
token_hash VARCHAR(64) CHARACTER SET ascii NOT NULL,
|
258
|
+
token_original TEXT NOT NULL,
|
259
|
+
user_id BIGINT UNSIGNED NOT NULL,
|
260
|
+
device_session_id BIGINT UNSIGNED NOT NULL,
|
261
|
+
expires_at TIMESTAMP(3) NOT NULL,
|
262
|
+
absolute_expires_at TIMESTAMP(3) NOT NULL,
|
263
|
+
rotation_count INT UNSIGNED NOT NULL DEFAULT 0,
|
264
|
+
max_rotations INT UNSIGNED NOT NULL DEFAULT 30,
|
265
|
+
is_revoked TINYINT(1) NOT NULL DEFAULT 0,
|
266
|
+
revoked_at TIMESTAMP(3) NULL DEFAULT NULL,
|
267
|
+
revoked_reason VARCHAR(50) NULL DEFAULT NULL,
|
268
|
+
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
269
|
+
last_used_at TIMESTAMP(3) NULL DEFAULT NULL,
|
270
|
+
|
271
|
+
UNIQUE KEY uk_refresh_tokens_token_hash (token_hash),
|
272
|
+
UNIQUE KEY uk_refresh_tokens_device_session (device_session_id),
|
273
|
+
KEY idx_refresh_tokens_user_id (user_id),
|
274
|
+
KEY idx_refresh_tokens_expires_at (expires_at),
|
275
|
+
KEY idx_refresh_tokens_absolute_expires_at (absolute_expires_at),
|
276
|
+
KEY idx_refresh_tokens_is_revoked (is_revoked),
|
277
|
+
|
278
|
+
CONSTRAINT fk_refresh_tokens_user_id
|
279
|
+
FOREIGN KEY (user_id)
|
280
|
+
REFERENCES users (id)
|
281
|
+
ON DELETE CASCADE
|
282
|
+
ON UPDATE CASCADE,
|
283
|
+
CONSTRAINT fk_refresh_tokens_device_session_id
|
284
|
+
FOREIGN KEY (device_session_id)
|
285
|
+
REFERENCES device_sessions (id)
|
286
|
+
ON DELETE CASCADE
|
287
|
+
ON UPDATE CASCADE
|
288
|
+
) ENGINE=InnoDB
|
289
|
+
DEFAULT CHARSET=utf8mb4
|
290
|
+
COLLATE=utf8mb4_0900_ai_ci
|
291
|
+
''')
|
292
|
+
|
293
|
+
# 登录日志表
|
294
|
+
cursor.execute('''
|
295
|
+
CREATE TABLE IF NOT EXISTS login_logs (
|
296
|
+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
297
|
+
user_id BIGINT UNSIGNED DEFAULT NULL,
|
298
|
+
username VARCHAR(50) DEFAULT NULL,
|
299
|
+
ip_address VARCHAR(45) CHARACTER SET ascii DEFAULT NULL,
|
300
|
+
user_agent TEXT DEFAULT NULL,
|
301
|
+
login_time TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
302
|
+
login_result ENUM('success', 'failure') NOT NULL,
|
303
|
+
failure_reason VARCHAR(100) DEFAULT NULL,
|
304
|
+
|
305
|
+
KEY idx_login_logs_user_id (user_id),
|
306
|
+
KEY idx_login_logs_username (username),
|
307
|
+
KEY idx_login_logs_login_time (login_time),
|
308
|
+
KEY idx_login_logs_login_result (login_result),
|
309
|
+
KEY idx_login_logs_ip_address (ip_address),
|
310
|
+
|
311
|
+
CONSTRAINT fk_login_logs_user_id
|
312
|
+
FOREIGN KEY (user_id)
|
313
|
+
REFERENCES users (id)
|
314
|
+
ON DELETE SET NULL
|
315
|
+
ON UPDATE CASCADE
|
316
|
+
) ENGINE=InnoDB
|
317
|
+
DEFAULT CHARSET=utf8mb4
|
318
|
+
COLLATE=utf8mb4_0900_ai_ci
|
319
|
+
''')
|
320
|
+
|
321
|
+
# IP限流表
|
322
|
+
cursor.execute('''
|
323
|
+
CREATE TABLE IF NOT EXISTS ip_rate_limits (
|
324
|
+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
325
|
+
ip_address VARCHAR(45) CHARACTER SET ascii NOT NULL,
|
326
|
+
action_type ENUM('login', 'register', 'password_reset') NOT NULL,
|
327
|
+
failure_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
328
|
+
last_failure TIMESTAMP(3) NULL DEFAULT NULL,
|
329
|
+
first_failure TIMESTAMP(3) NULL DEFAULT NULL,
|
330
|
+
locked_until TIMESTAMP(3) NULL DEFAULT NULL,
|
331
|
+
lockout_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
332
|
+
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
333
|
+
updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
334
|
+
|
335
|
+
UNIQUE KEY uk_ip_rate_limits_ip_action (ip_address, action_type),
|
336
|
+
KEY idx_ip_rate_limits_locked_until (locked_until),
|
337
|
+
KEY idx_ip_rate_limits_last_failure (last_failure),
|
338
|
+
KEY idx_ip_rate_limits_created_at (created_at)
|
339
|
+
) ENGINE=InnoDB
|
340
|
+
DEFAULT CHARSET=utf8mb4
|
341
|
+
COLLATE=utf8mb4_0900_ai_ci
|
342
|
+
''')
|
343
|
+
|
344
|
+
print("数据库表初始化完成")
|
345
|
+
|
346
|
+
except Exception as e:
|
347
|
+
print(f"数据库表创建失败: {e}")
|
348
|
+
raise
|
349
|
+
finally:
|
350
|
+
cursor.close()
|
351
|
+
conn.close()
|
352
|
+
|
353
|
+
def _hash_password(self, password, salt):
|
354
|
+
"""密码哈希"""
|
355
|
+
return hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000).hex()
|
356
|
+
|
357
|
+
def _verify_password(self, password, password_hash, salt):
|
358
|
+
"""验证密码"""
|
359
|
+
return self._hash_password(password, salt) == password_hash
|
360
|
+
|
361
|
+
def _log_login_attempt(self, username, ip_address, user_agent, result, failure_reason=None, user_id=None):
|
362
|
+
"""记录登录尝试"""
|
363
|
+
conn = self.pool.connection()
|
364
|
+
cursor = conn.cursor()
|
365
|
+
|
366
|
+
try:
|
367
|
+
cursor.execute('''
|
368
|
+
INSERT INTO login_logs (user_id, username, ip_address, user_agent, login_result, failure_reason)
|
369
|
+
VALUES (%s, %s, %s, %s, %s, %s)
|
370
|
+
''', (user_id, username, ip_address, user_agent, result, failure_reason))
|
371
|
+
except Exception as e:
|
372
|
+
print(f"记录登录日志失败: {e}")
|
373
|
+
finally:
|
374
|
+
cursor.close()
|
375
|
+
conn.close()
|
376
|
+
|
377
|
+
def register_user(self, username, password, role='user', permissions=None):
|
378
|
+
"""用户注册"""
|
379
|
+
conn = self.pool.connection()
|
380
|
+
cursor = conn.cursor()
|
381
|
+
|
382
|
+
try:
|
383
|
+
# 验证输入
|
384
|
+
if not username or not password:
|
385
|
+
return {'success': False, 'message': '用户名和密码不能为空'}
|
386
|
+
|
387
|
+
if len(username.strip()) < 3:
|
388
|
+
return {'success': False, 'message': '用户名至少需要3个字符'}
|
389
|
+
|
390
|
+
if len(password) < 6:
|
391
|
+
return {'success': False, 'message': '密码至少需要6个字符'}
|
392
|
+
|
393
|
+
username = username.strip()
|
394
|
+
|
395
|
+
# 检查用户名是否已存在
|
396
|
+
cursor.execute('SELECT id FROM users WHERE username = %s', (username,))
|
397
|
+
if cursor.fetchone():
|
398
|
+
return {'success': False, 'message': '用户名已被占用'}
|
399
|
+
|
400
|
+
# 生成盐值和密码哈希
|
401
|
+
salt = secrets.token_hex(32)
|
402
|
+
password_hash = self._hash_password(password, salt)
|
403
|
+
|
404
|
+
# 设置默认权限
|
405
|
+
if permissions is None:
|
406
|
+
permissions = ['read'] if role == 'user' else ['read', 'write']
|
407
|
+
permissions_json = json.dumps(permissions)
|
408
|
+
|
409
|
+
# 创建新用户
|
410
|
+
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))
|
414
|
+
|
415
|
+
user_id = cursor.lastrowid
|
416
|
+
|
417
|
+
return {
|
418
|
+
'success': True,
|
419
|
+
'message': '注册成功',
|
420
|
+
'user': {
|
421
|
+
'id': user_id,
|
422
|
+
'username': username,
|
423
|
+
'role': role,
|
424
|
+
'permissions': permissions
|
425
|
+
}
|
426
|
+
}
|
427
|
+
|
428
|
+
except Exception as e:
|
429
|
+
return {'success': False, 'message': f'注册失败: {str(e)}'}
|
430
|
+
finally:
|
431
|
+
cursor.close()
|
432
|
+
conn.close()
|
433
|
+
|
434
|
+
def authenticate_user(self, username, password, ip_address=None, user_agent=None):
|
435
|
+
"""用户身份验证"""
|
436
|
+
|
437
|
+
# 检查IP是否被限流
|
438
|
+
ip_check = self._check_ip_rate_limit(ip_address, 'login')
|
439
|
+
if ip_check['blocked']:
|
440
|
+
self._log_login_attempt(username, ip_address, user_agent, 'failure', f'ip_blocked_{ip_check["remaining_time"]}s')
|
441
|
+
return {
|
442
|
+
'success': False,
|
443
|
+
'error': 'ip_blocked',
|
444
|
+
'message': ip_check['reason'],
|
445
|
+
'remaining_time': ip_check['remaining_time']
|
446
|
+
}
|
447
|
+
|
448
|
+
conn = self.pool.connection()
|
449
|
+
cursor = conn.cursor()
|
450
|
+
|
451
|
+
try:
|
452
|
+
# 获取用户信息
|
453
|
+
cursor.execute('''
|
454
|
+
SELECT id, username, password_hash, salt, role, permissions,
|
455
|
+
is_active, login_attempts, locked_until
|
456
|
+
FROM users WHERE username = %s
|
457
|
+
''', (username,))
|
458
|
+
|
459
|
+
user = cursor.fetchone()
|
460
|
+
if not user:
|
461
|
+
self._log_login_attempt(username, ip_address, user_agent, 'failure', 'user_not_found')
|
462
|
+
self._record_ip_failure(ip_address, 'login')
|
463
|
+
return {
|
464
|
+
'success': False,
|
465
|
+
'error': 'invalid_credentials',
|
466
|
+
'message': '用户名或密码错误'
|
467
|
+
}
|
468
|
+
|
469
|
+
user_id = user['id']
|
470
|
+
password_hash = user['password_hash']
|
471
|
+
salt = user['salt']
|
472
|
+
role = user['role']
|
473
|
+
permissions = user['permissions']
|
474
|
+
is_active = user['is_active']
|
475
|
+
login_attempts = user['login_attempts']
|
476
|
+
locked_until = user['locked_until']
|
477
|
+
|
478
|
+
# 检查账户状态
|
479
|
+
if not is_active:
|
480
|
+
self._log_login_attempt(username, ip_address, user_agent, 'failure', 'account_disabled', user_id)
|
481
|
+
self._record_ip_failure(ip_address, 'login')
|
482
|
+
return {
|
483
|
+
'success': False,
|
484
|
+
'error': 'account_disabled',
|
485
|
+
'message': '账户已被禁用'
|
486
|
+
}
|
487
|
+
|
488
|
+
# 检查账户锁定状态
|
489
|
+
current_time_utc = datetime.now(timezone.utc)
|
490
|
+
if locked_until:
|
491
|
+
if locked_until.tzinfo is None:
|
492
|
+
locked_until = locked_until.replace(tzinfo=timezone.utc)
|
493
|
+
elif locked_until.tzinfo != timezone.utc:
|
494
|
+
locked_until = locked_until.astimezone(timezone.utc)
|
495
|
+
|
496
|
+
if locked_until > current_time_utc:
|
497
|
+
remaining_seconds = int((locked_until - current_time_utc).total_seconds())
|
498
|
+
self._log_login_attempt(username, ip_address, user_agent, 'failure', 'account_locked', user_id)
|
499
|
+
self._record_ip_failure(ip_address, 'login')
|
500
|
+
return {
|
501
|
+
'success': False,
|
502
|
+
'error': 'account_locked',
|
503
|
+
'message': f'账户已被锁定,请在 {remaining_seconds} 秒后重试',
|
504
|
+
'remaining_time': remaining_seconds
|
505
|
+
}
|
506
|
+
|
507
|
+
# 验证密码
|
508
|
+
if not self._verify_password(password, password_hash, salt):
|
509
|
+
# 记录失败尝试
|
510
|
+
login_attempts += 1
|
511
|
+
|
512
|
+
if login_attempts >= self.auth_config['max_login_attempts']:
|
513
|
+
lockout_duration = self.auth_config['lockout_duration']
|
514
|
+
locked_until = current_time_utc + timedelta(seconds=lockout_duration)
|
515
|
+
cursor.execute('''
|
516
|
+
UPDATE users SET login_attempts = %s, locked_until = %s WHERE id = %s
|
517
|
+
''', (login_attempts, locked_until, user_id))
|
518
|
+
|
519
|
+
self._log_login_attempt(username, ip_address, user_agent, 'failure', f'password_incorrect_locked_{lockout_duration}s', user_id)
|
520
|
+
self._record_ip_failure(ip_address, 'login')
|
521
|
+
|
522
|
+
return {
|
523
|
+
'success': False,
|
524
|
+
'error': 'account_locked',
|
525
|
+
'message': f'密码错误次数过多,账户已被锁定 {lockout_duration} 秒',
|
526
|
+
'remaining_time': lockout_duration
|
527
|
+
}
|
528
|
+
else:
|
529
|
+
cursor.execute('''
|
530
|
+
UPDATE users SET login_attempts = %s WHERE id = %s
|
531
|
+
''', (login_attempts, user_id))
|
532
|
+
|
533
|
+
self._log_login_attempt(username, ip_address, user_agent, 'failure', f'password_incorrect_attempt_{login_attempts}', user_id)
|
534
|
+
self._record_ip_failure(ip_address, 'login')
|
535
|
+
|
536
|
+
remaining_attempts = self.auth_config['max_login_attempts'] - login_attempts
|
537
|
+
return {
|
538
|
+
'success': False,
|
539
|
+
'error': 'invalid_credentials',
|
540
|
+
'message': f'用户名或密码错误,还可以尝试 {remaining_attempts} 次',
|
541
|
+
'remaining_attempts': remaining_attempts
|
542
|
+
}
|
543
|
+
|
544
|
+
# 登录成功,重置尝试次数
|
545
|
+
cursor.execute('''
|
546
|
+
UPDATE users SET login_attempts = 0, locked_until = NULL, last_login = %s WHERE id = %s
|
547
|
+
''', (current_time_utc, user_id))
|
548
|
+
|
549
|
+
# 记录成功登录
|
550
|
+
self._log_login_attempt(username, ip_address, user_agent, 'success', None, user_id)
|
551
|
+
self._reset_ip_failures(ip_address, 'login')
|
552
|
+
|
553
|
+
return {
|
554
|
+
'success': True,
|
555
|
+
'user_id': user_id,
|
556
|
+
'username': user['username'],
|
557
|
+
'role': role,
|
558
|
+
'permissions': self._safe_json_parse(permissions),
|
559
|
+
'last_login': current_time_utc.isoformat()
|
560
|
+
}
|
561
|
+
|
562
|
+
finally:
|
563
|
+
cursor.close()
|
564
|
+
conn.close()
|
565
|
+
|
566
|
+
def generate_access_token(self, user_info):
|
567
|
+
"""生成访问令牌"""
|
568
|
+
now_utc = datetime.now(timezone.utc)
|
569
|
+
exp_utc = now_utc + timedelta(seconds=self.auth_config['access_token_expires'])
|
570
|
+
|
571
|
+
payload = {
|
572
|
+
'user_id': user_info['user_id'],
|
573
|
+
'username': user_info['username'],
|
574
|
+
'role': user_info['role'],
|
575
|
+
'permissions': user_info['permissions'],
|
576
|
+
'iat': int(now_utc.timestamp()),
|
577
|
+
'exp': int(exp_utc.timestamp()),
|
578
|
+
'type': 'access'
|
579
|
+
}
|
580
|
+
|
581
|
+
return jwt.encode(payload, self.auth_config['secret_key'], algorithm=self.auth_config['algorithm'])
|
582
|
+
|
583
|
+
def verify_access_token(self, token):
|
584
|
+
"""验证访问令牌"""
|
585
|
+
try:
|
586
|
+
payload = jwt.decode(token, self.auth_config['secret_key'], algorithms=[self.auth_config['algorithm']])
|
587
|
+
|
588
|
+
if payload.get('type') != 'access':
|
589
|
+
return None
|
590
|
+
|
591
|
+
return payload
|
592
|
+
|
593
|
+
except jwt.ExpiredSignatureError:
|
594
|
+
return None
|
595
|
+
except jwt.InvalidTokenError:
|
596
|
+
return None
|
597
|
+
|
598
|
+
def create_or_update_device_session(self, user_id, ip_address, user_agent):
|
599
|
+
"""创建或更新设备会话"""
|
600
|
+
device_fingerprint = self._generate_device_fingerprint(user_agent, ip_address)
|
601
|
+
device_info = self._parse_user_agent(user_agent)
|
602
|
+
device_id = secrets.token_urlsafe(32)
|
603
|
+
|
604
|
+
conn = self.pool.connection()
|
605
|
+
cursor = conn.cursor()
|
606
|
+
|
607
|
+
try:
|
608
|
+
current_time_utc = datetime.now(timezone.utc)
|
609
|
+
|
610
|
+
# 检查设备是否已存在
|
611
|
+
cursor.execute('''
|
612
|
+
SELECT id, device_id FROM device_sessions
|
613
|
+
WHERE user_id = %s AND device_fingerprint = %s AND is_active = 1
|
614
|
+
''', (user_id, device_fingerprint))
|
615
|
+
|
616
|
+
existing_session = cursor.fetchone()
|
617
|
+
|
618
|
+
if existing_session:
|
619
|
+
# 更新现有设备会话
|
620
|
+
device_session_id = existing_session['id']
|
621
|
+
device_id = existing_session['device_id']
|
622
|
+
|
623
|
+
cursor.execute('''
|
624
|
+
UPDATE device_sessions
|
625
|
+
SET ip_address = %s, user_agent = %s, last_activity = %s,
|
626
|
+
device_name = %s, device_type = %s, platform = %s, browser = %s
|
627
|
+
WHERE id = %s
|
628
|
+
''', (ip_address, user_agent, current_time_utc,
|
629
|
+
device_info['device_name'], device_info['device_type'],
|
630
|
+
device_info['platform'], device_info['browser'], device_session_id))
|
631
|
+
else:
|
632
|
+
# 检查设备数量限制
|
633
|
+
cursor.execute('''
|
634
|
+
SELECT COUNT(*) as active_count FROM device_sessions
|
635
|
+
WHERE user_id = %s AND is_active = 1
|
636
|
+
''', (user_id,))
|
637
|
+
|
638
|
+
active_count = cursor.fetchone()['active_count']
|
639
|
+
|
640
|
+
if active_count >= self.auth_config['max_concurrent_devices']:
|
641
|
+
# 踢出最旧的设备
|
642
|
+
cursor.execute('''
|
643
|
+
SELECT id FROM device_sessions
|
644
|
+
WHERE user_id = %s AND is_active = 1
|
645
|
+
ORDER BY last_activity ASC
|
646
|
+
LIMIT %s
|
647
|
+
''', (user_id, active_count - self.auth_config['max_concurrent_devices'] + 1))
|
648
|
+
|
649
|
+
old_sessions = cursor.fetchall()
|
650
|
+
for old_session in old_sessions:
|
651
|
+
self._revoke_device_session(cursor, old_session['id'], 'device_limit')
|
652
|
+
|
653
|
+
# 创建新设备会话
|
654
|
+
cursor.execute('''
|
655
|
+
INSERT INTO device_sessions (
|
656
|
+
user_id, device_id, device_fingerprint, device_name, device_type,
|
657
|
+
platform, browser, ip_address, user_agent, last_activity
|
658
|
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
659
|
+
''', (user_id, device_id, device_fingerprint, device_info['device_name'],
|
660
|
+
device_info['device_type'], device_info['platform'], device_info['browser'],
|
661
|
+
ip_address, user_agent, current_time_utc))
|
662
|
+
|
663
|
+
device_session_id = cursor.lastrowid
|
664
|
+
|
665
|
+
return device_session_id, device_id, device_info['device_name']
|
666
|
+
|
667
|
+
finally:
|
668
|
+
cursor.close()
|
669
|
+
conn.close()
|
670
|
+
|
671
|
+
def generate_refresh_token(self, user_info, device_session_id):
|
672
|
+
"""生成刷新令牌"""
|
673
|
+
token = secrets.token_urlsafe(64)
|
674
|
+
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
675
|
+
current_time_utc = datetime.now(timezone.utc)
|
676
|
+
expires_at = current_time_utc + timedelta(seconds=self.auth_config['refresh_token_expires'])
|
677
|
+
absolute_expires_at = current_time_utc + timedelta(days=self.auth_config['absolute_refresh_expires_days'])
|
678
|
+
|
679
|
+
conn = self.pool.connection()
|
680
|
+
cursor = conn.cursor()
|
681
|
+
|
682
|
+
try:
|
683
|
+
# 清理该设备的旧token
|
684
|
+
cursor.execute('''
|
685
|
+
DELETE FROM refresh_tokens
|
686
|
+
WHERE device_session_id = %s
|
687
|
+
''', (device_session_id,))
|
688
|
+
|
689
|
+
# 插入新token
|
690
|
+
cursor.execute('''
|
691
|
+
INSERT INTO refresh_tokens (
|
692
|
+
token_hash, token_original, user_id, device_session_id, expires_at, absolute_expires_at,
|
693
|
+
rotation_count, max_rotations, created_at
|
694
|
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
695
|
+
''', (token_hash, token, user_info['user_id'], device_session_id, expires_at,
|
696
|
+
absolute_expires_at, 0, self.auth_config['max_refresh_rotations'], current_time_utc))
|
697
|
+
|
698
|
+
return token
|
699
|
+
|
700
|
+
finally:
|
701
|
+
cursor.close()
|
702
|
+
conn.close()
|
703
|
+
|
704
|
+
def refresh_access_token(self, refresh_token, ip_address=None, user_agent=None):
|
705
|
+
"""刷新访问令牌"""
|
706
|
+
try:
|
707
|
+
if not refresh_token:
|
708
|
+
return None
|
709
|
+
|
710
|
+
token_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
|
711
|
+
|
712
|
+
conn = self.pool.connection()
|
713
|
+
cursor = conn.cursor()
|
714
|
+
|
715
|
+
try:
|
716
|
+
# 验证刷新令牌
|
717
|
+
current_time_utc = datetime.now(timezone.utc)
|
718
|
+
current_time_naive = current_time_utc.replace(tzinfo=None)
|
719
|
+
|
720
|
+
cursor.execute('''
|
721
|
+
SELECT rt.user_id, rt.device_session_id, rt.expires_at, rt.absolute_expires_at,
|
722
|
+
rt.rotation_count, rt.max_rotations,
|
723
|
+
u.username, u.role, u.permissions,
|
724
|
+
ds.device_id, ds.device_name, ds.is_active as device_active
|
725
|
+
FROM refresh_tokens rt
|
726
|
+
JOIN users u ON rt.user_id = u.id
|
727
|
+
JOIN device_sessions ds ON rt.device_session_id = ds.id
|
728
|
+
WHERE rt.token_hash = %s
|
729
|
+
AND rt.expires_at > %s
|
730
|
+
AND rt.absolute_expires_at > %s
|
731
|
+
AND rt.rotation_count < rt.max_rotations
|
732
|
+
AND rt.is_revoked = 0
|
733
|
+
AND ds.is_active = 1
|
734
|
+
''', (token_hash, current_time_naive, current_time_naive))
|
735
|
+
|
736
|
+
result = cursor.fetchone()
|
737
|
+
|
738
|
+
if not result:
|
739
|
+
return None
|
740
|
+
|
741
|
+
# 更新设备活动时间
|
742
|
+
cursor.execute('''
|
743
|
+
UPDATE device_sessions
|
744
|
+
SET last_activity = %s
|
745
|
+
WHERE id = %s
|
746
|
+
''', (current_time_utc, result['device_session_id']))
|
747
|
+
|
748
|
+
# 生成新的tokens
|
749
|
+
user_info = {
|
750
|
+
'user_id': result['user_id'],
|
751
|
+
'username': result['username'],
|
752
|
+
'role': result['role'],
|
753
|
+
'permissions': self._safe_json_parse(result['permissions'])
|
754
|
+
}
|
755
|
+
|
756
|
+
# 生成新的access token
|
757
|
+
access_token = self.generate_access_token(user_info)
|
758
|
+
|
759
|
+
# 生成新的refresh token(轮换)
|
760
|
+
new_token = secrets.token_urlsafe(64)
|
761
|
+
new_token_hash = hashlib.sha256(new_token.encode()).hexdigest()
|
762
|
+
token_expires_at = current_time_utc + timedelta(seconds=self.auth_config['refresh_token_expires'])
|
763
|
+
|
764
|
+
# 删除旧token并插入新token
|
765
|
+
cursor.execute('''
|
766
|
+
DELETE FROM refresh_tokens
|
767
|
+
WHERE device_session_id = %s
|
768
|
+
''', (result['device_session_id'],))
|
769
|
+
|
770
|
+
cursor.execute('''
|
771
|
+
INSERT INTO refresh_tokens (
|
772
|
+
token_hash, token_original, user_id, device_session_id, expires_at, absolute_expires_at,
|
773
|
+
rotation_count, max_rotations, created_at
|
774
|
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
775
|
+
''', (new_token_hash, new_token, result['user_id'], result['device_session_id'],
|
776
|
+
token_expires_at, result['absolute_expires_at'],
|
777
|
+
result['rotation_count'] + 1, result['max_rotations'], current_time_utc))
|
778
|
+
|
779
|
+
return {
|
780
|
+
'access_token': access_token,
|
781
|
+
'refresh_token': new_token,
|
782
|
+
'user_id': result['user_id'],
|
783
|
+
'username': result['username'],
|
784
|
+
'device_info': {
|
785
|
+
'device_id': result['device_id'],
|
786
|
+
'device_name': result['device_name']
|
787
|
+
},
|
788
|
+
'rotation_info': {
|
789
|
+
'current_rotation': result['rotation_count'] + 1,
|
790
|
+
'max_rotations': result['max_rotations'],
|
791
|
+
'absolute_expires_at': result['absolute_expires_at'].isoformat()
|
792
|
+
}
|
793
|
+
}
|
794
|
+
|
795
|
+
finally:
|
796
|
+
cursor.close()
|
797
|
+
conn.close()
|
798
|
+
|
799
|
+
except Exception as e:
|
800
|
+
print(f"刷新访问令牌失败: {str(e)}")
|
801
|
+
return None
|
802
|
+
|
803
|
+
def logout_user(self, user_id, ip_address=None, user_agent=None):
|
804
|
+
"""用户登出(所有设备)"""
|
805
|
+
conn = self.pool.connection()
|
806
|
+
cursor = conn.cursor()
|
807
|
+
|
808
|
+
try:
|
809
|
+
current_time_utc = datetime.now(timezone.utc)
|
810
|
+
|
811
|
+
# 撤销用户的所有刷新令牌
|
812
|
+
cursor.execute('''
|
813
|
+
UPDATE refresh_tokens
|
814
|
+
SET is_revoked = 1, revoked_at = %s, revoked_reason = 'logout'
|
815
|
+
WHERE user_id = %s AND is_revoked = 0
|
816
|
+
''', (current_time_utc, user_id))
|
817
|
+
|
818
|
+
# 停用用户的所有设备会话
|
819
|
+
cursor.execute('''
|
820
|
+
UPDATE device_sessions
|
821
|
+
SET is_active = 0
|
822
|
+
WHERE user_id = %s AND is_active = 1
|
823
|
+
''', (user_id,))
|
824
|
+
|
825
|
+
return {'success': True, 'message': '已成功登出所有设备'}
|
826
|
+
|
827
|
+
except Exception as e:
|
828
|
+
return {'success': False, 'message': f'登出失败: {str(e)}'}
|
829
|
+
finally:
|
830
|
+
cursor.close()
|
831
|
+
conn.close()
|
832
|
+
|
833
|
+
# ==================== 辅助方法 ====================
|
834
|
+
|
835
|
+
def _check_ip_rate_limit(self, ip_address, action_type='login'):
|
836
|
+
"""检查IP是否被限流"""
|
837
|
+
if not ip_address:
|
838
|
+
return {'blocked': False, 'remaining_time': 0, 'reason': ''}
|
839
|
+
|
840
|
+
conn = self.pool.connection()
|
841
|
+
cursor = conn.cursor()
|
842
|
+
|
843
|
+
try:
|
844
|
+
cursor.execute('''
|
845
|
+
SELECT failure_count, locked_until, lockout_count, last_failure, first_failure
|
846
|
+
FROM ip_rate_limits
|
847
|
+
WHERE ip_address = %s AND action_type = %s
|
848
|
+
''', (ip_address, action_type))
|
849
|
+
|
850
|
+
record = cursor.fetchone()
|
851
|
+
|
852
|
+
if not record:
|
853
|
+
return {'blocked': False, 'remaining_time': 0, 'reason': ''}
|
854
|
+
|
855
|
+
locked_until = record['locked_until']
|
856
|
+
|
857
|
+
current_time_utc = datetime.now(timezone.utc)
|
858
|
+
if locked_until:
|
859
|
+
if locked_until.tzinfo is None:
|
860
|
+
locked_until = locked_until.replace(tzinfo=timezone.utc)
|
861
|
+
elif locked_until.tzinfo != timezone.utc:
|
862
|
+
locked_until = locked_until.astimezone(timezone.utc)
|
863
|
+
|
864
|
+
if locked_until > current_time_utc:
|
865
|
+
remaining_seconds = int((locked_until - current_time_utc).total_seconds())
|
866
|
+
return {
|
867
|
+
'blocked': True,
|
868
|
+
'remaining_time': remaining_seconds,
|
869
|
+
'reason': f'IP被锁定,剩余时间: {remaining_seconds}秒'
|
870
|
+
}
|
871
|
+
|
872
|
+
return {'blocked': False, 'remaining_time': 0, 'reason': ''}
|
873
|
+
|
874
|
+
finally:
|
875
|
+
cursor.close()
|
876
|
+
conn.close()
|
877
|
+
|
878
|
+
def _record_ip_failure(self, ip_address, action_type='login'):
|
879
|
+
"""记录IP级别的失败尝试"""
|
880
|
+
if not ip_address:
|
881
|
+
return
|
882
|
+
|
883
|
+
conn = self.pool.connection()
|
884
|
+
cursor = conn.cursor()
|
885
|
+
|
886
|
+
try:
|
887
|
+
now = datetime.now(timezone.utc)
|
888
|
+
|
889
|
+
cursor.execute('''
|
890
|
+
SELECT failure_count, first_failure, lockout_count
|
891
|
+
FROM ip_rate_limits
|
892
|
+
WHERE ip_address = %s AND action_type = %s
|
893
|
+
FOR UPDATE
|
894
|
+
''', (ip_address, action_type))
|
895
|
+
|
896
|
+
record = cursor.fetchone()
|
897
|
+
|
898
|
+
if record:
|
899
|
+
window_minutes = self.auth_config['ip_window_minutes']
|
900
|
+
window_start = now - timedelta(minutes=window_minutes)
|
901
|
+
first_failure = record['first_failure']
|
902
|
+
|
903
|
+
if first_failure and first_failure.replace(tzinfo=timezone.utc) <= window_start:
|
904
|
+
# 重置计数器
|
905
|
+
cursor.execute('''
|
906
|
+
UPDATE ip_rate_limits
|
907
|
+
SET failure_count = 1, first_failure = %s, last_failure = %s
|
908
|
+
WHERE ip_address = %s AND action_type = %s
|
909
|
+
''', (now, now, ip_address, action_type))
|
910
|
+
else:
|
911
|
+
# 增加失败计数
|
912
|
+
new_count = record['failure_count'] + 1
|
913
|
+
cursor.execute('''
|
914
|
+
UPDATE ip_rate_limits
|
915
|
+
SET failure_count = %s, last_failure = %s
|
916
|
+
WHERE ip_address = %s AND action_type = %s
|
917
|
+
''', (new_count, now, ip_address, action_type))
|
918
|
+
else:
|
919
|
+
# 创建新记录
|
920
|
+
cursor.execute('''
|
921
|
+
INSERT INTO ip_rate_limits
|
922
|
+
(ip_address, action_type, failure_count, first_failure, last_failure)
|
923
|
+
VALUES (%s, %s, 1, %s, %s)
|
924
|
+
''', (ip_address, action_type, now, now))
|
925
|
+
|
926
|
+
finally:
|
927
|
+
cursor.close()
|
928
|
+
conn.close()
|
929
|
+
|
930
|
+
def _reset_ip_failures(self, ip_address, action_type='login'):
|
931
|
+
"""重置IP失败计数"""
|
932
|
+
if not ip_address:
|
933
|
+
return
|
934
|
+
|
935
|
+
conn = self.pool.connection()
|
936
|
+
cursor = conn.cursor()
|
937
|
+
|
938
|
+
try:
|
939
|
+
cursor.execute('''
|
940
|
+
DELETE FROM ip_rate_limits
|
941
|
+
WHERE ip_address = %s AND action_type = %s
|
942
|
+
''', (ip_address, action_type))
|
943
|
+
finally:
|
944
|
+
cursor.close()
|
945
|
+
conn.close()
|
946
|
+
|
947
|
+
def _revoke_device_session(self, cursor, device_session_id, reason='manual'):
|
948
|
+
"""撤销设备会话"""
|
949
|
+
current_time_utc = datetime.now(timezone.utc)
|
950
|
+
|
951
|
+
# 撤销设备相关的refresh token
|
952
|
+
cursor.execute('''
|
953
|
+
UPDATE refresh_tokens
|
954
|
+
SET is_revoked = 1, revoked_at = %s, revoked_reason = %s
|
955
|
+
WHERE device_session_id = %s AND is_revoked = 0
|
956
|
+
''', (current_time_utc, reason, device_session_id))
|
957
|
+
|
958
|
+
# 停用设备会话
|
959
|
+
cursor.execute('''
|
960
|
+
UPDATE device_sessions
|
961
|
+
SET is_active = 0
|
962
|
+
WHERE id = %s
|
963
|
+
''', (device_session_id,))
|
964
|
+
|
965
|
+
def _generate_device_fingerprint(self, user_agent, ip_address):
|
966
|
+
"""生成设备指纹"""
|
967
|
+
fingerprint_data = f"{user_agent}:{ip_address}:{datetime.now().strftime('%Y%m%d')}"
|
968
|
+
return hashlib.sha256(fingerprint_data.encode()).hexdigest()[:32]
|
969
|
+
|
970
|
+
def _parse_user_agent(self, user_agent):
|
971
|
+
"""解析User-Agent获取设备信息"""
|
972
|
+
if not user_agent:
|
973
|
+
return {
|
974
|
+
'device_type': 'unknown',
|
975
|
+
'platform': None,
|
976
|
+
'browser': None,
|
977
|
+
'device_name': 'Unknown Device'
|
978
|
+
}
|
979
|
+
|
980
|
+
user_agent_lower = user_agent.lower()
|
981
|
+
|
982
|
+
# 设备类型判断
|
983
|
+
if any(mobile in user_agent_lower for mobile in ['mobile', 'android', 'iphone', 'ipad']):
|
984
|
+
device_type = 'tablet' if 'ipad' in user_agent_lower else 'mobile'
|
985
|
+
elif 'tablet' in user_agent_lower:
|
986
|
+
device_type = 'tablet'
|
987
|
+
else:
|
988
|
+
device_type = 'desktop'
|
989
|
+
|
990
|
+
# 平台识别
|
991
|
+
platform = None
|
992
|
+
if 'windows' in user_agent_lower:
|
993
|
+
platform = 'Windows'
|
994
|
+
elif 'mac' in user_agent_lower:
|
995
|
+
platform = 'macOS'
|
996
|
+
elif 'linux' in user_agent_lower:
|
997
|
+
platform = 'Linux'
|
998
|
+
elif 'android' in user_agent_lower:
|
999
|
+
platform = 'Android'
|
1000
|
+
elif 'iphone' in user_agent_lower or 'ipad' in user_agent_lower:
|
1001
|
+
platform = 'iOS'
|
1002
|
+
|
1003
|
+
# 浏览器识别
|
1004
|
+
browser = None
|
1005
|
+
if 'chrome' in user_agent_lower:
|
1006
|
+
browser = 'Chrome'
|
1007
|
+
elif 'firefox' in user_agent_lower:
|
1008
|
+
browser = 'Firefox'
|
1009
|
+
elif 'safari' in user_agent_lower:
|
1010
|
+
browser = 'Safari'
|
1011
|
+
elif 'edge' in user_agent_lower:
|
1012
|
+
browser = 'Edge'
|
1013
|
+
|
1014
|
+
# 生成设备名称
|
1015
|
+
device_name = f"{platform or 'Unknown'}"
|
1016
|
+
if browser:
|
1017
|
+
device_name += f" - {browser}"
|
1018
|
+
if device_type != 'desktop':
|
1019
|
+
device_name += f" ({device_type.title()})"
|
1020
|
+
|
1021
|
+
return {
|
1022
|
+
'device_type': device_type,
|
1023
|
+
'platform': platform,
|
1024
|
+
'browser': browser,
|
1025
|
+
'device_name': device_name
|
1026
|
+
}
|
1027
|
+
|
1028
|
+
def _safe_json_parse(self, json_str):
|
1029
|
+
"""安全解析JSON"""
|
1030
|
+
if isinstance(json_str, str):
|
1031
|
+
try:
|
1032
|
+
return json.loads(json_str)
|
1033
|
+
except:
|
1034
|
+
return []
|
1035
|
+
return json_str or []
|
1036
|
+
|
1037
|
+
|
1038
|
+
# Flask集成装饰器
|
1039
|
+
def require_auth(auth_manager):
|
1040
|
+
"""
|
1041
|
+
认证装饰器 - 需要Flask环境
|
1042
|
+
|
1043
|
+
使用方法:
|
1044
|
+
@require_auth(auth_manager)
|
1045
|
+
def protected_route():
|
1046
|
+
return {'user': request.current_user['username']}
|
1047
|
+
"""
|
1048
|
+
if not FLASK_AVAILABLE:
|
1049
|
+
raise ImportError("Flask未安装,无法使用require_auth装饰器。请安装Flask: pip install flask")
|
1050
|
+
|
1051
|
+
def decorator(f):
|
1052
|
+
@wraps(f)
|
1053
|
+
def decorated_function(*args, **kwargs):
|
1054
|
+
auth_header = request.headers.get('Authorization')
|
1055
|
+
if not auth_header or not auth_header.startswith('Bearer '):
|
1056
|
+
return {'status': 'error', 'message': '未提供认证令牌'}, 401
|
1057
|
+
|
1058
|
+
token = auth_header[7:] # 移除 "Bearer " 前缀
|
1059
|
+
payload = auth_manager.verify_access_token(token)
|
1060
|
+
|
1061
|
+
if not payload:
|
1062
|
+
return {'status': 'error', 'message': '无效或过期的令牌'}, 401
|
1063
|
+
|
1064
|
+
# 将用户信息添加到请求上下文
|
1065
|
+
request.current_user = payload
|
1066
|
+
return f(*args, **kwargs)
|
1067
|
+
return decorated_function
|
1068
|
+
return decorator
|
1069
|
+
|
1070
|
+
|
1071
|
+
def require_permissions(auth_manager, required_permissions):
|
1072
|
+
"""
|
1073
|
+
权限检查装饰器 - 需要Flask环境
|
1074
|
+
|
1075
|
+
使用方法:
|
1076
|
+
@require_permissions(auth_manager, ['admin', 'write'])
|
1077
|
+
def admin_route():
|
1078
|
+
return {'message': 'Admin only'}
|
1079
|
+
"""
|
1080
|
+
if not FLASK_AVAILABLE:
|
1081
|
+
raise ImportError("Flask未安装,无法使用require_permissions装饰器。请安装Flask: pip install flask")
|
1082
|
+
|
1083
|
+
def decorator(f):
|
1084
|
+
@wraps(f)
|
1085
|
+
def decorated_function(*args, **kwargs):
|
1086
|
+
auth_header = request.headers.get('Authorization')
|
1087
|
+
if not auth_header or not auth_header.startswith('Bearer '):
|
1088
|
+
return {'status': 'error', 'message': '未提供认证令牌'}, 401
|
1089
|
+
|
1090
|
+
token = auth_header[7:]
|
1091
|
+
payload = auth_manager.verify_access_token(token)
|
1092
|
+
|
1093
|
+
if not payload:
|
1094
|
+
return {'status': 'error', 'message': '无效或过期的令牌'}, 401
|
1095
|
+
|
1096
|
+
user_permissions = payload.get('permissions', [])
|
1097
|
+
|
1098
|
+
# 检查是否拥有所需权限
|
1099
|
+
if not any(perm in user_permissions for perm in required_permissions):
|
1100
|
+
return {'status': 'error', 'message': '权限不足'}, 403
|
1101
|
+
|
1102
|
+
request.current_user = payload
|
1103
|
+
return f(*args, **kwargs)
|
1104
|
+
return decorated_function
|
1105
|
+
return decorator
|
1106
|
+
|
1107
|
+
|
1108
|
+
def create_auth_middleware(auth_manager):
|
1109
|
+
"""
|
1110
|
+
创建通用的认证中间件函数 - 框架无关
|
1111
|
+
|
1112
|
+
使用方法:
|
1113
|
+
auth_middleware = create_auth_middleware(auth_manager)
|
1114
|
+
|
1115
|
+
# 在你的框架中使用
|
1116
|
+
def your_route_handler(request_headers):
|
1117
|
+
user = auth_middleware(request_headers.get('Authorization'))
|
1118
|
+
if user:
|
1119
|
+
return {'message': f'Hello {user["username"]}'}
|
1120
|
+
else:
|
1121
|
+
return {'error': 'Unauthorized'}, 401
|
1122
|
+
"""
|
1123
|
+
def middleware(auth_header):
|
1124
|
+
"""
|
1125
|
+
认证中间件函数
|
1126
|
+
|
1127
|
+
Args:
|
1128
|
+
auth_header (str): Authorization头部值
|
1129
|
+
|
1130
|
+
Returns:
|
1131
|
+
dict: 用户信息,如果认证失败返回None
|
1132
|
+
"""
|
1133
|
+
if not auth_header or not auth_header.startswith('Bearer '):
|
1134
|
+
return None
|
1135
|
+
|
1136
|
+
token = auth_header[7:] # 移除 "Bearer " 前缀
|
1137
|
+
payload = auth_manager.verify_access_token(token)
|
1138
|
+
|
1139
|
+
return payload
|
1140
|
+
|
1141
|
+
return middleware
|
1142
|
+
|
1143
|
+
|
1144
|
+
def create_permission_checker(auth_manager, required_permissions):
|
1145
|
+
"""
|
1146
|
+
创建权限检查函数 - 框架无关
|
1147
|
+
|
1148
|
+
使用方法:
|
1149
|
+
check_admin = create_permission_checker(auth_manager, ['admin'])
|
1150
|
+
|
1151
|
+
def your_admin_route(request_headers):
|
1152
|
+
user = check_admin(request_headers.get('Authorization'))
|
1153
|
+
if user:
|
1154
|
+
return {'message': 'Admin access granted'}
|
1155
|
+
else:
|
1156
|
+
return {'error': 'Permission denied'}, 403
|
1157
|
+
"""
|
1158
|
+
def permission_checker(auth_header):
|
1159
|
+
"""
|
1160
|
+
权限检查函数
|
1161
|
+
|
1162
|
+
Args:
|
1163
|
+
auth_header (str): Authorization头部值
|
1164
|
+
|
1165
|
+
Returns:
|
1166
|
+
dict: 用户信息,如果权限不足返回None
|
1167
|
+
"""
|
1168
|
+
if not auth_header or not auth_header.startswith('Bearer '):
|
1169
|
+
return None
|
1170
|
+
|
1171
|
+
token = auth_header[7:]
|
1172
|
+
payload = auth_manager.verify_access_token(token)
|
1173
|
+
|
1174
|
+
if not payload:
|
1175
|
+
return None
|
1176
|
+
|
1177
|
+
user_permissions = payload.get('permissions', [])
|
1178
|
+
|
1179
|
+
# 检查是否拥有所需权限
|
1180
|
+
if not any(perm in user_permissions for perm in required_permissions):
|
1181
|
+
return None
|
1182
|
+
|
1183
|
+
return payload
|
1184
|
+
|
1185
|
+
return permission_checker
|
1186
|
+
|
1187
|
+
|
1188
|
+
# 使用示例
|
1189
|
+
if __name__ == "__main__":
|
1190
|
+
# 数据库配置
|
1191
|
+
db_config = {
|
1192
|
+
'host': 'localhost',
|
1193
|
+
'port': 3306,
|
1194
|
+
'user': 'root',
|
1195
|
+
'password': 'password',
|
1196
|
+
'database': 'standalone_auth'
|
1197
|
+
}
|
1198
|
+
|
1199
|
+
# 认证配置
|
1200
|
+
auth_config = {
|
1201
|
+
'secret_key': 'your-secret-key',
|
1202
|
+
'access_token_expires': 30 * 60, # 30分钟
|
1203
|
+
'refresh_token_expires': 7 * 24 * 60 * 60, # 7天
|
1204
|
+
}
|
1205
|
+
|
1206
|
+
# 初始化认证管理器
|
1207
|
+
auth_manager = StandaloneAuthManager(db_config, auth_config)
|
1208
|
+
|
1209
|
+
# 注册用户
|
1210
|
+
result = auth_manager.register_user('admin', 'password123', 'admin', ['read', 'write', 'admin'])
|
1211
|
+
print("注册结果:", result)
|
1212
|
+
|
1213
|
+
# 用户认证
|
1214
|
+
auth_result = auth_manager.authenticate_user('admin', 'password123', '127.0.0.1', 'Mozilla/5.0...')
|
1215
|
+
print("认证结果:", auth_result)
|
1216
|
+
|
1217
|
+
if auth_result['success']:
|
1218
|
+
# 创建设备会话
|
1219
|
+
device_session_id, device_id, device_name = auth_manager.create_or_update_device_session(
|
1220
|
+
auth_result['user_id'], '127.0.0.1', 'Mozilla/5.0...'
|
1221
|
+
)
|
1222
|
+
|
1223
|
+
# 生成tokens
|
1224
|
+
access_token = auth_manager.generate_access_token(auth_result)
|
1225
|
+
refresh_token = auth_manager.generate_refresh_token(auth_result, device_session_id)
|
1226
|
+
|
1227
|
+
print("Access Token:", access_token)
|
1228
|
+
print("Refresh Token:", refresh_token)
|
1229
|
+
|
1230
|
+
# 验证token
|
1231
|
+
payload = auth_manager.verify_access_token(access_token)
|
1232
|
+
print("Token验证结果:", payload)
|
1233
|
+
|
1234
|
+
# 刷新token
|
1235
|
+
refresh_result = auth_manager.refresh_access_token(refresh_token, '127.0.0.1', 'Mozilla/5.0...')
|
1236
|
+
print("Token刷新结果:", refresh_result)
|
mdbq/js/__init__.py
CHANGED
mdbq/route/__init__.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
|
2
|
-
mdbq/__version__.py,sha256=
|
3
|
-
mdbq/
|
2
|
+
mdbq/__version__.py,sha256=1cAzEdaeikkGhjjrr91h97S03iZYSDZ8GqZTMSF7EXs,18
|
3
|
+
mdbq/auth/__init__.py,sha256=pnPMAt63sh1B6kEvmutUuro46zVf2v2YDAG7q-jV_To,24
|
4
|
+
mdbq/auth/auth_backend.py,sha256=Ku01bMXHPu0tD9x7-sWQg9kLremm_OxD_9FhtlrW_Jk,49803
|
5
|
+
mdbq/js/__init__.py,sha256=hpMi3_ZKwIWkzc0LnKL-SY9AS-7PYFHq0izYTgEvxjc,30
|
4
6
|
mdbq/js/jc.py,sha256=FOc6HOOTJwnoZLZmgmaE1SQo9rUnVhXmefhKMD2MlDA,13229
|
5
7
|
mdbq/log/__init__.py,sha256=Mpbrav0s0ifLL7lVDAuePEi1hJKiSHhxcv1byBKDl5E,15
|
6
8
|
mdbq/log/mylogger.py,sha256=DyBftCMNLe1pTTXsa830pUtDISJxpJHFIradYtE3lFA,26418
|
@@ -23,14 +25,14 @@ mdbq/pbix/pbix_refresh.py,sha256=JUjKW3bNEyoMVfVfo77UhguvS5AWkixvVhDbw4_MHco,239
|
|
23
25
|
mdbq/pbix/refresh_all.py,sha256=OBT9EewSZ0aRS9vL_FflVn74d4l2G00wzHiikCC4TC0,5926
|
24
26
|
mdbq/redis/__init__.py,sha256=YtgBlVSMDphtpwYX248wGge1x-Ex_mMufz4-8W0XRmA,12
|
25
27
|
mdbq/redis/getredis.py,sha256=vpBuNc22uj9Vr-_Dh25_wpwWM1e-072EAAIBdB_IpL0,23494
|
26
|
-
mdbq/route/__init__.py,sha256=
|
28
|
+
mdbq/route/__init__.py,sha256=BT_dAY7V-U2o72bevq1B9Mq9QA7GodwtkxyLNdGaoE8,22
|
27
29
|
mdbq/route/analytics.py,sha256=iJ-LyE_LNICg4LB9XOd0L-N3Ucfl6BWUTVu9jUNAplg,28069
|
28
30
|
mdbq/route/monitor.py,sha256=TLcPOJj_gPhNeCS4tSTrC4Y-2QbWW8poGO4Zu4wzDh8,42311
|
29
31
|
mdbq/route/routes.py,sha256=DHJg0eRNi7TKqhCHuu8ia3vdQ8cTKwrTm6mwDBtNboM,19111
|
30
32
|
mdbq/selenium/__init__.py,sha256=AKzeEceqZyvqn2dEDoJSzDQnbuENkJSHAlbHAD0u0ZI,10
|
31
33
|
mdbq/selenium/get_driver.py,sha256=1NTlVUE6QsyjTrVVVqTO2LOnYf578ccFWlWnvIXGtic,20903
|
32
34
|
mdbq/spider/__init__.py,sha256=RBMFXGy_jd1HXZhngB2T2XTvJqki8P_Fr-pBcwijnew,18
|
33
|
-
mdbq-4.0.
|
34
|
-
mdbq-4.0.
|
35
|
-
mdbq-4.0.
|
36
|
-
mdbq-4.0.
|
35
|
+
mdbq-4.0.89.dist-info/METADATA,sha256=beg2mp0p0B13KVemg-Pn87UFXcrNvBoG_NVHhwdtYrQ,364
|
36
|
+
mdbq-4.0.89.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
37
|
+
mdbq-4.0.89.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
|
38
|
+
mdbq-4.0.89.dist-info/RECORD,,
|
File without changes
|
File without changes
|