# Troubleshoot助手登录认证_计划执行

## 1. 项目概述

### 1.1 项目背景
问题排查助手涉及项目敏感信息，当前任何人都可以访问系统，存在信息泄露风险。同时缺乏权限控制，普通用户误操作可能影响其他人使用。

### 1.2 项目目标
- 添加登录认证功能，防止未授权访问
- 区分管理员和普通用户，实现权限分级管理
- 记录用户操作审计日志，支持追溯
- 提供用户管理界面，方便管理员维护账号

### 1.3 开发周期
**总计：2 天**
- 需求一：登录页面与身份认证 - 1 天
- 需求二：权限控制与审计日志 - 0.5 天
- 需求三：用户管理界面 - 0.5 天

---

## 2. 项目结构

```
.claude/skills/Troubleshoot/code/web/
├── templates/
│   ├── index.html              # 主页（需修改：添加用户信息显示）
│   └── login.html              # 登录页（新增）
├── server.py                   # 后端服务（需修改：添加认证逻辑）
├── config.json                 # 配置文件（需修改：添加认证配置）
├── users.json                  # 用户数据（新增）
├── auth.py                     # 认证模块（新增）
├── decorators.py               # 权限装饰器（新增）
└── requirements.txt            # 依赖包列表（需更新）
```

---

## 3. 技术方案

### 3.1 技术栈

| 技术 | 用途 | 版本 |
|------|------|------|
| Flask-Login | 用户会话管理 | >=0.6.3 |
| werkzeug.security | 密码加密 | >=2.3.0 |
| Flask-Session | Session 管理 | >=0.5.0 |

### 3.2 认证流程

```
┌─────────────────────────────────────────────────────────────┐
│                    用户认证流程                               │
├─────────────────────────────────────────────────────────────┤
│  1. 用户访问 http://192.168.5.60:8088                        │
│  2. 检查 Session 是否有效                                     │
│     ├─ 有效 → 显示主页                                        │
│     └─ 无效 → 重定向到 /login                                 │
│  3. 用户输入用户名密码提交                                    │
│  4. 后端验证用户名密码                                        │
│     ├─ 成功 → 创建 Session → 重定向到主页                     │
│     └─ 失败 → 返回错误提示                                    │
│  5. 主页显示用户信息（用户名、角色）                          │
│  6. 后续请求携带 Session Cookie                               │
│  7. 点击"退出" → 清除 Session → 重定向到登录页                │
└─────────────────────────────────────────────────────────────┘
```

### 3.3 权限控制流程

```
┌─────────────────────────────────────────────────────────────┐
│                    权限验证流程                               │
├─────────────────────────────────────────────────────────────┤
│  1. 前端发起 API 请求                                         │
│  2. 后端检查 Session                                          │
│  3. 获取用户角色（admin / user）                              │
│  4. 检查用户是否有权限访问该接口                               │
│     ├─ 有权限 → 执行业务逻辑 → 返回结果                       │
│     └─ 无权限 → 返回 403 Forbidden                           │
│  5. 记录审计日志                                              │
└─────────────────────────────────────────────────────────────┘
```

---

## 4. 详细设计

### 4.1 用户数据模型（users.json）

```json
{
  "users": [
    {
      "id": 1,
      "username": "admin",
      "password_hash": "pbkdf2:sha256:260000$...",
      "role": "admin",
      "created_at": "2026-07-12T15:00:00",
      "last_login": "2026-07-12T15:30:00",
      "login_count": 10,
      "active": true
    },
    {
      "id": 2,
      "username": "zhangsan",
      "password_hash": "pbkdf2:sha256:260000$...",
      "role": "user",
      "created_at": "2026-07-12T16:00:00",
      "last_login": null,
      "login_count": 0,
      "active": true
    }
  ]
}
```

### 4.2 认证模块（auth.py）

