SOONet部署安全加固:Gradio认证登录、API密钥控制、视频上传大小限制

1. 项目简介与安全加固背景

SOONet是一个基于自然语言输入的长视频时序片段定位系统。简单来说,你给它一段文字描述,它就能在长视频里快速找到匹配这个描述的时间片段。这个功能在视频内容分析、智能检索、媒体资产管理等场景下特别有用。

不过,当我们把这样一个强大的AI系统部署到实际环境中时,安全就成了必须考虑的问题。想象一下,如果你的SOONet服务直接暴露在公网上,任何人都能随意访问、上传视频、调用API,那会带来什么风险?

你可能遇到的安全问题

  • 未经授权的用户访问你的服务,消耗你的计算资源
  • 恶意上传超大视频文件,导致服务器存储空间爆满
  • API被滥用,影响正常服务的稳定性
  • 敏感视频数据被随意访问

今天我就来分享一套完整的SOONet部署安全加固方案。我会手把手教你如何给SOONet的Gradio界面加上登录认证,如何控制API访问权限,以及如何限制视频上传大小。这些措施实施后,你的SOONet服务就能安全地对外提供服务了。

2. 安全加固方案概览

在开始具体操作之前,我们先看看整个安全加固方案包含哪些内容。这样你就能对整个改造有个清晰的了解。

2.1 三个核心安全功能

我们的安全加固主要围绕三个核心功能展开:

  1. Gradio认证登录 - 给Web界面加把锁

    • 用户访问SOONet的Web界面时需要输入用户名和密码
    • 支持多用户管理,不同用户可以有不同的权限
    • 会话超时自动退出,防止长时间未操作的安全风险
  2. API密钥控制 - 管理程序化访问

    • 为API调用生成唯一的访问密钥
    • 可以设置API密钥的有效期和使用次数限制
    • 记录API调用日志,方便监控和审计
  3. 视频上传大小限制 - 防止资源滥用

    • 限制单个视频文件的最大大小
    • 限制总上传视频的数量和大小
    • 支持白名单机制,特定用户可以上传更大文件

2.2 改造前后的对比

为了让你更清楚地理解安全加固的价值,我做了个简单的对比:

功能原始SOONet安全加固后
Web访问完全开放,无需认证需要用户名密码登录
API调用直接调用,无限制需要有效的API密钥
文件上传无大小限制可配置大小限制
用户管理无用户概念支持多用户、多角色
访问日志无记录完整记录所有访问

2.3 技术实现思路

整个安全加固的实现思路其实很清晰:

  1. 在Gradio应用启动前,先加载用户配置和API密钥配置
  2. 修改app.py,在创建Gradio界面时加入认证参数
  3. 添加中间件,在视频上传时检查文件大小
  4. 封装API接口,在调用SOONet核心功能前验证API密钥
  5. 添加配置文件,把用户信息、API密钥、限制参数都放在配置文件里

这样的设计有几个好处:配置和代码分离,修改安全设置不需要改代码;功能模块化,每个安全功能相对独立;扩展性强,后续要加新的安全功能也很方便。

3. Gradio认证登录实现

现在我们来具体实现第一个安全功能:给SOONet的Web界面加上登录认证。

3.1 创建用户配置文件

首先,我们需要创建一个用户配置文件。这个文件里存放所有可以访问SOONet的用户信息。

创建一个名为 users_config.yaml 的文件:

# SOONet用户配置
users:
  # 管理员用户 - 拥有所有权限
  admin:
    password: "admin_secure_password_123"  # 实际使用时请修改
    role: "admin"
    permissions:
      - "full_access"
      - "upload_large_files"
      - "manage_users"
      - "view_logs"
  
  # 普通用户 - 基础使用权限
  user1:
    password: "user1_password_456"  # 实际使用时请修改
    role: "user"
    permissions:
      - "basic_access"
      - "upload_standard_files"
  
  # 只读用户 - 只能查看,不能上传
  viewer:
    password: "viewer_password_789"  # 实际使用时请修改
    role: "viewer"
    permissions:
      - "view_only"

# 会话配置
session:
  timeout_minutes: 30  # 30分钟后自动退出
  max_sessions_per_user: 3  # 每个用户最多3个并发会话

# 登录尝试限制
login:
  max_attempts: 5  # 最大尝试次数
  lockout_minutes: 15  # 锁定时间

配置文件说明

  • 密码建议使用强密码,包含大小写字母、数字和特殊字符
  • 角色分为admin、user、viewer三种,权限逐级递减
  • 会话超时机制可以防止用户离开后会话被他人利用
  • 登录尝试限制可以防止暴力破解

3.2 修改Gradio应用代码

接下来,我们需要修改SOONet的 app.py 文件,加入认证功能。下面是修改后的关键部分:

import gradio as gr
import yaml
import hashlib
import time
from functools import wraps
from typing import Dict, Optional

# 加载用户配置
def load_users_config(config_path: str = "users_config.yaml") -> Dict:
    """加载用户配置文件"""
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            config = yaml.safe_load(f)
        return config
    except FileNotFoundError:
        print(f"警告:配置文件 {config_path} 不存在,使用默认配置")
        return {
            "users": {
                "admin": {
                    "password": "admin",
                    "role": "admin",
                    "permissions": ["full_access"]
                }
            }
        }

