From b9edc4aaa2ae1be634dbdf65f46922894d1b7217 Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Thu, 11 Dec 2025 17:53:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A4=9A=E9=A1=B9=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 安全修复清单: 1. 验证码改为图片方式返回,防止明文泄露 2. CORS配置从环境变量读取,不再使用通配符"*" 3. VIP API添加@admin_required装饰器,统一认证 4. 用户登录统一错误消息,防止用户枚举 5. IP限流不再信任X-Forwarded-For头,防止伪造绕过 6. 密码强度要求提升(8位+字母+数字) 7. 日志不���记录完整session/cookie内容,防止敏感信息泄露 8. XSS防护:日志输出和Bug反馈内容转义HTML 9. SQL注入防护:LIKE查询参数转义 10. 路径遍历防护:截图目录白名单验证 11. 验证码重放防护:验证前删除验证码 12. 数据库连接池健康检查 13. 正则DoS防护:限制数字匹配长度 14. Account类密码私有化,__repr__不暴露密码 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app.py | 188 +++++++++++++++++++++++++++---------- app_security.py | 47 ++++++---- app_utils.py | 49 ++++++---- database.py | 29 ++++-- db_pool.py | 4 +- playwright_automation.py | 3 +- screenshot_worker.py | 20 ++++ templates/admin_login.html | 6 +- templates/login.html | 4 +- templates/register.html | 7 +- 10 files changed, 256 insertions(+), 101 deletions(-) diff --git a/app.py b/app.py index 82c8ad9..c1f888a 100755 --- a/app.py +++ b/app.py @@ -60,9 +60,17 @@ app.config.from_object(config) # 确保SECRET_KEY已设置 if not app.config.get('SECRET_KEY'): raise RuntimeError("SECRET_KEY未配置,请检查app_config.py") +# 安全修复:从环境变量获取允许的CORS源,不再使用"*" +cors_origins = os.environ.get('CORS_ALLOWED_ORIGINS', '').strip() +if cors_origins: + cors_allowed = [origin.strip() for origin in cors_origins.split(',') if origin.strip()] +else: + # 默认只允许同源,生产环��应该明确配置 + cors_allowed = [] + socketio = SocketIO( app, - cors_allowed_origins="*", + cors_allowed_origins=cors_allowed if cors_allowed else None, # None表示只允许同源 async_mode='threading', # 明确指定async模式 ping_timeout=60, # ping超时60秒 ping_interval=25, # 每25秒ping一次 @@ -169,12 +177,22 @@ class Admin(UserMixin): class Account: - """账号类""" + """账号类 + + 安全注意事项: + - 密码以私有属性存储,避免意外泄露 + - __repr__不包含密码,防止日志意外记录 + - 生产环境应考虑使用加密存储(如Fernet加密) + """ + __slots__ = ['id', 'user_id', 'username', '_password', 'remember', 'remark', + 'status', 'is_running', 'should_stop', 'total_items', + 'total_attachments', 'automation', 'last_browse_type', 'proxy_config'] + def __init__(self, account_id, user_id, username, password, remember=True, remark=''): self.id = account_id self.user_id = user_id self.username = username - self.password = password + self._password = password # 安全修复:使用私有属性存储密码 self.remember = remember self.remark = remark self.status = "未开始" @@ -186,6 +204,15 @@ class Account: self.last_browse_type = "注册前未读" self.proxy_config = None # 保存代理配置,浏览和截图共用 + @property + def password(self): + """获取密码(仅供自动化登录使用)""" + return self._password + + def __repr__(self): + """安全的字符串表示,不包含密码""" + return f"Account(id={self.id}, username={self.username}, status={self.status})" + def to_dict(self): result = { "id": self.id, @@ -307,8 +334,8 @@ def admin_required(f): """管理员权限装饰器""" @wraps(f) def decorated_function(*args, **kwargs): - logger.debug(f"[admin_required] Session内容: {dict(session)}") - logger.debug(f"[admin_required] Cookies: {request.cookies}") + # 安全修复:不再记录完整的session和cookies内容,防止敏感信息泄露 + logger.debug(f"[admin_required] 检查会话,admin_id存在: {'admin_id' in session}") if 'admin_id' not in session: logger.warning(f"[admin_required] 拒绝访问 {request.path} - session中无admin_id") return jsonify({"error": "需要管理员权限"}), 403 @@ -319,10 +346,12 @@ def admin_required(f): def log_to_client(message, user_id=None, account_id=None): """发送日志到Web客户端(用户隔离)""" + from app_security import escape_html timestamp = get_beijing_now().strftime('%H:%M:%S') + # 安全修复:转义HTML特殊字符,防止XSS攻击 log_data = { 'timestamp': timestamp, - 'message': message, + 'message': escape_html(str(message)) if message else '', 'account_id': account_id } @@ -579,10 +608,17 @@ def check_ip_rate_limit(ip_address): """检查IP是否被限流""" current_time = time.time() - # 清理过期的IP记录 - expired_ips = [ip for ip, data in ip_rate_limit.items() - if data.get("lock_until", 0) < current_time and - current_time - data.get("first_attempt", current_time) > 3600] + # 安全修复:修正过期IP清理逻辑 + # 原问题:first_attempt不存在时默认使用current_time,导致永远不会被清理 + expired_ips = [] + for ip, data in ip_rate_limit.items(): + lock_expired = data.get("lock_until", 0) < current_time + first_attempt = data.get("first_attempt") + # 修复:如果first_attempt不存在或超过1小时,视为过期 + attempt_expired = first_attempt is None or (current_time - first_attempt > 3600) + if lock_expired and attempt_expired: + expired_ips.append(ip) + for ip in expired_ips: del ip_rate_limit[ip] @@ -596,7 +632,8 @@ def check_ip_rate_limit(ip_address): return False, "IP已被锁定,请{}分钟后再试".format(remaining_time // 60 + 1) # 如果超过1小时,重置计数 - if current_time - ip_data.get("first_attempt", current_time) > 3600: + first_attempt = ip_data.get("first_attempt") + if first_attempt is None or current_time - first_attempt > 3600: ip_rate_limit[ip_address] = { "attempts": 0, "first_attempt": current_time @@ -627,26 +664,85 @@ def record_failed_captcha(ip_address): @app.route("/api/generate_captcha", methods=["POST"]) def generate_captcha(): - """生成4位数字验证码""" + """生成4位数字验证码图片 + + 安全修复:验证码不再以明文返回,而是生成base64图片 + """ import uuid + import base64 + from io import BytesIO + session_id = str(uuid.uuid4()) - + # 生成4位随机数字 code = "".join([str(random.randint(0, 9)) for _ in range(4)]) - + # 存储验证码,5分钟过期 captcha_storage[session_id] = { "code": code, "expire_time": time.time() + 300, "failed_attempts": 0 } - + # 清理过期验证码 expired_keys = [k for k, v in captcha_storage.items() if v["expire_time"] < time.time()] for k in expired_keys: del captcha_storage[k] - - return jsonify({"session_id": session_id, "captcha": code}) + + # 生成验证码图片 + try: + from PIL import Image, ImageDraw, ImageFont + import io + + # 创建图片 + width, height = 120, 40 + image = Image.new('RGB', (width, height), color=(255, 255, 255)) + draw = ImageDraw.Draw(image) + + # 添加干扰线 + for _ in range(5): + x1 = random.randint(0, width) + y1 = random.randint(0, height) + x2 = random.randint(0, width) + y2 = random.randint(0, height) + draw.line([(x1, y1), (x2, y2)], fill=(random.randint(0, 200), random.randint(0, 200), random.randint(0, 200))) + + # 添加干扰点 + for _ in range(50): + x = random.randint(0, width) + y = random.randint(0, height) + draw.point((x, y), fill=(random.randint(0, 200), random.randint(0, 200), random.randint(0, 200))) + + # 绘制验证码文字 + try: + font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 28) + except: + font = ImageFont.load_default() + + for i, char in enumerate(code): + x = 10 + i * 25 + random.randint(-3, 3) + y = random.randint(2, 8) + color = (random.randint(0, 150), random.randint(0, 150), random.randint(0, 150)) + draw.text((x, y), char, font=font, fill=color) + + # 转换为base64 + buffer = io.BytesIO() + image.save(buffer, format='PNG') + img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + + return jsonify({ + "session_id": session_id, + "captcha_image": f"data:image/png;base64,{img_base64}" + }) + except ImportError: + # 如果没有PIL,退回到简单文本(但添加混淆) + # 安全警告:生产环境应安装PIL + logger.warning("PIL未安装,验证码安全性降低") + # 不直接返回验证码,返回混淆后的提示 + return jsonify({ + "session_id": session_id, + "captcha_hint": "验证码图片生成失败,请联系管理员" + }), 500 @app.route('/api/login', methods=['POST']) @@ -666,16 +762,12 @@ def login(): if not success: return jsonify({"error": message}), 400 - # 先检查用户是否存在 - user_exists = database.get_user_by_username(username) - if not user_exists: - return jsonify({"error": "账号未注册", "need_captcha": True}), 401 - - # 检查密码是否正确 + # 安全修复:使用通用错误消息防止用户枚举 + # 不再分开检查用户是否存在和密码是否正确 user = database.verify_user(username, password) if not user: - # 密码错误 - return jsonify({"error": "密码错误", "need_captcha": True}), 401 + # 返回通用错误消息,不透露账号是否存在 + return jsonify({"error": "用户名或密码错误", "need_captcha": True}), 401 # 检查审核状态 if user['status'] != 'approved': @@ -699,8 +791,13 @@ def logout(): # ==================== 管理员认证API ==================== @app.route('/yuyx/api/debug-config', methods=['GET']) +@admin_required # 安全修复:添加管理员认证 def debug_config(): - """调试配置信息""" + """调试配置信息(仅管理员可访问,生产环境应禁用)""" + # 安全修复:生产环境禁用调试端点 + if not app.debug: + return jsonify({"error": "调试端点已在生产环境禁用"}), 403 + return jsonify({ "secret_key_set": bool(app.secret_key), "secret_key_length": len(app.secret_key) if app.secret_key else 0, @@ -711,7 +808,8 @@ def debug_config(): "SESSION_COOKIE_SAMESITE": app.config.get('SESSION_COOKIE_SAMESITE'), "PERMANENT_SESSION_LIFETIME": str(app.config.get('PERMANENT_SESSION_LIFETIME')), }, - "current_session": dict(session), + # 安全修复:移除敏感的session内容,只显示有无 + "has_session": bool(session), "cookies_received": list(request.cookies.keys()) }) @@ -751,9 +849,8 @@ def admin_login(): session.permanent = True # 设置为永久会话(使用PERMANENT_SESSION_LIFETIME配置) session.modified = True # 强制标记session为已修改,确保保存 - logger.info(f"[admin_login] 管理员 {username} 登录成功, session已设置: admin_id={admin['id']}") - logger.debug(f"[admin_login] Session内容: {dict(session)}") - logger.debug(f"[admin_login] Cookie将被设置: name={app.config.get('SESSION_COOKIE_NAME', 'session')}") + logger.info(f"[admin_login] 管理员 {username} 登录成功") + # 安全修复:不再记录完整session内容 # 根据请求类型返回不同响应 if request.is_json: @@ -1234,12 +1331,14 @@ def get_accounts(): user_id = current_user.id refresh = request.args.get('refresh', 'false').lower() == 'true' - # 如果user_accounts中没有数据或者请求刷新,则从数据库加载 - if user_id not in user_accounts or len(user_accounts.get(user_id, {})) == 0 or refresh: - load_user_accounts(user_id) + # 安全修复:使用锁保护检查和加载操作,防止竞态条件 + with user_accounts_lock: + # 如果user_accounts中没有数据或者请求刷新,则从数据库加载 + if user_id not in user_accounts or len(user_accounts.get(user_id, {})) == 0 or refresh: + load_user_accounts(user_id) - accounts = user_accounts.get(user_id, {}) - return jsonify([acc.to_dict() for acc in accounts.values()]) + accounts = user_accounts.get(user_id, {}) + return jsonify([acc.to_dict() for acc in accounts.values()]) @app.route('/api/accounts', methods=['POST']) @@ -2252,20 +2351,17 @@ def serve_static(filename): # ==================== 管理员VIP管理API ==================== @app.route('/yuyx/api/vip/config', methods=['GET']) +@admin_required # 安全修复:使用统一的装饰器 def get_vip_config_api(): """获取VIP配置""" - if 'admin_id' not in session: - return jsonify({"error": "需要管理员权限"}), 403 config = database.get_vip_config() return jsonify(config) @app.route('/yuyx/api/vip/config', methods=['POST']) +@admin_required # 安全修复:使用统一的装饰器 def set_vip_config_api(): """设置默认VIP天数""" - if 'admin_id' not in session: - return jsonify({"error": "需要管理员权限"}), 403 - data = request.json days = data.get('default_vip_days', 0) @@ -2277,11 +2373,9 @@ def set_vip_config_api(): @app.route('/yuyx/api/users//vip', methods=['POST']) +@admin_required # 安全修复:使用统一的装饰器 def set_user_vip_api(user_id): """设置用户VIP""" - if 'admin_id' not in session: - return jsonify({"error": "需要管理员权限"}), 403 - data = request.json days = data.get('days', 30) @@ -2297,22 +2391,18 @@ def set_user_vip_api(user_id): @app.route('/yuyx/api/users//vip', methods=['DELETE']) +@admin_required # 安全修复:使用统一的装饰器 def remove_user_vip_api(user_id): """移除用户VIP""" - if 'admin_id' not in session: - return jsonify({"error": "需要管理员权限"}), 403 - if database.remove_user_vip(user_id): return jsonify({"message": "VIP已移除"}) return jsonify({"error": "移除失败"}), 400 @app.route('/yuyx/api/users//vip', methods=['GET']) +@admin_required # 安全修复:使用统一的装饰器 def get_user_vip_info_api(user_id): """获取用户VIP信息(管理员)""" - if 'admin_id' not in session: - return jsonify({"error": "需要管理员权限"}), 403 - vip_info = database.get_user_vip_info(user_id) return jsonify(vip_info) diff --git a/app_security.py b/app_security.py index 9c92b99..71d74bf 100755 --- a/app_security.py +++ b/app_security.py @@ -240,12 +240,15 @@ def validate_username(username): return True, None -def validate_password(password): +def validate_password(password, require_complexity=True): """ 验证密码强度 + 安全修复:增强密码强度要求 + Args: password: 密码 + require_complexity: 是否要求复杂度(默认True) Returns: tuple: (is_valid, error_message) @@ -253,19 +256,19 @@ def validate_password(password): if not password: return False, "密码不能为空" - if len(password) < 6: - return False, "密码长度不能少于6个字符" + if len(password) < 8: # 安全修复:最少8位 + return False, "密码长度不能少于8个字符" if len(password) > 128: return False, "密码长度不能超过128个字符" - # 可选:强制密码复杂度 - # has_upper = bool(re.search(r'[A-Z]', password)) - # has_lower = bool(re.search(r'[a-z]', password)) - # has_digit = bool(re.search(r'\d', password)) - # - # if not (has_upper and has_lower and has_digit): - # return False, "密码必须包含大写字母、小写字母和数字" + # 安全修复:启用密码复杂度要求 + if require_complexity: + has_letter = bool(re.search(r'[a-zA-Z]', password)) + has_digit = bool(re.search(r'\d', password)) + + if not (has_letter and has_digit): + return False, "密码必须包含字母和数字" return True, None @@ -392,20 +395,28 @@ def check_security_config(): # ==================== 辅助函数 ==================== -def get_client_ip(): +def get_client_ip(trust_proxy=False): """ 获取客户端真实IP地址 + 安全修复:默认不信任代理头,防止IP伪造绕过限流 + + Args: + trust_proxy: 是否信任代理头(仅在已知可信代理后设置为True) + Returns: str: IP地址 """ - # 检查代理头 - if request.headers.get('X-Forwarded-For'): - return request.headers.get('X-Forwarded-For').split(',')[0].strip() - elif request.headers.get('X-Real-IP'): - return request.headers.get('X-Real-IP') - else: - return request.remote_addr + # 安全说明:X-Forwarded-For 可被伪造 + # 仅在确认请求来自可信代理时才使用代理头 + if trust_proxy: + if request.headers.get('X-Forwarded-For'): + return request.headers.get('X-Forwarded-For').split(',')[0].strip() + elif request.headers.get('X-Real-IP'): + return request.headers.get('X-Real-IP') + + # 默认使用remote_addr(更安全但可能是代理IP) + return request.remote_addr if __name__ == '__main__': diff --git a/app_utils.py b/app_utils.py index 0af0a0b..ee8936b 100755 --- a/app_utils.py +++ b/app_utils.py @@ -282,7 +282,11 @@ def check_user_ownership(user_id: int, resource_type: str, def verify_and_consume_captcha(session_id: str, code: str, captcha_storage: dict, max_attempts: int = 5) -> Tuple[bool, str]: """ - 验证并消费验证码 + 验证并消费验证码(安全增强版) + + 安全特性: + - 先删除验证码再验证,防止重放攻击 + - 异常情况下也确保验证码被删除 Args: session_id: 验证码会话ID @@ -307,30 +311,37 @@ def verify_and_consume_captcha(session_id: str, code: str, captcha_storage: dict """ import time + # 安全修复:先取出并删除验证码,无论验证是否成功都不能重用 + captcha_data = captcha_storage.pop(session_id, None) + # 检查验证码是否存在 - if session_id not in captcha_storage: + if captcha_data is None: return False, "验证码已过期或不存在,请重新获取" - captcha_data = captcha_storage[session_id] + try: + # 检查过期时间 + if captcha_data["expire_time"] < time.time(): + return False, "验证码已过期,请重新获取" - # 检查过期时间 - if captcha_data["expire_time"] < time.time(): - del captcha_storage[session_id] - return False, "验证码已过期,请重新获取" + # 检查尝试次数 + if captcha_data.get("failed_attempts", 0) >= max_attempts: + return False, f"验证码错误次数过多({max_attempts}次),请重新获取" - # 检查尝试次数 - if captcha_data.get("failed_attempts", 0) >= max_attempts: - del captcha_storage[session_id] - return False, f"验证码错误次数过多({max_attempts}次),请重新获取" + # 验证代码(不区分大小写) + if captcha_data["code"].lower() != code.lower(): + # 验证失败,增加失败计数后放回(允许继续尝试) + captcha_data["failed_attempts"] = captcha_data.get("failed_attempts", 0) + 1 + # 只有未超过最大尝试次数才放回 + if captcha_data["failed_attempts"] < max_attempts: + captcha_storage[session_id] = captcha_data + return False, "验证码错误" - # 验证代码(不区分大小写) - if captcha_data["code"].lower() != code.lower(): - captcha_data["failed_attempts"] = captcha_data.get("failed_attempts", 0) + 1 - return False, "验证码错误" - - # 验证成功,删除验证码(防止重复使用) - del captcha_storage[session_id] - return True, "验证成功" + # 验证成功,验证码已被删除,不会被重用 + return True, "验证成功" + except Exception as e: + # 异常情况下确保验证码不会被重用(已在函数开头删除) + logger.error(f"验证码验证异常: {e}") + return False, "验证码验证失败,请重新获取" if __name__ == '__main__': diff --git a/database.py b/database.py index c823480..cf68402 100755 --- a/database.py +++ b/database.py @@ -630,7 +630,10 @@ def remove_user_vip(user_id): def is_user_vip(user_id): - """检查用户是否是VIP""" + """检查用户是否是VIP + + 注意:数据库中存储的时间统一使用CST(Asia/Shanghai)时区 + """ cst_tz = pytz.timezone("Asia/Shanghai") user = get_user_by_id(user_id) @@ -638,9 +641,12 @@ def is_user_vip(user_id): return False try: + # 时区处理说明:数据库存储的是CST时间字符串 + # 如果将来改为UTC存储,需要修改此处逻辑 expire_time_naive = datetime.strptime(user['vip_expire_time'], '%Y-%m-%d %H:%M:%S') expire_time = cst_tz.localize(expire_time_naive) - return datetime.now(cst_tz) < expire_time + now = datetime.now(cst_tz) + return now < expire_time except (ValueError, AttributeError) as e: print(f"检查VIP状态失败 (user_id={user_id}): {e}") return False @@ -1137,8 +1143,11 @@ def get_task_logs(limit=100, offset=0, date_filter=None, status_filter=None, params.append(user_id_filter) if account_filter: - where_clauses.append("tl.username LIKE ?") - params.append(f"%{account_filter}%") + # 转义LIKE中的特殊字符,防止绕过过滤 + from app_security import sanitize_sql_like_pattern + safe_filter = sanitize_sql_like_pattern(account_filter) + where_clauses.append("tl.username LIKE ? ESCAPE '\\'") + params.append(f"%{safe_filter}%") where_sql = " AND ".join(where_clauses) @@ -1409,16 +1418,24 @@ def clean_old_operation_logs(days=30): # ==================== Bug反馈管理 ==================== def create_bug_feedback(user_id, username, title, description, contact=''): - """创建Bug反馈""" + """创建Bug反馈(带XSS防护)""" + from app_security import escape_html + with db_pool.get_db() as conn: cursor = conn.cursor() cst_tz = pytz.timezone("Asia/Shanghai") cst_time = datetime.now(cst_tz).strftime("%Y-%m-%d %H:%M:%S") + # 安全修复:转义用户输入,防止存储型XSS攻击 + safe_title = escape_html(title) if title else '' + safe_description = escape_html(description) if description else '' + safe_contact = escape_html(contact) if contact else '' + safe_username = escape_html(username) if username else '' + cursor.execute(''' INSERT INTO bug_feedbacks (user_id, username, title, description, contact, created_at) VALUES (?, ?, ?, ?, ?, ?) - ''', (user_id, username, title, description, contact, cst_time)) + ''', (user_id, safe_username, safe_title, safe_description, safe_contact, cst_time)) conn.commit() return cursor.lastrowid diff --git a/db_pool.py b/db_pool.py index bdca8cb..c9cede0 100755 --- a/db_pool.py +++ b/db_pool.py @@ -65,7 +65,7 @@ class ConnectionPool: def return_connection(self, conn): """ - 归还连接到连接池 [已修复Bug#7] + 归还连接到连接池 [已修复Bug#7, Bug#11] Args: conn: 要归还的连接 @@ -76,6 +76,8 @@ class ConnectionPool: try: # 回滚任何未提交的事务 conn.rollback() + # 安全修复:验证连接是否健康,防止损坏的连接污染连接池 + conn.execute("SELECT 1") self._pool.put(conn, block=False) except sqlite3.Error as e: # 数据库相关错误,连接可能损坏 diff --git a/playwright_automation.py b/playwright_automation.py index 6393855..52ec62d 100755 --- a/playwright_automation.py +++ b/playwright_automation.py @@ -907,7 +907,8 @@ class PlaywrightAutomation: # 解析"共XXX记录"获取总数 if expected_total is None: import re - match = re.search(r'共(\d+)记录', page_text) + # 安全修复:限制数字匹配长度,防止ReDoS攻击 + match = re.search(r'共(\d{1,10})记录', page_text) if match: expected_total = int(match.group(1)) self.log(f"[总数] 预期浏览 {expected_total} 条内容") diff --git a/screenshot_worker.py b/screenshot_worker.py index 38692f5..d49b081 100644 --- a/screenshot_worker.py +++ b/screenshot_worker.py @@ -19,6 +19,26 @@ def take_screenshot(config): screenshot_path = config['screenshot_path'] cookies_file = config.get('cookies_file', '') + # 安全修复:验证截图路径在允许的目录内,防止路径遍历攻击 + ALLOWED_SCREENSHOT_DIRS = [ + '/root/zsglpt/screenshots', + '/root/zsglpt/static/screenshots', + '/tmp/zsglpt_screenshots' + ] + + def is_safe_screenshot_path(path): + """验证截图路径是否安全""" + abs_path = os.path.abspath(path) + return any(abs_path.startswith(os.path.abspath(allowed_dir)) + for allowed_dir in ALLOWED_SCREENSHOT_DIRS) + + if not is_safe_screenshot_path(screenshot_path): + return { + 'success': False, + 'message': '非法截图路径', + 'screenshot_path': '' + } + result = { 'success': False, 'message': '', diff --git a/templates/admin_login.html b/templates/admin_login.html index 748a1d8..23a247e 100644 --- a/templates/admin_login.html +++ b/templates/admin_login.html @@ -184,7 +184,7 @@
- ---- + 验证码
@@ -278,9 +278,9 @@ const data = await response.json(); - if (data.session_id && data.captcha) { + if (data.session_id && data.captcha_image) { captchaSession = data.session_id; - document.getElementById('captchaCode').textContent = data.captcha; + document.getElementById('captchaImage').src = data.captcha_image; } } catch (error) { console.error('生成验证码失败:', error); diff --git a/templates/login.html b/templates/login.html index b890c24..0638c10 100644 --- a/templates/login.html +++ b/templates/login.html @@ -185,7 +185,7 @@
- ---- + 验证码
@@ -253,7 +253,7 @@ else { errorDiv.textContent = data.error || '申请失败'; errorDiv.style.display = 'block'; } } catch (error) { errorDiv.textContent = '网络错误'; errorDiv.style.display = 'block'; } } - async function generateCaptcha() { try { const response = await fetch('/api/generate_captcha', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.session_id && data.captcha) { captchaSession = data.session_id; document.getElementById('captchaCode').textContent = data.captcha; } } catch (error) { console.error('生成验证码失败:', error); } } + async function generateCaptcha() { try { const response = await fetch('/api/generate_captcha', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.session_id && data.captcha_image) { captchaSession = data.session_id; document.getElementById('captchaImage').src = data.captcha_image; } } catch (error) { console.error('生成验证码失败:', error); } } async function refreshCaptcha() { await generateCaptcha(); document.getElementById('captcha').value = ''; } document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeForgotPassword(); }); diff --git a/templates/register.html b/templates/register.html index 95d3e4e..6edb74a 100644 --- a/templates/register.html +++ b/templates/register.html @@ -181,7 +181,7 @@
- ---- + 验证码
@@ -263,7 +263,10 @@ async function generateCaptcha() { const resp = await fetch('/api/generate_captcha', {method: 'POST', headers: {'Content-Type': 'application/json'}}); const data = await resp.json(); - if (data.session_id && data.captcha) { captchaSession = data.session_id; document.getElementById('captchaCode').textContent = data.captcha; } + if (data.session_id && data.captcha_image) { + captchaSession = data.session_id; + document.getElementById('captchaImage').src = data.captcha_image; + } } async function refreshCaptcha() { await generateCaptcha(); document.getElementById('captcha').value = ''; }