```python
# -*- coding: utf-8 -*-
"""
auth.py — 用户认证模块

功能：
  - 用户登录验证
  - 密码加密与验证
  - Session 管理
  - 用户数据读写
"""

import os
import json
import time
from pathlib import Path
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash

# 用户数据文件路径
USERS_FILE = Path(__file__).parent / "users.json"

# 登录失败记录（防暴力破解）
login_attempts = {}  # {"username": {"count": 0, "lock_until": 0}}


class UserManager:
    """用户管理器"""

    def __init__(self):
        self.users = self._load_users()

    def _load_users(self):
        """加载用户数据"""
        if not USERS_FILE.exists():
            # 创建默认管理员账号
            default_users = {
                "users": [{
                    "id": 1,
                    "username": "admin",
                    "password_hash": generate_password_hash("Admin@2026"),
                    "role": "admin",
                    "created_at": datetime.now().isoformat(),
                    "last_login": None,
                    "login_count": 0,
                    "active": True
                }]
            }
            self._save_users(default_users)
            return default_users

        with open(USERS_FILE, 'r', encoding='utf-8') as f:
            return json.load(f)

    def _save_users(self, data):
        """保存用户数据"""
        with open(USERS_FILE, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

    def authenticate(self, username, password):
        """
        验证用户登录

        Returns:
          成功：用户字典
          失败：None
        """
        # 检查账号是否被锁定
        if self._is_locked(username):
            return None

        # 查找用户
        user = None
        for u in self.users.get('users', []):
            if u['username'] == username:
                user = u
                break

        if not user:
            self._record_failed_login(username)
            return None

        # 检查账号是否激活
        if not user.get('active', True):
            return None

        # 验证密码
        if check_password_hash(user['password_hash'], password):
            # 登录成功，清除失败记录
            if username in login_attempts:
                del login_attempts[username]
            
            # 更新登录信息
            user['last_login'] = datetime.now().isoformat()
            user['login_count'] = user.get('login_count', 0) + 1
            self._save_users(self.users)
            
            return user
        else:
            # 登录失败，记录
            self._record_failed_login(username)
            return None

    def _is_locked(self, username):
        """检查账号是否被锁定"""
        if username not in login_attempts:
            return False

        attempts = login_attempts[username]
        if attempts['count'] >= 5:
            # 检查锁定是否过期（15分钟）
            if time.time() < attempts['lock_until']:
                return True
            else:
                # 锁定过期，清除记录
                del login_attempts[username]
                return False

        return False

    def _record_failed_login(self, username):
        """记录登录失败"""
        if username not in login_attempts:
            login_attempts[username] = {'count': 0, 'lock_until': 0}

        login_attempts[username]['count'] += 1

        # 失败5次后锁定15分钟
        if login_attempts[username]['count'] >= 5:
            login_attempts[username]['lock_until'] = time.time() + 900

    def get_user(self, username):
        """获取用户信息"""
        for u in self.users.get('users', []):
            if u['username'] == username:
                return u
        return None

    def add_user(self, username, password, role='user'):
        """添加新用户"""
        # 检查用户名是否已存在
        if self.get_user(username):
            return False

        new_user = {
            "id": len(self.users['users']) + 1,
            "username": username,
            "password_hash": generate_password_hash(password),
            "role": role,
            "created_at": datetime.now().isoformat(),
            "last_login": None,
            "login_count": 0,
            "active": True
        }

        self.users['users'].append(new_user)
        self._save_users(self.users)
        return True

    def update_password(self, username, new_password):
        """修改密码"""
        user = self.get_user(username)
        if not user:
            return False

        user['password_hash'] = generate_password_hash(new_password)
        self._save_users(self.users)
        return True

    def toggle_user_active(self, username, active):
        """启用/禁用用户"""
        user = self.get_user(username)
        if not user:
            return False

        user['active'] = active
        self._save_users(self.users)
        return True


# 全局用户管理器实例
user_manager = UserManager()
```

### 4.3 权限装饰器（decorators.py）