# 用户认证装饰器
def require_auth(func):
    """认证装饰器,保护需要登录的功能"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 这里简化处理,实际应该检查会话
        # 完整的实现需要会话管理
        return func(*args, **kwargs)
    return wrapper

# 修改Gradio界面创建部分
def create_auth_interface():
    """创建带认证的Gradio界面"""
    
    # 加载用户配置
    users_config = load_users_config()
    
    # 创建登录状态管理
    login_status = {"username": None, "logged_in": False}
    
    def check_login(username: str, password: str) -> tuple:
        """检查用户名和密码"""
        if username not in users_config["users"]:
            return False, "用户不存在"
        
        user_info = users_config["users"][username]
        # 实际应该使用密码哈希,这里简化处理
        if password == user_info["password"]:
            login_status["username"] = username
            login_status["logged_in"] = True
            return True, f"登录成功!欢迎 {username}"
        else:
            return False, "密码错误"
    
    def logout():
        """退出登录"""
        login_status["username"] = None
        login_status["logged_in"] = False
        return "已退出登录"
    
    # 创建登录界面
    with gr.Blocks(title="SOONet - 视频时序定位系统") as demo:
        # 登录界面
        with gr.Column(visible=True) as login_section:
            gr.Markdown("## 🔐 SOONet 登录")
            username_input = gr.Textbox(label="用户名", placeholder="请输入用户名")
            password_input = gr.Textbox(
                label="密码", 
                placeholder="请输入密码",
                type="password"
            )
            login_button = gr.Button("登录", variant="primary")
            login_status_output = gr.Textbox(label="登录状态", interactive=False)
        
        # 主功能界面(初始隐藏)
        with gr.Column(visible=False) as main_section:
            # 原有的SOONet功能界面
            gr.Markdown("## 🎯 SOONet 视频时序定位")
            
            # 查询文本输入
            query_text = gr.Textbox(
                label="查询文本",
                placeholder="输入英文描述,如:a man takes food out of the refrigerator",
                lines=2
            )
            
            # 视频上传
            video_input = gr.Video(label="上传视频")
            
            # 定位按钮
            locate_button = gr.Button("🔍 开始定位", variant="primary")
            
            # 结果显示
            result_output = gr.Textbox(label="定位结果", interactive=False)
            
            # 退出按钮
            logout_button = gr.Button("退出登录", variant="secondary")
        
        # 登录按钮事件
        def on_login(username, password):
            success, message = check_login(username, password)
            if success:
                return [
                    gr.update(visible=False),  # 隐藏登录界面
                    gr.update(visible=True),   # 显示主界面
                    message
                ]
            else:
                return [
                    gr.update(visible=True),   # 保持登录界面
                    gr.update(visible=False),  # 隐藏主界面
                    message
                ]
        
        # 退出按钮事件
        def on_logout():
            logout()
            return [
                gr.update(visible=True),   # 显示登录界面
                gr.update(visible=False),  # 隐藏主界面
                "已退出登录"
            ]
        
        # 绑定事件
        login_button.click(
            fn=on_login,
            inputs=[username_input, password_input],
            outputs=[login_section, main_section, login_status_output]
        )
        
        logout_button.click(
            fn=on_logout,
            inputs=[],
            outputs=[login_section, main_section, login_status_output]
        )
        
        # 原有的定位功能(需要认证)
        @require_auth
        def locate_video(query, video):
            """需要认证的视频定位功能"""
            # 这里调用原有的SOONet定位逻辑
            # 为了简化示例,这里返回模拟结果
            if not login_status["logged_in"]:
                return "请先登录"
            
            # 模拟定位结果
            result = f"查询: {query}\n"
            result += f"视频: {video}\n"
            result += "定位结果: 00:01:15 - 00:01:45 (置信度: 0.87)"
            return result
        
        # 绑定定位功能
        locate_button.click(
            fn=locate_video,
            inputs=[query_text, video_input],
            outputs=result_output
        )
    
    return demo

# 启动应用
if __name__ == "__main__":
    demo = create_auth_interface()
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False  # 关闭Gradio的分享功能,增强安全性
    )

3.3 密码安全增强

上面的示例中,密码是明文存储的,这在实际生产环境中是不安全的。我们需要对密码进行哈希处理:

import bcrypt
import base64

def hash_password(password: str) -> str:
    """使用bcrypt哈希密码"""
    salt = bcrypt.gensalt()
    hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
    return hashed.decode('utf-8')

def verify_password(password: str, hashed_password: str) -> bool:
    """验证密码"""
    return bcrypt.checkpw(password.encode('utf-8'), hashed_password.encode('utf-8'))

# 更新用户配置,存储哈希后的密码
hashed_config = {
    "users": {
        "admin": {
            "password_hash": hash_password("your_secure_password"),
            "role": "admin",
            # ... 其他配置
        }
    }
}

3.4 会话管理

为了完善认证系统,我们还需要实现会话管理:

import uuid
from datetime import datetime, timedelta

class SessionManager:
    """会话管理器"""
    
    def __init__(self):
        self.sessions = {}  # session_id -> session_data
        self.user_sessions = {}  # username -> [session_ids]
    
    def create_session(self, username: str) -> str:
        """创建新会话"""
        session_id = str(uuid.uuid4())
        now = datetime.now()
        
        self.sessions[session_id] = {
            "username": username,
            "created_at": now,
            "last_activity": now,
            "expires_at": now + timedelta(minutes=30)
        }
        
        # 更新用户会话列表
        if username not in self.user_sessions:
            self.user_sessions[username] = []
        self.user_sessions[username].append(session_id)
        
        return session_id
    
    def validate_session(self, session_id: str) -> bool:
        """验证会话是否有效"""
        if session_id not in self.sessions:
            return False
        
        session = self.sessions[session_id]
        if datetime.now() > session["expires_at"]:
            # 会话过期,清理
            self.remove_session(session_id)
            return False
        
        # 更新最后活动时间
        session["last_activity"] = datetime.now()
        return True
    
    def remove_session(self, session_id: str):
        """移除会话"""
        if session_id in self.sessions:
            username = self.sessions[session_id]["username"]
            if username in self.user_sessions:
                self.user_sessions[username] = [
                    sid for sid in self.user_sessions[username] 
                    if sid != session_id
                ]
            del self.sessions[session_id]
    
    def cleanup_expired_sessions(self):
        """清理过期会话"""
        now = datetime.now()
        expired_sessions = [
            session_id for session_id, session in self.sessions.items()
            if now > session["expires_at"]
        ]
        
        for session_id in expired_sessions:
            self.remove_session(session_id)

4. API密钥控制实现

对于需要通过API调用SOONet服务的场景,我们需要实现API密钥控制。这样只有持有有效API密钥的程序才能调用我们的服务。

4.1 API密钥管理配置

创建 api_keys_config.yaml 配置文件:

# API密钥配置
api_keys:
  # 开发环境密钥
  dev_key_001:
    key: "sk_dev_abc123xyz789"
    name: "开发测试密钥"
    owner: "dev_team"
    created_at: "2024-01-15"
    expires_at: "2024-12-31"  # 过期时间,空表示永不过期
    rate_limit: 100  # 每分钟最大请求数
    total_calls: 0  # 总调用次数
    last_used: null
    permissions:
      - "video_upload"
      - "query_execution"
      - "result_retrieval"
    enabled: true
  
  # 生产环境密钥
  prod_key_001:
    key: "sk_prod_def456uvw012"
    name: "生产环境密钥"
    owner: "production"
    created_at: "2024-01-10"
    expires_at: null  # 永不过期
    rate_limit: 50  # 更严格的限流
    total_calls: 1250
    last_used: "2024-01-20 14:30:00"
    permissions:
      - "query_execution"
      - "result_retrieval"
    enabled: true
  
  # 只读密钥
  readonly_key_001:
    key: "sk_read_ghi789rst345"
    name: "只读访问密钥"
    owner: "analytics"
    created_at: "2024-01-05"
    expires_at: "2024-06-30"
    rate_limit: 30
    total_calls: 320
    last_used: "2024-01-19 09:15:00"
    permissions:
      - "result_retrieval"  # 只能获取结果,不能执行查询
    enabled: true

# API全局配置
api_config:
  default_rate_limit: 60  # 默认每分钟60次
  require_https: true  # 是否要求HTTPS
  enable_logging: true  # 是否启用日志
  log_file: "api_access.log"
  blocked_ips: []  # 被阻止的IP列表

4.2 API密钥验证中间件

创建一个API密钥验证的中间件,在请求到达SOONet处理逻辑之前进行验证:

import time
import json
from datetime import datetime
from functools import wraps
from typing import Dict, Optional, Tuple
import yaml
import redis  # 用于分布式环境下的限流

class APIKeyManager:
    """API密钥管理器"""
    
    def __init__(self, config_path: str = "api_keys_config.yaml"):
        self.config_path = config_path
        self.api_keys = self.load_api_keys()
        
        # 初始化Redis连接(用于分布式限流)
        try:
            self.redis_client = redis.Redis(
                host='localhost', 
                port=6379, 
                db=0,
                decode_responses=True
            )
            self.redis_available = True
        except:
            self.redis_available = False
            print("警告:Redis不可用,使用内存限流")
            self.local_rate_limits = {}
    
    def load_api_keys(self) -> Dict:
        """加载API密钥配置"""
        try:
            with open(self.config_path, 'r', encoding='utf-8') as f:
                config = yaml.safe_load(f)
            return config.get('api_keys', {})
        except FileNotFoundError:
            print(f"警告:API密钥配置文件 {self.config_path} 不存在")
            return {}
    
    def validate_api_key(self, api_key: str) -> Tuple[bool, Optional[Dict], str]:
        """验证API密钥有效性"""
        # 查找对应的密钥配置
        key_config = None
        key_id = None
        
        for key_id, config in self.api_keys.items():
            if config['key'] == api_key:
                key_config = config
                break
        
        if not key_config:
            return False, None, "无效的API密钥"
        
        # 检查是否启用
        if not key_config.get('enabled', True):
            return False, key_config, "API密钥已被禁用"
        
        # 检查过期时间
        expires_at = key_config.get('expires_at')
        if expires_at:
            try:
                expire_date = datetime.strptime(expires_at, "%Y-%m-%d")
                if datetime.now() > expire_date:
                    return False, key_config, "API密钥已过期"
            except ValueError:
                pass  # 日期格式错误,跳过检查
        
        return True, key_config, "验证成功"
    
    def check_rate_limit(self, api_key: str, key_config: Dict) -> Tuple[bool, int]:
        """检查速率限制"""
        rate_limit = key_config.get('rate_limit', 60)
        
        if self.redis_available:
            # 使用Redis进行分布式限流
            key = f"rate_limit:{api_key}:{int(time.time() // 60)}"
            current = self.redis_client.incr(key)
            if current == 1:
                self.redis_client.expire(key, 60)  # 60秒过期
            return current <= rate_limit, rate_limit - current
        else:
            # 使用内存限流(单机版)
            current_minute = int(time.time() // 60)
            key = f"{api_key}:{current_minute}"
            
            if key not in self.local_rate_limits:
                self.local_rate_limits[key] = {
                    'count': 1,
                    'minute': current_minute
                }
            else:
                self.local_rate_limits[key]['count'] += 1
            
            # 清理过期的记录
            for k in list(self.local_rate_limits.keys()):
                if self.local_rate_limits[k]['minute'] < current_minute - 1:
                    del self.local_rate_limits[k]
            
            current_count = self.local_rate_limits[key]['count']
            return current_count <= rate_limit, rate_limit - current_count
    
    def update_usage(self, key_id: str, key_config: Dict):
        """更新使用统计"""
        # 更新总调用次数
        key_config['total_calls'] = key_config.get('total_calls', 0) + 1
        key_config['last_used'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        
        # 保存回配置文件
        self.save_api_keys()
    
    def save_api_keys(self):
        """保存API密钥配置"""
        config = {
            'api_keys': self.api_keys,
            'api_config': {
                'default_rate_limit': 60,
                'require_https': True,
                'enable_logging': True,
                'log_file': 'api_access.log'
            }
        }
        
        with open(self.config_path, 'w', encoding='utf-8') as f:
            yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
    
    def log_api_access(self, api_key: str, endpoint: str, status: str, ip: str):
        """记录API访问日志"""
        if not self.api_keys.get('api_config', {}).get('enable_logging', True):
            return
        
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'api_key': api_key[:8] + '...',  # 只记录部分密钥
            'endpoint': endpoint,
            'status': status,
            'ip': ip
        }
        
        log_file = self.api_keys.get('api_config', {}).get('log_file', 'api_access.log')
        with open(log_file, 'a', encoding='utf-8') as f:
            f.write(json.dumps(log_entry) + '\n')

# API密钥验证装饰器
def require_api_key(func):
    """API密钥验证装饰器"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 从请求中获取API密钥
        # 这里假设API密钥通过HTTP头传递
        # 实际实现取决于你的Web框架
        
        # 示例:Flask框架的实现
        from flask import request, jsonify
        
        api_key = request.headers.get('X-API-Key')
        if not api_key:
            return jsonify({
                'error': '缺少API密钥',
                'message': '请在请求头中包含X-API-Key'
            }), 401
        
        # 初始化密钥管理器
        key_manager = APIKeyManager()
        
        # 验证密钥
        is_valid, key_config, message = key_manager.validate_api_key(api_key)
        if not is_valid:
            key_manager.log_api_access(
                api_key, 
                request.path, 
                'REJECTED', 
                request.remote_addr
            )
            return jsonify({
                'error': 'API密钥验证失败',
                'message': message
            }), 403
        
        # 检查速率限制
        rate_ok, remaining = key_manager.check_rate_limit(api_key, key_config)
        if not rate_ok:
            key_manager.log_api_access(
                api_key, 
                request.path, 
                'RATE_LIMITED', 
                request.remote_addr
            )
            return jsonify({
                'error': '请求频率超限',
                'message': f'每分钟最多{key_config.get("rate_limit", 60)}次请求',
                'retry_after': 60
            }), 429
        
        # 更新使用统计
        key_id = None
        for k, v in key_manager.api_keys.items():
            if v['key'] == api_key:
                key_id = k
                break
        
        if key_id:
            key_manager.update_usage(key_id, key_config)
        
        # 记录成功的访问
        key_manager.log_api_access(
            api_key, 
            request.path, 
            'SUCCESS', 
            request.remote_addr
        )
        
        # 将密钥信息添加到请求上下文
        request.api_key_info = {
            'key_id': key_id,
            'config': key_config,
            'remaining_quota': remaining
        }
        
        # 添加剩余配额到响应头
        response = func(*args, **kwargs)
        if hasattr(response, 'headers'):
            response.headers['X-RateLimit-Remaining'] = str(remaining)
            response.headers['X-RateLimit-Limit'] = str(key_config.get('rate_limit', 60))
        
        return response
    
    return wrapper

