From e88d472c63e96e98ee0ff74eca3b0ee1998a65ba Mon Sep 17 00:00:00 2001 From: qinglong Date: Sat, 13 Jun 2026 14:14:34 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20API=20=E8=AE=A4=E8=AF=81=E4=B8=8E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=AE=BF=E9=97=AE=E5=85=A8=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API_Auth_Flow.md | 286 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 docs/API_Auth_Flow.md diff --git a/docs/API_Auth_Flow.md b/docs/API_Auth_Flow.md new file mode 100644 index 0000000..2b0cdbe --- /dev/null +++ b/docs/API_Auth_Flow.md @@ -0,0 +1,286 @@ +# SenSu API 认证与数据访问流程 + +> 版本: v0.7.0+ +> 更新: 2026-06-13 + +--- + +## 一、概述 + +所有 API 端点(除 `/health` 和 `/SenSu/api/login`)均需要认证。认证基于 **Panel Session Token** 机制: + +- 登录后获取 32 字符随机 token +- 后续请求通过 Cookie (`panel_token`) 或 WebSocket URL 参数 (`?token=`) 传递 +- 写操作额外需要 `X-CSRF-Token` 请求头 + +--- + +## 二、登录流程 + +``` +POST /SenSu/api/login +Content-Type: application/json + +{"username": "admin", "password": "admin"} +``` + +### 处理步骤 + +| 步骤 | 说明 | 代码位置 | +|------|------|---------| +| 1 | **频率检查** — IP 是否因 5 次失败被锁定 60s | `auth.py:_check_rate_limit()` | +| 2 | **哈希比较** — `SHA256(password + salt) == stored_hash` | `manager.py:_hash_pw()` | +| 3 | **生成 token** — `secrets.token_hex(16)` → 32 字符 | `auth.py:handle_login()` | +| 4 | **存入内存** — `PANEL_SESSION_STORE[token] = {username, perms, login_time}` | 同上 | +| 5 | **持久化到 DB** — `sensu_db.config_kv['panel_sessions']` JSON | `auth.py:_save_sessions_to_db()` | +| 6 | **设置 Cookie** — `panel_token={token}; HttpOnly; Max-Age=259200; SameSite=Lax` | 同上 | + +### 响应 + +```json +{"success": true, "username": "admin"} +``` + +### 安全特性 + +- 密码使用 `secrets.compare_digest()` 时序安全比较 +- 每用户独立随机盐 `secrets.token_hex(16)` +- 5 次/IP 失败后锁定 60 秒,返回 `429 Too Many Requests` +- Cookie 设置 `HttpOnly` (JS 不可读) + `SameSite=Lax` + +--- + +## 三、HTTP API 请求(读操作) + +``` +GET /SenSu/api/files/list +Cookie: panel_token=f93f18546742b6c4... +``` + +### 鉴权流程 (`panel_auth()`) + +``` +request + │ + ├─ 1. 从 Cookie 取 panel_token + │ (或 Authorization: Bearer xxx header) + │ + ├─ 2. 查 PANEL_SESSION_STORE[token] + │ ├─ 找到 → 注入 request['user'] + │ └─ 未找到 → 401 {"error": "未认证或会话已过期"} + │ + └─ 3. handler(request) → JSON 响应 +``` + +### 受保护的读端点 + +| 端点 | 说明 | +|------|------| +| `GET /SenSu/api/files/list` | 文件列表 | +| `GET /SenSu/api/files/info` | 文件信息 | +| `GET /SenSu/api/files/read` | 读取文本 | +| `GET /SenSu/api/files/download` | 下载文件 | +| `GET /SenSu/api/files/picker` | 文件选择器 | +| `GET /SenSu/api/projects` | 项目列表 | +| `GET /SenSu/api/projects/{name}/logs` | 项目日志 | +| `GET /SenSu/api/proxy` | 代理列表 | +| `GET /SenSu/api/system` | 系统状态 | +| `GET /SenSu/api/framework` | 框架状态 | +| `GET /SenSu/api/plugins` | 插件列表 | +| `GET /SenSu/api/commands` | 命令列表 | +| `GET /SenSu/api/auth/status` | 认证状态 | +| `GET /plugin/{name}` | 插件页面 | +| `GET /plugin/{name}/sse` | 插件 SSE | + +--- + +## 四、HTTP API 请求(写操作 — CSRF 保护) + +``` +POST /SenSu/api/files/delete +Cookie: panel_token=f93f18546742b6c4... +X-CSRF-Token: f93f18546742b6c4... +Content-Type: application/json + +{"path": "/some/file"} +``` + +### 鉴权流程 (`panel_auth(csrf_protect=True)`) + +``` +request + │ + ├─ 1-2. 同上 (token 验证) + │ + ├─ 3. CSRF 检查 + │ X-CSRF-Token header == panel_token cookie ? + │ ├─ 匹配 → 继续 + │ └─ 不匹配 → 403 {"error": "CSRF 验证失败"} + │ + └─ 4. handler(request) → JSON 响应 +``` + +### CSRF 保护的写端点 + +| 端点 | 说明 | +|------|------| +| `POST /SenSu/api/files/delete` | 删除文件/目录 | +| `POST /SenSu/api/files/write` | 写入文件 | +| `POST /SenSu/api/files/mkdir` | 创建目录 | +| `POST /SenSu/api/files/touch` | 创建文件 | +| `POST /SenSu/api/files/rename` | 重命名 | +| `POST /SenSu/api/files/upload` | 上传文件 | +| `POST /SenSu/api/projects/run` | 启动项目 | +| `POST /SenSu/api/projects/{name}/stop` | 停止项目 | +| `POST /SenSu/api/projects/{name}/stdin` | 向项目发送输入 | +| `POST /SenSu/api/proxy` | 添加代理 | +| `DELETE /SenSu/api/proxy/{path}` | 删除代理 | + +--- + +## 五、WebSocket 连接 + +``` +GET /SenSu/api/system/ws?token=f93f18546742b6c4... +Upgrade: websocket +``` + +### 鉴权流程 (`_ws_auth_wrapper`) + +``` +WebSocket 握手请求 + │ + ├─ 1. 从 query string 取 token + │ + ├─ 2. 查 PANEL_SESSION_STORE[token] + │ ├─ 找到 → 建立 WS 连接 → 每 2s 推送 + │ └─ 未找到 → {"error":"Unauthorized"} → close(4001) + │ + └─ 3. 连接建立后持续推送 + {"type":"sys", "system":{cpu,memory,network}, "framework":{...}} +``` + +### 前端调用方式 + +```javascript +// app.js 提供的工具函数 +function getCookie(name) { + var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + return match ? match[2] : ''; +} + +// 仪表盘 WebSocket +var tok = getCookie("panel_token"); +var ws = new WebSocket("ws://" + location.host + base + "/api/system/ws?token=" + (tok || "")); + +// 日志 WebSocket +var ws = new WebSocket("ws://" + location.host + base + "/api/logs/ws?token=" + (tok || "")); +``` + +### WebSocket 端点 + +| 端点 | 鉴权方式 | 用途 | +|------|---------|------| +| `/SenSu/api/system/ws?token=` | query param | 系统监控实时推送 | +| `/SenSu/api/logs/ws` | Cookie (同源自动带) | 日志实时推送 | + +--- + +## 六、插件路由(双层鉴权) + +插件通过 `PluginNetworkBridge.register_http_route()` 注册的路由使用双层鉴权: + +``` +GET /example_plugin/api/example/info +Cookie: panel_token=f93f18546742b6c4... +``` + +### 鉴权流程 (`_check_plugin_auth()`) + +``` +request + │ + ├─ 第1层: 用户身份验证 + │ ├─ 从 Cookie 取 panel_token + │ ├─ (或 Authorization: Bearer xxx header) + │ ├─ 查 PANEL_SESSION_STORE + │ └─ 失败 → 403 {"reason": "未认证"} + │ + ├─ 第2层: 插件权限检查 + │ ├─ 查 permission_service + │ ├─ 插件是否有 plugin.network.access ? + │ └─ 失败 → 403 {"reason": "插件没有网络访问权限"} + │ + └─ handler(request) → 响应 +``` + +### 插件命令 REST 端点(自动暴露) + +``` +POST /example_plugin/api/plugin/echo +Content-Type: application/json + +{"args": ["hello", "world"]} +``` + +每个 `@plugin_command` 方法自动生成 REST 端点: +- 路径: `POST /api/plugin/{command_name}` +- 请求体: `{"args": [...], "kwargs": {...}}` +- 响应: `{"ok": true, "result": "..."}` + +--- + +## 七、公开端点(无需认证) + +| 端点 | 说明 | 安全措施 | +|------|------|---------| +| `GET /health` | 健康检查 | 仅返回 `{"status":"healthy"}` | +| `POST /SenSu/api/login` | 登录 | 频率限制 + 哈希比较 | + +--- + +## 八、安全响应头 + +所有响应自动添加: + +| Header | 值 | +|--------|-----| +| `X-Content-Type-Options` | `nosniff` | +| `X-Frame-Options` | `DENY` | +| `X-XSS-Protection` | `1; mode=block` | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | + +--- + +## 九、错误响应格式 + +### 认证失败 +```json +{"error": "未认证或会话已过期", "status": 401} +``` + +### CSRF 失败 +```json +{"error": "CSRF 验证失败", "status": 403} +``` + +### 频率限制 +```json +{"success": false, "msg": "尝试次数过多,请 60 秒后重试"} +``` +HTTP 状态码: `429` + +### 服务器错误(脱敏) +```json +{"error": "Internal server error"} +``` +详细信息仅写入日志,不返回客户端。 + +--- + +## 十、会话持久化 + +- 登录时 session 写入 `sensu.db` → `config_kv` 表 +- 框架重启时从 DB 恢复: `auth.py:_load_sessions_from_db()` +- 登出时从 DB 清除 +- Token 有效期: Cookie `max-age=259200` (3 天)