```python
# -*- coding: utf-8 -*-
"""
decorators.py — 权限验证装饰器

功能：
  - 登录验证装饰器
  - 角色权限验证装饰器
"""

from functools import wraps
from flask import session, jsonify


def login_required(f):
    """登录验证装饰器"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user' not in session:
            return jsonify({
                'success': False,
                'error': '未登录',
                'code': 401
            }), 401
        return f(*args, **kwargs)
    return decorated_function


def admin_required(f):
    """管理员权限装饰器"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user' not in session:
            return jsonify({
                'success': False,
                'error': '未登录',
                'code': 401
            }), 401

        user = session.get('user', {})
        if user.get('role') != 'admin':
            return jsonify({
                'success': False,
                'error': '权限不足，仅管理员可访问',
                'code': 403
            }), 403

        return f(*args, **kwargs)
    return decorated_function
```

### 4.4 后端接口实现（server.py）

```python
# ============================================================
# 导入认证模块
# ============================================================
from flask import session, redirect, url_for
from auth import user_manager
from decorators import login_required, admin_required

# 配置 Session
app.secret_key = os.environ.get('SECRET_KEY', 'troubleshoot-secret-key-2026')
app.config['SESSION_TYPE'] = 'filesystem'
app.config['PERMANENT_SESSION_LIFETIME'] = 86400  # 24小时

# ============================================================
# 登录认证接口
# ============================================================

@app.route('/login', methods=['GET', 'POST'])
def login():
    """登录页面"""
    if request.method == 'GET':
        # 已登录则直接跳转主页
        if 'user' in session:
            return redirect('/')
        return render_template('login.html')

    # POST 请求：处理登录
    data = request.get_json() or {}
    username = data.get('username', '').strip()
    password = data.get('password', '').strip()
    remember = data.get('remember', False)

    if not username or not password:
        return jsonify({
            'success': False,
            'error': '用户名和密码不能为空'
        }), 400

    # 验证登录
    user = user_manager.authenticate(username, password)

    if user:
        # 创建 Session
        session['user'] = {
            'id': user['id'],
            'username': user['username'],
            'role': user['role']
        }

        if remember:
            session.permanent = True

        # 记录审计日志
        log_audit({
            'timestamp': datetime.now().isoformat(),
            'user': username,
            'role': user['role'],
            'action': 'login',
            'ip': request.remote_addr,
            'result': 'success'
        })

        return jsonify({
            'success': True,
            'user': session['user']
        })
    else:
        # 记录失败日志
        log_audit({
            'timestamp': datetime.now().isoformat(),
            'user': username,
            'action': 'login',
            'ip': request.remote_addr,
            'result': 'failed'
        })

        return jsonify({
            'success': False,
            'error': '用户名或密码错误，或账号已被锁定'
        }), 401


@app.route('/logout', methods=['POST'])
def logout():
    """注销登录"""
    user = session.get('user', {})

    # 记录审计日志
    if user:
        log_audit({
            'timestamp': datetime.now().isoformat(),
            'user': user.get('username'),
            'role': user.get('role'),
            'action': 'logout',
            'ip': request.remote_addr,
            'result': 'success'
        })

    session.clear()
    return jsonify({'success': True})


@app.route('/api/user/info', methods=['GET'])
@login_required
def get_user_info():
    """获取当前用户信息"""
    return jsonify({
        'success': True,
        'user': session.get('user')
    })


# ============================================================
# 修改现有接口：添加权限验证
# ============================================================

@app.route('/api/cache/stats', methods=['GET'])
@admin_required
def get_cache_stats():
    """获取缓存统计信息（仅管理员）"""
    # ... 原有代码 ...


@app.route('/api/cache/clear', methods=['POST'])
@admin_required
def clear_cache():
    """清空所有缓存（仅管理员）"""
    # ... 原有代码 ...
```

### 4.5 前端登录页面（login.html）

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登录 - 问题排查助手</title>
    <style>
        /* 样式代码省略，见完整实现 */
    </style>