4.3 安全的API端点实现

现在我们可以创建安全的API端点,这些端点要求有效的API密钥才能访问:

from flask import Flask, request, jsonify
import os

app = Flask(__name__)

# 初始化SOONet pipeline
soonet_pipeline = None  # 这里应该初始化你的SOONet模型

@app.route('/api/v1/locate', methods=['POST'])
@require_api_key
def locate_video():
    """视频时序定位API端点"""
    try:
        # 检查权限
        if 'query_execution' not in request.api_key_info['config']['permissions']:
            return jsonify({
                'error': '权限不足',
                'message': '该API密钥没有执行查询的权限'
            }), 403
        
        # 获取请求数据
        data = request.json
        if not data:
            return jsonify({'error': '请求体必须为JSON格式'}), 400
        
        query_text = data.get('query_text')
        video_path = data.get('video_path')
        
        if not query_text or not video_path:
            return jsonify({
                'error': '参数缺失',
                'message': 'query_text和video_path为必填参数'
            }), 400
        
        # 检查视频文件是否存在
        if not os.path.exists(video_path):
            return jsonify({
                'error': '文件不存在',
                'message': f'视频文件不存在: {video_path}'
            }), 404
        
        # 执行定位(这里调用SOONet的核心功能)
        # result = soonet_pipeline((query_text, video_path))
        
        # 模拟结果
        result = {
            'query': query_text,
            'video': video_path,
            'timestamps': [
                {'start': 75, 'end': 105, 'score': 0.87},
                {'start': 210, 'end': 240, 'score': 0.65}
            ],
            'status': 'success'
        }
        
        return jsonify(result), 200
        
    except Exception as e:
        return jsonify({
            'error': '处理失败',
            'message': str(e)
        }), 500

