Compare commits

...
2 Commits
Author SHA1 Message Date
qing e89578fa82 security: Token 改为 HttpOnly Cookie 下发,消除 XSS 窃取面
登录时网关签发 HttpOnly+Secure+SameSite=Lax 的 q4_token Cookie,前端不再经手 Token;网关代理时支持从 Cookie 验签并向下游注入 Authorization(dnfs 零改动),新增 /logout 清除 Cookie,static 代理路由兼容 Cookie 转 Bearer。
2026-07-28 09:07:12 +08:00
qing 65cfaf97a1 docs: 按代码实况修正根 README 与实现不符的描述
db 实为 pymsg/TCP(非 HTTP),ai_review 实为 fail-closed,captcha 路由与实现方式、static 的 dnfs 代理与账号旁路、gateway 用户存储与 core 守护机制等均按代码核实结果更正。
2026-07-28 08:33:30 +08:00
5 changed files with 144 additions and 67 deletions

No files matched your search

+42 -39
View File
@@ -18,25 +18,24 @@
│ waf │ │ gateway │ │ static │ │ dnfs │ │ db │ │ ai_review │
│ WAF 网关 │ │ 应用层网关 │ │前端托管│ │ 树状菜单 │ │ 通用数据库 │ │ AI内容审核 │
│ :443 (入口) │ │ :8443 (内部) │ │ :9200 │ │ :8006 │ │ :8005 │ │ :8007 │
│ coraza+反代 │ │ 鉴权 + 反代 │ │静态/媒体│ │ 节点CRUD │ │ SQL+日志收集 │ │ DeepSeek+RAG │
└──────┬───────┘ └──────┬───────┘ └────────┘ └──────┬───────┘ └──────────────┘ └──────────────┘
│ HTTPS终止 │ 反向代理 │ 调用
└──────────────────►│ ├──────────► db:8005
├────────────────► static:9200
└────────────────► dnfs:8006 ──► ai_review:8007
日志链路: waf / gateway / static / dnfs / ai_review / captcha / smtp / aibox ──(log_client)──► db:8005/log/ingest
│ coraza+反代 │ │ 鉴权 + 反代 │ │静态/媒体│ │ 节点CRUD │ │ pymsg/TCP │ │ DeepSeek+RAG │
└──────┬───────┘ └──────┬───────┘ └───┬────┘ └──────┬───────┘ └──────▲───────┘ └──────▲───────┘
│ HTTPS终止 │ 反向代理 │ 代理笔记请求 │ pymsg │ pymsg │ HTTP
└──────────────────►│ ├─────────────►│─────────────────┘ │
├────────────►│ └─────── HTTP /review ──────────────┘
└────────────►│
日志链路: waf / gateway / static / dnfs / ai_review / captcha / smtp / aibox ──(log_client, pymsg)──► db:8005 (Logs 表)
```
### 请求链路
1. 客户端所有请求先打到 `waf:443`(HTTPS),WAF 终止 TLS、运行 coraza 规则匹配,命中则直接返回 403。
2. 未命中规则的请求由 WAF 以明文 HTTP 转发到 `gateway:8443`,并设置 `X-Forwarded-For` / `X-Forwarded-Proto` 头。
3. `gateway` 对 `/login`、`/register` 做账号鉴权(SQLite + PBKDF2 密码哈希 + RSA 签名 Token + 可选 CAPTCHA + 速率限制)。
3. `gateway` 对 `/login`、`/register` 做账号鉴权(PBKDF2 密码哈希 + RSA 签名 Token + 可选 CAPTCHA + 速率限制),用户数据通过 pymsg/TCP 调用 `db:8005` 存取。
4. 其余请求携带 Token,经 `gateway` 校验后反向代理到 `Proxy.opened` 中放开的内部服务(`static:9200` / `dnfs:8006`)。
5. `db:8005` 与 `ai_review:8007` 在 `Proxy.closed` 中,仅允许内部服务间调用,不对外暴露。
6. `dnfs` 处理节点增删改查时,通过 HTTP 调用 `db` 服务完成数据持久化,新增节点前调用 `ai_review` 进行内容审核。
7. 各服务通过 `log_client.py` SDK 异步批量推送运行日志到 `db:8005/log/ingest`,实现集中式日志收集(fail-safe,db 不可用时静默丢弃)。
6. `dnfs` 处理节点增删改查时,通过 pymsg/TCP 调用 `db` 服务完成数据持久化,新增节点前调用 `ai_review` 进行内容审核。
7. 各服务通过 `log_client.py` SDK 异步批量推送运行日志(pymsg/TCP 写入 db 的 `Logs` 表),实现集中式日志收集(fail-safe,db 不可用时静默丢弃)。
## 各服务说明
@@ -47,15 +46,15 @@
| [gateway](gateway/) | C++20 | 8443 | 应用层网关(内部 HTTP),用户注册/登录鉴权、Token 签发与校验、反向代理到内部服务 |
| [static](static/) | Python(Flask) | 9200 | 前端托管服务,社区首页、静态页面与文件浏览、视频/音频流媒体播放、路径遍历防护 |
| [dnfs](dnfs/) | Python(Flask) | 8006 | 树状网络文件系统(Dendrachy Network File-System),基于父子 pid 的节点树增删改查,集成 AI 内容审核 |
| [db](db/) | Python(Flask) | 8005 | 通用数据库服务,基于 SQLAlchemy Core 的 SQL-over-HTTP,支持 select/insert/update/delete,参数化查询防注入;内置集中式日志收集(/log/* 路由) |
| [db](db/) | Python(pymsg) | 8005 | 通用数据库服务,基于 pymsg 协议(TCP 长度前缀帧 + JSON)的 SQL-over-TCP,SQLAlchemy Core 参数化查询防注入;内置集中式日志收集(Logs 表 + 自动清理) |
| [ai_review](ai_review/) | Python(Flask) | 8007 | AI 内容审核服务,DeepSeek LLM + ChromaDB RAG,支持人工反馈纠偏 |
| [captcha](captcha/) | Python(Flask) | 8009 | 验证码服务,生成图形验证码(5 分钟过期、一次性),供 gateway 登录/注册可选启用 CAPTCHA 校验 |
| [captcha](captcha/) | Python(stdlib+PIL) | 8009 | 验证码服务,生成图形验证码(5 分钟过期、一次性),供 gateway 登录/注册可选启用 CAPTCHA 校验 |
| [smtp](smtp/) | Python(Flask) | 8008 | 邮件服务,发送/校验邮箱验证码(/send_email、/verify_email),注册流程的邮件验证码后端,SMTP 账号配置在 .env |
| [aibox](aibox/) | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度,由 core 拉起但与主干微服务无调用关系 |
### core(核心模块 · 进程托管服务)
- 可执行文件 `nidus.out`,作为 systemd 服务(`Nidus.service`)常驻运行。
- 可执行文件 `nidus.out`,作为 systemd 服务(`Nidus.service`)常驻运行(崩溃守护由 systemd 负责,core 自身不含自动重启逻辑)。
- 支持两种工作模式:命令行子系统调用(`--send/--exec/--list/--kill`)与 HTTP 伺服器模式(`--serve <port>`)。
- 通过命名管道(Pipe)与消息队列(Mq)实现进程间通信。
- 读取 `init.txt` 自启动列表,按行拉起各业务服务(ai_review / db / dnfs / smtp / static / waf / gateway / captcha / aibox)。
@@ -74,7 +73,8 @@
- 内部 HTTP 服务,默认监听 127.0.0.1:8443,不再承载 TLS(TLS 已由 WAF 终止)。
- 通过 `Server.tls = on/off` 配置切换 HTTPS/HTTP 模式([example.ini](gateway/example.ini)),独立部署时仍可启用 TLS。
- **密码安全**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节随机盐),密码强度校验(8+ 字符、4 类中至少 3 类),登录时自动迁移旧 SHA256 哈希,常数时间比较防 timing attack。
- **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期,验签通过才放行。
- **用户存储**:用户数据通过 pymsg/TCP 调用 `db:8005`(`DbC::call_msg`)存取,不再直连本地 SQLite。
- **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期,验签通过才放行;RSA 密钥对启动时内存生成,重启后旧 Token 全部失效。
- **CAPTCHA**:`/captcha` 接口生成验证码(5 分钟过期、一次性使用),登录与注册均可选启用。
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟,按 `X-Forwarded-For`(WAF 注入)客户端 IP 计数。
- **反向代理**:`Target-Url` 必须在 `Proxy.opened` 白名单中(`closed` 仅记录用途,实际隔离靠"不在 opened 中即拒绝"),缺省走 `Proxy.default`(不需 Token,服务前端静态资源)。
@@ -84,48 +84,50 @@
### static(前端托管服务)
- 托管 `public/` 下的前端页面:社区首页(`index.html`)、登录/注册、树状笔记(`dnfs.html`)、文件浏览(`__view.html`)等。
- 支持文件浏览、视频/音频在线播放(HTTP Range 分段请求)、一键下载。
- 路径遍历防护,仅允许访问 `directory.root` 之内。
- 支持文件浏览、视频/音频在线播放(HTTP Range 分段请求,返回 206/416)、一键下载。
- 路径遍历防护(`safe_path_join`:realpath 前缀校验),仅允许访问 `directory.root` 之内。
- **dnfs 代理**:`/get_notes`、`/add_note`、`/delete_note`、`/update_note` 透传 Authorization 转发到 `dnfs:8006`(解决浏览器跨域)。
- **账号旁路**:`/api/check_user`、`/api/reset_password` 等接口直接读写 `gateway/sqlite3.db` 的 Users 表;`/yhdet_callback`、`/api/yhdet_*` 对接 yhdet.top OAuth 登录。
- 统一品牌"树状菜单",支持深色模式(`css/theme.css`)。
- 可使用 Nuitka 编译为独立二进制(`fsys.bin`)。
- 配置见 [static/config.ini](static/config.ini)。
- 配置见 [static/config.ini](static/config.ini)(注:CORS 白名单硬编码于 `fsys.py`,config.ini 的 cors 段未生效)。
### dnfs(树状菜单服务)
- 基于 `pid`(父节点 id)组织的树状节点,节点含 `title` 与 `content`。
- 提供 `/get_notes`、`/add_note`、`/delete_note`、`/update_note` 四个接口。
- 数据持久化通过 HTTP 调用 `db` 服务(`http://127.0.0.1:8005/execute`),不再直接操作数据库。
- 数据持久化通过 pymsg/TCP 调用 `db` 服务(`127.0.0.1:8005`,execute 路由),不再直接操作数据库。
- 新增节点前调用 `ai_review` 服务(`http://127.0.0.1:8007/review`)进行内容审核,审核不通过则拒绝写入。
### db(通用数据库服务)
- 单一接口 `POST /execute`,接收结构化 JSON(action + table + where + values),翻译为 SQL 执行。
- 基于自研 [pymsg](pymsg/) 协议(TCP 长度前缀帧 + JSON),单一路由 `_route: "execute"`,接收结构化 JSON(action + table + where + values),翻译为 SQL 执行。
- 支持 select / insert / update / delete,运算符包括 eq、ne、gt、lt、ge、le、like、in,支持排序与分页。
- 基于 SQLAlchemy Core 表反射,参数化查询,零 SQL 注入风险。
- 仅监听 127.0.0.1,单线程,自动回滚,不暴露 DDL 操作。
- 仅监听 127.0.0.1,自动提交/回滚,不暴露 DDL 操作。
- 默认使用 SQLite(`database.db`),可通过 `DATABASE_URL` 环境变量切换至 PostgreSQL / MySQL。
- **内置日志子模块**([log_module.py](db/log_module.py)):
- `log_internal()` — db 进程内部直接写 Logs 表,无 HTTP 往返,防递归设计。
- `/log/ingest` — 接收外部服务批量推送日志(单次上限 100 条,校验 service / level / message)。
- `/log/query` — 结构化查询(service / level / trace_id / 时间区间 / 关键字模糊搜索 / 排序分页)。
- `/log/stats` — 按 service / level 分组统计。
- `/log/purge` — 手动清理指定时间之前的日志。
- `/log/health` — 健康检查。
- **内置日志子模块**:
- 启动时自动建 `Logs` 表(service / level / message / trace_id / extra + 4 个索引)。
- 外部服务通过 `log_client.py` 以 pymsg 调 execute 路由逐条 INSERT 写入日志。
- `_log_internal()` — db 进程内部直接写 Logs 表,无网络往返,防递归设计。
- 后台守护线程每小时自动清理:保留 30 天 / 上限 100 万条。
- **统一日志客户端 SDK**([log_client.py](db/log_client.py)):各服务复制即用,纯标准库实现(urllib),异步非阻塞(内存队列 + 后台线程,50 条/2 秒批量 flush),fail-safe 设计(db 不可用时静默丢弃,不影响业务)。
- 注:`db/log_module.py`(Flask `/log/*` Blueprint)为旧版 HTTP 接口的遗留文件,当前 dbv2 未挂载。
- **统一日志客户端 SDK**(各服务目录下的 `log_client.py`,如 [dnfs/log_client.py](dnfs/log_client.py)):各服务复制即用,纯标准库实现,异步非阻塞(内存队列 + 后台线程,50 条/2 秒批量 flush),fail-safe 设计(db 不可用时静默丢弃,不影响业务)。
### ai_review(AI 内容审核服务)
- 基于 DeepSeek LLM + ChromaDB 向量检索的 RAG 审核架构。
- 审核类别:色情、违禁药品、赌博、诈骗、仇恨/暴力/恐怖主义,灰色地带内容从严判定。
- 接口:`POST /review`(提交审核)、`POST /rag/feedback`(人工纠偏)、`GET /rag/list`(知识库列表)、`GET /health`(健康检查)。
- RAG 检索 top-3 相似已标注样本注入 prompt,MD5 去重,API 异常时 fail-open。
- RAG 检索 top-3 相似已标注样本注入 prompt,MD5 去重。
- **fail-closed 设计**:API 异常或响应解析失败时一律返回 `{"invalid": "true"}`(拦截),宁可误拦不可漏放。
- 配置通过 `.env` 文件(已 gitignore),含 DeepSeek API 密钥、模型参数、监听地址。
### captcha(验证码服务)
- 生成图形验证码,Token 一次性使用、5 分钟过期。
- 接口:`GET /captcha`(生成)、`POST /verify`(校验)、`POST /new`(刷新);答案错误返回 `200 {valid:false}`(Token 照常消费),仅参数 / Token 错误返回 `400`。
- 生成图形验证码,Token 一次性使用、5 分钟过期;纯标准库实现(http.server + PIL,无 Flask)。
- 接口:`GET /captcha/image`(生成 PNG,Token 在 `X-Captcha-Token` 响应头)、`POST /captcha/verify`(校验,body 含 token/answer)、`GET /health`。
- 答案错误返回 `200 {valid:false}`(Token 照常消费),仅参数 / Token 错误返回 `400`;比较采用 XOR 防时序攻击。
- 由 `gateway` 在 `/login`、`/register` 中可选启用 CAPTCHA 校验(字段 `captcha_token` / `captcha_answer`)。
- 内存存储,不支持多实例水平扩展(需换 Redis);详见 [captcha/README.md](captcha/README.md)。
@@ -154,8 +156,9 @@
| 文件 | 说明 |
|------|------|
| [log_client.py](db/log_client.py) | 统一日志客户端 SDK,各服务复制即用(纯标准库,异步批量推送) |
| [security_middleware.py](db/security_middleware.py) | Flask 安全响应头中间件(CSP / X-Frame-Options / nosniff 等),各服务各持一份副本 |
| [pymsg](pymsg/) | 自研 RPC 基础库(setuptools 包),协议为 `[4字节大端长度][UTF-8 JSON]`,提供 `send/recv/MsgClient/MsgServer`;db 服务端与各 log_client 的基础依赖,requirements 以 `../pymsg` 本地路径引用 |
| log_client.py | 统一日志客户端 SDK,各服务目录各持一份副本(如 [dnfs/log_client.py](dnfs/log_client.py)),纯标准库 + pymsg,异步批量推送 |
| security_middleware.py | Flask 安全响应头中间件(CSP / X-Frame-Options / nosniff 等),各服务各持一份副本 |
## 端口一览
@@ -166,9 +169,9 @@
| 5200 | core | — | PMC HTTP 管理接口 |
| 9200 | static | 127.0.0.1 | 前端托管(经 gateway 代理) |
| 8006 | dnfs | 127.0.0.1 | 树状菜单(经 gateway 代理) |
| 8005 | db | 127.0.0.1 | 数据库服务(仅内部调用) |
| 8005 | db | 127.0.0.1 | 数据库服务(pymsg/TCP,仅内部调用) |
| 8007 | ai_review | 127.0.0.1 | AI 审核(仅内部调用) |
| 8009 | captcha | 127.0.0.1 | 验证码服务(独立组件) |
| 8009 | captcha | 127.0.0.1 | 验证码服务(stdlib http.server,独立组件) |
| 8008 | smtp | 127.0.0.1 | 邮件服务(仅内部调用,注册验证码邮件后端) |
| 8765 | aibox | 127.0.0.1 | AI 智能体(WebSocket,独立产品) |
@@ -249,10 +252,10 @@ gateway 改为内部 HTTP 模式([example.ini](gateway/example.ini) 中 `tls =
- **服务按业务能力拆分**:WAF 网关、应用网关、前端托管、节点树、数据库、内容审核各自独立、独立端口、独立数据存储。
- **API 网关模式**:`waf` 作为边缘网关统一入口,`gateway` 承担鉴权与反向代理,内部服务通过 `closed` 列表隔离。
- **进程编排/守护**:`core` 作为服务管理器拉起并守护子进程,类似简易版 supervisor。
- **多语言异构**:C++(core / gateway)、Python(static / dnfs / db / ai_review)、Go(waf)混合,通过 HTTP 通信。
- **数据访问层解耦**:`db` 服务将数据库操作收敛为统一的 HTTP 接口,业务服务无需直连数据库。
- **多语言异构**:C++(core / gateway)、Python(static / dnfs / db / ai_review)、Go(waf)混合,对外 HTTP、内部数据访问走 pymsg/TCP。
- **数据访问层解耦**:`db` 服务将数据库操作收敛为统一的 pymsg/TCP 接口,业务服务无需直连数据库。
- **AI 能力集成**:`ai_review` 以 RAG 增强的 LLM 审核为业务层提供内容安全能力,支持人工反馈闭环。
- **集中式可观测性**:`db` 内置日志收集服务(/log/* 路由),各服务通过 `log_client.py` SDK 异步推送日志,实现全链路汇聚、查询与统计,无需引入额外中间件。
- **集中式可观测性**:`db` 内置日志表与自动清理,各服务通过 `log_client.py` SDK 异步推送日志(pymsg),实现全链路汇聚,无需引入额外中间件。
- **安全分层**:`waf` 在边缘做规则匹配(OWASP CRS / 自定义 SecLang 规则),`gateway` 在应用层做鉴权,`db` 仅监听本机,多层防护。
与云原生微服务的差异:未引入服务注册发现、容器编排、消息总线,服务地址在 `waf/config.yaml` 与 `gateway/example.ini` 中静态配置。对于个人/中小规模的 DIY 场景,这种取舍在简洁性与可控性上是合理的。
+79 -3
View File
@@ -26,7 +26,7 @@ using namespace qing;
namespace json = boost::json;
namespace po = boost::program_options;
constexpr const char *_VER = "0.4.0";
constexpr const char *_VER = "0.4.1";
//////////////////////////////////
#include <sstream>
@@ -291,6 +291,13 @@ http::response<http::string_body> handleCaptcha(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params);
http::response<http::string_body> handleLogout(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params);
/* 从 Cookie 头中解析指定名称的值(找不到返回空串) */
std::string get_cookie_value(const std::string& cookie_header, const std::string& name);
/* headers can't processed */
bool is_special_header(const std::string &header);
@@ -532,6 +539,11 @@ void registerRoutes(HttpServer &server) {
[](const auto& req, const auto& params) { return handleRegister(req, params); });
});
server.post("/logout", [](const auto& req, const auto& params) -> http::response<http::string_body> {
return wrapperHttp(req, params,
[](const auto& req, const auto& params) { return handleLogout(req, params); });
});
server.get("*", [](const auto& req, const auto& params) -> http::response<http::string_body> {
return wrapperHttp(req, params,
[](const auto& req, const auto& params) { return handleProxy(req, params, "get"); });
@@ -776,12 +788,38 @@ http::response<http::string_body> handleLogin(
}
};
res.set("Connection", "close");
/* 签发 HttpOnly Cookie(防 XSS 窃取 Token;JS 不再经手 Token) */
{
std::string ck = "q4_token=" + token +
"; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=" +
std::to_string(_TOKEN_KEEP_TIME);
res.set("Set-Cookie", ck);
}
res.result(http::status::ok);
res.body() = json::serialize(jv_rr);
res.prepare_payload();
return res;
}
/* 退出登录 /logout:清除 q4_token Cookie */
http::response<http::string_body> handleLogout(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params)
{
http::response<http::string_body> res;
res.version(req.version());
res.set("Set-Cookie",
"q4_token=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0");
res.set("Connection", "close");
res.result(http::status::ok);
res.body() = R"({"success": true, "message": "logged out"})";
res.prepare_payload();
return res;
}
/* 处理注册 /register */
http::response<http::string_body> handleRegister(
const http::request<http::string_body>& req,
@@ -878,6 +916,7 @@ http::response<http::string_body> handleProxy(
std::string t_url = "";
std::string url = "";
std::string auth = "";
std::string cookie_token = ""; /* 从 Cookie 提取的 Token(用于向下游注入 Authorization) */
json::value jv_t;
{
auto flag = false;
@@ -912,10 +951,21 @@ http::response<http::string_body> handleProxy(
url += "/";
url += target_path;
std::string raw = "";
it = req.base().find("Authorization");
if (it == req.base().end() && !flag)
if (it != req.base().end()) {
raw = std::string(it->value());
} else {
/* 回退:从 HttpOnly Cookie 读取 q4_token(浏览器同源请求自动携带) */
auto cit = req.base().find("Cookie");
if (cit != req.base().end()) {
cookie_token = get_cookie_value(std::string(cit->value()), "q4_token");
if (!cookie_token.empty())
raw = "Bearer " + cookie_token;
}
}
if (raw.empty() && !flag)
THROW_EX_CUSTOM(HttpException, 2301, "Unauthorization", 401, "*");
std::string raw = req["Authorization"];
if (raw.substr(0, 7) == "Bearer ") {
raw = raw.substr(7, raw.size() - 7);
size_t idx = raw.find("_");
@@ -996,6 +1046,11 @@ http::response<http::string_body> handleProxy(
client.set_header("X-Forwarded-For: unknown");
}
/* Token 来自 Cookie 时,向下游补齐 Authorization 头(dnfs 等下游仍按 Bearer 解析,无需改动) */
if (!cookie_token.empty()) {
client.set_header("Authorization: Bearer " + cookie_token);
}
/* 转发请求 */
if (method == "get")
client.get_stream_sync(url, callback);
@@ -1031,6 +1086,27 @@ bool is_special_header(const std::string &header) {
|| header == "Forwarded";
}
std::string get_cookie_value(const std::string& cookie_header, const std::string& name) {
// Cookie 头格式:"k1=v1; k2=v2; q4_token=xxx"
size_t pos = 0;
while (pos <= cookie_header.size()) {
size_t end = cookie_header.find(';', pos);
if (end == std::string::npos) end = cookie_header.size();
std::string pair = cookie_header.substr(pos, end - pos);
size_t eq = pair.find('=');
if (eq != std::string::npos) {
std::string k = pair.substr(0, eq);
size_t b = k.find_first_not_of(" \t");
size_t e = k.find_last_not_of(" \t");
if (b != std::string::npos) k = k.substr(b, e - b + 1);
if (k == name) return pair.substr(eq + 1);
}
if (end == cookie_header.size()) break;
pos = end + 1;
}
return "";
}
bool is_safe_target(const std::string& target) {
// 请求路径安全性校验(P1-1)
// 防御目标:路径遍历、协议注入、空字节、非绝对路径形式
+4
View File
@@ -411,6 +411,7 @@ def proxy_get_notes():
data = request.get_json(force=True)
h = {"Content-Type": "application/json"}
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer
req = _urlreq.Request(f"{DNFS_URL}/get_notes", data=_json.dumps(data).encode(), headers=h, method="POST")
with _urlreq.urlopen(req, timeout=15) as r:
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
@@ -426,6 +427,7 @@ def proxy_add_note():
data = request.get_json(force=True)
h = {"Content-Type": "application/json"}
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer
req = _urlreq.Request(f"{DNFS_URL}/add_note", data=_json.dumps(data).encode(), headers=h, method="POST")
with _urlreq.urlopen(req, timeout=15) as r:
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
@@ -440,6 +442,7 @@ def proxy_delete_note():
data = request.get_json(force=True)
h = {"Content-Type": "application/json"}
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer
req = _urlreq.Request(f"{DNFS_URL}/delete_note", data=_json.dumps(data).encode(), headers=h, method="POST")
with _urlreq.urlopen(req, timeout=15) as r:
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
@@ -454,6 +457,7 @@ def proxy_update_note():
data = request.get_json(force=True)
h = {"Content-Type": "application/json"}
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer
req = _urlreq.Request(f"{DNFS_URL}/update_note", data=_json.dumps(data).encode(), headers=h, method="POST")
with _urlreq.urlopen(req, timeout=15) as r:
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
+18 -23
View File
@@ -1,11 +1,10 @@
// Session restore from localStorage
if(!sessionStorage.getItem('q4_token')){var t=localStorage.getItem('q4_token');if(t)sessionStorage.setItem('q4_token',t);}
// 认证改为 HttpOnly Cookie(gateway 下发 q4_token),浏览器同源请求自动携带,JS 不再经手 Token
/* by qing.
*
* 用于获取一个节点下的所有子节点信息,0为根节点。
*/
async function getNode(token, id) {
async function getNode(id) {
try {
obj = { "pid": id }
jsonBody = JSON.stringify(obj)
@@ -14,7 +13,6 @@ async function getNode(token, id) {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 告诉服务器内容为json
'Authorization': `Bearer ${token}`,
'Target-Url': 'http://127.0.0.1:8006'
},
body: jsonBody
@@ -54,7 +52,7 @@ async function getNode(token, id) {
*
* 用于向dnfs添加节点
*/
async function addNode(token, pid, title, content) {
async function addNode(pid, title, content) {
try {
obj = {
"pid": pid,
@@ -67,7 +65,6 @@ async function addNode(token, pid, title, content) {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 告诉服务器内容为json
'Authorization': `Bearer ${token}`,
'Target-Url': 'http://127.0.0.1:8006'
},
body: jsonBody
@@ -89,8 +86,8 @@ async function addNode(token, pid, title, content) {
input_note_title.value = "";
input_note_content.value = "";
getNode(token, stack[stack.length - 1]).then(load_notes);
getNode(token, stack[stack.length - 1]).then(loadMap)
getNode(stack[stack.length - 1]).then(load_notes);
getNode(stack[stack.length - 1]).then(loadMap)
//if (result.success) {
alert(`添加节点成功`, result);
@@ -108,7 +105,7 @@ async function addNode(token, pid, title, content) {
}
}
async function deleteNode(token, id) {
async function deleteNode(id) {
if (confirm("确认删除此节点?") == false) {
return;
}
@@ -121,7 +118,6 @@ async function deleteNode(token, id) {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'Target-Url': 'http://127.0.0.1:8006'
},
body: jsonBody
@@ -142,8 +138,8 @@ async function deleteNode(token, id) {
}
stack.pop();
getNode(token, stack[stack.length - 1]).then(load_notes);
getNode(token, stack[stack.length - 1]).then(loadMap)
getNode(stack[stack.length - 1]).then(load_notes);
getNode(stack[stack.length - 1]).then(loadMap)
console.log(`删除节点成功`);
return result
@@ -182,7 +178,7 @@ input_note_submit.addEventListener('click', function(event) {
var pid = (stack.length == 0) ? 0 : stack[stack.length - 1];
//alert(stack.length)
//alert(pid)
addNode(token, pid, title, content).then(result => {
addNode(pid, title, content).then(result => {
//alert(result.message);
})
})
@@ -199,7 +195,7 @@ delete_note.addEventListener('click', (event) => {
console.log("节点未删除");
}
else {
deleteNode(token, id);
deleteNode(id);
}
})
@@ -232,8 +228,8 @@ function load_notes(main_node) {
card.addEventListener('click', function(event) {
var id = Number(item.id)
stack.push(id)
getNode(token, id).then(load_notes)
getNode(token, id).then(loadMap)
getNode(id).then(load_notes)
getNode(id).then(loadMap)
});
let ttl = document.createElement('center');
let ttl1 =document.createElement('h3');
@@ -274,10 +270,10 @@ function loadMap(children_json) {
fold_div.addEventListener("click", () => {
if (is_expand == true) {
is_expand = false;
getNode(token, stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap);
getNode(stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap);
} else {
is_expand = true;
getNode(token, stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap);
getNode(stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap);
}
});
if (need_return == true) {
@@ -290,8 +286,8 @@ function loadMap(children_json) {
back.addEventListener("click", () => {
stack.pop();
var id = (stack.length >= 0) ? stack[stack.length - 1] : 0;
getNode(token, id).then(load_notes);
getNode(token, id).then(loadMap);
getNode(id).then(load_notes);
getNode(id).then(loadMap);
});
minimap.appendChild(back);
}
@@ -309,6 +305,5 @@ function loadMap(children_json) {
}
}
var token = sessionStorage.getItem('q4_token')
getNode(token, 0).then(load_notes);
getNode(token, 0).then(loadMap)
getNode(0).then(load_notes);
getNode(0).then(loadMap)
+1 -2
View File
@@ -76,8 +76,7 @@ form.addEventListener('submit', async function(e) {
try { result = JSON.parse(responseText); } catch { throw new Error('服务器响应格式异常'); }
if (!res.ok) throw new Error(result.message || '登录失败');
if (result.success) {
localStorage.setItem('q4_token', result.data.token);
sessionStorage.setItem('q4_token', result.data.token);
// Token 已由网关通过 HttpOnly Cookie 下发,JS 不再经手
alert('登录成功');
location.href = '/dnfs.html';
} else {