</head>
<body>
    <div class="login-container">
        <div class="login-card">
            <h1>🔐 问题排查助手登录</h1>
            
            <form id="loginForm">
                <div class="form-group">
                    <label>用户名</label>
                    <input type="text" id="username" 
                           class="form-input" 
                           placeholder="请输入用户名"
                           autocomplete="username">
                </div>

                <div class="form-group">
                    <label>密码</label>
                    <input type="password" id="password" 
                           class="form-input" 
                           placeholder="请输入密码"
                           autocomplete="current-password">
                </div>

                <div class="form-group">
                    <label class="checkbox-label">
                        <input type="checkbox" id="remember">
                        记住登录状态
                    </label>
                </div>

                <button type="submit" class="btn-login">
                    🔑 登  录
                </button>

                <div id="errorMsg" class="error-msg" style="display:none;"></div>
            </form>

            <div class="login-footer">
                <p>提示：默认管理员账号 admin / Admin@2026</p>
            </div>
        </div>
    </div>

    <script>
        // 登录表单提交
        document.getElementById('loginForm').addEventListener('submit', async (e) => {
            e.preventDefault();

            const username = document.getElementById('username').value.trim();
            const password = document.getElementById('password').value;
            const remember = document.getElementById('remember').checked;

            if (!username || !password) {
                showError('请输入用户名和密码');
                return;
            }

            try {
                const resp = await fetch('/login', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ username, password, remember }),
                    credentials: 'include'
                });

                const data = await resp.json();

                if (data.success) {
                    // 登录成功，跳转主页
                    window.location.href = '/';
                } else {
                    showError(data.error || '登录失败');
                }
            } catch (e) {
                showError('网络错误，请稍后重试');
            }
        });

        function showError(msg) {
            const errorEl = document.getElementById('errorMsg');
            errorEl.textContent = msg;
            errorEl.style.display = 'block';
        }
    </script>
</body>
</html>
```

### 4.6 主页用户信息显示（index.html 修改）

在主页顶部添加用户信息区域：

```html
<!-- 用户信息栏 -->
<div class="user-info-bar">
    <span id="userDisplayName">加载中...</span>
    <button class="btn-logout" onclick="logout()">退出</button>
</div>
```

```javascript
// 页面加载时获取用户信息
async function loadUserInfo() {
    try {
        const resp = await fetch('/api/user/info', { credentials: 'include' });
        const data = await resp.json();

        if (data.success) {
            const user = data.user;
            const displayName = `${user.username}（${user.role === 'admin' ? '管理员' : '用户'}）`;
            document.getElementById('userDisplayName').textContent = displayName;

            // 根据角色显示/隐藏缓存管理按钮
            if (user.role !== 'admin') {
                const cacheBtn = document.querySelector('button[onclick="showCacheModal()"]');
                if (cacheBtn) {
                    cacheBtn.style.display = 'none';
                }
            }
        } else {
            // 未登录，跳转登录页
            window.location.href = '/login';
        }
    } catch (e) {
        console.error('获取用户信息失败:', e);
    }
}

// 注销登录
async function logout() {
    if (!confirm('确定要退出登录吗？')) {
        return;
    }

    try {
        const resp = await fetch('/logout', { method: 'POST', credentials: 'include' });
        window.location.href = '/login';
    } catch (e) {
        alert('退出失败');
    }
}