@app.route('/api/v1/status', methods=['GET'])
@require_api_key
def get_status():
    """获取服务状态"""
    return jsonify({
        'status': 'running',
        'version': '1.0.0',
        'timestamp': datetime.now().isoformat()
    }), 200

@app.route('/api/v1/usage', methods=['GET'])
@require_api_key
def get_usage():
    """获取API使用情况(仅管理员)"""
    # 检查管理员权限
    if 'view_logs' not in request.api_key_info['config']['permissions']:
        return jsonify({
            'error': '权限不足',
            'message': '需要管理员权限'
        }), 403
    
    key_manager = APIKeyManager()
    key_id = request.api_key_info['key_id']
    
    if key_id in key_manager.api_keys:
        usage_info = {
            'key_id': key_id,
            'total_calls': key_manager.api_keys[key_id].get('total_calls', 0),
            'last_used': key_manager.api_keys[key_id].get('last_used'),
            'rate_limit': key_manager.api_keys[key_id].get('rate_limit', 60)
        }
        return jsonify(usage_info), 200
    else:
        return jsonify({'error': '密钥不存在'}), 404

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, ssl_context='adhoc')  # 启用HTTPS

5. 视频上传大小限制实现

视频文件上传是SOONet的重要功能,但如果不加限制,恶意用户可能会上传超大文件耗尽服务器资源。下面我们来实现视频上传的大小限制。

5.1 上传限制配置

创建 upload_config.yaml 配置文件:

# 视频上传配置
upload:
  # 全局限制
  global:
    max_file_size_mb: 500  # 单个文件最大500MB
    max_total_size_mb: 5000  # 总上传大小限制5GB
    allowed_extensions:  # 允许的文件扩展名
      - ".mp4"
      - ".avi"
      - ".mov"
      - ".mkv"
      - ".webm"
    max_files_per_hour: 10  # 每小时最多上传文件数
  
  # 用户组限制
  user_groups:
    admin:
      max_file_size_mb: 2000  # 管理员可上传2GB文件
      max_total_size_mb: 20000  # 总限制20GB
      max_files_per_hour: 50
    
    premium:
      max_file_size_mb: 1000  # 高级用户可上传1GB文件
      max_total_size_mb: 10000  # 总限制10GB
      max_files_per_hour: 30
    
    standard:
      max_file_size_mb: 500  # 标准用户500MB限制
      max_total_size_mb: 5000  # 总限制5GB
      max_files_per_hour: 10
    
    trial:
      max_file_size_mb: 100  # 试用用户100MB限制
      max_total_size_mb: 1000  # 总限制1GB
      max_files_per_hour: 5
  
  # 存储配置
  storage:
    upload_dir: "/var/soonet/uploads"  # 上传目录
    temp_dir: "/tmp/soonet_uploads"  # 临时目录
    cleanup_days: 7  # 7天后清理旧文件
    enable_compression: true  # 是否启用压缩
    compression_quality: 85  # 压缩质量(0-100)
  
  # 安全检查
  security:
    scan_for_malware: true  # 扫描恶意软件
    check_file_type: true  # 检查文件类型
    block_executables: true  # 阻止可执行文件
    max_filename_length: 255  # 最大文件名长度
  
  # 监控配置
  monitoring:
    enable_logging: true
    log_file: "/var/log/soonet_uploads.log"
    alert_on_limit: true  # 达到限制时告警
    alert_email: "admin@example.com"

5.2 文件上传处理器

创建一个专门处理文件上传的类,包含所有安全检查:

import os
import shutil
import magic  # 用于文件类型检测
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Tuple, Optional
import yaml
import hashlib

