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 CHANGED
@@ -1 +1 @@
1
- VERSION = '4.0.87'
1
+ VERSION = '4.0.89'
mdbq/auth/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+
2
+
3
+
4
+ # 用户认证系统
@@ -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
@@ -1,4 +1,4 @@
1
1
 
2
2
 
3
3
 
4
- #
4
+ # JS/CSS加密混淆工具
mdbq/route/__init__.py CHANGED
@@ -1 +1,3 @@
1
- #
1
+
2
+
3
+ # 路由监听系统
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdbq
3
- Version: 4.0.87
3
+ Version: 4.0.89
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -1,6 +1,8 @@
1
1
  mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
2
- mdbq/__version__.py,sha256=kgAHeV5rmBm2NSVTiA-VE9pJX4ZQJV-1Zlkl3EXsWBQ,18
3
- mdbq/js/__init__.py,sha256=i-5dOsgWnoqs9DtclORulncneyyNFIBW9qlrgkyfeuc,6
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=M0NZuQ7-112l8K2h1eayVvSmvQrufrOcD5AYKgIf_Is,1
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.87.dist-info/METADATA,sha256=DABl5p4FqCv3Ib6zAu2KjsT9N-GjFwD3Qtzw1yt0dIw,364
34
- mdbq-4.0.87.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
- mdbq-4.0.87.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
36
- mdbq-4.0.87.dist-info/RECORD,,
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