mdbq 4.1.2__py3-none-any.whl → 4.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mdbq might be problematic. Click here for more details.
- mdbq/__version__.py +1 -1
- mdbq/auth/auth_backend.py +440 -0
- mdbq/myconf/myconf.py +30 -43
- mdbq/mysql/s_query.py +1 -1
- mdbq/redis/getredis.py +33 -28
- mdbq/redis/redis_cache.py +103 -134
- mdbq/route/monitor.py +58 -201
- {mdbq-4.1.2.dist-info → mdbq-4.1.4.dist-info}/METADATA +1 -1
- {mdbq-4.1.2.dist-info → mdbq-4.1.4.dist-info}/RECORD +11 -11
- {mdbq-4.1.2.dist-info → mdbq-4.1.4.dist-info}/WHEEL +0 -0
- {mdbq-4.1.2.dist-info → mdbq-4.1.4.dist-info}/top_level.txt +0 -0
mdbq/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = '4.1.
|
|
1
|
+
VERSION = '4.1.4'
|
mdbq/auth/auth_backend.py
CHANGED
|
@@ -27,6 +27,12 @@ import re
|
|
|
27
27
|
from datetime import datetime, timedelta, timezone
|
|
28
28
|
from functools import wraps
|
|
29
29
|
from dbutils.pooled_db import PooledDB # type: ignore
|
|
30
|
+
from PIL import Image, ImageDraw, ImageFilter, ImageEnhance
|
|
31
|
+
import random
|
|
32
|
+
import time
|
|
33
|
+
import os
|
|
34
|
+
import io
|
|
35
|
+
import base64
|
|
30
36
|
|
|
31
37
|
# Flask相关导入 - 用于装饰器功能
|
|
32
38
|
try:
|
|
@@ -2247,6 +2253,440 @@ class StandaloneAuthManager:
|
|
|
2247
2253
|
conn.close()
|
|
2248
2254
|
|
|
2249
2255
|
|
|
2256
|
+
class SlideCaptchaManager:
|
|
2257
|
+
"""滑块验证码管理器"""
|
|
2258
|
+
|
|
2259
|
+
def __init__(self, redis_client, slide_image_dir):
|
|
2260
|
+
self.redis_client = redis_client
|
|
2261
|
+
self.captcha_width = 300
|
|
2262
|
+
self.captcha_height = 150
|
|
2263
|
+
self.puzzle_size = 42
|
|
2264
|
+
self.puzzle_tolerance = 8
|
|
2265
|
+
self.expire_time = 300 # 5分钟过期
|
|
2266
|
+
self.slide_image_dir = slide_image_dir
|
|
2267
|
+
|
|
2268
|
+
def generate_captcha(self, session_id):
|
|
2269
|
+
"""生成滑块验证码"""
|
|
2270
|
+
try:
|
|
2271
|
+
# 生成背景图片
|
|
2272
|
+
background = self._create_background()
|
|
2273
|
+
|
|
2274
|
+
# 随机生成拼图位置
|
|
2275
|
+
puzzle_x = random.randint(self.puzzle_size + 10, self.captcha_width - self.puzzle_size - 10)
|
|
2276
|
+
puzzle_y = random.randint(10, self.captcha_height - self.puzzle_size - 10)
|
|
2277
|
+
|
|
2278
|
+
# 创建拼图形状
|
|
2279
|
+
puzzle_img, puzzle_outline = self._create_puzzle_shape()
|
|
2280
|
+
|
|
2281
|
+
# 在背景上应用拼图轮廓
|
|
2282
|
+
background_with_hole = background.copy()
|
|
2283
|
+
self._apply_puzzle_hole(background_with_hole, puzzle_x, puzzle_y, puzzle_outline)
|
|
2284
|
+
|
|
2285
|
+
# 创建拼图块
|
|
2286
|
+
puzzle_piece = self._extract_puzzle_piece(background, puzzle_x, puzzle_y, puzzle_img)
|
|
2287
|
+
|
|
2288
|
+
# 转换为base64
|
|
2289
|
+
background_b64 = self._image_to_base64(background_with_hole)
|
|
2290
|
+
puzzle_b64 = self._image_to_base64(puzzle_piece)
|
|
2291
|
+
|
|
2292
|
+
# 存储验证信息到Redis
|
|
2293
|
+
captcha_data = {
|
|
2294
|
+
'puzzle_x': puzzle_x,
|
|
2295
|
+
'puzzle_y': puzzle_y,
|
|
2296
|
+
'created_at': time.time(),
|
|
2297
|
+
'attempts': 0,
|
|
2298
|
+
'max_attempts': 3
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
self.redis_client.setex(
|
|
2302
|
+
f"captcha:{session_id}",
|
|
2303
|
+
self.expire_time,
|
|
2304
|
+
json.dumps(captcha_data)
|
|
2305
|
+
)
|
|
2306
|
+
|
|
2307
|
+
return {
|
|
2308
|
+
'success': True,
|
|
2309
|
+
'session_id': session_id,
|
|
2310
|
+
'background': background_b64,
|
|
2311
|
+
'puzzle': puzzle_b64,
|
|
2312
|
+
'puzzle_y': puzzle_y,
|
|
2313
|
+
'captcha_width': self.captcha_width,
|
|
2314
|
+
'captcha_height': self.captcha_height
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
except Exception as e:
|
|
2318
|
+
return {'success': False, 'message': '生成验证码失败'}
|
|
2319
|
+
|
|
2320
|
+
def verify_captcha(self, session_id, slide_x, slide_track, slide_time):
|
|
2321
|
+
"""验证滑块位置"""
|
|
2322
|
+
try:
|
|
2323
|
+
# 获取验证码信息
|
|
2324
|
+
captcha_key = f"captcha:{session_id}"
|
|
2325
|
+
captcha_data_str = self.redis_client.get(captcha_key)
|
|
2326
|
+
|
|
2327
|
+
if not captcha_data_str:
|
|
2328
|
+
return {'success': False, 'message': '验证码已过期,请重新获取'}
|
|
2329
|
+
|
|
2330
|
+
captcha_data = json.loads(captcha_data_str)
|
|
2331
|
+
|
|
2332
|
+
# 检查尝试次数
|
|
2333
|
+
captcha_data['attempts'] += 1
|
|
2334
|
+
if captcha_data['attempts'] > captcha_data['max_attempts']:
|
|
2335
|
+
self.redis_client.delete(captcha_key)
|
|
2336
|
+
return {'success': False, 'message': '尝试次数过多,请重新获取验证码'}
|
|
2337
|
+
|
|
2338
|
+
# 验证滑动距离
|
|
2339
|
+
expected_x = captcha_data['puzzle_x']
|
|
2340
|
+
distance_error = abs(slide_x - expected_x)
|
|
2341
|
+
|
|
2342
|
+
# 验证滑动时间(防止机器人)
|
|
2343
|
+
if slide_time < 0.3: # 滑动时间太短
|
|
2344
|
+
self._update_captcha_attempts(captcha_key, captcha_data)
|
|
2345
|
+
return {'success': False, 'message': '校检失败'}
|
|
2346
|
+
|
|
2347
|
+
if slide_time > 60: # 滑动时间太长
|
|
2348
|
+
self._update_captcha_attempts(captcha_key, captcha_data)
|
|
2349
|
+
return {'success': False, 'message': '校检失败'}
|
|
2350
|
+
|
|
2351
|
+
# 验证滑动轨迹(简单的轨迹分析)
|
|
2352
|
+
if not self._validate_slide_track(slide_track, slide_x, slide_time):
|
|
2353
|
+
self._update_captcha_attempts(captcha_key, captcha_data)
|
|
2354
|
+
return {'success': False, 'message': '校检失败'}
|
|
2355
|
+
|
|
2356
|
+
# 验证位置精度
|
|
2357
|
+
if distance_error <= self.puzzle_tolerance:
|
|
2358
|
+
# 验证成功,删除验证码数据
|
|
2359
|
+
self.redis_client.delete(captcha_key)
|
|
2360
|
+
|
|
2361
|
+
# 生成验证通过的token
|
|
2362
|
+
verify_token = self._generate_verify_token(session_id)
|
|
2363
|
+
self.redis_client.setex(
|
|
2364
|
+
f"captcha_verified:{session_id}",
|
|
2365
|
+
300, # 5分钟内有效
|
|
2366
|
+
verify_token
|
|
2367
|
+
)
|
|
2368
|
+
|
|
2369
|
+
return {
|
|
2370
|
+
'success': True,
|
|
2371
|
+
'message': '验证成功',
|
|
2372
|
+
'verify_token': verify_token
|
|
2373
|
+
}
|
|
2374
|
+
else:
|
|
2375
|
+
# 更新尝试次数
|
|
2376
|
+
self._update_captcha_attempts(captcha_key, captcha_data)
|
|
2377
|
+
return {
|
|
2378
|
+
'success': False,
|
|
2379
|
+
'message': '校检失败',
|
|
2380
|
+
'attempts_left': captcha_data['max_attempts'] - captcha_data['attempts']
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
except Exception as e:
|
|
2384
|
+
return {'success': False, 'message': '验证失败,请重试'}
|
|
2385
|
+
|
|
2386
|
+
def _create_background(self):
|
|
2387
|
+
"""创建背景图片"""
|
|
2388
|
+
try:
|
|
2389
|
+
|
|
2390
|
+
# 如果文件夹不存在,创建文件夹并使用默认背景
|
|
2391
|
+
if not os.path.exists(self.slide_image_dir):
|
|
2392
|
+
os.makedirs(self.slide_image_dir, exist_ok=True)
|
|
2393
|
+
return self._create_default_background()
|
|
2394
|
+
|
|
2395
|
+
# 获取所有支持的图片文件(jpg, jpeg, png)
|
|
2396
|
+
image_files = [f for f in os.listdir(self.slide_image_dir)
|
|
2397
|
+
if f.lower().endswith(('.jpg', '.jpeg', '.png')) and os.path.isfile(os.path.join(self.slide_image_dir, f))]
|
|
2398
|
+
|
|
2399
|
+
if not image_files:
|
|
2400
|
+
return self._create_default_background()
|
|
2401
|
+
|
|
2402
|
+
# 随机选择一张图片
|
|
2403
|
+
selected_image = random.choice(image_files)
|
|
2404
|
+
image_path = os.path.join(self.slide_image_dir, selected_image)
|
|
2405
|
+
|
|
2406
|
+
# 加载并处理背景图片
|
|
2407
|
+
with Image.open(image_path) as bg_img:
|
|
2408
|
+
# 转换为RGB模式(如果需要)
|
|
2409
|
+
if bg_img.mode != 'RGB':
|
|
2410
|
+
bg_img = bg_img.convert('RGB')
|
|
2411
|
+
|
|
2412
|
+
# 智能裁剪图片到验证码尺寸,而不是简单缩放
|
|
2413
|
+
background = self._smart_crop_image(bg_img, self.captcha_width, self.captcha_height)
|
|
2414
|
+
|
|
2415
|
+
# 添加轻微的滤镜效果,增加验证难度
|
|
2416
|
+
background = background.filter(ImageFilter.GaussianBlur(radius=0.5))
|
|
2417
|
+
|
|
2418
|
+
# 调整亮度和对比度,确保拼图轮廓清晰可见
|
|
2419
|
+
enhancer = ImageEnhance.Brightness(background)
|
|
2420
|
+
background = enhancer.enhance(0.9) # 稍微变暗
|
|
2421
|
+
|
|
2422
|
+
enhancer = ImageEnhance.Contrast(background)
|
|
2423
|
+
background = enhancer.enhance(1.1) # 增加对比度
|
|
2424
|
+
|
|
2425
|
+
return background
|
|
2426
|
+
|
|
2427
|
+
except Exception as e:
|
|
2428
|
+
return self._create_default_background()
|
|
2429
|
+
|
|
2430
|
+
def _smart_crop_image(self, img, target_width, target_height):
|
|
2431
|
+
"""
|
|
2432
|
+
智能裁剪图片到目标尺寸
|
|
2433
|
+
优先保持图片的主要内容,避免简单缩放导致的变形
|
|
2434
|
+
"""
|
|
2435
|
+
original_width, original_height = img.size
|
|
2436
|
+
target_ratio = target_width / target_height
|
|
2437
|
+
original_ratio = original_width / original_height
|
|
2438
|
+
|
|
2439
|
+
if abs(original_ratio - target_ratio) < 0.1:
|
|
2440
|
+
# 如果长宽比接近,直接缩放
|
|
2441
|
+
return img.resize((target_width, target_height), Image.Resampling.LANCZOS)
|
|
2442
|
+
|
|
2443
|
+
if original_ratio > target_ratio:
|
|
2444
|
+
# 原图更宽,需要裁剪宽度
|
|
2445
|
+
# 计算需要的高度来匹配目标比例
|
|
2446
|
+
new_height = original_height
|
|
2447
|
+
new_width = int(new_height * target_ratio)
|
|
2448
|
+
|
|
2449
|
+
# 从中心开始裁剪
|
|
2450
|
+
left = (original_width - new_width) // 2
|
|
2451
|
+
top = 0
|
|
2452
|
+
right = left + new_width
|
|
2453
|
+
bottom = new_height
|
|
2454
|
+
|
|
2455
|
+
else:
|
|
2456
|
+
# 原图更高,需要裁剪高度
|
|
2457
|
+
# 计算需要的宽度来匹配目标比例
|
|
2458
|
+
new_width = original_width
|
|
2459
|
+
new_height = int(new_width / target_ratio)
|
|
2460
|
+
|
|
2461
|
+
# 从中心开始裁剪(稍微偏上,保留重要内容)
|
|
2462
|
+
left = 0
|
|
2463
|
+
top = (original_height - new_height) // 3 # 偏上1/3处开始裁剪
|
|
2464
|
+
right = new_width
|
|
2465
|
+
bottom = top + new_height
|
|
2466
|
+
|
|
2467
|
+
# 执行裁剪
|
|
2468
|
+
cropped_img = img.crop((left, top, right, bottom))
|
|
2469
|
+
|
|
2470
|
+
# 缩放到目标尺寸
|
|
2471
|
+
return cropped_img.resize((target_width, target_height), Image.Resampling.LANCZOS)
|
|
2472
|
+
|
|
2473
|
+
def _create_default_background(self):
|
|
2474
|
+
"""创建默认背景图片"""
|
|
2475
|
+
# 创建渐变背景
|
|
2476
|
+
img = Image.new('RGB', (self.captcha_width, self.captcha_height), color='white')
|
|
2477
|
+
draw = ImageDraw.Draw(img)
|
|
2478
|
+
|
|
2479
|
+
# 添加渐变色
|
|
2480
|
+
for y in range(self.captcha_height):
|
|
2481
|
+
color_intensity = int(240 - (y / self.captcha_height) * 40)
|
|
2482
|
+
color = (color_intensity, color_intensity + 5, color_intensity + 10)
|
|
2483
|
+
draw.line([(0, y), (self.captcha_width, y)], fill=color)
|
|
2484
|
+
|
|
2485
|
+
# 添加噪点
|
|
2486
|
+
for _ in range(100):
|
|
2487
|
+
x = random.randint(0, self.captcha_width - 1)
|
|
2488
|
+
y = random.randint(0, self.captcha_height - 1)
|
|
2489
|
+
draw.point((x, y), fill=(200, 200, 200))
|
|
2490
|
+
|
|
2491
|
+
# 添加干扰线
|
|
2492
|
+
for _ in range(5):
|
|
2493
|
+
x1 = random.randint(0, self.captcha_width)
|
|
2494
|
+
y1 = random.randint(0, self.captcha_height)
|
|
2495
|
+
x2 = random.randint(0, self.captcha_width)
|
|
2496
|
+
y2 = random.randint(0, self.captcha_height)
|
|
2497
|
+
draw.line([(x1, y1), (x2, y2)], fill=(220, 220, 220), width=1)
|
|
2498
|
+
|
|
2499
|
+
return img
|
|
2500
|
+
|
|
2501
|
+
def _create_puzzle_shape(self):
|
|
2502
|
+
"""创建拼图形状"""
|
|
2503
|
+
# 创建拼图轮廓
|
|
2504
|
+
img = Image.new('RGBA', (self.puzzle_size, self.puzzle_size), (0, 0, 0, 0))
|
|
2505
|
+
draw = ImageDraw.Draw(img)
|
|
2506
|
+
|
|
2507
|
+
# 基础矩形
|
|
2508
|
+
base_size = self.puzzle_size - 4
|
|
2509
|
+
offset = 2
|
|
2510
|
+
|
|
2511
|
+
# 绘制拼图形状(带凸起)
|
|
2512
|
+
points = []
|
|
2513
|
+
|
|
2514
|
+
# 左边
|
|
2515
|
+
points.extend([(offset, offset), (offset, base_size // 3)])
|
|
2516
|
+
|
|
2517
|
+
# 左侧凸起
|
|
2518
|
+
bump_size = 8
|
|
2519
|
+
points.extend([
|
|
2520
|
+
(offset - bump_size, base_size // 3),
|
|
2521
|
+
(offset - bump_size, base_size // 3 + bump_size * 2),
|
|
2522
|
+
(offset, base_size // 3 + bump_size * 2)
|
|
2523
|
+
])
|
|
2524
|
+
|
|
2525
|
+
points.extend([(offset, base_size + offset), (base_size // 3, base_size + offset)])
|
|
2526
|
+
|
|
2527
|
+
# 底部凸起
|
|
2528
|
+
points.extend([
|
|
2529
|
+
(base_size // 3, base_size + offset + bump_size),
|
|
2530
|
+
(base_size // 3 + bump_size * 2, base_size + offset + bump_size),
|
|
2531
|
+
(base_size // 3 + bump_size * 2, base_size + offset)
|
|
2532
|
+
])
|
|
2533
|
+
|
|
2534
|
+
points.extend([
|
|
2535
|
+
(base_size + offset, base_size + offset),
|
|
2536
|
+
(base_size + offset, offset),
|
|
2537
|
+
(offset, offset)
|
|
2538
|
+
])
|
|
2539
|
+
|
|
2540
|
+
draw.polygon(points, fill=(255, 255, 255, 255), outline=(0, 0, 0, 100))
|
|
2541
|
+
|
|
2542
|
+
# 创建轮廓遮罩
|
|
2543
|
+
outline = img.copy()
|
|
2544
|
+
outline_draw = ImageDraw.Draw(outline)
|
|
2545
|
+
outline_draw.polygon(points, fill=(0, 0, 0, 0), outline=(100, 100, 100, 200), width=2)
|
|
2546
|
+
|
|
2547
|
+
return img, outline
|
|
2548
|
+
|
|
2549
|
+
def _apply_puzzle_hole(self, background, x, y, puzzle_outline):
|
|
2550
|
+
"""在背景上应用拼图洞"""
|
|
2551
|
+
# 创建遮罩
|
|
2552
|
+
mask = Image.new('L', (self.puzzle_size, self.puzzle_size), 0)
|
|
2553
|
+
mask_draw = ImageDraw.Draw(mask)
|
|
2554
|
+
|
|
2555
|
+
# 绘制拼图形状到遮罩
|
|
2556
|
+
points = self._get_puzzle_points()
|
|
2557
|
+
mask_draw.polygon(points, fill=255)
|
|
2558
|
+
|
|
2559
|
+
# 在背景上创建洞
|
|
2560
|
+
hole_overlay = Image.new('RGBA', background.size, (0, 0, 0, 0))
|
|
2561
|
+
hole_draw = ImageDraw.Draw(hole_overlay)
|
|
2562
|
+
|
|
2563
|
+
# 绘制阴影洞
|
|
2564
|
+
shadow_points = [(px + x, py + y) for px, py in points]
|
|
2565
|
+
hole_draw.polygon(shadow_points, fill=(0, 0, 0, 80))
|
|
2566
|
+
|
|
2567
|
+
# 绘制边框
|
|
2568
|
+
hole_draw.polygon(shadow_points, outline=(100, 100, 100, 150), width=1)
|
|
2569
|
+
|
|
2570
|
+
# 合成到背景
|
|
2571
|
+
background.paste(hole_overlay, (0, 0), hole_overlay)
|
|
2572
|
+
|
|
2573
|
+
def _extract_puzzle_piece(self, background, x, y, puzzle_img):
|
|
2574
|
+
"""提取拼图块"""
|
|
2575
|
+
# 从背景中提取拼图区域
|
|
2576
|
+
puzzle_area = background.crop((x, y, x + self.puzzle_size, y + self.puzzle_size))
|
|
2577
|
+
|
|
2578
|
+
# 创建最终的拼图块
|
|
2579
|
+
result = Image.new('RGBA', (self.puzzle_size, self.puzzle_size), (0, 0, 0, 0))
|
|
2580
|
+
|
|
2581
|
+
# 使用拼图形状作为遮罩
|
|
2582
|
+
mask = Image.new('L', (self.puzzle_size, self.puzzle_size), 0)
|
|
2583
|
+
mask_draw = ImageDraw.Draw(mask)
|
|
2584
|
+
points = self._get_puzzle_points()
|
|
2585
|
+
mask_draw.polygon(points, fill=255)
|
|
2586
|
+
|
|
2587
|
+
# 应用遮罩
|
|
2588
|
+
puzzle_area.putalpha(mask)
|
|
2589
|
+
result.paste(puzzle_area, (0, 0), puzzle_area)
|
|
2590
|
+
|
|
2591
|
+
# 添加边框
|
|
2592
|
+
draw = ImageDraw.Draw(result)
|
|
2593
|
+
draw.polygon(points, outline=(255, 255, 255, 200), width=1)
|
|
2594
|
+
|
|
2595
|
+
return result
|
|
2596
|
+
|
|
2597
|
+
def _get_puzzle_points(self):
|
|
2598
|
+
"""获取拼图形状的点坐标"""
|
|
2599
|
+
base_size = self.puzzle_size - 4
|
|
2600
|
+
offset = 2
|
|
2601
|
+
bump_size = 8
|
|
2602
|
+
|
|
2603
|
+
points = [
|
|
2604
|
+
(offset, offset),
|
|
2605
|
+
(offset, base_size // 3),
|
|
2606
|
+
(offset - bump_size, base_size // 3),
|
|
2607
|
+
(offset - bump_size, base_size // 3 + bump_size * 2),
|
|
2608
|
+
(offset, base_size // 3 + bump_size * 2),
|
|
2609
|
+
(offset, base_size + offset),
|
|
2610
|
+
(base_size // 3, base_size + offset),
|
|
2611
|
+
(base_size // 3, base_size + offset + bump_size),
|
|
2612
|
+
(base_size // 3 + bump_size * 2, base_size + offset + bump_size),
|
|
2613
|
+
(base_size // 3 + bump_size * 2, base_size + offset),
|
|
2614
|
+
(base_size + offset, base_size + offset),
|
|
2615
|
+
(base_size + offset, offset),
|
|
2616
|
+
(offset, offset)
|
|
2617
|
+
]
|
|
2618
|
+
return points
|
|
2619
|
+
|
|
2620
|
+
def _image_to_base64(self, img):
|
|
2621
|
+
"""将图片转换为base64"""
|
|
2622
|
+
buffer = io.BytesIO()
|
|
2623
|
+
img.save(buffer, format='PNG')
|
|
2624
|
+
img_str = base64.b64encode(buffer.getvalue()).decode()
|
|
2625
|
+
return f"data:image/png;base64,{img_str}"
|
|
2626
|
+
|
|
2627
|
+
def _validate_slide_track(self, track, final_x, slide_time):
|
|
2628
|
+
"""验证滑动轨迹"""
|
|
2629
|
+
if not track or len(track) < 2:
|
|
2630
|
+
return True # 简化验证,允许较少的轨迹点
|
|
2631
|
+
|
|
2632
|
+
# 检查轨迹的连续性和合理性
|
|
2633
|
+
total_distance = 0
|
|
2634
|
+
prev_x = 0
|
|
2635
|
+
back_track_count = 0
|
|
2636
|
+
|
|
2637
|
+
for point in track:
|
|
2638
|
+
if 'x' not in point or 'timestamp' not in point:
|
|
2639
|
+
return False
|
|
2640
|
+
|
|
2641
|
+
current_x = point['x']
|
|
2642
|
+
if current_x < prev_x: # 允许少量回退
|
|
2643
|
+
back_track_count += 1
|
|
2644
|
+
if back_track_count > 3: # 回退次数过多才判定异常
|
|
2645
|
+
return False
|
|
2646
|
+
|
|
2647
|
+
total_distance += abs(current_x - prev_x)
|
|
2648
|
+
prev_x = current_x
|
|
2649
|
+
|
|
2650
|
+
# 检查总距离是否合理(放宽限制)
|
|
2651
|
+
if abs(total_distance - final_x) > 50:
|
|
2652
|
+
return False
|
|
2653
|
+
|
|
2654
|
+
# 检查平均速度(放宽限制)
|
|
2655
|
+
avg_speed = final_x / slide_time if slide_time > 0 else 0
|
|
2656
|
+
if avg_speed > 800 or avg_speed < 5: # 速度异常
|
|
2657
|
+
return False
|
|
2658
|
+
|
|
2659
|
+
return True
|
|
2660
|
+
|
|
2661
|
+
def _update_captcha_attempts(self, captcha_key, captcha_data):
|
|
2662
|
+
"""更新验证码尝试次数"""
|
|
2663
|
+
self.redis_client.setex(
|
|
2664
|
+
captcha_key,
|
|
2665
|
+
self.expire_time,
|
|
2666
|
+
json.dumps(captcha_data)
|
|
2667
|
+
)
|
|
2668
|
+
|
|
2669
|
+
def _generate_verify_token(self, session_id):
|
|
2670
|
+
"""生成验证通过token"""
|
|
2671
|
+
payload = {
|
|
2672
|
+
'session_id': session_id,
|
|
2673
|
+
'timestamp': time.time(),
|
|
2674
|
+
'type': 'slide_captcha'
|
|
2675
|
+
}
|
|
2676
|
+
return base64.b64encode(json.dumps(payload).encode()).decode()
|
|
2677
|
+
|
|
2678
|
+
def verify_token(self, session_id, verify_token):
|
|
2679
|
+
"""验证token是否有效"""
|
|
2680
|
+
try:
|
|
2681
|
+
stored_token = self.redis_client.get(f"captcha_verified:{session_id}")
|
|
2682
|
+
if not stored_token:
|
|
2683
|
+
return False
|
|
2684
|
+
|
|
2685
|
+
return stored_token.decode() == verify_token
|
|
2686
|
+
except:
|
|
2687
|
+
return False
|
|
2688
|
+
|
|
2689
|
+
|
|
2250
2690
|
# Flask集成装饰器
|
|
2251
2691
|
def require_auth(auth_manager):
|
|
2252
2692
|
"""
|