feat(ca): 认证独立化 CA 模块 v1.0.0 + gateway v0.9.0 纯转发瘦身

拓扑:waf:443 → ca:8443(认证+前置转发)→ gateway:8445(仅 loopback 纯转发),
WAF upstream 不变,core/dnfs2/前端零改动。

ca v1.0.0(新模块):
- 认证资产逐字搬迁自 gateway v0.8.0(9 条认证路由、Token 双轨吊销、
  PBKDF2、限流、WS 代理鉴权、wrapperHttp、优雅关闭)
- handleForward 前置转发层:固定上游 gateway:8445;无 Target-Url 公开直转,
  有则本地验签后注入 X-Auth-User/X-Auth-Admin/Authorization(strip-then-set)
- is_special_header 新增剥离 x-auth-user/x-auth-admin;上游不可达统一 502
- confs/configs/ca.ini 新增([Upstream] gateway + UPSTREAM_GATEWAY env)

gateway v0.9.0(2470→约 560 行):
- 删除全部认证代码,退居 127.0.0.1:8445
- 启动 loopback 强制自检(非 loopback 拒绝启动)
- X-Auth-User 存在性不变量:显式 Target-Url 无该头一律 401
- gateway.ini 精简至 [Server]/[Proxy]/[Downstream db]

验收:黑盒 34 断言对 8443 原样全绿(32 passed/2 skipped,与基线一致);
探针实测伪造 X-Auth-User 经 CA 被剥离(echo 实证)、直连 8445 无头 401、
白名单外 403、gateway 宕机 CA 502。core/init.txt 加入 ca/_run.sh。
This commit is contained in:
qing committed 2026-08-09 12:15:35 +00:00
1 parent bbea1ee9d8
commit 407ce3c366
19 files changed
+3991 -2523

No files matched your search