class VideoUploadHandler:
    """视频上传处理器"""
    
    def __init__(self, config_path: str = "upload_config.yaml"):
        self.config = self.load_config(config_path)
        self.ensure_directories()
        self.upload_stats = self.load_upload_stats()
    
    def load_config(self, config_path: str) -> Dict:
        """加载上传配置"""
        try:
            with open(config_path, 'r', encoding='utf-8') as f:
                return yaml.safe_load(f)
        except FileNotFoundError:
            print(f"警告:上传配置文件 {config_path} 不存在,使用默认配置")
            return {
                'upload': {
                    'global': {
                        'max_file_size_mb': 500,
                        'allowed_extensions': ['.mp4', '.avi', '.mov', '.mkv']
                    },
                    'storage': {
                        'upload_dir': './uploads',
                        'temp_dir': './temp'
                    }
                }
            }
    
    def ensure_directories(self):
        """确保必要的目录存在"""
        upload_dir = self.config['upload']['storage']['upload_dir']
        temp_dir = self.config['upload']['storage']['temp_dir']
        
        os.makedirs(upload_dir, exist_ok=True)
        os.makedirs(temp_dir, exist_ok=True)
    
    def load_upload_stats(self) -> Dict:
        """加载上传统计"""
        stats_file = os.path.join(
            self.config['upload']['storage']['upload_dir'],
            '.upload_stats.json'
        )
        
        if os.path.exists(stats_file):
            import json
            with open(stats_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        else:
            return {}
    
    def save_upload_stats(self):
        """保存上传统计"""
        stats_file = os.path.join(
            self.config['upload']['storage']['upload_dir'],
            '.upload_stats.json'
        )
        
        import json
        with open(stats_file, 'w', encoding='utf-8') as f:
            json.dump(self.upload_stats, f, indent=2)
    
    def get_user_limits(self, username: str) -> Dict:
        """获取用户的上传限制"""
        user_group = self.get_user_group(username)
        group_limits = self.config['upload']['user_groups'].get(
            user_group, 
            self.config['upload']['user_groups']['standard']
        )
        
        return {
            'max_file_size': group_limits['max_file_size_mb'] * 1024 * 1024,  # 转换为字节
            'max_total_size': group_limits['max_total_size_mb'] * 1024 * 1024,
            'max_files_per_hour': group_limits['max_files_per_hour'],
            'allowed_extensions': self.config['upload']['global']['allowed_extensions']
        }
    
    def get_user_group(self, username: str) -> str:
        """获取用户所属组(简化实现)"""
        # 这里应该根据用户信息确定用户组
        # 简化实现:根据用户名前缀判断
        if username.startswith('admin_'):
            return 'admin'
        elif username.startswith('premium_'):
            return 'premium'
        elif username.startswith('trial_'):
            return 'trial'
        else:
            return 'standard'
    
    def validate_file(self, file_path: str, username: str) -> Tuple[bool, str]:
        """验证上传文件"""
        try:
            # 检查文件是否存在
            if not os.path.exists(file_path):
                return False, "文件不存在"
            
            # 获取文件信息
            file_size = os.path.getsize(file_path)
            file_ext = os.path.splitext(file_path)[1].lower()
            
            # 获取用户限制
            limits = self.get_user_limits(username)
            
            # 检查文件大小
            if file_size > limits['max_file_size']:
                max_mb = limits['max_file_size'] / (1024 * 1024)
                return False, f"文件大小超过限制(最大{max_mb}MB)"
            
            # 检查文件扩展名
            if file_ext not in limits['allowed_extensions']:
                allowed = ', '.join(limits['allowed_extensions'])
                return False, f"不支持的文件类型,允许的类型:{allowed}"
            
            # 检查文件类型(防止文件扩展名欺骗)
            if self.config['upload']['security']['check_file_type']:
                file_type = self.get_file_type(file_path)
                if not self.is_video_file(file_type):
                    return False, f"检测到非视频文件:{file_type}"
            
            # 检查文件名长度
            filename = os.path.basename(file_path)
            if len(filename) > self.config['upload']['security']['max_filename_length']:
                return False, "文件名过长"
            
            # 检查恶意文件(简化实现)
            if self.config['upload']['security']['scan_for_malware']:
                if self.detect_malicious_patterns(file_path):
                    return False, "文件可能包含恶意内容"
            
            # 检查可执行文件
            if self.config['upload']['security']['block_executables']:
                if self.is_executable(file_path):
                    return False, "可执行文件被禁止上传"
            
            # 检查用户上传频率
            if not self.check_upload_rate(username):
                return False, "上传频率超限,请稍后再试"
            
            # 检查用户总上传量
            if not self.check_total_usage(username, file_size):
                user_total_mb = self.get_user_total_usage(username) / (1024 * 1024)
                max_total_mb = limits['max_total_size'] / (1024 * 1024)
                return False, f"总上传量超限(已用{user_total_mb:.1f}MB,限制{max_total_mb}MB)"
            
            return True, "验证通过"
            
        except Exception as e:
            return False, f"文件验证失败:{str(e)}"
    
    def get_file_type(self, file_path: str) -> str:
        """获取文件真实类型"""
        try:
            import magic
            mime = magic.Magic(mime=True)
            return mime.from_file(file_path)
        except:
            # 如果magic不可用,使用备用方法
            import mimetypes
            mime_type, _ = mimetypes.guess_type(file_path)
            return mime_type or "application/octet-stream"
    
    def is_video_file(self, mime_type: str) -> bool:
        """检查是否为视频文件"""
        video_mimes = [
            'video/mp4',
            'video/avi',
            'video/quicktime',
            'video/x-matroska',
            'video/webm',
            'video/x-msvideo'
        ]
        return mime_type in video_mimes
    
    def detect_malicious_patterns(self, file_path: str) -> bool:
        """检测恶意模式(简化实现)"""
        # 这里应该实现更复杂的恶意文件检测
        # 简化实现:检查文件头
        try:
            with open(file_path, 'rb') as f:
                header = f.read(100)  # 读取前100字节
            
            # 检查常见的恶意模式(简化)
            malicious_patterns = [
                b'MZ',  # Windows可执行文件
                b'#!/',  # 脚本文件
                b'%PDF',  # PDF可能包含恶意代码
            ]
            
            for pattern in malicious_patterns:
                if header.startswith(pattern):
                    return True
                    
            return False
        except:
            return False
    
    def is_executable(self, file_path: str) -> bool:
        """检查是否为可执行文件"""
        # 检查文件扩展名
        executable_extensions = ['.exe', '.bat', '.sh', '.bin', '.app']
        file_ext = os.path.splitext(file_path)[1].lower()
        
        if file_ext in executable_extensions:
            return True
        
        # 检查文件权限(Unix系统)
        if os.name == 'posix':
            import stat
            mode = os.stat(file_path).st_mode
            if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH:
                return True
        
        return False
    
    def check_upload_rate(self, username: str) -> bool:
        """检查用户上传频率"""
        current_hour = datetime.now().strftime("%Y-%m-%d %H")
        hour_key = f"{username}:{current_hour}"
        
        if hour_key not in self.upload_stats:
            self.upload_stats[hour_key] = {
                'count': 0,
                'total_size': 0
            }
        
        user_limits = self.get_user_limits(username)
        current_count = self.upload_stats[hour_key]['count']
        
        if current_count >= user_limits['max_files_per_hour']:
            return False
        
        # 更新计数
        self.upload_stats[hour_key]['count'] += 1
        self.save_upload_stats()
        
        return True
    
    def check_total_usage(self, username: str, new_file_size: int) -> bool:
        """检查用户总上传量"""
        user_total = self.get_user_total_usage(username)
        user_limits = self.get_user_limits(username)
        
        return (user_total + new_file_size) <= user_limits['max_total_size']
    
    def get_user_total_usage(self, username: str) -> int:
        """获取用户总上传量"""
        total = 0
        for key, stats in self.upload_stats.items():
            if key.startswith(f"{username}:"):
                total += stats.get('total_size', 0)
        return total
    
    def generate_unique_filename(self, original_filename: str) -> str:
        """生成唯一文件名"""
        import uuid
        import time
        
        # 获取文件扩展名
        ext = os.path.splitext(original_filename)[1]
        
        # 生成唯一ID(时间戳+UUID)
        timestamp = int(time.time())
        unique_id = str(uuid.uuid4())[:8]
        
        # 添加原始文件名哈希(前8位)
        name_hash = hashlib.md5(original_filename.encode()).hexdigest()[:8]
        
        return f"{timestamp}_{unique_id}_{name_hash}{ext}"
    
    def save_uploaded_file(self, temp_path: str, username: str, original_filename: str) -> Tuple[bool, str, str]:
        """保存上传的文件"""
        try:
            # 验证文件
            is_valid, message = self.validate_file(temp_path, username)
            if not is_valid:
                return False, message, ""
            
            # 生成唯一文件名
            unique_filename = self.generate_unique_filename(original_filename)
            upload_dir = self.config['upload']['storage']['upload_dir']
            final_path = os.path.join(upload_dir, unique_filename)
            
            # 如果需要压缩,先压缩
            if self.config['upload']['storage']['enable_compression']:
                compressed_path = self.compress_video(temp_path)
                if compressed_path:
                    shutil.move(compressed_path, final_path)
                    os.remove(temp_path)  # 删除临时文件
                else:
                    shutil.move(temp_path, final_path)
            else:
                shutil.move(temp_path, final_path)
            
            # 更新上传统计
            file_size = os.path.getsize(final_path)
            current_hour = datetime.now().strftime("%Y-%m-%d %H")
            hour_key = f"{username}:{current_hour}"
            
            if hour_key not in self.upload_stats:
                self.upload_stats[hour_key] = {
                    'count': 0,
                    'total_size': 0
                }
            
            self.upload_stats[hour_key]['total_size'] += file_size
            self.save_upload_stats()
            
            # 记录上传日志
            self.log_upload(username, original_filename, unique_filename, file_size)
            
            return True, "上传成功", final_path
            
        except Exception as e:
            return False, f"保存文件失败:{str(e)}", ""
    
    def compress_video(self, input_path: str) -> Optional[str]:
        """压缩视频文件(简化实现)"""
        # 这里应该使用ffmpeg等工具进行实际压缩
        # 简化实现:直接返回原路径
        return input_path
    
    def log_upload(self, username: str, original_name: str, saved_name: str, file_size: int):
        """记录上传日志"""
        if not self.config['upload']['monitoring']['enable_logging']:
            return
        
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'username': username,
            'original_filename': original_name,
            'saved_filename': saved_name,
            'file_size': file_size,
            'file_size_mb': file_size / (1024 * 1024)
        }
        
        log_file = self.config['upload']['monitoring']['log_file']
        try:
            import json
            with open(log_file, 'a', encoding='utf-8') as f:
                f.write(json.dumps(log_entry) + '\n')
        except:
            pass  # 日志记录失败不影响主要功能
    
    def cleanup_old_files(self):
        """清理旧文件"""
        try:
            upload_dir = self.config['upload']['storage']['upload_dir']
            cleanup_days = self.config['upload']['storage']['cleanup_days']
            
            cutoff_time = datetime.now() - timedelta(days=cleanup_days)
            
            for file_path in Path(upload_dir).glob('*'):
                if file_path.is_file():
                    file_time = datetime.fromtimestamp(file_path.stat().st_mtime)
                    if file_time < cutoff_time:
                        os.remove(file_path)
                        print(f"清理旧文件:{file_path}")
        except Exception as e:
            print(f"清理文件时出错:{str(e)}")

