docs: API 认证与数据访问全流程文档
This commit is contained in:
@@ -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 天)
|
||||
Reference in New Issue
Block a user