+28 -20
View File
@@ -2,7 +2,7 @@
# micro_servers # micro_servers
一套 DIY 的轻量级微服务架构(UniServe Framework),由 C++、Python、Go 与仓颉(Cangjie) 混合实现。 一套 DIY 的轻量级微服务架构(UniServe Framework),由 C++、Python、Go 与仓颉(Cangjie) 混合实现。
`core` 作为进程托管服务负责拉起并守护各业务子进程,`redirect80` 将 80 端口流量重定向到 443,`waf` 作为前置 WAF 网关终止 TLS 并做规则匹配,`gateway` 作为应用层网关统一鉴权与反向代理,`db` 提供通用数据库访问与集中式日志收集,`confs` 提供统一配置分发,`ai_review` 提供 AI 内容审核,`static` 与 `dnfs` 作为业务服务对外提供能力。`db2` / `dnfs2` 分别为 `db` / `dnfs` 的 C++20 重实现,接口完全兼容可平替。此外包含 aibox 的终端前端 `BotOps`、独立的机器人硬件控制模块 `bot4cj`、嵌入式帧缓冲图形界面 `lci` 和嵌入式脚本引擎 `libtry`。 `core` 作为进程托管服务负责拉起并守护各业务子进程,`redirect80` 将 80 端口流量重定向到 443,`waf` 作为前置 WAF 网关终止 TLS 并做规则匹配,`ca` 作为中央认证服务统一鉴权(waf → ca → gateway 前置拓扑),`gateway` 退化为纯转发反向代理,`db` 提供通用数据库访问与集中式日志收集,`confs` 提供统一配置分发,`ai_review` 提供 AI 内容审核,`static` 与 `dnfs` 作为业务服务对外提供能力。`db2` / `dnfs2` 分别为 `db` / `dnfs` 的 C++20 重实现,接口完全兼容可平替。此外包含 aibox 的终端前端 `BotOps`、独立的机器人硬件控制模块 `bot4cj`、嵌入式帧缓冲图形界面 `lci` 和嵌入式脚本引擎 `libtry`。
## 架构总览 ## 架构总览
@@ -29,7 +29,8 @@
┌─────┴──────┐ ┌─────┴──────┐
│ redirect80 │ :80 → :443(GET/HEAD 301 / 其余 308,重定向到 waf) │ redirect80 │ :80 → :443(GET/HEAD 301 / 其余 308,重定向到 waf)
└────────────┘ └────────────┘
日志链路: waf / gateway / static / dnfs / ai_review / captcha / smtp / aibox / confs / redirect80 ──(log_client, pymsg)──► db:8005 (Logs 表) 日志链路: waf / ca / gateway / static / dnfs / ai_review / captcha / smtp / aibox / confs / redirect80 ──(log_client, pymsg)──► db:8005 (Logs 表)
认证链路(v0.9.0 起): waf:443 ──HTTP──► ca:8443 (认证+前置转发) ──HTTP──► gateway:8445 (纯转发, 仅 loopback) ──► 内部服务
配置链路: static 等业务服务 ──(pymsg get)──► confs:8010(配置统一存放于 confs/configs/,mtime 变化自动重载) 配置链路: static 等业务服务 ──(pymsg get)──► confs:8010(配置统一存放于 confs/configs/,mtime 变化自动重载)
``` ```
@@ -39,9 +40,9 @@
1. 客户端若以 HTTP 访问 `:80`,由 `redirect80` 重定向到 HTTPS `:443`(GET/HEAD 返回 301,其余方法返回 308 保留 body)。 1. 客户端若以 HTTP 访问 `:80`,由 `redirect80` 重定向到 HTTPS `:443`(GET/HEAD 返回 301,其余方法返回 308 保留 body)。
2. 客户端所有请求先打到 `waf:443`(HTTPS),WAF 终止 TLS、运行 coraza 规则匹配,命中则直接返回 403。 2. 客户端所有请求先打到 `waf:443`(HTTPS),WAF 终止 TLS、运行 coraza 规则匹配,命中则直接返回 403。
3. 未命中规则的请求由 WAF 以明文 HTTP 转发到 `gateway:8443`,并设置 `X-Forwarded-For` / `X-Forwarded-Proto` 头。 3. 未命中规则的请求由 WAF 以明文 HTTP 转发到 `ca:8443`,并设置 `X-Forwarded-For` / `X-Forwarded-Proto` 头。
4. `gateway` 对 `/login`、`/register` 做账号鉴权(PBKDF2 密码哈希 + RSA 签名 Token + 可选 CAPTCHA + 速率限制;注册需邮箱验证码,经 pymsg/TCP 调 `smtp:8008` 发送与校验),用户数据通过 pymsg/TCP 调用 `db:8005` 存取。 4. `ca` 对 `/login`、`/register` 做账号鉴权(PBKDF2 密码哈希 + RSA 签名 Token + 可选 CAPTCHA + 速率限制;注册需邮箱验证码,经 pymsg/TCP 调 `smtp:8008` 发送与校验),用户数据通过 pymsg/TCP 调用 `db:8005` 存取。
5. 其余请求携带 Token,经 `gateway` 校验后反向代理到 `Proxy.opened` 中放开的内部服务(`static:9200` / `dnfs:8006`)。 5. 其余请求携带 Token,经 `ca` 验签后注入 `X-Auth-User` 身份头转发到 `gateway:8445`(仅 loopback,不验签仅做存在性检查),再由 gateway 反向代理到 `Proxy.opened` 中放开的内部服务(`static:9200` / `dnfs:8006`)。
6. `db:8005` 与 `ai_review:8007` 在 `Proxy.closed` 中,仅允许内部服务间调用,不对外暴露。 6. `db:8005` 与 `ai_review:8007` 在 `Proxy.closed` 中,仅允许内部服务间调用,不对外暴露。
7. `dnfs` 处理节点增删改查时,通过 pymsg/TCP 调用 `db` 服务完成数据持久化,新增节点前调用 `ai_review` 进行内容审核。 7. `dnfs` 处理节点增删改查时,通过 pymsg/TCP 调用 `db` 服务完成数据持久化,新增节点前调用 `ai_review` 进行内容审核。
8. 各服务通过 `log_client.py` SDK 异步批量推送运行日志(pymsg/TCP 写入 db 的 `Logs` 表),实现集中式日志收集(fail-safe,db 不可用时静默丢弃)。 8. 各服务通过 `log_client.py` SDK 异步批量推送运行日志(pymsg/TCP 写入 db 的 `Logs` 表),实现集中式日志收集(fail-safe,db 不可用时静默丢弃)。
@@ -52,15 +53,16 @@
|------|------|------|------| |------|------|------|------|
| [core](core/) | C++20 | 5200 | 进程托管服务(PMC),创建/列举/杀死/执行子进程,读取自启动列表守护业务进程,提供 HTTP 管理接口 | | [core](core/) | C++20 | 5200 | 进程托管服务(PMC),创建/列举/杀死/执行子进程,读取自启动列表守护业务进程,提供 HTTP 管理接口 |
| [redirect80](redirect80/) | Python | 80 | HTTP→HTTPS 重定向服务,将 80 端口请求重定向到 443(GET/HEAD 301 / 其余 308),纯标准库实现 | | [redirect80](redirect80/) | Python | 80 | HTTP→HTTPS 重定向服务,将 80 端口请求重定向到 443(GET/HEAD 301 / 其余 308),纯标准库实现 |
| [waf](waf/) | Go | 443 | WAF 网关(入口),终止 TLS、coraza 规则匹配(SecLang)、反向代理到 gateway,支持 SIGHUP 热加载规则 | | [waf](waf/) | Go | 443 | WAF 网关(入口),终止 TLS、coraza 规则匹配(SecLang)、反向代理到 ca,支持 SIGHUP 热加载规则 |
| [gateway](gateway/) | C++20 | 8443 | 应用层网关(内部 HTTP),用户注册/登录鉴权、Token 签发与校验、反向代理到内部服务 | | [ca](ca/) | C++20 | 8443 | 中央认证服务(内部 HTTP),用户注册/登录鉴权、Token 签发与校验、身份头注入后前置转发到 gateway |
| [gateway](gateway/) | C++20 | 8445 | 应用层网关(仅 loopback),纯转发反向代理到内部服务,不验签(v0.9.0 起认证迁至 ca) |
| [static](static/) | Python(Flask) | 9200 | 前端托管服务,社区首页、静态页面与文件浏览、视频/音频流媒体播放、路径遍历防护 | | [static](static/) | Python(Flask) | 9200 | 前端托管服务,社区首页、静态页面与文件浏览、视频/音频流媒体播放、路径遍历防护 |
| [dnfs](dnfs/) | Python(Flask) | 8006 | 树状网络文件系统(Dendrachy Network File-System),基于父子 pid 的节点树增删改查,集成 AI 内容审核 | | [dnfs](dnfs/) | Python(Flask) | 8006 | 树状网络文件系统(Dendrachy Network File-System),基于父子 pid 的节点树增删改查,集成 AI 内容审核 |
| [dnfs2](dnfs2/) | C++20 | 8006 | dnfs 的 C++20 重实现(libhttp + libmsg),接口与 Python 版完全兼容,修复 P0/P1 安全问题,可平替 | | [dnfs2](dnfs2/) | C++20 | 8006 | dnfs 的 C++20 重实现(libhttp + libmsg),接口与 Python 版完全兼容,修复 P0/P1 安全问题,可平替 |
| [db](db/) | Python(pymsg) | 8005 | 通用数据库服务,基于 pymsg 协议(TCP 长度前缀帧 + JSON)的 SQL-over-TCP,SQLAlchemy Core 参数化查询防注入;内置集中式日志收集(Logs 表 + 自动清理) | | [db](db/) | Python(pymsg) | 8005 | 通用数据库服务,基于 pymsg 协议(TCP 长度前缀帧 + JSON)的 SQL-over-TCP,SQLAlchemy Core 参数化查询防注入;内置集中式日志收集(Logs 表 + 自动清理) |
| [db2](db2/) | C++20 | 8005 | db 的 C++20 重实现(libmsg + boost::json + libsqlite3),execute 路由/where 语法/响应格式完全对齐 Python 版,可平替 | | [db2](db2/) | C++20 | 8005 | db 的 C++20 重实现(libmsg + boost::json + libsqlite3),execute 路由/where 语法/响应格式完全对齐 Python 版,可平替 |
| [ai_review](ai_review/) | Python(Flask) | 8007 | AI 内容审核服务,DeepSeek LLM + ChromaDB RAG,支持人工反馈纠偏 | | [ai_review](ai_review/) | Python(Flask) | 8007 | AI 内容审核服务,DeepSeek LLM + ChromaDB RAG,支持人工反馈纠偏 |
| [captcha](captcha/) | Python(pymsg) | 8009 | 验证码服务(pymsg/TCP),生成图形验证码(5 分钟过期、一次性),经 gateway 的 `GET /captcha/image` 代理给浏览器 | | [captcha](captcha/) | Python(pymsg) | 8009 | 验证码服务(pymsg/TCP),生成图形验证码(5 分钟过期、一次性),经 ca 的 `GET /captcha/image` 代理给浏览器 |
| [smtp](smtp/) | Python(pymsg) | 8008 | 邮件服务,发送/校验邮箱验证码(pymsg 路由 send_email/verify_email/health),注册流程的邮件验证码后端,SMTP 账号配置在 .env | | [smtp](smtp/) | Python(pymsg) | 8008 | 邮件服务,发送/校验邮箱验证码(pymsg 路由 send_email/verify_email/health),注册流程的邮件验证码后端,SMTP 账号配置在 .env |
| [confs](confs/) | Python(pymsg) | 8010 | 统一配置服务,集中存放并分发各服务配置文件(pymsg 路由 get/list/reload/health),内存缓存 + mtime 自动重载,路径遍历防护 | | [confs](confs/) | Python(pymsg) | 8010 | 统一配置服务,集中存放并分发各服务配置文件(pymsg 路由 get/list/reload/health),内存缓存 + mtime 自动重载,路径遍历防护 |
| [aibox](aibox/) | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度,由 core 拉起但与主干微服务无调用关系 | | [aibox](aibox/) | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度,由 core 拉起但与主干微服务无调用关系 |
@@ -73,29 +75,35 @@
- 可执行文件 `nidus.out`,作为 systemd 服务(`Nidus.service`)常驻运行(崩溃守护由 systemd 负责,core 自身不含自动重启逻辑)。 - 可执行文件 `nidus.out`,作为 systemd 服务(`Nidus.service`)常驻运行(崩溃守护由 systemd 负责,core 自身不含自动重启逻辑)。
- 支持两种工作模式:命令行子系统调用(`--send/--exec/--list/--kill`)与 HTTP 伺服器模式(`--serve <port>`)。 - 支持两种工作模式:命令行子系统调用(`--send/--exec/--list/--kill`)与 HTTP 伺服器模式(`--serve <port>`)。
- 通过命名管道(Pipe)与消息队列(Mq)实现进程间通信。 - 通过命名管道(Pipe)与消息队列(Mq)实现进程间通信。
- 读取 `init.txt` 自启动列表,按行拉起各业务服务(ai_review / db / dnfs / smtp / static / waf / gateway / captcha / aibox)。 - 读取 `init.txt` 自启动列表,按行拉起各业务服务(ai_review / db / dnfs / smtp / static / waf / ca / gateway / captcha / aibox)。
### waf(WAF 网关) ### waf(WAF 网关)
- 唯一对外入口,终止 TLS(`certs/full_chain.crt` / `certs/server.key`),监听 443 端口。 - 唯一对外入口,终止 TLS(`certs/full_chain.crt` / `certs/server.key`),监听 443 端口。
- 基于 [coraza](https://github.com/corazawaf/coraza) v3.3.0(ModSecurity SecLang 的 Go 实现),规则匹配命中后返回 403。 - 基于 [coraza](https://github.com/corazawaf/coraza) v3.3.0(ModSecurity SecLang 的 Go 实现),规则匹配命中后返回 403。
- 未命中规则的请求以明文 HTTP 反向代理到 `gateway:8443`,透传 `X-Forwarded-For` / `X-Forwarded-Proto`。 - 未命中规则的请求以明文 HTTP 反向代理到 `ca:8443`,透传 `X-Forwarded-For` / `X-Forwarded-Proto`。
- 规则库位于 [waf/rules/](waf/rules/),分两段:本地自定义(ID 100000-199999)与 OWASP CRS(ID 900000-999999,可选)。 - 规则库位于 [waf/rules/](waf/rules/),分两段:本地自定义(ID 100000-199999)与 OWASP CRS(ID 900000-999999,可选)。
- 极客操作:`kill -HUP <pid>` 热加载规则;`waf -list` 列规则;`waf -test <file>` 校验语法;`waf -check "<input>"` 测试单条输入。 - 极客操作:`kill -HUP <pid>` 热加载规则;`waf -list` 列规则;`waf -test <file>` 校验语法;`waf -check "<input>"` 测试单条输入。
- 配置见 [waf/config.yaml](waf/config.yaml),详见 [waf/README.md](waf/README.md)。 - 配置见 [waf/config.yaml](waf/config.yaml),详见 [waf/README.md](waf/README.md)。
### gateway(应用层网关 · v0.8.0) ### ca(中央认证服务 · v1.0.0)
- 内部 HTTP 服务,默认监听 127.0.0.1:8443,不再承载 TLS(TLS 已由 WAF 终止)。 - 内部 HTTP 服务,默认监听 127.0.0.1:8443(接管原 gateway 端口,WAF upstream 不变),不再承载 TLS(TLS 已由 WAF 终止)。
- 通过 `Server.tls = on/off` 配置切换 HTTPS/HTTP 模式([example.ini](gateway/example.ini)),独立部署时仍可启用 TLS。
- **密码安全**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节随机盐),密码强度校验(8+ 字符、4 类中至少 3 类),常数时间比较防 timing attack。 - **密码安全**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节随机盐),密码强度校验(8+ 字符、4 类中至少 3 类),常数时间比较防 timing attack。
- **用户存储**:用户数据通过 pymsg/TCP 调用 `db:8005`(`DbC::call_msg`)存取,不再直连本地 SQLite;登录成功即更新 `Users.last_login`(`"$now"` 哨兵)。 - **用户存储**:用户数据通过 pymsg/TCP 调用 `db:8005`(`DbC::call_msg`)存取;登录成功即更新 `Users.last_login`(`"$now"` 哨兵)。
- **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期,验签通过才放行;RSA 密钥对启动时内存生成,重启后旧 Token 全部失效。 - **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期;RSA 密钥对启动时内存生成,重启后旧 Token 全部失效;jti 黑名单 + 用户版本号双轨吊销。
- **Cookie 认证**:登录成功同时签发 `HttpOnly + Secure + SameSite=Lax` 的 `q4_token` Cookie;代理时无 `Authorization` 头则回退读 Cookie 验签,并向下游注入 `Authorization: Bearer`(下游零改动);`POST /logout` 清除 Cookie。公开目标(`Proxy.default`)不做凭证校验,避免旧 Cookie 影响公开页面。 - **Cookie 认证**:登录成功同时签发 `HttpOnly + Secure + SameSite=Lax` 的 `q4_token` Cookie;验签时无 `Authorization` 头则回退读 Cookie,并向下游注入 `Authorization: Bearer`(下游零改动);`POST /logout` 清除 Cookie。
- **CAPTCHA**:`/captcha` 接口生成验证码(5 分钟过期、一次性使用),登录与注册均可选启用;`GET /captcha/image` 为 captcha 服务(pymsg/TCP :8009)的图形验证码代理路由。 - **前置转发**:无 `Target-Url` 为公开请求(剥身份头直转 gateway);有 `Target-Url` 为受保护请求(验签后注入 `X-Auth-User`/`X-Auth-Admin` 转发,strip-then-set 契约剥离客户端伪造值);上游不可达统一 502。
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟,按 `X-Forwarded-For`(WAF 注入)客户端 IP 计数。 - **CAPTCHA**:`GET /captcha/image` 为 captcha 服务(pymsg/TCP :8009)的图形验证码代理路由;`/login` 强制 CAPTCHA。
- **反向代理**:`Target-Url` 必须在 `Proxy.opened` 白名单中(`closed` 仅记录用途,实际隔离靠"不在 opened 中即拒绝"),缺省走 `Proxy.default`(不需 Token,服务前端静态资源)。 - **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟、发码 IP 级 + 邮箱级双窗口,按 `X-Forwarded-For`(WAF 注入)客户端 IP 计数。
- **用户表**:`Users` 表由 [db/create_sqlite3.sql](db/create_sqlite3.sql) 建立(gateway 不再自动建表),无默认管理员账号,首位注册者即为管理员。 - **用户表**:`Users` 表由 [db/create_sqlite3.sql](db/create_sqlite3.sql) 建立,无默认管理员账号,首位注册者即为管理员。
- 详见 [ca/README.md](ca/README.md)。
### gateway(应用层网关 · v0.9.0)
- 纯转发服务,仅监听 127.0.0.1:8445(loopback 启动强制自检),不再验签(认证已迁至 ca)。
- **存在性不变量**:显式 `Target-Url` 请求必须携带 ca 注入的 `X-Auth-User` 头,缺失一律 401。
- **反向代理**:`Target-Url` 必须在 `Proxy.opened` 白名单中,缺省走 `Proxy.default`;`X-Auth-User`/`X-Auth-Admin`/`Authorization` 原样透传下游。
- 详见 [gateway/README.md](gateway/README.md)。 - 详见 [gateway/README.md](gateway/README.md)。
### static(前端托管服务) ### static(前端托管服务)
+74
View File
@@ -0,0 +1,74 @@
#PRJ
*.db
*.crt
*.key
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Linker files
*.ilk
# Debugger Files
*.pdb
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
*.so.*
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Build directories
build/
Build/
build-*/
# CMake generated files
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
#Makefile
install_manifest.txt
compile_commands.json
# Temporary files
*.tmp
*.log
*.bak
*.swp
# vcpkg
vcpkg_installed/
# debug information files
*.dwo
# test output & cache
Testing/
.cache/
+19
View File
@@ -0,0 +1,19 @@
VERSION := 1.0.0
APP_NAME := ca
INSTL_DIR := /usr/local/bin
CPP := g++
INCS := -I inc/ -I ../libmsg/inc
MY_LIBS := -L/usr/lib -L../libmsg/build -lus_http -lus_httpclient -lus_crypt -lus_msg
LIBS := -pthread -lcurl -lssl -lz -lcrypto -lboost_json -lboost_program_options -Wl,-rpath,$(shell cd ../libmsg/build && pwd)
CPPFLAGS := --std=c++20 -pipe -pthread # -Wall -Werror
BUILD_DIR = build
$(shell mkdir -p $(BUILD_DIR))
#--------
all: $(BUILD_DIR)/$(APP_NAME).out
$(BUILD_DIR)/$(APP_NAME).out: $(BUILD_DIR)/main.o
$(CPP) $^ $(LIBS) $(MY_LIBS) -o $@
$(BUILD_DIR)/main.o: src/main.cpp inc/DbC.hpp inc/LogC.hpp inc/Conf.hpp inc/ExpBase.hpp inc/LocalTime.hpp
$(CPP) $(CPPFLAGS) -c $< $(INCS) -o $@
clear:
@rm -rvf build/
+142
View File
@@ -0,0 +1,142 @@
# ca
UniServe Framework 的中央认证服务(Central Auth),C++20 实现,v1.0.0 起承接原 gateway v0.8.0 的全部认证职责。
## 拓扑定位(waf → ca → gateway 前置拓扑)
```
客户端 ──HTTPS──▶ waf:443 (终止 TLS + coraza 规则) ──HTTP──▶ ca:8443 (认证+前置转发) ──HTTP──▶ gateway:8445 (纯转发) ──▶ static:9200 / dnfs2:8006 / core:5200
```
- CA 接管原 gateway 的 **8443** 端口,WAF upstream 配置不变(`waf/config.yaml` 仍指向 8443)。
- gateway 退居 **8445**(仅 loopback),退化为纯转发服务,不再验签。
- 验签在 CA 本地完成(RSA 密钥同进程),对受保护请求零网络跳。
- 下游 core / dnfs2 / 前端 / WAF 全部零改动。
## 职责
- **账号鉴权**:`/register` 注册(需邮箱验证码,经 pymsg 调 smtp:8008 校验)、`/login` 登录,签发 RSA 签名的 Token。
- **Token 生命周期**:`/logout`(jti 吊销)、`/refresh_token`(宽限期刷新 + 次数上限)、`/whoami`、`/admin/verify`(供 core 等内部服务调用)。
- **前置转发层**:通配 `GET/POST *` 把请求转发到唯一上游 gateway:8445:
- 无 `Target-Url` 头 = 公开请求:剥离伪造身份头(`X-Auth-User`/`X-Auth-Admin`)后直转。
- 有 `Target-Url` 头 = 受保护请求:本地验签(RSA + 过期 + jti 黑名单 + 用户版本号),通过后注入 `X-Auth-User`(管理员追加 `X-Auth-Admin: 1`)与 `Authorization: Bearer` 再转发。
- 上游不可达/超时统一返回 `502 Bad Gateway`(区别于 CA 自身故障的 500)。
- **WebSocket 反向代理(仅管理员)**:`[WsProxy]` 段配置的路径在 HTTP Upgrade 握手时校验 Token(`is_admin=true`),通过后透明桥接到后端 ws:// 或 wss:// 服务。
- **CAPTCHA 代理**:`GET /captcha/image` 代理 captcha2 签发图形验证码(PNG + `X-Captcha-Token` 响应头,10 次/分钟/IP,禁缓存)。
- **邮箱验证码**:`/register/send_code`、`/reset/send_code` 发送 6 位验证码邮件(10 分钟有效);`/reset_password` 重置密码(防枚举恒成功 + 改密后版本号吊销全端 Token)。
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟、发验证码 5 次/10 分钟(IP 级)+ 3 次/小时/10 次/天(邮箱级),按 `X-Forwarded-For` 客户端 IP 计数(由 WAF 注入),超限一律返回 429。
- **OPTIONS 预检**:由 libhttp 处理,`Access-Control-Allow-Origin` 反射请求的 Origin 头(见审计 P2-3)。
## 身份头契约(strip-then-set)
| 头 | 方向 | 语义 |
|----|------|------|
| `X-Auth-User` | CA → gateway → 下游 | 已认证用户名(邮箱)。客户端携带的同名单被 CA 剥离,仅 CA 注入的值可信 |
| `X-Auth-Admin: 1` | CA → gateway → 下游 | 仅当 Token `is_admin=true` 时注入 |
| `Authorization: Bearer <token>` | CA → gateway → 下游 | Token 来自 Cookie 时由 CA 补齐;来自客户端 Authorization 头时透传 |
gateway 侧仅做**存在性不变量**检查(显式 `Target-Url` 请求必须携带 `X-Auth-User`,否则 401),不复验签名——信任边界是 loopback。
## 安全特性
| 特性 | 实现 |
|------|------|
| 密码哈希 | PBKDF2-HMAC-SHA256,100000 轮,16 字节随机盐,32 字节哈希 |
| 密码强度校验 | 至少 8 字符;大写/小写/数字/特殊字符中至少 3 类 |
| Token 机制 | `Base64(payload) + "_" + RSA-Sign(payload)`,验签 + 过期校验(默认 1 小时) |
| Token 吊销 | 双轨纯内存:jti 黑名单(5000 条阈值清理)+ 用户版本号(改密全端吊销) |
| 常数时间比较 | PBKDF2 验证用 `diff |= hash[i] ^ expected[i]` 防 timing attack |
| 防用户枚举 | 登录对不存在用户执行 dummy PBKDF2 拉平时序;重置密码恒返回成功 |
| CAPTCHA | 统一走 captcha2 远端校验(Token 一次性消费);`/login` 强制 |
| TLS 可选 | `Server.tls = off` 时纯 HTTP(配合 WAF),`on` 时用 `full_chain.crt` / `server.key` |
| 下游容错 | pymsg 客户端 `retry_on_refused`:db/confs 未就绪时阻塞等待;日志客户端 fail-safe 静默丢弃 |
## 接口
### `GET /captcha/image`
代理 captcha2 签发图形验证码:响应体为 PNG,答案 Token 在 `X-Captcha-Token` 响应头(64 位 hex,一次性消费),`Cache-Control: no-store`。限流 10 次/分钟/IP(429)。
### `POST /login`
```json
{
"username": "user@example.com",
"password": "P@ssw0rd",
"captcha_token": "a1b2c3d4...",
"captcha_answer": "Ab3xy"
}
```
成功:`{"success": true, "data": {"token": "<base64_payload>_<rsa_sign>"}}`,同时签发 `HttpOnly + Secure + SameSite=Lax` 的 `q4_token` Cookie。
错误码:`400`(参数/CAPTCHA 错误)、`401`(账号/密码错)、`429`(限流)。
### `POST /register` / `POST /register/send_code` / `POST /reset/send_code` / `POST /reset_password`
与 gateway v0.8.0 契约完全一致(详见 `docs/reports/auth_service_plan.md` §5 与黑盒 tests/blackbox/test_gateway.py)。
### `POST /logout` / `POST /refresh_token` / `POST /whoami` / `POST /admin/verify`
- `/logout`:吊销当前 Token 的 jti 并清除 Cookie。
- `/refresh_token`:宽限期(iat + keep_time × refresh_grace_multiplier)内换发新 Token,旧 jti 吊销,刷新次数超 `max_refresh` 后需重新登录。
- `/whoami`:恒 200,未登录返回 `{"login": false}`。
- `/admin/verify`:fail-closed,非管理员/无效 Token 一律 401(供 core:5200 管理员接口鉴权)。
### `GET/POST *`(前置转发)
```
Authorization: Bearer <base64_payload>_<rsa_sign> // 或 Cookie: q4_token=...
Target-Url: http://127.0.0.1:8006 // 可选;携带即视为受保护请求
```
- 无 `Target-Url`:公开请求,剥身份头直转 gateway(由其 `Proxy.default` 路由)。
- 有 `Target-Url`:验签通过后注入身份头转发;`Target-Url` 白名单由 gateway 侧 `Proxy.opened` 裁决(403)。
- 上游故障:`502 {"success": false, "message": "Bad Gateway"}`。
### WebSocket 代理(`[WsProxy]` 配置的路径)
握手阶段从 `Authorization: Bearer` / `Cookie: q4_token` 提取 Token,验签且 `is_admin=true` 才升级连接并桥接后端;否则拒绝。路由精确匹配,优先级高于 HTTP 通配 `*`。
## 配置
两层加载:**confs 统一配置服务**(启动时 pymsg `get` 拉取 `ca.ini`,`retry_on_refused=true`)→ **环境变量覆盖**(最高优先级)。
配置文件:[confs/configs/ca.ini](../confs/configs/ca.ini)。
- `Server.address`/`port`/`tls`:监听地址/端口/TLS(默认 `127.0.0.1:8443`,env `LISTEN_ADDR`/`LISTEN_PORT`)。
- `Upstream.gateway`:唯一上游 gateway 地址(默认 `http://127.0.0.1:8445`,env `UPSTREAM_GATEWAY`)。
- `Token.*`:`keep_time` / `refresh_grace_multiplier` / `max_refresh`。
- `Admin.users`:管理员白名单(分号分隔邮箱),登录命中时 Token payload 写入 `is_admin=true`。
- `CORS.allowed_origins`:已读取但尚未用于预检响应(审计 P2-3)。
- `WsProxy.routes`:`/path=ws://host:port/target`,分号分隔。
- `Downstream.*`:db/captcha/smtp 地址(env `DB_*`/`CAPTCHA_*`/`SMTP_*`/`CONFS_*`)。
## 编译与运行
```bash
bash _set.sh # make clean && make
bash _run.sh # 从 confs 拉取配置启动(先 sleep 2 等待 confs/db)
```
命令行参数:`-h/--help`、`-v/--version`。日志经 LogC 异步批量推送 db:8005 `Logs` 表(service 名 `ca`)。
## 目录结构
```
ca/
├── src/
│ └── main.cpp # 全部实现:认证 handler、Token 机制、限流、前置转发层、WS 代理
├── inc/
│ ├── DbC.hpp # db 服务客户端(pymsg,retry_on_refused=true;client 名 "ca")
│ ├── LogC.hpp # 统一日志客户端(异步批量,fail-safe;client 名 "ca-log")
│ ├── Conf.hpp # ConfIni:ini 解析 + env 覆盖
│ ├── ExpBase.hpp # 异常基类(带 source_location)
│ └── LocalTime.hpp # 本地时间工具
├── Makefile
├── _set.sh / _run.sh
└── README.md
```
## 版本
`1.0.0`:认证独立化首发——自 gateway v0.8.0 整体搬迁全部认证资产(逐字保留实现),新增前置转发层(固定上游 + strip-then-set 身份头契约 + 上游 502 语义)。原 gateway 同步瘦身为 v0.9.0 纯转发服务。
Executable
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# ca 启动脚本
# 配置统一由 confs 服务管理(confs/configs/ca.ini),
# 启动时从 confs(CONFS_HOST:CONFS_PORT,默认 127.0.0.1:8010)拉取配置。
cd "$(dirname "$0")" || exit
# 等待依赖服务启动(confs/db)
sleep 2
exec ./build/ca.out "$@"
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
make clear && make $@
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include <iostream>
#include <exception>
#include <string>
#include <sstream>
#include <cstdlib>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/ini_parser.hpp>
namespace property_tree = boost::property_tree;
using ptree = property_tree::ptree;
namespace qing {
class ConfIni {
public:
/// 默认构造(空配置,后续通过赋值覆盖,仅供 confs 加载流程内部使用)
ConfIni() = default;
/// 从文件路径解析 ini(保留用于开发/调试模式)
ConfIni(const std::string &filename) {
if (filename.empty()) return; // 空文件名不报错,后续会通过赋值覆盖
try {
std::cout << "[conf] 从文件加载配置: " << filename << std::endl;
property_tree::ini_parser::read_ini(filename, pt);
}
catch (const boost::property_tree::ini_parser_error& e) {
std::cerr << "[conf] INI 解析错误: " << e.what() << std::endl;
throw IniConfigureFileParseException("INI parsing error!");
}
}
/// 从 ini 文本字符串解析(用于从 confs 拉取后解析)
static ConfIni fromString(const std::string &ini_text) {
ConfIni c;
try {
std::istringstream iss(ini_text);
property_tree::ini_parser::read_ini(iss, c.pt);
}
catch (const boost::property_tree::ini_parser_error& e) {
std::cerr << "[conf] INI 文本解析错误: " << e.what() << std::endl;
throw IniConfigureFileParseException("INI parsing error!");
}
return c;
}
template <typename T>
T get(const std::string &path, const T &_defaut) {
return pt.get<T>(path, _defaut);
}
/// 读取环境变量,不存在则返回默认值
static std::string env(const char *name, const std::string &default_val = "") {
const char *v = std::getenv(name);
return v ? std::string(v) : default_val;
}
static int env_int(const char *name, int default_val) {
const char *v = std::getenv(name);
if (!v || !*v) return default_val;
try { return std::stoi(v); }
catch (...) { return default_val; }
}
class IniConfigureFileParseException: public std::exception {
public:
explicit IniConfigureFileParseException(const std::string &msg)
: message(msg) {}
const char* what() const noexcept override {
return message.c_str();
}
private:
std::string message;
};
private:
ptree pt;
};
}
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <us/HttpClient.hpp>
#include <us/Msg.hpp>
#include <boost/json.hpp>
namespace qing {
namespace json = boost::json;
class DbC {
public:
/* 本机 db 轻量查询的默认超时(秒);慢操作由调用方显式放宽。
* smtp/captcha 等其他下游的超时常量见 main.cpp。 */
static constexpr double DEFAULT_TIMEOUT = 5.0;
/// 初始化 db 服务地址(启动时从配置设置,默认 127.0.0.1:8005)
static void init(const std::string& host, int port) {
_host = host;
_port = port;
}
// 【保留】旧版 HTTP 接口:通过 HttpClient POST 到 dbv2 HTTP /execute 端点。
// 保留用于兼容性参考,新代码优先使用 call_msg()(pymsg TCP 协议,更可靠)。
static json::value call_http(json::value& params) {
auto client = pmc::net::HttpClient();
client.set_header("Content-Type: application/json");
std::string payload = json::serialize(params);
std::string res = "";
std::string url = "http://" + _host + ":" + std::to_string(_port) + "/execute";
client.post_stream_sync(url, payload, [&res](const auto& ret) {
res += ret;
});
return json::parse(res);
}
// 新版 pymsg 接口:通过 MsgClient(TCP 长度前缀帧协议)调用 dbv2。
// 自动添加 _route = "execute",返回 dbv2 pymsg 完整 JSON 响应
// {"success":..., "data":[...]}。
// 响应格式与 call_http 兼容,调用方可直接替换。
// db 是强依赖,启用 retry_on_refused:db 未启动时阻塞等待,避免启动顺序问题。
static json::value call_msg(const json::value& params,
double timeout = DEFAULT_TIMEOUT) {
json::value jv_route = params;
jv_route.as_object()["_route"] = "execute";
pmc::net::MsgClient client(_host, _port, "ca");
std::string resp = client.call(
json::serialize(jv_route), timeout,
/*retry_on_refused=*/true);
return json::parse(resp);
}
static const std::string& host() { return _host; }
static int port() { return _port; }
private:
static inline std::string _host = "127.0.0.1";
static inline int _port = 8005;
};
}
+411
View File
@@ -0,0 +1,411 @@
#ifndef EXCEPTION_BASE_H
#define EXCEPTION_BASE_H
#include <exception>
#include <string>
#include <cstring>
#include <memory>
#include <source_location>
#include <sstream>
#include <type_traits>
/**
* @brief 通用异常基类
*
* 特性:
* 1. 完整的错误信息管理(错误码、消息、位置信息)
* 2. 支持异常链(嵌套异常)
* 3. 多态支持(clone、自定义消息)
* 4. 线程安全的 what() 缓存
* 5. 支持 C++20 source_location
*/
class ExceptionBase : public std::exception {
public:
// ============ 构造函数 ============
explicit ExceptionBase(
int code = 0,
const std::string& message = "",
const std::source_location& location = std::source_location::current()
) noexcept
: m_code(code)
, m_message(message)
, m_file(location.file_name())
, m_line(location.line())
, m_function(location.function_name())
, m_what_cache()
{
}
// 支持从字符串字面量构造
explicit ExceptionBase(
const std::string& message,
const std::source_location& location = std::source_location::current()
) noexcept
: m_code(0)
, m_message(message)
, m_file(location.file_name())
, m_line(location.line())
, m_function(location.function_name())
, m_what_cache()
{
}
// ============ 拷贝和移动 ============
ExceptionBase(const ExceptionBase& other) noexcept
: std::exception(other)
, m_code(other.m_code)
, m_message(other.m_message)
, m_file(other.m_file)
, m_line(other.m_line)
, m_function(other.m_function)
, m_nested(other.m_nested ? other.m_nested->clone() : nullptr)
, m_what_cache()
{
}
ExceptionBase(ExceptionBase&& other) noexcept
: std::exception(std::move(other))
, m_code(other.m_code)
, m_message(std::move(other.m_message))
, m_file(std::move(other.m_file))
, m_line(other.m_line)
, m_function(std::move(other.m_function))
, m_nested(std::move(other.m_nested))
, m_what_cache(std::move(other.m_what_cache))
{
other.m_code = 0;
other.m_line = 0;
}
virtual ~ExceptionBase() noexcept = default;
// ============ 赋值运算符 ============
ExceptionBase& operator=(const ExceptionBase& other) noexcept {
if (this != &other) {
m_code = other.m_code;
m_message = other.m_message;
m_file = other.m_file;
m_line = other.m_line;
m_function = other.m_function;
m_nested = other.m_nested ? other.m_nested->clone() : nullptr;
m_what_cache.clear();
}
return *this;
}
ExceptionBase& operator=(ExceptionBase&& other) noexcept {
if (this != &other) {
m_code = other.m_code;
m_message = std::move(other.m_message);
m_file = std::move(other.m_file);
m_line = other.m_line;
m_function = std::move(other.m_function);
m_nested = std::move(other.m_nested);
m_what_cache = std::move(other.m_what_cache);
other.m_code = 0;
other.m_line = 0;
}
return *this;
}
// ============ 访问器 ============
int code() const noexcept { return m_code; }
const std::string& message() const noexcept { return m_message; }
const std::string& file() const noexcept { return m_file; }
int line() const noexcept { return m_line; }
const std::string& function() const noexcept { return m_function; }
const ExceptionBase* nested() const noexcept { return m_nested.get(); }
// ============ 异常链操作 ============
void set_nested(std::unique_ptr<ExceptionBase> nested) noexcept {
m_nested = std::move(nested);
m_what_cache.clear();
}
template<typename ExType>
void set_nested(const ExType& nested_ex) noexcept {
static_assert(std::is_base_of<ExceptionBase, ExType>::value,
"ExType must inherit from ExceptionBase");
m_nested = std::make_unique<ExType>(nested_ex);
m_what_cache.clear();
}
// ============ std::exception 接口 ============
const char* what() const noexcept override {
if (m_what_cache.empty()) {
build_what_message();
}
return m_what_cache.c_str();
}
// ============ 多态支持 ============
virtual std::unique_ptr<ExceptionBase> clone() const {
return std::make_unique<ExceptionBase>(*this);
}
// ============ 重新抛出 ============
[[noreturn]] void rethrow() const {
throw *this;
}
protected:
// ============ 可重写的消息构建方法 ============
virtual std::string build_message() const {
std::string result;
// 错误码
if (m_code != 0) {
result += "[Error " + std::to_string(m_code) + "] ";
}
// 错误消息
result += m_message;
/* ===== 以下「位置信息」与「嵌套异常」暂时以注释方式保留 =====
* 原因:黑盒检测发现 what() 的返回值会透传到 HTTP 响应体,
* 导致泄露源码文件路径、行号、C++ 模板类型签名(函数名),
* 便于攻击者定位代码、确认技术栈、构造针对性 payload(P0-3)。
* 排障:完整信息仍由各 handler 通过 std::cerr 输出到服务端日志。
* 恢复:如需调试可临时取消下方注释;生产环境必须保持注释状态,
* 并确保 what() 不直接进入客户端响应。
* ====================================================================
// 位置信息
if (!m_file.empty()) {
result += " (at " + m_file;
if (m_line > 0) {
result += ":" + std::to_string(m_line);
}
if (!m_function.empty()) {
result += " in " + m_function;
}
result += ")";
}
// 嵌套异常
if (m_nested) {
result += "\n Caused by: " + std::string(m_nested->what());
}
* ==================================================================== */
return result;
}
private:
// ============ 内部方法 ============
void build_what_message() const noexcept {
try {
m_what_cache = build_message();
} catch (...) {
// 降级处理
m_what_cache = "Exception: " + m_message;
}
}
private:
// ============ 成员变量 ============
int m_code;
std::string m_message;
std::string m_file;
int m_line;
std::string m_function;
std::unique_ptr<ExceptionBase> m_nested;
mutable std::string m_what_cache;
};
// ============================================================================
// 宏定义 - 方案二完整版
// ============================================================================
/**
* @brief 抛出指定类型的异常(自动添加 source_location)
*
* 适用场景:子类构造函数与 ExceptionBase 相同
*
* 使用示例:
* THROW_EX(NetworkException, 404, "Server not found")
* THROW_EX(DatabaseException, 100, "Connection timeout")
*/
#define THROW_EX(ExType, code, msg) \
throw ExType(code, msg, std::source_location::current())
/**
* @brief 抛出异常(仅消息,不传错误码)
*
* 适用场景:子类支持只传消息的构造函数
*
* 使用示例:
* THROW_EX_MSG(ValidationException, "Invalid input data")
*/
#define THROW_EX_MSG(ExType, msg) \
throw ExType(msg, std::source_location::current())
/**
* @brief 抛出异常(支持任意构造函数参数)
*
* 适用场景:子类有额外的构造函数参数
*
* 使用示例:
* // 子类定义
* class FileException : public ExceptionBase {
* public:
* FileException(int code, const std::string& msg,
* const std::string& filename,
* const std::source_location& loc = std::source_location::current())
* : ExceptionBase(code, msg, loc), m_filename(filename) {}
* };
*
* // 使用
* THROW_EX_CUSTOM(FileException, 404, "File not found", "/etc/config.txt")
*/
#define THROW_EX_CUSTOM(ExType, ...) \
throw ExType(__VA_ARGS__, std::source_location::current())
/**
* @brief 抛出异常并携带嵌套异常
*
* 适用场景:需要保留原始异常信息
*
* 使用示例:
* try {
* connect_to_server();
* } catch (const NetworkException& e) {
* THROW_EX_NESTED(ServiceException, 500, "Service failed", e)
* }
*/
#define THROW_EX_NESTED(ExType, code, msg, nested_ex) \
do { \
auto ex = ExType(code, msg, std::source_location::current()); \
ex.set_nested(nested_ex); \
throw ex; \
} while(0)
/**
* @brief 抛出异常并携带嵌套异常(支持自定义参数)
*
* 使用示例:
* THROW_EX_NESTED_CUSTOM(FileException, 500, "Read failed",
* original_ex, "/data/file.txt")
*/
#define THROW_EX_NESTED_CUSTOM(ExType, code, msg, nested_ex, ...) \
do { \
auto ex = ExType(code, msg, __VA_ARGS__, std::source_location::current()); \
ex.set_nested(nested_ex); \
throw ex; \
} while(0)
/**
* @brief 检查条件,失败则抛出异常
*
* 使用示例:
* THROW_IF(ptr == nullptr, NullPointerException, 100, "Pointer is null")
* THROW_IF(data.empty(), ValidationException, 200, "Data cannot be empty")
*/
#define THROW_IF(cond, ExType, code, msg) \
do { \
if (cond) { \
THROW_EX(ExType, code, msg); \
} \
} while(0)
/**
* @brief 检查条件,失败则抛出异常(仅消息)
*/
#define THROW_IF_MSG(cond, ExType, msg) \
do { \
if (cond) { \
THROW_EX_MSG(ExType, msg); \
} \
} while(0)
/**
* @brief 检查条件,失败则抛出异常(自定义参数)
*/
#define THROW_IF_CUSTOM(cond, ExType, ...) \
do { \
if (cond) { \
THROW_EX_CUSTOM(ExType, __VA_ARGS__); \
} \
} while(0)
/**
* @brief 检查条件,失败则抛出异常(带嵌套异常)
*/
#define THROW_IF_NESTED(cond, ExType, code, msg, nested_ex) \
do { \
if (cond) { \
THROW_EX_NESTED(ExType, code, msg, nested_ex); \
} \
} while(0)
/**
* @brief 检查条件,失败则抛出异常(带嵌套异常和自定义参数)
*/
#define THROW_IF_NESTED_CUSTOM(cond, ExType, code, msg, nested_ex, ...) \
do { \
if (cond) { \
THROW_EX_NESTED_CUSTOM(ExType, code, msg, nested_ex, __VA_ARGS__); \
} \
} while(0)
// ============================================================================
// 辅助宏 - 用于格式化错误消息
// ============================================================================
/**
* @brief 格式化错误消息(使用流式语法)
*
* 使用示例:
* THROW_EX_FMT(NetworkException, 404, "Failed to connect to " << host << ":" << port)
*/
#define THROW_EX_FMT(ExType, code, msg_expr) \
do { \
std::ostringstream _oss; \
_oss << msg_expr; \
THROW_EX(ExType, code, _oss.str()); \
} while(0)
/**
* @brief 格式化错误消息(使用 printf 风格)
*
* 使用示例:
* THROW_EX_PRINTF(NetworkException, 404, "Failed to connect to %s:%d", host.c_str(), port)
*/
#define THROW_EX_PRINTF(ExType, code, fmt, ...) \
do { \
char _buf[1024]; \
snprintf(_buf, sizeof(_buf), fmt, __VA_ARGS__); \
THROW_EX(ExType, code, std::string(_buf)); \
} while(0)
/**
* @brief 条件检查 + 格式化消息
*/
#define THROW_IF_FMT(cond, ExType, code, msg_expr) \
do { \
if (cond) { \
THROW_EX_FMT(ExType, code, msg_expr); \
} \
} while(0)
/**
* @brief 条件检查 + printf 风格格式化
*/
#define THROW_IF_PRINTF(cond, ExType, code, fmt, ...) \
do { \
if (cond) { \
THROW_EX_PRINTF(ExType, code, fmt, __VA_ARGS__); \
} \
} while(0)
#endif // EXCEPTION_BASE_H
+71
View File
@@ -0,0 +1,71 @@
#pragma once
#include <chrono>
#include <string>
#include <optional>
#include <sstream>
#include <iomanip>
#include <ctime>
namespace qing {
class LocalTime {
public:
// 获取当前本地时间字符串
static std::string getCurrent(const std::string& format = "%Y-%m-%d %H:%M:%S") {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf{};
localtime_r(&t, &tm_buf);
char buf[128];
std::strftime(buf, sizeof(buf), format.c_str(), &tm_buf);
return std::string(buf);
}
// 解析时间字符串为 system_clock::time_point(本地时间 → UTC)
static std::optional<std::chrono::system_clock::time_point> parse(
const std::string& str,
const std::string& format = "%Y-%m-%d %H:%M:%S"
) {
std::tm tm_buf{};
std::istringstream iss{str};
iss >> std::get_time(&tm_buf, format.c_str());
if (iss.fail()) {
return std::nullopt;
}
// mktime 将本地时间转换为 time_t(UTC)
std::time_t t = std::mktime(&tm_buf);
if (t == -1) {
return std::nullopt;
}
return std::chrono::system_clock::from_time_t(t);
}
// ----- 过期判断 -----
static bool isExpired(const std::chrono::system_clock::time_point& tp) {
return std::chrono::system_clock::now() > tp;
}
template <typename Rep, typename Period>
static bool isExpired(const std::chrono::system_clock::time_point& tp,
const std::chrono::duration<Rep, Period>& gracePeriod) {
return std::chrono::system_clock::now() > tp + gracePeriod;
}
static bool isExpired(const std::string& timeStr,
const std::string& format = "%Y-%m-%d %H:%M:%S") {
auto parsed = parse(timeStr, format);
if (!parsed) return true;
return isExpired(*parsed);
}
template <typename Rep, typename Period>
static bool isExpired(const std::string& timeStr,
const std::chrono::duration<Rep, Period>& gracePeriod,
const std::string& format = "%Y-%m-%d %H:%M:%S") {
auto parsed = parse(timeStr, format);
if (!parsed) return true;
return isExpired(*parsed, gracePeriod);
}
};
}
+206
View File
@@ -0,0 +1,206 @@
#pragma once
/** LogC — ca 统一日志客户端(C++ 版,对应各 Python 服务的 log_client.py)
*
* 设计要点(与 Python 版对齐):
* - 通过 pymsg TCP 调用 dbv2 execute 路由,逐条 INSERT 到 Logs 表
* - 异步非阻塞:基于 pmc::net::AsyncQueue 内存队列 + 后台线程批量推送,不拖慢业务
* - fail-safe:db 服务不可用时静默丢弃(仅 stderr 提示),绝不影响业务
* - 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
* - 背压保护:队列上限 10000 条,满时丢最旧日志,防止内存无限增长
*
* 用法:
* #include "LogC.hpp"
*
* qing::LogC::instance().info("ca", "启动完成");
* qing::LogC::instance().error("ca", "数据库连接失败",
* std::nullopt, R"({"errno":2002})");
*
* // 进程退出前:
* qing::LogC::instance().shutdown(); // 停止后台线程并 flush 剩余日志(幂等)
*/
#include <atomic>
#include <chrono>
#include <cstdio>
#include <ctime>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
#include <boost/json.hpp>
#include <us/AsyncQueue.hpp>
#include <us/Msg.hpp>
namespace qing {
namespace json = boost::json;
class LogC {
public:
struct Entry {
std::string service;
std::string level;
std::string message;
std::optional<std::string> trace_id;
std::optional<std::string> extra; // 已序列化的 JSON 字符串(存 Logs.extra TEXT)
};
static LogC& instance() {
static LogC inst;
return inst;
}
// ---- 公开接口 --------------------------------------------------------
// 推送一条日志(非阻塞,立即返回)
void push(const std::string& service, const std::string& level,
const std::string& message,
std::optional<std::string> trace_id = std::nullopt,
std::optional<std::string> extra = std::nullopt) {
_queue.push(Entry{service, level, message,
std::move(trace_id), std::move(extra)});
}
void debug(const std::string& service, const std::string& message,
std::optional<std::string> trace_id = std::nullopt,
std::optional<std::string> extra = std::nullopt) {
push(service, "DEBUG", message, std::move(trace_id), std::move(extra));
}
void info(const std::string& service, const std::string& message,
std::optional<std::string> trace_id = std::nullopt,
std::optional<std::string> extra = std::nullopt) {
push(service, "INFO", message, std::move(trace_id), std::move(extra));
}
void warning(const std::string& service, const std::string& message,
std::optional<std::string> trace_id = std::nullopt,
std::optional<std::string> extra = std::nullopt) {
push(service, "WARNING", message, std::move(trace_id), std::move(extra));
}
void error(const std::string& service, const std::string& message,
std::optional<std::string> trace_id = std::nullopt,
std::optional<std::string> extra = std::nullopt) {
push(service, "ERROR", message, std::move(trace_id), std::move(extra));
}
void critical(const std::string& service, const std::string& message,
std::optional<std::string> trace_id = std::nullopt,
std::optional<std::string> extra = std::nullopt) {
push(service, "CRITICAL", message, std::move(trace_id), std::move(extra));
}
// 手动 flush:通知后台线程立即发送当前积攒的批次,并等待其完成
void flush() {
_queue.flush();
}
// 停止后台线程并 flush 剩余日志(进程退出前调用,幂等)
void shutdown() {
_queue.shutdown();
}
// ---- 配置(构造后、首次推送前可改)------------------------------------
void set_endpoint(const std::string& addr, int port) {
std::lock_guard<std::mutex> lock(_endpoint_mtx);
_db_addr = addr;
_db_port = port;
}
private:
LogC()
: _queue(
[this](std::vector<Entry>&& batch) { this->_send_batch(std::move(batch)); },
{_BATCH_SIZE, _FLUSH_INTERVAL, _MAX_QUEUE_SIZE, 0.5},
[](const std::exception& e, std::vector<Entry>&&) {
std::cerr << "[LogC] 发送失败: " << e.what() << std::endl;
})
{
_queue.start();
}
~LogC() { shutdown(); }
LogC(const LogC&) = delete;
LogC& operator=(const LogC&) = delete;
// 通过 pymsg 使用 execute 路由逐条 INSERT 到 dbv2 Logs 表
void _send_batch(const std::vector<Entry>& batch) {
std::string addr;
int port;
{
std::lock_guard<std::mutex> lock(_endpoint_mtx);
addr = _db_addr;
port = _db_port;
}
const std::string now = _utc_now_iso(); // 批次共享同一时间戳(对齐 Python 版)
try {
pmc::net::MsgClient client(addr, port, "ca-log");
for (const auto& e : batch) {
json::object values;
values["ts"] = now;
values["service"] = e.service;
values["level"] = e.level;
values["message"] = e.message;
values["trace_id"] = e.trace_id ? json::value(*e.trace_id)
: json::value(nullptr);
values["extra"] = e.extra ? json::value(*e.extra)
: json::value(nullptr);
json::object payload;
payload["_route"] = "execute";
payload["action"] = "insert";
payload["table"] = "Logs";
payload["values"] = std::move(values);
const std::string resp = client.call(
json::serialize(payload), _TIMEOUT,
/*retry_on_refused=*/false); // 日志是 fail-safe,db 不可用时静默丢弃,绝不阻塞重试
auto obj = json::parse(resp).as_object();
if (!obj.at("success").as_bool()) {
std::string err = "unknown";
if (auto* ev = obj.if_contains("error")) {
if (ev->is_string()) err = ev->as_string().c_str();
}
std::cerr << "[LogC] INSERT 失败: " << err << std::endl;
}
}
} catch (const std::exception& e) {
// fail-safe:db 不可用时静默丢弃,绝不影响业务
std::cerr << "[LogC] 发送失败: " << e.what() << std::endl;
}
}
// UTC ISO8601 时间戳(对齐 Python datetime.now(timezone.utc).isoformat())
// 形如 2026-07-29T08:30:00.123456+00:00,字典序即时间序,便于 db 按 ts 清理
static std::string _utc_now_iso() {
const auto now = std::chrono::system_clock::now();
const auto t = std::chrono::system_clock::to_time_t(now);
const auto us = std::chrono::duration_cast<std::chrono::microseconds>(
now.time_since_epoch()).count() % 1000000;
std::tm tm{};
::gmtime_r(&t, &tm);
char buf[40];
std::snprintf(buf, sizeof buf,
"%04d-%02d-%02dT%02d:%02d:%02d.%06d+00:00",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, static_cast<int>(us));
return buf;
}
// ---- 配置项 -----------------------------------------------------------
std::string _db_addr = "127.0.0.1";
int _db_port = 8005;
std::mutex _endpoint_mtx; // 保护 _db_addr/_db_port 读写(set_endpoint 与后台线程并发)
static constexpr size_t _BATCH_SIZE = 50; // 队列满 50 条时 flush
static constexpr double _FLUSH_INTERVAL = 2.0; // 超时 2 秒强制 flush
static constexpr size_t _MAX_QUEUE_SIZE = 10000; // 队列上限,满时丢最旧
static constexpr double _TIMEOUT = 5.0; // socket 超时(秒)
// ---- 核心队列 ---------------------------------------------------------
pmc::net::AsyncQueue<Entry> _queue;
};
} // namespace qing
+2463
View File
File diff suppressed because it is too large. Load diff
+51
View File
@@ -0,0 +1,51 @@
[Server]
# CA(Central Auth)前置认证服务:接管原 gateway 的 8443 端口,
# WAF upstream 不变(waf → ca → gateway 前置拓扑)。
# - tls = off 启用 HTTP 模式(配合 waf 模块使用)
# - tls = on 启用 HTTPS 模式(独立部署,证书为 full_chain.crt / server.key)
address = 127.0.0.1
port = 8443
tls = off
[Upstream]
# 前置拓扑的唯一上游:认证通过后转发到 gateway(仅 loopback)
# 环境变量 UPSTREAM_GATEWAY 可覆盖
gateway = http://127.0.0.1:8445
[Token]
# Token 有效期(秒),默认 3600 = 1 小时
keep_time = 3600
# Token 刷新宽限期倍数:过期后 iat + keep_time * 倍数 窗口内仍可刷新,默认 2
refresh_grace_multiplier = 2
# Token 生命周期内最大刷新次数,超过后需重新登录,默认 10
max_refresh = 10
[Admin]
# 管理员账号白名单(分号分隔)。登录时命中即在 Token payload 写入 is_admin=true,
# 下游服务(dnfs 等)凭该字段豁免所有权校验,无需各自维护名单。
# 修改后需重启 ca 生效;旧 Token 无此字段,按非管理员处理。
users = 1776177662@qq.com;277665039@qq.com;
[CORS]
# 允许跨域的 Origin 白名单(分号分隔)
# 仅命中的 Origin 才会回填 Access-Control-Allow-Origin;缺省为空(不回填)
allowed_origins = https://qsont.me;https://www.qsont.me;http://127.0.0.1:8443;http://localhost:8443
[WsProxy]
# WebSocket 反向代理路由(仅管理员可访问)
# 格式:routes = /path=ws://host:port/target;/path2=ws://host2:port2/target2
# 握手阶段会校验 HTTP Upgrade 请求中的 Token(Authorization: Bearer xxx 或 Cookie: q4_token=xxx),
# 仅 Token 中 is_admin=true 的用户才能升级连接;非管理员或无 Token 直接拒绝。
# 支持 ws:// 和 wss:// 后端
# 示例:routes = /ws/aibox=ws://127.0.0.1:8765;/ws/other=wss://example.com/ws
routes = /ws/aibox=ws://127.0.0.1:8765;
[Downstream]
# 下游 pymsg 服务地址(其他微服务的 host:port)
# 环境变量可覆盖:DB_HOST/DB_PORT, CAPTCHA_HOST/CAPTCHA_PORT, SMTP_HOST/SMTP_PORT, CONFS_HOST/CONFS_PORT
db_host = 127.0.0.1
db_port = 8005
captcha_host = 127.0.0.1
captcha_port = 8009
smtp_host = 127.0.0.1
smtp_port = 8008
+8 -41
View File
@@ -1,9 +1,11 @@
[Server] [Server]
# 当 WAF 在前面终止 TLS 时,gateway 退化为内部 HTTP 服务: # v0.9.0:认证独立化(waf → ca → gateway 前置拓扑)
# - tls = off 启用 HTTP 模式(配合 waf 模块使用) # gateway 退化为纯转发服务,仅 loopback 监听(启动强制自检,非 loopback 拒绝启动):
# - tls = on 启用 HTTPS 模式(独立部署,证书为 full_chain.crt / server.key) # - 8443 由 ca 接管(WAF upstream 不变),gateway 退居 8445
# - 认证/验签/限流/WS 代理鉴权已全部迁至 ca(confs/configs/ca.ini)
# - tls = off 启用 HTTP 模式(ca 前置后的内部链路)
address = 127.0.0.1 address = 127.0.0.1
port = 8443 port = 8445
tls = off tls = off
[Proxy] [Proxy]
@@ -11,43 +13,8 @@ default = http://127.0.0.1:9200
closed = http://127.0.0.1:8005;http://127.0.0.1:8007; closed = http://127.0.0.1:8005;http://127.0.0.1:8007;
opened = http://127.0.0.1:9200;http://127.0.0.1:8006;http://127.0.0.1:5200; opened = http://127.0.0.1:9200;http://127.0.0.1:8006;http://127.0.0.1:5200;
[Token]
# Token 有效期(秒),默认 3600 = 1 小时
keep_time = 3600
# Token 刷新宽限期倍数:过期后 iat + keep_time * 倍数 窗口内仍可刷新,默认 2
refresh_grace_multiplier = 2
# Token 生命周期内最大刷新次数,超过后需重新登录,默认 10
max_refresh = 10
[Admin]
# 管理员账号白名单(分号分隔)。登录时命中即在 Token payload 写入 is_admin=true,
# 下游服务(dnfs 等)凭该字段豁免所有权校验,无需各自维护名单。
# 修改后需重启 gateway 生效;旧 Token 无此字段,按非管理员处理。
users = 1776177662@qq.com;277665039@qq.com;
[CORS]
# 允许跨域的 Origin 白名单(分号分隔)
# 仅命中的 Origin 才会回填 Access-Control-Allow-Origin;缺省为空(不回填)
allowed_origins = https://qsont.me;https://www.qsont.me;http://127.0.0.1:8443;http://localhost:8443
[Database]
path = sqlite3.db
[WsProxy]
# WebSocket 反向代理路由(仅管理员可访问)
# 格式:routes = /path=ws://host:port/target;/path2=ws://host2:port2/target2
# 握手阶段会校验 HTTP Upgrade 请求中的 Token(Authorization: Bearer xxx 或 Cookie: q4_token=xxx),
# 仅 Token 中 is_admin=true 的用户才能升级连接;非管理员或无 Token 直接拒绝。
# 支持 ws:// 和 wss:// 后端
# 示例:routes = /ws/aibox=ws://127.0.0.1:8765;/ws/other=wss://example.com/ws
routes = /ws/aibox=ws://127.0.0.1:8765;
[Downstream] [Downstream]
# 下游 pymsg 服务地址(其他微服务的 host:port) # 日志推送端点(db pymsg 服务)
# 环境变量可覆盖:DB_HOST/DB_PORT, CAPTCHA_HOST/CAPTCHA_PORT, SMTP_HOST/SMTP_PORT, CONFS_HOST/CONFS_PORT # 环境变量可覆盖:DB_HOST/DB_PORT, CONFS_HOST/CONFS_PORT
db_host = 127.0.0.1 db_host = 127.0.0.1
db_port = 8005 db_port = 8005
captcha_host = 127.0.0.1
captcha_port = 8009
smtp_host = 127.0.0.1
smtp_port = 8008
+1
View File
@@ -5,6 +5,7 @@ bash /opt/micro_servers/dnfs2/_run.sh
bash /opt/micro_servers/smtp/_run.sh bash /opt/micro_servers/smtp/_run.sh
bash /opt/micro_servers/fsys/_run.sh bash /opt/micro_servers/fsys/_run.sh
bash /opt/micro_servers/waf/_run.sh bash /opt/micro_servers/waf/_run.sh
bash /opt/micro_servers/ca/_run.sh
bash /opt/micro_servers/gateway/_run.sh bash /opt/micro_servers/gateway/_run.sh
bash /opt/micro_servers/captcha2/_run.sh bash /opt/micro_servers/captcha2/_run.sh
bash /opt/micro_servers/aibox/_run.sh bash /opt/micro_servers/aibox/_run.sh
+265 -283
View File
@@ -1,136 +1,146 @@
# 权限系统独立化计划书 # 认证独立化(CA 模块)开发文档
## 一、现状分析 **日期**: 2026-08-09(v2 重写)
**背景**: gateway v0.8.0 单文件 [main.cpp](../../gateway/src/main.cpp)(2470 行),认证逻辑(约 65%)与反向代理耦合;core/dnfs2 通过 HTTP 回调 gateway 验签,gateway 既是流量入口又是认证权威,形成环形依赖
**范围**: `ca`(新模块)、`gateway`(瘦身为纯代理);**WAF / core / dnfs2 / static / 前端零改动**
**前版**: 2026-08-08 初版计划书(auth 双栈方案)。本版按已选定拓扑重写:**数据流 `waf → ca → gateway`,gateway 不再验签**。原双栈/后置方案见 §2.2 决策记录
### 1.1 当前架构 ---
## 0. 摘要
新建 CA(Central Auth)模块:C++20 服务,接管 gateway 当前监听的 `127.0.0.1:8443`(WAF upstream 不变),承载**全部认证面**(登录/注册/Token 生命周期/CAPTCHA 代理/限流/WS 代理),并作为**认证前置层**:对业务请求本地验签后注入可信身份头,转发给退居 `127.0.0.1:8445` 的 gateway。gateway 删除全部认证代码,不再验签,仅保留白名单反代。
核心收益:验签在 CA 本地完成(RSA 密钥同进程,**零新增网络跳**);core/dnfs2 仍调 `8443` 的 `/admin/verify`、`/whoami`,**无感知切换**;前端 URL 不变;现有黑盒 34 断言原样充当验收门。
---
## 1. 现状分析
### 1.1 当前调用链
``` ```
客户端 ──HTTPS──▶ WAF:443 ──HTTP──▶ gateway:8443 ──HTTP──▶ 下游服务 客户端 ──HTTPS──▶ WAF:443 ──HTTP──▶ gateway:8443 ──HTTP──▶ static:9200 / dnfs2:8006 / core:5200
( auth + proxy + captcha + rate-limit + ws-proxy ) │ ├─ 本地验签(RSA 内存密钥)+ Cookie 回退 + Authorization 注入
│ │ ├─ /login /register /logout /refresh_token /whoami /admin/verify
┌───────────────┼───────────────┐ │ ├─ /register/send_code /reset/send_code /reset_password
▼ ▼ ▼ │ ├─ GET /captcha/image(pymsg 代理 captcha2:8009)
db:8005 captcha:8009 smtp:8008 │ └─ WS 代理 /ws/aibox(握手验签 is_admin)
(Users表) (图形验证码) (邮件发送) └── pymsg: db:8005 / captcha2:8009 / smtp3:8008 / confs:8010
下游鉴权调用链: 下游验签回调(HTTP 回环):
core:5200 ──HTTP POST──▶ gateway:/admin/verify (管理员验签) core:5200 ──HTTP──▶ gateway:8443/admin/verify (HttpTask.cpp,GATEWAY_HOST/GATEWAY_PORT 可配,fail-closed)
dnfs2:8006 ──HTTP POST──▶ gateway:/whoami (Token 验签) dnfs2:8006 ──HTTP──▶ gateway:8443/whoami (gateway_url 配置项,fail-closed)
``` ```
### 1.2 Gateway 职责清单(main.cpp 2508 行,单文件) ### 1.2 gateway 职责盘点与归属
| 职责域 | 函数/组件 | 行数占比 | | 职责域 | 函数/组件 | 归属 |
|--------|-----------|---------| |--------|-----------|------|
| **用户认证** | handleLogin, handleRegister, handleLogout, handleResetPassword, handleSendEmailCode, handleResetSendCode, PBKDF2Hasher, PasswordValidator | ~40% | | 用户认证 | `handleLogin` / `handleRegister` / `handleLogout` / `handleResetPassword` / `handleSendEmailCode` / `handleResetSendCode` | → CA |
| **Token 管理** | verify_token, generate_jti, revoke_jti, revoke_all_user_tokens, handleRefreshToken, handleWhoami, handleAdminVerify, TokenInfo, JtiDenylist, UserVerMap | ~25% | | Token 管理 | `verify_token` / `generate_jti` / `revoke_jti` / `revoke_all_user_tokens` / `handleRefreshToken` / `handleWhoami` / `handleAdminVerify` / jti 黑名单 / 用户版本号表 / RSA 密钥对 | → CA |
| **CAPTCHA** | handleCaptcha, handleCaptchaImage, CaptchaEntry, generate_captcha_text, verify_captcha | ~8% | | 密码体系 | `PBKDF2Hasher` / `PasswordValidator` / 防枚举 dummy 校验 | → CA |
| **速率限制** | 8 个 RateLimiter 实例(login/register/email/refresh/reset 等) | ~5% | | 验证码/邮箱 | `handleCaptchaImage` / `verify_captcha_remote` / `verify_email_code_remote` | → CA |
| **HTTP 反向代理** | handleProxy, is_special_header, is_safe_target | ~12% | | 速率限制 | 全部 RateLimiter 实例(登录 5/5min、注册 3/10min、发码 5/10min + 邮箱级 3/h+10/d、验证码 10/min 等) | → CA |
| **WebSocket 代理** | verifyWsAdminToken, ws_proxy 路由注册 | ~5% | | WS 代理 | `verifyWsAdminToken` + `_WS_PROXY_ROUTES` 注册循环 | → CA(验签+桥接一体) |
| **配置/基础设施** | main(), registerRoutes(), ConfIni, DbC, LogC, 信号处理 | ~5% | | Cookie | `q4_token` 签发/清除、`extract_token_from_request`(Cookie 回退)、代理时 `Authorization: Bearer` 注入 | → CA |
| HTTP 反代 | `handleProxy`(白名单/路径安全/头转发/XFF) | 留 gateway(去验签化) |
| 头卫生 | `is_special_header` / `is_safe_target` | 留 gateway |
| 基础设施 | `wrapperHttp` / `ConfIni` / `DbC` / `LogC` / 信号处理 / 优雅关闭 | 两侧各持一份(模板见 smtp3) |
### 1.3 核心问题 ### 1.3 核心问题
1. **职责严重耦合**:auth(~65%代码量)与 proxy(~17%)捆绑在同一进程,任一域的重构/扩展都影响全局 1. **职责耦合**:认证与代理同进程,任一域改动爆炸半径为全文件
2. **不可独立扩展**:认证密集型场景(批量登录/注册)和代理密集型场景(大文件传输)无法分别扩容 2. **环形依赖**:gateway 是流量入口又是验签权威,core/dnfs2 须 HTTP 回调入口
3. **安全隔离缺失**:密码哈希、RSA 私钥、Token 黑名单等敏感数据与代理逻辑共存于同一进程内存空间 3. **敏感面混杂**:RSA 私钥、密码哈希校验、jti 黑名单与转发逻辑共用内存空间
4. **下游耦合 gateway**:core/dnfs2 通过 HTTP 回调 gateway 做验签,gateway 既是流量入口又是认证权威,形成环形依赖 4. **不可独立扩展**:认证密集与代理密集负载无法分别扩容;认证状态纯内存,绑定单实例
5. **单文件维护瓶颈**:2508 行 C++ 单文件,所有功能交叉耦合,测试和 code review 成本高
6. **Token 状态纯内存**:jti 黑名单和版本号在 gateway 重启后丢失(虽然 RSA 密钥同步重建可兜底,但限制了多实例部署)
--- ---
## 二、目标架构 ## 2. 目标架构
### 2.1 拓扑
``` ```
客户端 ──HTTPS──▶ WAF:443 ──HTTP──▶ gateway:8443 ──HTTP──▶ 下游服务 客户端 ──HTTPS──▶ WAF:443 ──HTTP──▶ CA:8443 ──HTTP──▶ gateway:8445 ──▶ static:9200 / dnfs2:8006 / core:5200
( 纯反向代理 + Token 透传 ) │
│ ├─ 本地处理:/login /register /logout /refresh_token /whoami /admin/verify
▼ │ /register/send_code /reset/send_code /reset_password
auth:8444 (独立权限服务) │ GET /captcha/image
┌────┼────┐ ├─ WS:/ws/aibox 握手验签(is_admin) → 直接桥接 ws://127.0.0.1:8765
▼ ▼ ▼ ├─ 其余路径:本地验签 → 注入 X-Auth-User/Authorization → 转发 gateway:8445
db:8005 captcha smtp └─ pymsg: db:8005 / captcha2:8009 / smtp3:8008 / confs:8010
下游鉴权调用链(改造后): 端口安排:CA 接管 8443(WAF upstream 配置不变);gateway 迁 8445(仅 loopback,仅 CA 可达)。
core:5200 ──pymsg──▶ auth:8444:/verify (管理员验签)
dnfs2:8006 ──pymsg──▶ auth:8444:/whoami (Token 验签)
gateway ──pymsg──▶ auth:8444:/verify (代理转发前的 Token 校验)
``` ```
### 2.1 职责划分 职责划分:**CA = 认证权威 + 认证前置代理**;**gateway = 纯路由/转发策略**(opened 白名单、路径安全、XFF 透传)。
| 模块 | 改造后职责 | ### 2.2 决策记录
|------|-----------|
| **auth(新)** | 用户注册/登录/注销、Token 签发/验签/刷新/吊销、CAPTCHA、密码重置、邮箱验证码、速率限制、管理员验证 | | 方案 | 拓扑 | 结论 |
| **gateway(精简)** | HTTP/WebSocket 反向代理、Target-Url 白名单过滤、XFF 注入、CORS 预检;认证路由(/login, /register 等)作为代理目标指向 auth | |------|------|------|
| **core / dnfs2** | 验签调用目标由 gateway 改为 auth(pymsg 协议,消除 HTTP 回调) | | **选定:CA 前置** | `waf → ca → gateway` | ✅ 验签在 CA 本地(零网络跳);core/dnfs2 调 8443 不变;WAF/前端不变;gateway 无需新增路径路由机制 |
| 否决 A:CA 后置双栈 | `waf → gateway → ca`(HTTP+pymsg 双栈) | gateway 需新增路径级路由表;CORS/Set-Cookie/错误映射下沉 CA;两服务改动面都大 |
| 否决 B:CA 后置纯 pymsg | `waf → gateway →(pymsg) ca` | gateway 保留 HTTP↔pymsg 翻译层与 Cookie 职责,认证面仍在入口进程内,瘦身不彻底;且每请求多一跳 pymsg 验签或引入吊销延迟的缓存 |
| 否决 C:验签缓存 | gateway 缓存验签结果 60s | logout/改密后 Token 在窗口内仍可用,吊销语义弱化(随拓扑选定自然消失) |
### 2.3 信任模型(重要)
1. **loopback 信任基线不变**:本架构 loopback 内部服务本就不互鉴权(db:8005 execute 无认证即先例)。gateway:8445 仅绑 `127.0.0.1`,与本基线一致。
2. **身份头契约(strip-then-set)**:CA 在边缘剥离客户端伪造的 `X-Auth-User` / `X-Auth-Admin`(Beast fields 大小写不敏感,`erase` 后 `set`),验签通过才注入。模式 A 下 CA 的唯一上游是 WAF(loopback)。
3. **gateway 存在性不变量**:显式 `Target-Url` 请求必须携带 `X-Auth-User`,否则 401(非验签,仅防线编程,防本地误直连/SSR​​F 兜底)。
4. **模式 B 警示迁移**:CA 直连公网(无 WAF)时 XFF 可被伪造、限流可绕过——原 gateway README 的 A4 降级提示随限流一并迁入 CA README;根治方案 [libhttp_remote_addr_dev_doc.md](libhttp_remote_addr_dev_doc.md) 中的 `trust_xff`/`rate_limit_key()` 配套改在 CA 落地。
--- ---
## 三、实施阶段 ## 3. CA 模块设计
### 阶段一:auth 服务搭建(核心剥离) ### 3.1 目录与构建(沿用 smtp3/captcha2 约定)
**目标**:将认证逻辑整体迁入独立 C++ 服务
**新建目录结构**:
``` ```
auth/ ca/
├── src/ ├── src/main.cpp # 入口 + HTTP 路由 + 认证逻辑 + 前置转发(迁移代码为主)
│ └── main.cpp # 入口 + 路由注册
├── inc/ ├── inc/
│ ├── AuthService.hpp # 核心认证逻辑(PBKDF2、Token 签发/验签/吊销) │ ├── Conf.hpp # ConfIni(confs 拉取 + env 覆盖 + 完整性校验)
│ ├── RateLimiter.hpp # 速率限制器(从 gateway 迁移) │ ├── DbC.hpp # 自 gateway 随迁(已基于 libmsg us/Msg.hpp;client 名 "gateway"→"ca")
│ ├── CaptchaService.hpp # CAPTCHA 生成/验证 │ ├── LogC.hpp # 异步日志(smtp3 长连接版)
│ ├── DbC.hpp # 复用 gateway 的 DbC(提取为公共库或复制) │ ├── ExpBase.hpp # 异常基类
│ ├── LogC.hpp # 复用 LogC │ └── LocalTime.hpp # 时间工具
│ ├── Conf.hpp # 复用 ConfIni ├── Makefile # 链接 us_http / us_msg / us_crypt / ssl / crypto / boost_json / curl
│ ├── LocalTime.hpp # 复用 ├── _set.sh / _run.sh
│ └── ExpBase.hpp # 复用
├── Makefile
├── _run.sh
├── _set.sh
└── README.md └── README.md
``` ```
**迁移清单**(从 gateway/src/main.cpp 提取): - 二进制 `ca.bin`;版本从 `1.0.0` 起;日志服务名 `ca`
- 依赖:libhttp(HttpServer/HttpClient/ws_proxy)、libmsg(MsgClient/RpcClient)、libcrypt(MyRSA/Base64)、OpenSSL(PBKDF2/RAND_bytes)、boost::json/program_options/property_tree、libcurl
- **不依赖 libsqlite3**:Users 表访问全部经 db:8005(现状即如此)
| 组件 | 源位置 | 目标文件 | ### 3.2 配置(`confs/configs/ca.ini` 新增)
|------|--------|---------|
| PBKDF2Hasher | main.cpp L62-85 | AuthService.hpp |
| PasswordValidator | main.cpp L87-160 | AuthService.hpp |
| TokenInfo / generate_jti / verify_token / revoke_* | main.cpp L340-760 | AuthService.hpp |
| handleLogin | main.cpp L1346-1525 | main.cpp (auth) |
| handleRegister | main.cpp L1891-1981 | main.cpp (auth) |
| handleLogout | main.cpp L1527-1554 | main.cpp (auth) |
| handleWhoami | main.cpp L1556-1606 | main.cpp (auth) |
| handleAdminVerify | main.cpp L1608-1657 | main.cpp (auth) |
| handleRefreshToken | main.cpp L1672-1837 | main.cpp (auth) |
| handleSendEmailCode | main.cpp ~L1983-2080 | main.cpp (auth) |
| handleResetSendCode | main.cpp ~L2080-2170 | main.cpp (auth) |
| handleResetPassword | main.cpp ~L2170-2308 | main.cpp (auth) |
| handleCaptcha / handleCaptchaImage | main.cpp ~L1158,1845-1890 | CaptchaService.hpp |
| RateLimiter 实例(8 个) | main.cpp L417-427 | RateLimiter.hpp |
| CAPTCHA_STORE | main.cpp L438-499 | CaptchaService.hpp |
| JTI_DENYLIST / USER_VER_MAP | main.cpp L447-465 | AuthService.hpp |
**新增配置**(`confs/configs/auth.ini`):
```ini ```ini
[Server] [Server]
address = 127.0.0.1 address = 127.0.0.1
port = 8444 port = 8443 # 接管 gateway 现端口,WAF upstream 不变
[Token] [Upstream]
gateway = http://127.0.0.1:8445 # 业务请求唯一上游
[Token] # 自 gateway.ini 原样迁入
keep_time = 3600 keep_time = 3600
refresh_grace_multiplier = 2 refresh_grace_multiplier = 2
max_refresh = 10 max_refresh = 10
[Admin] [Admin] # 原样迁入
users = 1776177662@qq.com;277665039@qq.com; users = 1776177662@qq.com;277665039@qq.com;
[Downstream] [WsProxy] # 自 gateway.ini 迁入
routes = /ws/aibox=ws://127.0.0.1:8765;
[CORS] # 原样迁入(P2-3:预检反射由 libhttp 处理,白名单暂仍不生效)
allowed_origins = https://qsont.me;https://www.qsont.me;http://127.0.0.1:8443;http://localhost:8443
[Downstream] # 原样迁入
db_host = 127.0.0.1 db_host = 127.0.0.1
db_port = 8005 db_port = 8005
captcha_host = 127.0.0.1 captcha_host = 127.0.0.1
@@ -139,235 +149,207 @@ smtp_host = 127.0.0.1
smtp_port = 8008 smtp_port = 8008
``` ```
**对外接口**(pymsg TCP 协议 + HTTP 双栈): 环境变量覆盖(与 gateway 现机制一致):`LISTEN_ADDR`/`LISTEN_PORT`、`UPSTREAM_GATEWAY`、`DB_*`、`CAPTCHA_*`、`SMTP_*`、`CONFS_HOST`/`CONFS_PORT`。配置加载:confs `get` 拉取(retry_on_refused 阻塞)→ env 覆盖 → 完整性校验。
| 路由 | 协议 | 说明 | ### 3.3 HTTP 路由表(对外契约与原 gateway 完全一致)
| 路由 | 处理 | 说明 |
|------|------|------| |------|------|------|
| POST /login | HTTP | 登录(兼容前端直访) | | POST `/login` | 本地 | 强制远端 CAPTCHA;dummy PBKDF2 防枚举;签发 Token + `q4_token` Cookie |
| POST /register | HTTP | 注册 | | POST `/register` | 本地 | 密码强度→邮箱格式→CAPTCHA→邮箱验证码(smtp fail-closed)→写 Users |
| POST /logout | HTTP | 注销 | | POST `/register/send_code`、`/reset/send_code` | 本地 | IP+邮箱级限流、强制 CAPTCHA、smtp 60s 冷却 429 透传 |
| POST /refresh_token | HTTP | Token 刷新 | | POST `/reset_password` | 本地 | 防枚举恒成功文案;改密后版本号递增全设备吊销 |
| POST /whoami | HTTP | 身份查询 | | POST `/logout` | 本地 | jti 吊销 + 清 Cookie |
| POST /register/send_code | HTTP | 发送注册验证码 | | POST `/refresh_token` | 本地 | 宽限期刷新、旧 jti 吊销、max_refresh 上限 |
| POST /reset/send_code | HTTP | 发送重置验证码 | | POST `/whoami` | 本地 | 恒 200,`{login:false}` 不泄露细节;core/dnfs2 依赖此契约 |
| POST /reset_password | HTTP | 重置密码 | | POST `/admin/verify` | 本地 | fail-closed 401;core 依赖此契约 |
| POST /captcha | HTTP | 生成验证码 | | GET `/captcha/image` | 本地 | pymsg 调 captcha2,PNG + `X-Captcha-Token` + `no-store`,10/min/IP |
| GET /captcha/image | HTTP | 验证码图片代理 | | WS `[WsProxy]` 路径 | 本地 | 握手验签 `is_admin=true` → 桥接后端(ws/wss),双向透传 |
| verify | pymsg | 内部验签(替代 /admin/verify,供 core/dnfs2/gateway 调用) | | GET/POST `*`(通配) | **转发** | 见 §3.4 |
| whoami | pymsg | 内部身份查询(供 dnfs2 调用) |
### 阶段二:gateway 瘦身 ### 3.4 前置转发层(通配路由 → gateway:8445)
**目标**:gateway 仅保留反向代理 + WebSocket 代理 迁移 `handleProxy` 的转发骨架,认证语义前置:
**保留内容**: 1. **路径安全**:`is_safe_target()` 校验(`..`/`%2e`/`%2f`/`%5c`/空字节 → 400),CA/gateway 双层各执行一次(防线叠加,gateway 侧保留原校验)。
- `handleProxy`(HTTP 反代 + Token 透传) 2. **公开 vs 受保护**:无 `Target-Url` 头 → 公开目标(Proxy.default 语义在 gateway),**剥离**客户端 `X-Auth-User`/`X-Auth-Admin` 后直接转发,不注入任何凭证。
- WebSocket 代理路由注册 + `verifyWsAdminToken`(改为调 auth 的 pymsg verify 接口) 3. **受保护**:有 `Target-Url` → 提取 Token(`Authorization` 优先,回退 `q4_token` Cookie)→ `verify_token` 本地验签(RSA→过期→jti→版本号)→ 失败按现契约 400/401,**不转发**;成功则注入:
- `is_special_header` / `is_safe_target` / `get_cookie_value` - `Authorization: Bearer <token>`(Cookie 来源也统一补齐,dnfs2 等下游零改动)
- 配置加载、信号处理、LogC - `X-Auth-User: <email>`、`X-Auth-Admin: true`(仅管理员;供 gateway 不变量与下游未来免回调使用)
4. **头卫生**:剥离客户端自带的 `X-Auth-User`/`X-Auth-Admin`(strip-then-set);XFF 透传 WAF 值(缺省 `unknown`);`Host`/`connection`/`Accept-Encoding` 等逐跳头处理沿用现 `is_special_header` 集合 + `x-auth-user`/`x-auth-admin` 新增。
5. **转发失败语义**:gateway:8445 不可达 → 502(与现 captcha/smtp fail-closed 风格一致),记 warning 日志。
6. **响应**:状态码与响应头原样回传(现 `handleProxy` 行为),`Connection: close`。
**删除内容**: ### 3.5 迁移资产清单(gateway `main.cpp` → CA `main.cpp`)
- 所有 auth handler(handleLogin ~ handleResetPassword)
- PBKDF2Hasher / PasswordValidator / TokenInfo / JTI_DENYLIST / USER_VER_MAP 代码以**整体搬迁**为主,保持实现逐字不变(降低回归面),仅做两处适配:日志服务名 `gateway`→`ca`;DbC client 名同上。
- CAPTCHA 相关代码
- 8 个 RateLimiter 实例(迁移到 auth,auth 路由不再经过 gateway 限流) | 资产 | 备注 |
- RSA 密钥对生成(`_KEYGEN`,迁移到 auth) |------|------|
| PBKDF2Hasher / PasswordValidator / 常数时间比较 | 含 SHA256 旧哈希迁移注释块(保持注释状态) |
| TokenInfo / verify_token / generate_jti / revoke_jti / revoke_all_user_tokens / jti 黑名单(5000 上限清理)/ 用户版本号表 | RSA 密钥对启动内存生成(重启即全吊销,语义不变) |
| 8 个 RateLimiter 实例 + 邮箱级双窗口限流 | 限流键仍取 XFF(模式 A 可信;模式 B 警示见 §2.3-4) |
| handleLogin ~ handleResetPassword 全部 handler | 错误文案/状态码契约逐字保留(黑盒断言依赖) |
| handleWhoami / handleAdminVerify / handleRefreshToken / handleLogout | 同上 |
| handleCaptchaImage / verify_captcha_remote / verify_email_code_remote | fail-closed 语义保留 |
| verifyWsAdminToken + ws_proxy 注册 | 桥接目标直接为后端(不再经 gateway) |
| handleProxy 转发骨架 / is_safe_target / is_special_header / get_cookie_value / extract_token_from_request | 按 §3.4 改造 |
| wrapperHttp / 信号处理 / 优雅关闭(8s+3s 超时)/ ExpBase / ConfIni / LocalTime / DbC / LogC | 基础设施随迁 |
### 3.6 不变量自检(启动时)
- `[Upstream] gateway` 非空且为 `http://127.0.0.1:*`(防误配到公网)
- 模式 B 检测:`Server.address` 非 loopback 时启动日志打印 A4 安全降级警告
---
## 4. gateway 瘦身设计(v0.9.0)
### 4.1 删除清单(预计 -1700 行,2470 → ~750 行)
- 全部认证 handler 与 Token/PBKDF2/RSA/jti/版本号/限流/CAPTCHA/邮箱验证码代码
- `q4_token` Cookie 读写、`extract_token_from_request`、代理内验签与 Authorization 注入
- `[Token]`/`[Admin]`/`[WsProxy]`/`[Database]` 配置读取;下游 captcha/smtp 配置(db 保留供 LogC)
- `/login` 等 9 条认证路由 + `/captcha/image` 路由 + WS 代理注册循环
### 4.2 保留清单
- `handleProxy`(去验签化)+ `is_safe_target` + `is_special_header` + `wrapperHttp`
- 存在性不变量:显式 `Target-Url` 且无 `X-Auth-User` → 401(§2.3-3)
- 公开目标规则:无 `Target-Url` → `Proxy.default`,不校验身份头
- XFF 处理:透传 CA 值(缺省 `unknown`)
- 配置加载、信号处理、优雅关闭、LogC
### 4.3 精简后 `confs/configs/gateway.ini`
**gateway.ini 精简**:
```ini ```ini
[Server] [Server]
address = 127.0.0.1 address = 127.0.0.1
port = 8443 port = 8445 # 迁出 8443;仅 CA 可达
tls = off tls = off
[Proxy] [Proxy]
default = http://127.0.0.1:9200 default = http://127.0.0.1:9200
opened = http://127.0.0.1:9200;http://127.0.0.1:8006;http://127.0.0.1:5200; closed = http://127.0.0.1:8005;http://127.0.0.1:8007;
opened = http://127.0.0.1:9200;http://127.0.0.1:8006;http://127.0.0.1:5200;
[Auth] [Downstream] # 仅供 LogC 日志落库
host = 127.0.0.1 db_host = 127.0.0.1
port = 8444 db_port = 8005
[WsProxy]
routes = /ws/aibox=ws://127.0.0.1:8765;
[CORS]
allowed_origins = https://qsont.me;https://www.qsont.me
``` ```
**认证路由处理方案**(二选一): ### 4.4 启动自检
- **方案 A(推荐):gateway 代理认证路由** - `Server.address` 必须为 loopback(`127.0.0.1`/`::1`):gateway 不再验签后,绑定公网等于裸奔,**非 loopback 直接拒绝启动**并打印致命错误。
- gateway 将 `/login`、`/register` 等路径加入代理白名单,Target-Url 自动指向 auth 服务
- 前端请求路径不变:`POST /login` → WAF → gateway → auth
- 优势:前端零改动,gateway 仍是唯一入口
- **方案 B:auth 直暴露**
- WAF 配置分流规则,认证路径直接转发到 auth:8444
- 需改动 WAF 配置,增加路由复杂度
### 阶段三:下游服务适配
**目标**:core 和 dnfs2 的验签调用目标从 gateway 切换为 auth
#### 3.1 core 适配
**当前**(`core/src/main/HttpTask.cpp` L155-204):
```cpp
// HTTP POST → gateway:8443/admin/verify
std::string url = "http://" + host + ":" + std::to_string(port) + "/admin/verify";
pmc::net::HttpClient client;
client.set_header("Authorization: " + raw);
client.post_stream_sync(url, "", ...);
```
**改造后**:
```cpp
// pymsg → auth:8444 verify 路由(消除 HTTP 回调,统一内部协议)
pmc::net::MsgClient client(auth_host, auth_port, "core");
json::value req = {{"_route", "verify"}, {"token", raw_token}};
std::string resp = client.call(json::serialize(req), 5.0);
```
#### 3.2 dnfs2 适配
**当前**(`dnfs2/src/dnfs2.cpp` L446-495):
```cpp
// HTTP POST → gateway:8443/whoami
http.post_stream_sync(_cfg.gateway_url, "{}", ...);
```
**改造后**:
```cpp
// pymsg → auth:8444 whoami 路由
RpcClient auth_client(_cfg.auth_host, _cfg.auth_port, "dnfs2", timeout);
json::value result = auth_client.call("whoami", {{"token", token}});
```
**配置文件变更**(`confs/configs/dnfs.ini`):
```ini
[auth] # 新增,替代 [gateway]
host = 127.0.0.1
port = 8444
timeout = 5
```
### 阶段四:启动编排与部署
**core/init.txt 更新**:
```
bash /opt/micro_servers/db2/_run.sh
bash /opt/micro_servers/confs/_run.sh
bash /opt/micro_servers/auth/_run.sh # 新增,须在 gateway 之前启动
bash /opt/micro_servers/ai_review/_run.sh
bash /opt/micro_servers/dnfs2/_run.sh
bash /opt/micro_servers/smtp/_run.sh
bash /opt/micro_servers/static/_run.sh
bash /opt/micro_servers/waf/_run.sh
bash /opt/micro_servers/gateway/_run.sh
bash /opt/micro_servers/captcha/_run.sh
bash /opt/micro_servers/aibox/_run.sh
bash /opt/micro_servers/redirect80/_run.sh
```
**docker-compose.yml 新增 auth 服务**:
```yaml
auth:
build:
context: .
dockerfile: auth/Dockerfile
container_name: micro-auth
ports:
- "127.0.0.1:8444:8444"
environment:
- LISTEN_ADDR=0.0.0.0
- LISTEN_PORT=8444
- DB_HOST=db
- DB_PORT=8005
depends_on:
db:
condition: service_healthy
```
--- ---
## 四、接口契约设计 ## 5. 接口契约
### 4.1 auth 内部 pymsg 接口(供其他微服务调用) ### 5.1 对外 HTTP(不变量)
#### verify 路由(替代 /admin/verify) `https://<host>/login`、`/register`、`/whoami` 等全部 URL、请求/响应 JSON 结构、状态码(400/401/403/429/500/502)、`q4_token` Cookie 属性、`X-Captcha-Token` 头——**逐字不变**。前端、WAF、core、dnfs2 零改动的依据即此。
```json
// 请求
{"_route": "verify", "token": "base64payload_signature", "require_admin": true}
// 响应(成功) ### 5.2 内部头契约(CA ↔ gateway ↔ 下游)
{"success": true, "user": "admin@example.com", "is_admin": true}
// 响应(失败) | 头 | 方向 | 语义 |
{"success": false, "error": "token expired"} |----|------|------|
``` | `X-Auth-User` | CA→gateway→下游 | 已验签身份;CA strip-then-set;gateway 对显式 Target-Url 要求其存在 |
| `X-Auth-Admin` | CA→gateway→下游 | `true` 表示管理员;供下游免回调豁免(可选消费) |
| `Authorization` | CA→gateway→下游 | Cookie 来源统一补齐为 Bearer;下游解析不变 |
| `X-Forwarded-For` | WAF→CA→gateway→下游 | 逐层透传,缺省 `unknown` |
| `Target-Url` | 客户端→CA→gateway | 路由策略头,语义不变 |
#### whoami 路由(替代 /whoami) ### 5.3 Token 格式(不变)
```json
// 请求
{"_route": "whoami", "token": "base64payload_signature"}
// 响应(已登录) `Base64(payload)_RSA-Sign(payload)`;payload 字段(user/jti/ver/iat/created_at/keep_time/is_admin/refresh_count/max_refresh)不变。切换瞬间旧 Token 全部失效(CA 新密钥),用户重新登录——一次性事件,cutover 通告中说明。
{"success": true, "login": true, "user": "user@example.com", "is_admin": false}
// 响应(未登录/Token 无效) ### 5.4 下游零改动验证点
{"success": true, "login": false}
```
### 4.2 Token 格式不变 | 消费方 | 现状调用 | 切换后 |
|--------|----------|--------|
保持现有 `Base64(payload)_RSA-Sign(payload)` 格式,payload 字段不变: | core `verify_admin_token` | `http://127.0.0.1:8443/admin/verify` | 同 URL 由 CA 服务,契约不变 |
```json | dnfs2 Token 验签 | `http://127.0.0.1:8443/whoami` | 同上 |
{ | static `/api/check_user` | 直连 db 查 Users(账号旁路) | 不受影响;遗留项另行收口 |
"user": "email@example.com",
"jti": "random_hex_32",
"ver": 1,
"iat": "2026-08-08 12:00:00",
"created_at": "2026-08-08 12:00:00",
"keep_time": 3600,
"is_admin": true,
"refresh_count": 0,
"max_refresh": 10
}
```
--- ---
## 五、风险评估与缓解 ## 6. 上线步骤(cutover 与回滚)
| 风险 | 影响 | 缓解措施 | | 步骤 | 内容 |
|------|------|---------| |------|------|
| auth 服务不可用导致全系统鉴权失败 | 高 | auth 的 pymsg 客户端启用 `retry_on_refused`;gateway 对 auth 路由的代理失败返回 503(而非静默放行) | | 1 | CA 开发完成,`LISTEN_PORT=18443` 并行实例联调(不影响现网 8443) |
| 前端请求路径变化 | 中 | 采用方案 A(gateway 代理认证路由),前端零改动 | | 2 | gateway 瘦身版(v0.9.0)构建,`gateway.ini` 改 8445 |
| Token 验签增加一跳网络延迟 | 低 | pymsg TCP 本机通信延迟 <1ms;gateway 代理层可增加 Token 缓存(TTL 60s) | | 3 | cutover(秒级窗口):停 gateway:8443 → 启 CA:8443 → 启 gateway:8445 → 黑盒全量验证 |
| RSA 密钥迁移 | 中 | auth 启动时自生成密钥对(与当前 gateway 行为一致);过渡期可共享密钥文件 | | 4 | `core/init.txt` 增加 `ca/_run.sh`(gateway 之前);README/INDEX 文档同步 |
| 过渡期双系统并存 | 中 | 分阶段切换:先 auth 上线 → gateway 认证路由代理到 auth → 删除 gateway 内置 auth 代码 | | 5 | 观察 24h:CA/gateway 日志、db Logs 表 4xx/5xx 基线对比 |
| Token 黑名单持久化需求 | 低 | 初期保持纯内存方案(与现状一致);后续可选 Redis 持久化 |
**回滚**:停 CA → 旧 gateway 二进制 + 旧配置回 8443 → 重启。Token 全部重签(用户重新登录),无数据迁移。
--- ---
## 六、时间估算 ## 7. 测试计划
| 阶段 | 工作量 | 说明 | ### 7.1 验收门(零改动复用)
|------|--------|------|
| 阶段一:auth 服务搭建 | 2-3 天 | 代码迁移 + 新配置 + 测试 | - `tests/blackbox/test_gateway.py` 34 断言**原样**对 8443 执行:认证路径打到 CA、代理路径经 CA→gateway,外部契约不变则全绿(当前基线 32 passed / 2 env-skipped)
| 阶段二:gateway 瘦身 | 1 天 | 删除代码 + 代理路由配置 + 回归测试 | - 修复期两个红测试(A3 缺字段 400)已在 v0.8.0 转绿,迁移后必须保持
| 阶段三:下游适配 | 1 天 | core/dnfs2 验签调用改造 + 集成测试 |
| 阶段四:部署编排 | 0.5 天 | init.txt + docker-compose + 端到端测试 | ### 7.2 新增探针(test_ca.py,黑盒补充)
| **合计** | **4.5-5.5 天** | 含测试缓冲 |
| 探针 | 预期 |
|------|------|
| 客户端伪造 `X-Auth-User: admin@x` + 显式 Target-Url(无 Token) | CA 剥离后 401,不到下游 |
| 伪造 `X-Auth-User` + 有效非管理员 Token 访问 WS 路由 | 403(以验签结果为准,不以头为准) |
| 直连 gateway:8445,显式 Target-Url、无 `X-Auth-User` | 401(存在性不变量) |
| 经 CA 全链路(8443)带 Token 访问 dnfs2 | 下游收到 `X-Auth-User` 与 `Authorization` |
| CA 存活、gateway:8445 停止 | 业务路径 502(不静默放行),认证路径正常 |
| gateway 存活、CA 停止 | WAF 502(入口不可达,符合预期 fail-closed) |
| 公开目标(无 Target-Url)带伪造身份头 | 下游收不到 `X-Auth-User`/`Authorization` |
### 7.3 集成回归
- core HTTP 管理接口(`/admin/verify` 链路)实测;dnfs2 笔记 CRUD(`/whoami` 链路 + AI 审核)实测
- WS 代理端到端:管理员 Token 升级 `/ws/aibox` 双向帧;非管理员 403
- 限流全谱在 CA 复测(随机 XFF 技巧沿用);cutover/回滚演练各一次
### 7.4 测试资产演进(后续)
- `test_gateway.py` 按进程边界拆分为 `test_ca.py`(A/B/C/E 段)+ 精简 `test_gateway.py`(D 段代理矩阵 + 8445 直连探针),README/INDEX 同步
- 认证 happy path 仍依赖 captcha2/smtp 测试钩子(既有遗留,CA 落地后可仅在 CA 层加 `[Security] captcha_required` 开关,单点解锁)
--- ---
## 七、验收标准 ## 8. 风险与缓解
1. auth 服务独立启动,通过 pymsg 和 HTTP 双栈提供全部认证接口 | 风险 | 等级 | 缓解 |
2. gateway 代码量从 ~2500 行降至 ~800 行,仅含代理逻辑 |------|------|------|
3. 前端所有认证操作(登录/注册/注销/刷新/重置密码)路径不变、行为不变 | CA 不可用 → 全站入口中断 | 高 | CA 职责内聚后可用性等同于现 gateway;core 守护 + 黑盒监控;回滚方案秒级 |
4. core/dnfs2 通过 pymsg 调用 auth 验签,延迟 ≤ 5ms(本机) | 迁移遗漏导致认证行为漂移 | 中 | 代码逐字搬迁 + 34 断言验收门 + §7.2 探针矩阵 |
5. auth 服务重启不影响 gateway 代理转发(已认证请求在 Token 过期前仍有效) | 身份头伪造/泄露 | 中 | strip-then-set + gateway 存在性不变量 + 公开目标不注入 + 直连探针回归 |
6. 所有现有安全特性(PBKDF2、RSA 签名、常数时间比较、jti 黑名单、版本号吊销)完整保留 | gateway:8445 被公网暴露 | 高 | 启动强制 loopback 自检(§4.4);部署审计 checklist 增加端口扫描项 |
| cutover 窗口 Token 全失效 | 低 | 预期内(RSA 换钥),通告用户重新登录 |
| 双重转发延迟 | 低 | 本机 HTTP 转发 <1ms;CA→gateway 短连接可后续优化为 keep-alive |
| 模式 B 限流绕过(A4 迁移) | 中 | CA README 降级警示;根治按 libhttp_remote_addr_dev_doc(配套改落 CA) |
---
## 9. 工作量估算
| 部分 | 规模 | 估时 |
|------|------|------|
| CA 脚手架(目录/Makefile/_set.sh/_run.sh/LogC 等) | 模板复制 + 适配 | 0.5 天 |
| 认证资产搬迁 + 前置转发层改造 | ~1700 行搬迁 + ~150 行改造 | 1.5 天 |
| gateway 瘦身 + 启动自检 | -1700 行 + ~20 行 | 0.5 天 |
| 配置/init.txt/文档(README×2、INDEX、confs) | — | 0.5 天 |
| 黑盒验收 + §7.2 探针 + 集成回归 + cutover 演练 | — | 1 天 |
| **合计** | | **4 天**(含缓冲) |
---
## 10. 与既有文档的关系
- 本文档**替代** 2026-08-08 初版计划书中的双栈拓扑与阶段划分(初版的职责盘点/迁移清单仍然有效,已并入 §1.2/§3.5)
- [libhttp_remote_addr_dev_doc.md](libhttp_remote_addr_dev_doc.md)(A4 根治):实施时 `trust_xff`/`rate_limit_key()` 配套由 gateway 改为落 CA;`is_special_header` 两侧同步加 `x-remote-addr`
- [gateway_SECURITY_AUDIT_20260809.md](../security/gateway_SECURITY_AUDIT_20260809.md):A2(白名单 403)断言、P2-3(CORS 白名单)随转发层/配置迁入 CA;P1-7(CSRF Token)在 CA 层评估
- gateway README / 项目 README 的架构图、端口表、请求链路在上线步骤第 4 步同步更新
+3 -3
View File
@@ -1,10 +1,10 @@
VERSION := 0.8.0 VERSION := 0.9.0
APP_NAME := gateway APP_NAME := gateway
INSTL_DIR := /usr/local/bin INSTL_DIR := /usr/local/bin
CPP := g++ CPP := g++
INCS := -I inc/ -I ../libmsg/inc INCS := -I inc/ -I ../libmsg/inc
MY_LIBS := -L/usr/lib -L../libmsg/build -lus_http -lus_httpclient -lus_crypt -lus_msg MY_LIBS := -L/usr/lib -L../libmsg/build -lus_http -lus_httpclient -lus_crypt -lus_msg
LIBS := -pthread -lcurl -lssl -lsqlite3 -lz -lcrypto -lboost_json -lboost_program_options -Wl,-rpath,$(shell cd ../libmsg/build && pwd) LIBS := -pthread -lcurl -lssl -lz -lcrypto -lboost_json -lboost_program_options -Wl,-rpath,$(shell cd ../libmsg/build && pwd)
CPPFLAGS := --std=c++20 -pipe -pthread # -Wall -Werror CPPFLAGS := --std=c++20 -pipe -pthread # -Wall -Werror
BUILD_DIR = build BUILD_DIR = build
$(shell mkdir -p $(BUILD_DIR)) $(shell mkdir -p $(BUILD_DIR))
@@ -13,7 +13,7 @@ $(shell mkdir -p $(BUILD_DIR))
all: $(BUILD_DIR)/$(APP_NAME).out all: $(BUILD_DIR)/$(APP_NAME).out
$(BUILD_DIR)/$(APP_NAME).out: $(BUILD_DIR)/main.o $(BUILD_DIR)/$(APP_NAME).out: $(BUILD_DIR)/main.o
$(CPP) $^ $(LIBS) $(MY_LIBS) -o $@ $(CPP) $^ $(LIBS) $(MY_LIBS) -o $@
$(BUILD_DIR)/main.o: src/main.cpp inc/DbC.hpp inc/LogC.hpp inc/Conf.hpp inc/ExpBase.hpp inc/LocalTime.hpp $(BUILD_DIR)/main.o: src/main.cpp inc/LogC.hpp inc/Conf.hpp inc/ExpBase.hpp
$(CPP) $(CPPFLAGS) -c $< $(INCS) -o $@ $(CPP) $(CPPFLAGS) -c $< $(INCS) -o $@
clear: clear:
@rm -rvf build/ @rm -rvf build/
+41 -241
View File
@@ -2,301 +2,101 @@
UniServe Framework 的应用层网关,C++20 实现。 UniServe Framework 的应用层网关,C++20 实现。
在引入 [waf](../waf/) 模块后,gateway 退化为内部 HTTP 服务:TLS 由 WAF 终止,gateway 专注做账号鉴权与反向代理。通过 `Server.tls` 配置可一键切换独立部署(HTTPS)或与 WAF 协作(HTTP)两种模式。 **v0.9.0 起为纯转发服务**:认证职责(Token 签发/验签、登录/注册/重置、限流、WS 代理鉴权)已独立为 [ca](../ca/) 模块。请求链路变为 `waf:443 → ca:8443 → gateway:8445 → 内部服务`,gateway 仅监听 loopback,不再验签。
## 职责 ## 职责
- **账号鉴权**:`/register` 注册(需邮箱验证码,经 pymsg 调 smtp:8008 校验)、`/login` 登录,签发 RSA 签名的 Token。 - **HTTP 反向代理**:`GET/POST *` 通配路由,按 `Target-Url` 头转发到 `Proxy.opened` 白名单内的内部服务;缺省走 `Proxy.default`(前端静态资源)。
- **HTTP 反向代理**:携带 Token 的请求校验后转发到 `Proxy.opened` 中放开的内部服务。 - **存在性不变量(非验签)**:显式携带 `Target-Url` 的受保护请求必须带有 ca 注入的 `X-Auth-User` 头,缺失一律 401——用于拦截"未经 ca 直连 8445"的误配/调试流量,不是安全边界(安全边界是 loopback 监听 + ca 的 strip-then-set)。
- **WebSocket 反向代理(仅管理员)**:`[WsProxy]` 段配置的路径在 HTTP Upgrade 握手时校验 Token(`is_admin=true`),通过后透明桥接到后端 ws:// 或 wss:// 服务,支持文本/二进制帧双向转发。 - **身份头透传**:`X-Auth-User` / `X-Auth-Admin` / `Authorization` 不在 `is_special_header` 剥离列表中,原样转发给下游(dnfs2/core 等零改动)。
- **CAPTCHA 接口**:`/captcha` 生成验证码 token + 文本(内存存储,5 分钟过期,一次性使用)。 - **XFF 覆写**:剥离客户端伪造的 `X-Forwarded-For`/`X-Real-IP`/`Forwarded`,以收到的 XFF(ca 链路注入)为准重新设置,下游直接信任。
- **邮箱验证码**:`/register/send_code` 发送 6 位验证码邮件(10 分钟有效);注册时校验,一次性消费。
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟、发验证码 5 次/10 分钟(IP 级)+ 3 次/小时/10 次/天(邮箱级),按 `X-Forwarded-For` 客户端 IP 计数(由 WAF 注入),超限一律返回 429。
- **邮件轰炸防护**:`/register/send_code` 强制 CAPTCHA 校验 + 邮箱级双窗口限流(3 次/小时 + 10 次/天),smtp 层兜底同样限流 + 全局熔断(默认 100 封/小时,可配)。
- **OPTIONS 预检**:由 libhttp 处理,`Access-Control-Allow-Origin` 反射请求的 Origin 头(任意来源均被放行);`CORS.allowed_origins` 白名单已读取但尚未用于预检响应(见审计 P2-3)。
## 安全特性 ## 信任模型
| 特性 | 实现 | ```
|------|------| waf:443 ──HTTP──▶ ca:8443 ──(注入 X-Auth-User)──▶ gateway:8445 ──▶ static:9200 / dnfs2:8006 / core:5200
| 密码哈希 | PBKDF2-HMAC-SHA256,100000 轮,16 字节随机盐,32 字节哈希 | ▲ 安全边界 ▲ 仅 loopback
| 密码强度校验 | 至少 8 字符;大写/小写/数字/特殊字符中至少 3 类 | ```
| 邮箱格式校验 | 正则匹配 `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$` |
| 旧哈希自动迁移 | 登录时若发现 SHA256 旧哈希,验证通过后自动迁移为 PBKDF2(⚠️ 当前源码中该迁移逻辑已被注释禁用,见 `main.cpp` L1102-1109,未生效) |
| Token 机制 | `Base64(payload) + "_" + RSA-Sign(payload)`,验签 + 过期校验(默认 1 小时) |
| 常数时间比较 | PBKDF2 验证用 `diff |= hash[i] ^ expected[i]` 防 timing attack |
| CAPTCHA | 一次性、5 分钟过期,登录与注册均可选启用 |
| 速率限制 | 内存计数器,自动清理超过 1 万条的老条目 |
| TLS 可选 | `Server.tls = off` 时纯 HTTP(配合 WAF),`on` 时用 `full_chain.crt` / `server.key` |
| 下游服务容错 | pymsg 客户端 `retry_on_refused` 机制:db 等强依赖服务未启动时自动阻塞重试,避免启动顺序问题;日志客户端 fail-safe 静默丢弃 |
## pymsg 内部通信协议 - gateway 不持有 RSA 密钥、不验签;身份完全信任 ca 注入的头。
- 因此 **启动强制 loopback 自检**:`Server.address` 非 `127.0.0.1`/`::1`/`localhost` 时拒绝启动(防止误配导致外部直连伪造身份)。
gateway 通过项目内的 [pymsg](../pymsg/)(长度前缀帧 TCP 协议)与内部微服务通信,C++ 实现在 `inc/Msg.hpp`,与 Python 端字节级兼容。 - `db:8005` 的 `execute` 路由本就无认证,loopback 信任模型与既有基线一致。
**线协议**:`[4字节大端uint32长度][UTF-8载荷]`,最大 64 MiB。
**JSON RPC 约定**:请求形如 `{"_route": "<路由名>", ...}`,响应形如 `{"success": true, "data": ...}` 或 `{"success": false, "error": "..."}`。
**下游服务调用方式**:
| 下游 | 默认端口 | `_route` | 调用位置 | 重试策略 |
|------|---------|----------|---------|---------|
| db | 8005 | `execute` | `DbC::call_msg()`(所有数据库 CRUD) | `retry_on_refused=true`,db 未就绪时阻塞等待 |
| db | 8005 | `execute` | `LogC::_send()`(日志异步批量推送) | `retry_on_refused=false`,fail-safe 静默丢弃 |
| captcha | 8009 | `verify` / `image` | `verify_captcha_remote()`、`/captcha/image` 代理 | 默认不重试,连接失败 fail-closed 返回 502/false |
| smtp | 8008 | `verify_email` / `send_email` | `verify_email_code_remote()`、`/register/send_code` | 默认不重试,连接失败 fail-closed 返回 502/false |
| confs | 8010 | `get` | 启动时拉取 `gateway.ini` 配置 | `retry_on_refused=true`,confs 未就绪时阻塞等待 |
**retry_on_refused 机制**(`MsgClient::call()`,对应 Python 端 `MsgClient.call(retry_on_refused=True)`):
- 仅对 `ECONNREFUSED`(目标端口无服务监听)重试,超时、连接重置、业务错误立即抛出
- 固定 1 秒间隔重试;首次失败立即打印 stderr 提示,之后每 5 秒打印一次等待日志(节制日志,避免刷屏)
- 用于解决微服务启动顺序不确定问题:gateway 启动时 db 未就绪,首次 db 调用自动阻塞等待直到 db 上线
## 接口 ## 接口
### `POST /captcha`
生成验证码。返回:
```json
{
"token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"text": "Ab3xy"
}
```
### `POST /login`
请求体:
```json
{
"username": "user@example.com",
"password": "P@ssw0rd",
"captcha_token": "a1b2c3d4...", // 可选
"captcha_answer": "Ab3xy" // 可选
}
```
成功响应:
```json
{
"success": true,
"data": { "token": "<base64_payload>_<rsa_sign>" }
}
```
失败响应:
```json
{ "success": false, "message": "Invalid Username or Password" }
```
错误码:`400`(参数/CAPTCHA 错误)、`401`(账号/密码错或 Token 过期)、`429`(速率超限)。
### `POST /register`
请求体在 `/login` 基础上增加 `email_code`(必填,6 位数字邮箱验证码):
```json
{ "username": "user@example.com", "password": "...", "email_code": "123456" }
```
校验顺序:密码强度 → 邮箱格式 → CAPTCHA(可选)→ **邮箱验证码**(经 pymsg 调 smtp:8008 `verify_email`,一次性消费,连续失败 5 次自动作废,fail-closed),全部通过后写入 `Users` 表。
错误码:`400`(密码/邮箱/CAPTCHA/邮箱验证码不合规)、`429`(速率超限)、`500`(数据库异常)。
### `POST /register/send_code`
向目标邮箱发送注册验证码(经 pymsg 调 smtp:8008 `send_email`,10 分钟有效)。
**请求体**(CAPTCHA 必填,防邮件轰炸):
```json
{
"email": "user@example.com",
"captcha_token": "a1b2c3d4...",
"captcha_answer": "Ab3xy"
}
```
**防护层次**:
1. IP 限流:5 次/10 分钟(`X-Forwarded-For`)
2. CAPTCHA 强制校验:经 captcha:8009 pymsg 校验,Token 一次性消费
3. 邮箱级双窗口限流:3 次/小时 + 10 次/天
4. smtp 同邮箱 60 秒发送冷却(冷却期拒绝时返回 `retry_after` 剩余秒数与提示文案,gateway 按 429 透传;已发出且在有效期内的验证码不受影响,发送失败不计入冷却)
5. smtp 邮箱级长期限流兜底 + 全局熔断(默认 100 封/小时,`SMTP_HOURLY_LIMIT` 可配)
错误码:`400`(邮箱格式错误/CAPTCHA 缺失或错误)、`429`(IP 速率超限/邮箱级限流超限/同邮箱发送冷却)、`502`(smtp 服务不可用)、`500`(发送失败)。
### `GET/POST *`(反向代理) ### `GET/POST *`(反向代理)
携带 header:
``` ```
Authorization: Bearer <base64_payload>_<rsa_sign> X-Auth-User: user@example.com // 受保护请求必需(由 ca 注入)
Target-Url: http://127.0.0.1:9200 // 可选,缺省取 Proxy.default Target-Url: http://127.0.0.1:8006 // 可选,缺省取 Proxy.default
``` ```
- `Target-Url` 缺省时走 `Proxy.default`(不需 Token)。 - `Target-Url` 缺省时走 `Proxy.default`(公开,不检查 `X-Auth-User`)。
- 自定义 `Target-Url` 必须在 `Proxy.opened` 白名单中,否则返回 403。 - 自定义 `Target-Url` 必须在 `Proxy.opened` 白名单中,否则 403。
- 验签通过且未过期(默认 1 小时)才转发,否则返回 401。 - 显式 `Target-Url` 且无 `X-Auth-User` → 401。
- `Proxy.closed`(如 db:8005、ai_review:8007)不在 `opened` 中,外部无法直访。 - `Proxy.closed`(如 db:8005、ai_review:8007)不在 `opened` 中,无法经 gateway 直访。
### `OPTIONS *` 错误响应统一脱敏:`HttpException` 仅返回纯消息(内部前缀留服务端日志),其余异常一律 `500 {"success": false, "message": "Internal Server Error"}`。
返回 CORS 预检响应:`Access-Control-Allow-Origin` 反射请求 Origin 头(等价于允许任意来源,见审计 P2-3)、`Access-Control-Allow-Methods: GET, POST, OPTIONS`、`Max-Age: 86400`。
### WebSocket 代理(`[WsProxy]` 配置的路径)
握手阶段鉴权规则:
1. 从 `Authorization: Bearer <token>` 头读取 Token;若缺失则回退到 `Cookie: q4_token=<token>`。
2. RSA 验签 + 过期校验(与 HTTP 代理同样的 Token 机制)。
3. Token payload 中必须 `is_admin=true`(即登录账号在 `Admin.users` 白名单内)。
4. 鉴权失败 → 返回 HTTP 403 Forbidden,连接被拒绝。
5. 鉴权通过 → 升级为 WebSocket 并透明桥接到配置的后端 ws:// 或 wss:// 地址,双向文本/二进制帧原样转发。
客户端使用方式与普通 WebSocket 一致,只需携带有效管理员 Token:
```javascript
// 浏览器(同源会自动带 q4_token Cookie)
const ws = new WebSocket("wss://qsont.me/ws/aibox");
// 或显式带 Authorization(构造子协议或连接后首条消息均可,这里由网关在 Upgrade 头校验)
```
注意:WS 代理路由必须精确匹配(与 `ws()` 一致,非通配),且优先级高于 HTTP 通配 `*` 路由。
## 数据库
用户数据存于 db 服务(pymsg/TCP :8005)的 SQLite 库,表 `Users`(由 [db/create_sqlite3.sql](../db/create_sqlite3.sql) 建立,gateway 不再自动建表):
| 字段 | 类型 | 说明 |
|------|------|------|
| id | INTEGER PK AUTOINCREMENT | |
| username | TEXT UNIQUE NOT NULL | 登录账号(邮箱格式) |
| password_hash | TEXT NOT NULL | `pbkdf2:100000:<salt_hex>:<hash_hex>` |
| create_at | DATETIME DEFAULT CURRENT_TIMESTAMP | |
| last_login | DATETIME | 每次登录更新 |
| is_active | BOOLEAN DEFAULT 1 | |
gateway 不直连数据库(早期直连本地 sqlite 的代码已注释保留),所有读写经 `DbC::call_msg` 调 db 的 `execute` 路由。无默认管理员账号——首位注册者即为管理员。
## 配置 ## 配置
gateway 配置采用**两层加载模式**,与其他 Python 服务(dnfs/db/static)保持一致: 两层加载:confs 拉取(pymsg `get`,`retry_on_refused=true`)→ 环境变量覆盖。
1. **confs 统一配置服务**:启动时从 confs(`127.0.0.1:8010`,可通过 `CONFS_HOST`/`CONFS_PORT` 环境变量覆盖)通过 pymsg `get` 路由拉取 `gateway.ini`,`retry_on_refused=true` 阻塞等待 confs 就绪。 配置文件:[confs/configs/gateway.ini](../confs/configs/gateway.ini)。
2. **环境变量覆盖**(最高优先级,Docker 兼容):`LISTEN_ADDR`/`LISTEN_PORT`、`DB_HOST`/`DB_PORT`、`CAPTCHA_HOST`/`CAPTCHA_PORT`、`SMTP_HOST`/`SMTP_PORT` 覆盖 ini 中对应值。
3. **完整性校验**:必需配置项缺失则启动失败。
配置文件统一存放于 [confs/configs/gateway.ini](../confs/configs/gateway.ini),gateway 目录下不再保留本地配置文件。 - `Server.address`/`port`/`tls`:默认 `127.0.0.1:8445`(v0.9.0 起 8443 归 ca);env `LISTEN_ADDR`/`LISTEN_PORT`。
- `Proxy.default` / `Proxy.opened` / `Proxy.closed`:转发目标与白名单(分号分隔)。
配置项说明: - `Downstream.db_host`/`db_port`:日志推送端点(env `DB_HOST`/`DB_PORT`)。
- `Server.address`/`port`/`tls`:监听地址、端口、TLS 开关;`address`/`port` 可被 `LISTEN_ADDR`/`LISTEN_PORT` 环境变量覆盖。
- `Proxy.default`:未带 `Target-Url` header 时的默认转发地址(不需 Token)。
- `Proxy.opened`:允许外部带 Token 访问的内部服务白名单(分号分隔)。
- `Proxy.closed`:仅文档记录用途,实际靠"不在 opened 中即拒绝"实现隔离。
- `Token.*`:Token 有效期(`keep_time`)、刷新宽限期倍数(`refresh_grace_multiplier`)、最大刷新次数(`max_refresh`)。
- `Admin.users`:管理员白名单(分号分隔邮箱),登录命中时 Token payload 写入 `is_admin=true`。
- `CORS.allowed_origins`:允许跨域的 Origin 白名单(分号分隔)。⚠️ 当前已读取但**尚未用于预检响应**——OPTIONS 预检由 libhttp 反射请求 Origin,白名单暂不生效(见审计 P2-3)。
- `WsProxy.routes`:WebSocket 代理路由,格式 `/path=ws://host:port/target`,分号分隔。
- `Downstream.*`:下游 pymsg 服务地址(db/captcha/smtp),可被 `DB_HOST`/`DB_PORT`、`CAPTCHA_HOST`/`CAPTCHA_PORT`、`SMTP_HOST`/`SMTP_PORT` 环境变量覆盖。
## 部署模式
### 模式 A:与 WAF 协作(推荐)
```
客户端 ──HTTPS──▶ waf:443 (终止 TLS + coraza 规则) ──HTTP──▶ gateway:8443
```
```ini
address = 127.0.0.1
port = 8443
tls = off
```
证书放在 `waf/certs/`。RateLimiter 从 WAF 注入的 `X-Forwarded-For` 取真实客户端 IP。
### 模式 B:独立部署
```
客户端 ──HTTPS──▶ gateway:443 (终止 TLS + 鉴权 + 反代)
```
```ini
address = 0.0.0.0
port = 443
tls = on
```
证书 `full_chain.crt` / `server.key` 放在 gateway 目录。RateLimiter 取不到 `X-Forwarded-For`,所有请求记为 `"direct"`。
## 编译与运行 ## 编译与运行
依赖系统库(由根目录 [_setup.deb.sh](../_setup.deb.sh) 或 [_setup.rhel.sh](../_setup.rhel.sh) 安装):
- `libboost_json`、`libboost_program_options`
- `libcurl`
- `libssl` / `libcrypto`(OpenSSL,PBKDF2 + RSA + RAND_bytes)
- `libsqlite3`
- 项目内库:`libus_http`、`libus_crypt`(由 [libhttp](../libhttp/)、[libcrypt](../libcrypt/) 编译安装)
```bash ```bash
bash _set.sh # make clean && make bash _set.sh # make clean && make
bash _run.sh # 从 confs 拉取配置启动 bash _run.sh # 从 confs 拉取配置启动
``` ```
无需传参,配置统一由 confs 管理。命令行参数:`-h/--help`、`-v/--version`。 命令行参数:`-h/--help`、`-v/--version`。日志经 LogC 异步批量推送 db:8005 `Logs` 表(service 名 `gateway`)。
## 目录结构 ## 目录结构
``` ```
gateway/ gateway/
├── src/ ├── src/
│ └── main.cpp # 全部实现:配置加载(confs → 环境变量覆盖)、PBKDF2、RateLimiter、路由/handler │ └── main.cpp # 全部实现:配置加载、存在性不变量、反向代理(约 560 行)
├── inc/ ├── inc/
│ ├── Msg.hpp # pymsg C++ 实现:长度前缀帧协议、MsgClient(含 retry_on_refused)、MsgException 族 │ ├── LogC.hpp # 统一日志客户端(异步批量,fail-safe)
│ ├── DbC.hpp # db 服务客户端:通过 pymsg 调 dbv2(地址可配置,retry_on_refused=true) │ ├── Conf.hpp # ConfIni:ini 解析 + env 覆盖
│ ├── LogC.hpp # 统一日志客户端:异步批量队列 + pymsg INSERT(fail-safe)
│ ├── Conf.hpp # ConfIni:boost::property_tree ini 解析,支持 fromString/env/env_int
│ ├── ExpBase.hpp # 异常基类(带 source_location) │ ├── ExpBase.hpp # 异常基类(带 source_location)
│ └── LocalTime.hpp # 本地时间工具(getCurrent / parse) │ ├── DbC.hpp # (保留备用)db 服务客户端
│ └── LocalTime.hpp # (保留备用)本地时间工具
├── Makefile ├── Makefile
├── _set.sh / _run.sh ├── _set.sh / _run.sh
└── README.md └── README.md
``` ```
配置文件统一在 [confs/configs/gateway.ini](../confs/configs/gateway.ini),gateway 目录无本地配置文件。
## 版本 ## 版本
`0.8.0`(见 [src/main.cpp:35](src/main.cpp#L35))。启动时打印: `0.9.0`(见 `src/main.cpp`)。启动时打印:
``` ```
Gateway 0.8.0 started Gateway 0.9.0 started
``` ```
近期变更: 近期变更:
- `0.9.0`:**认证独立化瘦身**——认证资产(Token 签发/验签、`/login` `/register` `/logout` `/refresh_token` `/whoami` `/admin/verify` `/captcha/image` `/reset_*` 全部 handler、限流、WS 代理鉴权、PBKDF2/RSA)整体迁至新模块 [ca](../ca/)(v1.0.0,逐字保留实现);gateway 退居 `127.0.0.1:8445` 纯转发(8443 由 ca 接管,WAF upstream 不变);新增启动 loopback 强制自检与 `X-Auth-User` 存在性不变量(显式 `Target-Url` 无该头 → 401);移除对 libcrypt/OpenSSL/sqlite3 的运行时依赖(编译仍链接 libus_crypt 库)。黑盒 tests/blackbox/test_gateway.py 34 断言对 8443(现为 ca)原样全绿。
- `0.8.0`:**P0 安全问题全部修复**(安全评级 B+ → A-)——HttpException 错误消息脱敏(4 处内部步骤消息替换为用户友好文案,修正注册失败状态码 400→500);**进程退出 bug 修复**——信号处理改为 async-signal-safe(仅设 `volatile sig_atomic_t` 标志 + `write()` 输出)、新增 SIGPIPE 忽略、stop() 添加 8 秒超时保护 + 日志 3 秒超时(保证 11 秒内必定退出)、服务器事件循环改在独立线程运行;Token 双轨吊销机制(jti 黑名单 + 用户版本号)、错误信息脱敏(不返回源码路径/C++ 类型名)、JTI 黑名单内存有界保护(5000 条阈值 + 过期清理)、反向代理响应大小限制(libhttp 默认 10MB)、集中式异步日志客户端 LogC、WebSocket 代理(管理员权限校验)、多层速率限制(IP 级 + 邮箱级)、PBKDF2-HMAC-SHA256 密码哈希(10 万轮)、RSA Token 签名验签、HttpOnly+Secure+SameSite Cookie、密码强度校验。 - `0.8.0`:**P0 安全问题全部修复**(安全评级 B+ → A-)——HttpException 错误消息脱敏(4 处内部步骤消息替换为用户友好文案,修正注册失败状态码 400→500);**进程退出 bug 修复**——信号处理改为 async-signal-safe(仅设 `volatile sig_atomic_t` 标志 + `write()` 输出)、新增 SIGPIPE 忽略、stop() 添加 8 秒超时保护 + 日志 3 秒超时(保证 11 秒内必定退出)、服务器事件循环改在独立线程运行;Token 双轨吊销机制(jti 黑名单 + 用户版本号)、错误信息脱敏(不返回源码路径/C++ 类型名)、JTI 黑名单内存有界保护(5000 条阈值 + 过期清理)、反向代理响应大小限制(libhttp 默认 10MB)、集中式异步日志客户端 LogC、WebSocket 代理(管理员权限校验)、多层速率限制(IP 级 + 邮箱级)、PBKDF2-HMAC-SHA256 密码哈希(10 万轮)、RSA Token 签名验签、HttpOnly+Secure+SameSite Cookie、密码强度校验。
- `0.7.0`:配置体系完全迁移到 confs 统一配置服务——配置文件统一存放于 `confs/configs/gateway.ini`(原 example.ini 内容 + `[Downstream]` 段),删除本地 example.ini,移除 `-i/--input` 命令行参数(不再支持本地 ini 文件回退,与 static 模块一致);启动时从 confs(`CONFS_HOST`/`CONFS_PORT`,默认 `127.0.0.1:8010`)通过 pymsg `get` 路由拉取配置(`retry_on_refused=true`);`ConfIni` 增加 `fromString()`/`env()`/`env_int()`;环境变量覆盖支持 `LISTEN_ADDR`/`LISTEN_PORT`/`DB_HOST`/`DB_PORT`/`CAPTCHA_HOST`/`CAPTCHA_PORT`/`SMTP_HOST`/`SMTP_PORT`;配置完整性校验;`DbC` 增加 `init(host, port)`,main.cpp 所有硬编码 captcha/smtp/db 地址改为配置变量,`logs().set_endpoint()` 启动时初始化;`--version/-v` 选项补全实现;版本升至 0.7.0。 - `0.7.0`:配置体系完全迁移到 confs 统一配置服务——配置文件统一存放于 `confs/configs/gateway.ini`,删除本地 example.ini,移除 `-i/--input` 命令行参数;启动时从 confs 通过 pymsg `get` 路由拉取配置(`retry_on_refused=true`);`ConfIni` 增加 `fromString()`/`env()`/`env_int()`;环境变量覆盖;配置完整性校验;`--version/-v` 选项补全实现。
- `0.6.1`:pymsg 客户端增加 `retry_on_refused` 重试机制——新增 `MsgConnectionRefusedException`;`MsgClient::call()` 新增 `retry_on_refused`/`retry_interval` 参数;`DbC::call_msg()` 启用重试等待 db 就绪;`LogC::_send()` 显式不重试(fail-safe)。 - `0.6.1`:pymsg 客户端增加 `retry_on_refused` 重试机制——新增 `MsgConnectionRefusedException`;`MsgClient::call()` 新增 `retry_on_refused`/`retry_interval` 参数;`DbC::call_msg()` 启用重试等待 db 就绪;`LogC::_send()` 显式不重试(fail-safe)。
- `0.6.0`:WebSocket 反向代理(仅管理员)——新增 `[WsProxy]` 配置段,支持 `routes = /path=ws://host:port/target;...` 精确路径映射;握手阶段校验 Token 中 `is_admin=true`(基于 `Admin.users` 白名单),通过后双向透明桥接文本/二进制帧到后端,支持 `ws://` 和 `wss://`;后端连接错误/鉴权失败均优雅关闭。依赖 libhttp v3.1+ 的 `ws_proxy()` 接口。顺带修复 `LocalTime.hpp` 在 Apple clang 下 C++20 tzdb 不可用的编译问题(改用 `strftime`/`get_time`/`mktime`)。 - `0.6.0`:WebSocket 反向代理(仅管理员)——新增 `[WsProxy]` 配置段;握手阶段校验 Token 中 `is_admin=true`,通过后双向透明桥接文本/二进制帧到后端。依赖 libhttp v3.1+ 的 `ws_proxy()` 接口。(v0.9.0 起迁至 ca)
- `0.5.4`:修复邮件轰炸漏洞(审计报告 P0-1)——`/register/send_code` 强制 CAPTCHA 校验(经 captcha:8009,Token 一次性消费),新增邮箱级双窗口限流(3 次/小时 + 10 次/天);smtp 层同步增加邮箱级长期限流兜底 + 全局熔断(`SMTP_HOURLY_LIMIT` 环境变量,默认 100 封/小时);前端 `register.html`/`register.js` 同步接入图形验证码(复用 `/captcha/image` 代理路由,方案 A:仅在「获取验证码」时要求 CAPTCHA,注册提交不加)。 - `0.5.4`:修复邮件轰炸漏洞(审计报告 P0-1)——`/register/send_code` 强制 CAPTCHA 校验,新增邮箱级双窗口限流;smtp 层同步增加邮箱级长期限流兜底 + 全局熔断。(v0.9.0 起迁至 ca)
- `0.5.3`:配合 smtp 同邮箱 60 秒发送冷却——`/register/send_code` 在 smtp 返回 `retry_after` 时按 429 透传其提示文案(其余发送失败仍用通用 500 文案,SMTP 内部错误细节不向外暴露)。 - `0.5.3`:配合 smtp 同邮箱 60 秒发送冷却——`/register/send_code` 在 smtp 返回 `retry_after` 时按 429 透传其提示文案。
- `0.5.2`:限流错误码统一为 429(登录原 400、注册/发码原 403,并修正 "attampts" 拼写);登录时用户不存在不再兜底 500——空结果统一走 401,并执行 dummy PBKDF2 校验拉平时序防用户名枚举;db 查询失败消息由误名的 "Login (Update Step) Failed" 改为 "Login (Query Step) Failed";pymsg 下游调用超时常量化(`DbC::DEFAULT_TIMEOUT`、`_TIMEOUT_CAPTCHA/_SMTP_VERIFY/_SMTP_SEND`)。 - `0.5.2`:限流错误码统一为 429;登录时用户不存在不再兜底 500——空结果统一走 401,并执行 dummy PBKDF2 校验拉平时序防用户名枚举;pymsg 下游调用超时常量化。
- `0.5.1`:登录成功后通过 db 更新 `Users.last_login`(`"$now"` 哨兵,db 日期适配层转为服务端当前 UTC 时间;更新失败经 THROW_EX_CUSTOM 抛 500,与其他步骤一致)。 - `0.5.1`:登录成功后通过 db 更新 `Users.last_login`(`"$now"` 哨兵)。
## 依赖 ## 依赖
- C++20(`g++`) - C++20(`g++`)
- Boost:`boost::json`、`boost::program_options`、`boost::property_tree` - Boost:`boost::json`、`boost::program_options`、`boost::property_tree`
- OpenSSL:PBKDF2-HMAC-SHA256、RSA 签名/验签、`RAND_bytes`
- libcurl(反向代理出站请求) - libcurl(反向代理出站请求)
- libsqlite3(用户表存储) - 项目库:[libhttp](../libhttp/)(HttpServer/HttpClient)、[libmsg](../libmsg/)
- 项目库:[libhttp](../libhttp/)(HttpServer/HttpClient)、[libcrypt](../libcrypt/)(MyRSA/Base64/SHA256)、[libsqlite3](../libsqlite3/)
+61 -1935
View File
File diff suppressed because it is too large. Load diff