5.3 集成到Gradio界面

现在我们将文件上传限制集成到Gradio界面中:

import gradio as gr
from upload_handler import VideoUploadHandler

class SecureSOONetApp:
    """安全的SOONet应用"""
    
    def __init__(self):
        self.upload_handler = VideoUploadHandler()
        self.current_user = None
    
    def create_interface(self):
        """创建带上传限制的界面"""
        
        with gr.Blocks(title="SOONet - 安全视频时序定位系统") as demo:
            # 登录界面(同上,略)
            
            # 主功能界面
            with gr.Column(visible=False) as main_section:
                gr.Markdown("## 🎯 SOONet 视频时序定位")
                
                # 显示当前用户和限制信息
                with gr.Row():
                    user_info = gr.Markdown("")
                    limit_info = gr.Markdown("")
                
                # 查询文本输入
                query_text = gr.Textbox(
                    label="查询文本",
                    placeholder="输入英文描述,如:a man takes food out of the refrigerator",
                    lines=2
                )
                
                # 安全的视频上传组件
                video_input = gr.File(
                    label="上传视频",
                    file_types=["video"],
                    file_count="single"
                )
                
                # 上传状态显示
                upload_status = gr.Textbox(
                    label="上传状态",
                    interactive=False,
                    visible=False
                )
                
                # 定位按钮
                locate_button = gr.Button("🔍 开始定位", variant="primary")
                
                # 结果显示
                result_output = gr.Textbox(label="定位结果", interactive=False)
                
                # 退出按钮
                logout_button = gr.Button("退出登录", variant="secondary")
            
            # 更新用户信息
            def update_user_info(username):
                """更新用户信息和限制显示"""
                if not username:
                    return "", ""
                
                limits = self.upload_handler.get_user_limits(username)
                max_mb = limits['max_file_size'] / (1024 * 1024)
                max_total_mb = limits['max_total_size'] / (1024 * 1024)
                
                user_text = f"**当前用户**:{username}"
                limit_text = f"**上传限制**:单文件≤{max_mb}MB,总量≤{max_total_mb}MB"
                
                return user_text, limit_text
            
            # 文件上传处理
            def handle_file_upload(file, username):
                """处理文件上传"""
                if not file:
                    return None, "请选择文件", gr.update(visible=True)
                
                if not username:
                    return None, "请先登录", gr.update(visible=True)
                
                # 保存上传的文件
                success, message, saved_path = self.upload_handler.save_uploaded_file(
                    file.name, username, os.path.basename(file.name)
                )
                
                if success:
                    return saved_path, f"✅ {message}", gr.update(visible=False)
                else:
                    return None, f"❌ {message}", gr.update(visible=True)
            
            # 绑定事件
            # ...(登录/退出事件绑定,同上)
            
            # 文件上传事件
            video_input.upload(
                fn=lambda file: handle_file_upload(file, self.current_user),
                inputs=[video_input],
                outputs=[video_input, upload_status, upload_status]
            )
            
            # 定位功能(需要检查文件大小)
            def secure_locate(query, video_path):
                """安全的定位功能"""
                if not self.current_user:
                    return "请先登录"
                
                if not video_path:
                    return "请先上传视频"
                
                # 检查文件大小(再次验证)
                if os.path.exists(video_path):
                    file_size = os.path.getsize(video_path)
                    limits = self.upload_handler.get_user_limits(self.current_user)
                    
                    if file_size > limits['max_file_size']:
                        max_mb = limits['max_file_size'] / (1024 * 1024)
                        return f"文件大小超过限制(最大{max_mb}MB)"
                
                # 调用SOONet定位功能
                # result = soonet_pipeline((query, video_path))
                
                # 模拟结果
                result = f"查询: {query}\n"
                result += f"视频: {os.path.basename(video_path)}\n"
                result += "定位结果: 00:01:15 - 00:01:45 (置信度: 0.87)"
                
                return result
            
            locate_button.click(
                fn=secure_locate,
                inputs=[query_text, video_input],
                outputs=result_output
            )
        
        return demo
    
    def run(self):
        """运行应用"""
        # 定期清理旧文件
        import threading
        import time
        
        def cleanup_task():
            while True:
                time.sleep(3600)  # 每小时清理一次
                self.upload_handler.cleanup_old_files()
        
        # 启动清理线程
        cleanup_thread = threading.Thread(target=cleanup_task, daemon=True)
        cleanup_thread.start()
        
        # 启动Gradio应用
        demo = self.create_interface()
        demo.launch(
            server_name="0.0.0.0",
            server_port=7860,
            share=False
        )

# 启动应用
if __name__ == "__main__":
    app = SecureSOONetApp()
    app.run()

6. 完整部署与配置指南

现在我们已经实现了所有安全功能,接下来看看如何完整部署这个加固版的SOONet系统。

6.1 项目结构

部署前,先确保项目结构正确:

/root/multi-modal_soonet_video-temporal-grounding/
├── app.py                          # 主应用文件(已修改)
├── requirements.txt                # Python依赖
├── users_config.yaml              # 用户认证配置
├── api_keys_config.yaml           # API密钥配置
├── upload_config.yaml             # 上传限制配置
├── auth/                          # 认证模块
│   ├── __init__.py
│   ├── session_manager.py         # 会话管理
│   └── password_utils.py          # 密码工具
├── api/                           # API模块
│   ├── __init__.py
│   ├── key_manager.py             # API密钥管理
│   └── rate_limiter.py           # 速率限制
├── upload/                        # 上传模块
│   ├── __init__.py
│   ├── upload_handler.py          # 上传处理器
│   └── file_validator.py          # 文件验证
├── logs/                          # 日志目录
│   ├── access.log                 # 访问日志
│   └── uploads.log               # 上传日志
└── uploads/                       # 上传文件存储
    └── .gitkeep

