Files
SenSu/docs/API_Auth_Flow.md
qinglong ff693465f6 docs: README + API_Auth + Security_Plan + Plugin_Guide 更新至 v0.7.0
- README: 新功能列表、start.sh、架构图
- API_Auth_Flow: 新增 API Key 认证章节
- Security_Hardening_Plan: 标记全部完成
- Plugin_Dev_Guide: 新增插件导入安装章节

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 20:56:50 +08:00

315 lines
8.1 KiB
Markdown

# 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` | 登录 | 频率限制 + 哈希比较 |
---
## 十一、API Key 认证 (服务器间通信)
### 创建 API Key
在 WebUI "框架设置" → "API Key 管理" 中创建,支持三种权限模板:
| 模板 | 权限 |
|------|------|
| `readonly` | framework.status.read, plugin.info.read |
| `monitor` | + framework.event.subscribe |
| `full` | admin (全部) |
每个 Key 可选 TTL 过期时间,格式 `sk-` + 48 hex chars。
### 使用方式
```
GET /SenSu/api/system
Authorization: Bearer sk-xxxxxxxx...
```
### 验证流程
```
panel_auth() 拦截
├─ Cookie panel_token → Session Store
├─ 失败 → API Key Store (validate_api_key)
├─ 检查过期时间
└─ 注入 key 自身权限范围
```
## 八、安全响应头
所有响应自动添加:
| 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 天)