// 页面加载时初始化
document.addEventListener('DOMContentLoaded', () => {
    loadUserInfo();
    // ... 其他初始化代码
});
```

---

## 5. 实施计划

### 5.1 实施步骤

| 序号 | 任务 | 预计时间 | 状态 | 依赖 |
|------|------|----------|------|------|
| 1 | 创建认证模块（auth.py） | 0.3天 | ⏳ 待开始 | - |
| 2 | 创建权限装饰器（decorators.py） | 0.1天 | ⏳ 待开始 | 1 |
| 3 | 创建用户数据文件（users.json） | 0.1天 | ⏳ 待开始 | - |
| 4 | 后端添加登录接口 | 0.2天 | ⏳ 待开始 | 1,2 |
| 5 | 后端添加 Session 管理 | 0.1天 | ⏳ 待开始 | 4 |
| 6 | 后端为现有接口添加权限验证 | 0.2天 | ⏳ 待开始 | 4,5 |
| 7 | 前端创建登录页面（login.html） | 0.3天 | ⏳ 待开始 | - |
| 8 | 前端主页添加用户信息显示 | 0.2天 | ⏳ 待开始 | 7 |
| 9 | 审计日志完善 | 0.2天 | ⏳ 待开始 | 4,5,6 |
| 10 | 用户管理界面（可选） | 0.5天 | ⏳ 待开始 | 全部 |
| 11 | 功能测试 | 0.2天 | ⏳ 待开始 | 全部 |
| 12 | 部署上线 | 0.1天 | ⏳ 待开始 | 全部 |

**总计：** 约 2 天

### 5.2 依赖包更新

在 `requirements.txt` 中新增：

```
Flask-Login>=0.6.3
Flask-Session>=0.5.0
werkzeug>=2.3.0
```

---

## 6. 测试验证

### 6.1 功能测试清单

| 测试项 | 测试步骤 | 预期结果 |
|--------|---------|---------|
| 未登录访问 | 直接访问主页 | 重定向到登录页 |
| 正确登录 | 输入 admin / Admin@2026 | 登录成功，跳转主页 |
| 错误密码 | 输入 admin / wrong | 显示"用户名或密码错误" |
| 记住登录 | 勾选"记住登录"并登录 | 关闭浏览器后重新打开仍保持登录 |
| 权限控制（管理员） | 管理员访问 /api/cache/stats | 正常返回数据 |
| 权限控制（普通用户） | 普通用户访问 /api/cache/stats | 返回 403 错误 |
| 前端权限显示 | 普通用户登录 | "缓存管理"按钮隐藏 |
| 注销登录 | 点击"退出"按钮 | 清除 Session，跳转登录页 |
| 暴力破解防护 | 连续 5 次错误密码 | 账号锁定 15 分钟 |
| 审计日志 | 执行登录、清缓存等操作 | 审计日志正确记录 |

### 6.2 安全测试

- [ ] 密码是否加密存储（检查 users.json 中无明文密码）
- [ ] Session 是否设置过期时间
- [ ] HTTPS 是否启用（生产环境）
- [ ] 审计日志是否记录用户信息
- [ ] 暴力破解防护是否生效

---

## 7. 部署方案

### 7.1 部署步骤

1. **上传新文件**
   - auth.py
   - decorators.py
   - templates/login.html
   - 更新 server.py
   - 更新 templates/index.html
   - 更新 requirements.txt

2. **安装依赖**
   ```bash
   pip install -r requirements.txt
   ```

3. **初始化用户数据**
   - 首次运行会自动创建 users.json
   - 默认管理员账号：admin / Admin@2026

4. **重启服务**
   ```bash
   pkill -f "python.*server.py"
   cd /opt/troubleshoot/web && nohup python server.py > /tmp/troubleshoot.log 2>&1 &
   ```

5. **验证**
   - 访问 http://192.168.5.60:8088
   - 应自动跳转到登录页
   - 使用默认账号登录

### 7.2 部署后验证

```bash
# 1. 检查服务是否启动
curl http://localhost:8088/login

# 2. 测试登录
curl -X POST http://localhost:8088/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"Admin@2026"}' \
  -c cookies.txt

# 3. 测试访问主页
curl http://localhost:8088/ -b cookies.txt
```

---

## 8. 注意事项

1. **默认密码修改**：首次部署后立即修改 admin 密码
2. **HTTPS 部署**：生产环境建议启用 HTTPS（使用 Nginx 反向代理）
3. **Session 存储**：当前使用文件系统存储，用户量大时建议改为 Redis
4. **密码策略**：建议要求密码包含大小写字母、数字、特殊字符
5. **定期审计**：定期检查审计日志，发现异常登录行为

---

## 9. 后续优化

- [ ] 支持 LDAP/AD 域认证
- [ ] 双因素认证（2FA）
- [ ] 登录验证码（防止机器人）
- [ ] 密码强度检测
- [ ] 用户自助修改密码
- [ ] 登录历史查看（用户侧）
- [ ] IP 白名单限制
- [ ] SSO 单点登录

---

## 10. 相关文档

- 需求文档：`Docs/PRD/问题知识库/_PRD_Troubleshoot助手登录认证_需求文档.md`
- 部署报告：待生成