6.2 依赖安装

创建 requirements.txt 文件:

# 基础依赖
torch>=1.10.0
torchvision>=0.11.0
modelscope>=1.0.0
gradio==6.4.0
opencv-python>=4.5.0

# 安全加固新增依赖
Flask>=2.0.0
Flask-CORS>=3.0.0
PyYAML>=6.0
bcrypt>=4.0.0
redis>=4.0.0
python-magic>=0.4.0
python-magic-bin>=0.4.0  # Windows系统需要

# 文本处理
ftfy>=6.0.0
regex>=2021.0.0

# 注意:numpy需要<2.0版本
numpy<2.0

安装所有依赖:

pip install -r requirements.txt

6.3 配置文件设置

1. 用户认证配置 (users_config.yaml):

# 生产环境用户配置
users:
  # 管理员
  admin:
    password_hash: "$2b$12$YourHashedPasswordHere"  # 使用bcrypt哈希
    role: "admin"
    permissions:
      - "full_access"
      - "upload_large_files"
      - "manage_users"
      - "view_logs"
    email: "admin@yourcompany.com"
    created_at: "2024-01-01"
  
  # 开发人员
  developer:
    password_hash: "$2b$12$AnotherHashedPassword"
    role: "premium"
    permissions:
      - "video_upload"
      - "query_execution"
      - "result_retrieval"
    email: "dev@yourcompany.com"
    created_at: "2024-01-01"
  
  # 普通用户
  user:
    password_hash: "$2b$12$UserHashedPassword"
    role: "standard"
    permissions:
      - "video_upload"
      - "query_execution"
    email: "user@yourcompany.com"
    created_at: "2024-01-01"

session:
  timeout_minutes: 30
  max_sessions_per_user: 3
  secure_cookies: true  # 仅HTTPS传输cookie

login:
  max_attempts: 5
  lockout_minutes: 15
  require_2fa: false  # 是否要求双因素认证

2. API密钥配置 (api_keys_config.yaml):

api_keys:
  # 内部服务密钥
  internal_service_001:
    key: "sk_live_xxxxxxxxxxxxxxxx"
    name: "内部服务密钥"
    owner: "backend_service"
    created_at: "2024-01-01"
    expires_at: "2024-12-31"
    rate_limit: 1000
    total_calls: 0
    last_used: null
    permissions:
      - "video_upload"
      - "query_execution"
      - "result_retrieval"
      - "batch_processing"
    enabled: true
    ip_whitelist:  # IP白名单
      - "10.0.0.0/8"
      - "192.168.1.0/24"
  
  # 合作伙伴API密钥
  partner_001:
    key: "sk_partner_yyyyyyyyyyyy"
    name: "合作伙伴A"
    owner: "partner_a"
    created_at: "2024-01-01"
    expires_at: "2024-06-30"
    rate_limit: 100
    total_calls: 0
    last_used: null
    permissions:
      - "query_execution"
      - "result_retrieval"
    enabled: true
    monthly_quota: 10000  # 每月调用限额
  
  # 移动应用密钥
  mobile_app_001:
    key: "sk_mobile_zzzzzzzzzzzz"
    name: "移动应用"
    owner: "mobile_team"
    created_at: "2024-01-01"
    expires_at: null
    rate_limit: 60
    total_calls: 0
    last_used: null
    permissions:
      - "query_execution"
    enabled: true
    device_limit: 1000  # 最大设备数

api_config:
  default_rate_limit: 60
  require_https: true
  enable_logging: true
  log_file: "/var/log/soonet_api.log"
  log_retention_days: 30
  blocked_ips: []
  enable_metrics: true
  metrics_port: 9090

3. 上传限制配置 (upload_config.yaml):

upload:
  global:
    max_file_size_mb: 500
    max_total_size_mb: 5000
    allowed_extensions:
      - ".mp4"
      - ".avi"
      - ".mov"
      - ".mkv"
      - ".webm"
      - ".flv"
      - ".wmv"
    max_files_per_hour: 10
    max_files_per_day: 50
  
  user_groups:
    admin:
      max_file_size_mb: 5000
      max_total_size_mb: 50000
      max_files_per_hour: 100
      max_files_per_day: 500
    
    premium:
      max_file_size_mb: 2000
      max_total_size_mb: 20000
      max_files_per_hour: 50
      max_files_per_day: 200
    
    standard:
      max_file_size_mb: 500
      max_total_size_mb: 5000
      max_files_per_hour: 10
      max_files_per_day: 50
    
    trial:
      max_file_size_mb: 100
      max_total_size_mb: 1000
      max_files_per_hour: 5
      max_files_per_day: 20
  
  storage:
    upload_dir: "/data/soonet/uploads"
    temp_dir: "/tmp/soonet_uploads"
    cleanup_days: 7
    enable_compression: true
    compression_quality: 85
    max_storage_gb: 100  # 最大存储空间
    enable_encryption: true  # 启用文件加密
    
    # 存储分区配置
    partitions:
      - path: "/data/soonet/uploads/partition1"
        max_size_gb: 50
      - path: "/data/soonet/uploads/partition2"
        max_size_gb: 50
  
  security:
    scan_for_malware: true
    check_file_type: true
    block_executables: true
    max_filename_length: 255
    block_suspicious_names: true  # 阻止可疑文件名
    virus_scan_command: "clamscan --no-summary"  # 病毒扫描命令
    
    # 文件类型深度检查
    deep_file_check: true
    allowed_mime_types:
      - "video/mp4"
      - "video/avi"
      - "video/quicktime"
      - "video/x-matroska"
      - "video/webm"
      - "video/x-msvideo"
  
  monitoring:
    enable_logging: true
    log_file: "/var/log/soonet_uploads.log"
    alert_on_limit: true
    alert_email: "alerts@yourcompany.com"
    alert_webhook: "https://hooks.slack.com/services/xxx"
    
    # 监控指标
    enable_metrics: true
    metrics_port: 9091
    
    # 自动清理
    auto_cleanup: true
    cleanup_schedule: "0 2 * * *"  # 每天凌晨2点

6.4 启动脚本

创建启动脚本 start_soonet.sh

#!/bin/bash

# SOONet安全加固版启动脚本

set -e

echo "========================================"
echo "启动 SOONet 安全加固版"
echo "========================================"

# 检查Python版本
PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
echo "Python版本: $PYTHON_VERSION"

if [[ "$PYTHON_VERSION" < "3.7" ]]; then
    echo "错误: 需要Python 3.7或更高版本"
    exit 1
fi

# 检查依赖
echo "检查依赖..."
pip install -r requirements.txt

# 创建必要的目录
echo "创建目录..."
mkdir -p logs
mkdir -p uploads
mkdir -p /tmp/soonet_uploads

# 设置权限
echo "设置权限..."
chmod 755 uploads
chmod 755 logs
chmod 600 *.yaml  # 配置文件设置为只读

# 检查配置文件
echo "检查配置文件..."
if [ ! -f "users_config.yaml" ]; then
    echo "警告: users_config.yaml 不存在,创建示例配置"
    cp users_config.example.yaml users_config.yaml
fi

if [ ! -f "api_keys_config.yaml" ]; then
    echo "警告: api_keys_config.yaml 不存在,创建示例配置"
    cp api_keys_config.example.yaml api_keys_config.yaml
fi

if [ ! -f "upload_config.yaml" ]; then
    echo "警告: upload_config.yaml 不存在,创建示例配置"
    cp upload_config.example.yaml upload_config.yaml
fi

# 检查模型文件
echo "检查模型文件..."
MODEL_DIR="/root/ai-models/iic/multi-modal_soonet_video-temporal-grounding"
if [ ! -d "$MODEL_DIR" ]; then
    echo "错误: 模型目录不存在: $MODEL_DIR"
    exit 1
fi

# 设置环境变量
export SOONET_MODEL_PATH="$MODEL_DIR"
export PYTHONPATH="/root/multi-modal_soonet_video-temporal-grounding:$PYTHONPATH"

# 启动Redis(用于速率限制)
echo "启动Redis..."
if ! command -v redis-server &> /dev/null; then
    echo "警告: Redis未安装,速率限制将使用内存模式"
else
    # 检查Redis是否已在运行
    if ! pgrep -x "redis-server" > /dev/null; then
        redis-server --daemonize yes
        echo "Redis已启动"
    else
        echo "Redis已在运行"
    fi
fi

# 启动监控服务(可选)
echo "启动监控服务..."
python monitoring_service.py &

# 启动SOONet应用
echo "启动SOONet应用..."
cd /root/multi-modal_soonet_video-temporal-grounding

# 使用gunicorn生产环境部署(推荐)
if command -v gunicorn &> /dev/null; then
    echo "使用gunicorn启动..."
    gunicorn \
        --bind 0.0.0.0:7860 \
        --workers 4 \
        --threads 2 \
        --timeout 120 \
        --access-logfile logs/access.log \
        --error-logfile logs/error.log \
        app:app
else
    echo "使用Python直接启动..."
    python app.py
fi

echo "========================================"
echo "SOONet 已启动"
echo "访问地址: http://localhost:7860"
echo "API地址: http://localhost:5000/api/v1"
echo "========================================"

6.5 监控与维护脚本

创建监控脚本 monitor_soonet.py

#!/usr/bin/env python3
"""
SOONet监控脚本
监控系统状态、资源使用、安全事件等
"""

import psutil
import requests
import json
import time
from datetime import datetime
import logging
from typing import Dict, Any

class SOONetMonitor:
    """SOONet监控器"""
    
    def __init__(self, config_path: str = "monitor_config.yaml"):
        self.config = self.load_config(config_path)
        self.setup_logging()
        
    def load_config(self, config_path: str) -> Dict[str, Any]:
        """加载监控配置"""
        import yaml
        try:
            with open(config_path, 'r', encoding='utf-8') as f:
                return yaml.safe_load(f)
        except FileNotFoundError:
            return {
                'monitoring': {
                    'check_interval': 60,  # 检查间隔(秒)
                    'alerts': {
                        'cpu_threshold': 80,  # CPU使用率阈值
                        'memory_threshold': 85,  # 内存使用率阈值
                        'disk_threshold': 90,  # 磁盘使用率阈值
                        'api_error_threshold': 10,  # API错误阈值
                        'upload_failure_threshold': 5  # 上传失败阈值
                    },
                    'notifications': {
                        'enable_email': False,
                        'enable_slack': False,
                        'enable_webhook': False
                    }
                }
            }
    
    def setup_logging(self):
        """设置日志"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('logs/monitor.log'),
                logging.StreamHandler()
            ]
        )
        self.logger = logging.getLogger('SOONetMonitor')
    
    def check_system_resources(self) -> Dict[str, Any]:
        """检查系统资源"""
        resources = {
            'timestamp': datetime.now().isoformat(),
            'cpu_percent': psutil.cpu_percent(interval=1),
            'memory_percent': psutil.virtual_memory().percent,
            'disk_percent': psutil.disk_usage('/').percent,
            'network_io': psutil.net_io_counters()._asdict(),
            'process_count': len(psutil.pids())
        }
        
        # 检查阈值
        alerts = []
        if resources['cpu_percent'] > self.config['monitoring']['alerts']['cpu_threshold']:
            alerts.append(f"CPU使用率过高: {resources['cpu_percent']}%")
        
        if resources['memory_percent'] > self.config['monitoring']['alerts']['memory_threshold']:
            alerts.append(f"内存使用率过高: {resources['memory_percent']}%")
        
        if resources['disk_percent'] > self.config['monitoring']['alerts']['disk_threshold']:
            alerts.append(f"磁盘使用率过高: {resources['disk_percent']}%")
        
        resources['alerts'] = alerts
        return resources
    
    def check_service_health(self) -> Dict[str, Any]:
        """检查服务健康状态"""
        health = {
            'timestamp': datetime.now().isoformat(),
            'services': {}
        }
        
        # 检查Gradio服务
        try:
            response = requests.get('http://localhost:7860', timeout=5)
            health['services']['gradio'] = {
                'status': 'up' if response.status_code == 200 else 'down',
                'response_time': response.elapsed.total_seconds(),
                'status_code': response.status_code
            }
        except requests.RequestException as e:
            health['services']['gradio'] = {
                'status': 'down',
                'error': str(e)
            }
        
        # 检查API服务
        try:
            response = requests.get('http://localhost:5000/api/v1/status', timeout=5)
            health['services']['api'] = {
                'status': 'up' if response.status_code == 200 else 'down',
                'response_time': response.elapsed.total_seconds(),
                'status_code': response.status_code
            }
        except requests.RequestException as e:
            health['services']['api'] = {
                'status': 'down',
                'error': str(e)
            }
        
        return health
    
    def check_security_events(self) -> Dict[str, Any]:
        """检查安全事件"""
        import os
        import glob
        
        security = {
            'timestamp': datetime.now().isoformat(),
            'events': []
        }
        
        # 检查失败登录尝试
        auth_log = 'logs/auth.log'
        if os.path.exists(auth_log):
            with open(auth_log, 'r', encoding='utf-8') as f:
                lines = f.readlines()[-100:]  # 最近100行
                
            failed_attempts = 0
            for line in lines:
                if 'FAILED' in line or '失败' in line:
                    failed_attempts += 1
            
            if failed_attempts > 0:
                security['events'].append({
                    'type': 'failed_login',
                    'count': failed_attempts,
                    'message': f'最近有{failed_attempts}次失败登录尝试'
                })
        
        # 检查API滥用
        api_log = 'logs/api_access.log'
        if os.path.exists(api_log):
            with open(api_log, 'r', encoding='utf-8') as f:
                lines = f.readlines()[-100:]
            
            rate_limited = 0
            for line in lines:
                if 'RATE_LIMITED' in line:
                    rate_limited += 1
            
            if rate_limited > self.config['monitoring']['alerts']['api_error_threshold']:
                security['events'].append({
                    'type': 'api_abuse',
                    'count': rate_limited,
                    'message': f'API限流次数过多: {rate_limited}次'
                })
        
        return security
    
    def send_alert(self, alert_type: str, message: str, level: str = 'warning'):
        """发送
Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