fix(gateway): 限流错误码统一429、登录用户不存在改401并拉平时序、pymsg超时常量化(v0.5.2)
- 三处限流错误码统一为 429(login 400→429 并修 attampts 拼写;register/send_code 403→429) - 登录用户不存在不再兜底 500:空结果统一走 401,dummy PBKDF2 校验拉平时序防用户枚举 - db 查询失败消息误名修正:Login (Update Step) Failed → (Query Step) - 超时常量化:DbC::DEFAULT_TIMEOUT=5.0;_TIMEOUT_CAPTCHA/_SMTP_VERIFY=5.0、_SMTP_SEND=20.0 - 文档:gateway/README 数据库节 email→username 重写、建表归 db/create_sqlite3.sql;根 README 版本号与自动建表描述同步
This commit is contained in:
1 parent
c2e9cce6a7
commit
6a0551ab34
4 files changed
+50
-23
No files matched your search
+12
-9
@@ -10,7 +10,7 @@ UniServe Framework 的应用层网关,C++20 实现。
|
||||
- **反向代理**:携带 Token 的请求校验后转发到 `Proxy.opened` 中放开的内部服务。
|
||||
- **CAPTCHA 接口**:`/captcha` 生成验证码 token + 文本(内存存储,5 分钟过期,一次性使用)。
|
||||
- **邮箱验证码**:`/register/send_code` 发送 6 位验证码邮件(60 秒有效);注册时校验,一次性消费。
|
||||
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟、发验证码 5 次/10 分钟,按 `X-Forwarded-For` 客户端 IP 计数(由 WAF 注入)。
|
||||
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟、发验证码 5 次/10 分钟,按 `X-Forwarded-For` 客户端 IP 计数(由 WAF 注入),超限一律返回 429。
|
||||
- **OPTIONS 预检**:通配 `*` 返回 CORS 头,允许跨域。
|
||||
|
||||
## 安全特性
|
||||
@@ -80,7 +80,7 @@ UniServe Framework 的应用层网关,C++20 实现。
|
||||
|
||||
校验顺序:密码强度 → 邮箱格式 → CAPTCHA(可选)→ **邮箱验证码**(经 pymsg 调 smtp:8008 `verify_email`,一次性消费,fail-closed),全部通过后写入 `Users` 表。
|
||||
|
||||
错误码:`400`(密码/邮箱/CAPTCHA/邮箱验证码不合规)、`403`(速率超限)、`500`(数据库异常)。
|
||||
错误码:`400`(密码/邮箱/CAPTCHA/邮箱验证码不合规)、`429`(速率超限)、`500`(数据库异常)。
|
||||
|
||||
### `POST /register/send_code`
|
||||
|
||||
@@ -90,7 +90,7 @@ UniServe Framework 的应用层网关,C++20 实现。
|
||||
{ "email": "user@example.com" }
|
||||
```
|
||||
|
||||
限流 5 次/10 分钟/IP。错误码:`400`(邮箱格式错误)、`403`(速率超限)、`502`(smtp 服务不可用)、`500`(发送失败)。
|
||||
限流 5 次/10 分钟/IP。错误码:`400`(邮箱格式错误)、`429`(速率超限)、`502`(smtp 服务不可用)、`500`(发送失败)。
|
||||
|
||||
### `GET/POST *`(反向代理)
|
||||
|
||||
@@ -112,18 +112,18 @@ Target-Url: http://127.0.0.1:9200 // 可选,缺省取 Proxy.default
|
||||
|
||||
## 数据库
|
||||
|
||||
SQLite3,表 `Users`:
|
||||
用户数据存于 db 服务(pymsg/TCP :8005)的 SQLite 库,表 `Users`(由 [db/create_sqlite3.sql](../db/create_sqlite3.sql) 建立,gateway 不再自动建表):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | INTEGER PK AUTOINCREMENT | |
|
||||
| email | TEXT UNIQUE NOT NULL | 登录账号 |
|
||||
| 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 | |
|
||||
|
||||
启动时若 `Users` 表不存在则自动建表(含 `idx_users_email` 索引),无默认管理员账号——首位注册者即为管理员。
|
||||
gateway 不直连数据库(早期直连本地 sqlite 的代码已注释保留),所有读写经 `DbC::call_msg` 调 db 的 `execute` 路由。无默认管理员账号——首位注册者即为管理员。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -226,13 +226,16 @@ gateway/
|
||||
|
||||
## 版本
|
||||
|
||||
`0.5.1`(见 [src/main.cpp:29](src/main.cpp#L29))。启动时打印:
|
||||
`0.5.2`(见 [src/main.cpp:29](src/main.cpp#L29))。启动时打印:
|
||||
|
||||
```
|
||||
Gateway 0.5.1 started
|
||||
Gateway 0.5.2 started
|
||||
```
|
||||
|
||||
近期变更:登录成功后通过 db 更新 `Users.last_login`(`"$now"` 哨兵,db 日期适配层转为服务端当前 UTC 时间;更新失败经 THROW_EX_CUSTOM 抛 500,与其他步骤一致)。
|
||||
近期变更:
|
||||
|
||||
- `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.1`:登录成功后通过 db 更新 `Users.last_login`(`"$now"` 哨兵,db 日期适配层转为服务端当前 UTC 时间;更新失败经 THROW_EX_CUSTOM 抛 500,与其他步骤一致)。
|
||||
|
||||
## 依赖
|
||||
|
||||
|
||||
+5
-1
@@ -6,6 +6,10 @@ namespace qing {
|
||||
namespace json = boost::json;
|
||||
class DbC {
|
||||
public:
|
||||
/* 本机 db 轻量查询的默认超时(秒);慢操作由调用方显式放宽。
|
||||
* smtp/captcha 等其他下游的超时常量见 main.cpp。 */
|
||||
static constexpr double DEFAULT_TIMEOUT = 5.0;
|
||||
|
||||
// 【保留】旧版 HTTP 接口:通过 HttpClient POST 到 dbv2 HTTP /execute 端点。
|
||||
// 保留用于兼容性参考,新代码优先使用 call_msg()(pymsg TCP 协议,更可靠)。
|
||||
static json::value call_http(json::value& params) {
|
||||
@@ -25,7 +29,7 @@ namespace qing {
|
||||
// {"success":..., "data":[...]}。
|
||||
// 响应格式与 call_http 兼容,调用方可直接替换。
|
||||
static json::value call_msg(const json::value& params,
|
||||
double timeout = 5.0) {
|
||||
double timeout = DEFAULT_TIMEOUT) {
|
||||
json::value jv_route = params;
|
||||
jv_route.as_object()["_route"] = "execute";
|
||||
pmc::net::MsgClient client("127.0.0.1", 8005, "gateway");
|
||||
|
||||
+31
-11
@@ -26,7 +26,7 @@ using namespace qing;
|
||||
namespace json = boost::json;
|
||||
namespace po = boost::program_options;
|
||||
|
||||
constexpr const char *_VER = "0.5.1";
|
||||
constexpr const char *_VER = "0.5.2";
|
||||
|
||||
//////////////////////////////////
|
||||
#include <sstream>
|
||||
@@ -333,6 +333,14 @@ static RateLimiter _LOGIN_RATE;
|
||||
static RateLimiter _REGISTER_RATE;
|
||||
static RateLimiter _EMAIL_CODE_RATE; // 发送邮箱验证码限流
|
||||
|
||||
/* pymsg 下游调用超时(秒):
|
||||
* captcha / smtp verify_email 均为本机内存操作,5s 足够;
|
||||
* smtp send_email 需完成一次完整外部 SMTP 会话(握手 + TLS + 投递),放宽到 20s。
|
||||
* db 调用的默认超时见 DbC.hpp 的 DbC::DEFAULT_TIMEOUT。 */
|
||||
static constexpr double _TIMEOUT_CAPTCHA = 5.0;
|
||||
static constexpr double _TIMEOUT_SMTP_VERIFY = 5.0;
|
||||
static constexpr double _TIMEOUT_SMTP_SEND = 20.0;
|
||||
|
||||
/* CAPTCHA store: token -> answer (in-memory, with expiry) */
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
@@ -385,7 +393,7 @@ bool verify_captcha_remote(const std::string& token, const std::string& answer)
|
||||
{"token", token},
|
||||
{"answer", answer}
|
||||
};
|
||||
std::string resp = client.call(json::serialize(jv_req), 5.0);
|
||||
std::string resp = client.call(json::serialize(jv_req), _TIMEOUT_CAPTCHA);
|
||||
auto jv = json::parse(resp);
|
||||
auto& obj = jv.as_object();
|
||||
if (!obj.at("success").as_bool()) return false;
|
||||
@@ -405,7 +413,7 @@ bool verify_email_code_remote(const std::string& email, const std::string& code)
|
||||
{"email", email},
|
||||
{"code", code}
|
||||
};
|
||||
std::string resp = client.call(json::serialize(jv_req), 5.0);
|
||||
std::string resp = client.call(json::serialize(jv_req), _TIMEOUT_SMTP_VERIFY);
|
||||
auto jv = json::parse(resp);
|
||||
auto& obj = jv.as_object();
|
||||
if (!obj.at("success").as_bool()) return false;
|
||||
@@ -728,7 +736,7 @@ http::response<http::string_body> handleLogin(
|
||||
client_ip = "direct";
|
||||
|
||||
if (!_LOGIN_RATE.check_and_record(client_ip, 5, 300))
|
||||
THROW_EX_CUSTOM(HttpException, 2001, "Too many login attampts. Please try again later.", 400, "/login");
|
||||
THROW_EX_CUSTOM(HttpException, 2001, "Too many login attempts. Please try again later.", 429, "/login");
|
||||
|
||||
// parse json req body
|
||||
json::value jv;
|
||||
@@ -774,10 +782,14 @@ http::response<http::string_body> handleLogin(
|
||||
auto jv_db_r = DbC::call_msg(jv_select);
|
||||
auto obj_r = jv_db_r.as_object();
|
||||
auto success = obj_r["success"].as_bool();
|
||||
if (!success) THROW_EX_CUSTOM(HttpException, 2102, "Login (Update Step) Failed", 500, "/login");
|
||||
if (!success) THROW_EX_CUSTOM(HttpException, 2102, "Login (Query Step) Failed", 500, "/login");
|
||||
auto obj_rd = obj_r.at("data").as_array();
|
||||
auto obj_ru = obj_rd.at(0).as_object();
|
||||
stored_hash = obj_ru.at("password_hash").as_string();
|
||||
/* 用户不存在时 data 为空数组:直接 at(0) 会抛异常被 wrapper 兜底成 500,
|
||||
* 此处保持 stored_hash 为空,统一走下面的 401 分支 */
|
||||
if (!obj_rd.empty()) {
|
||||
auto obj_ru = obj_rd.at(0).as_object();
|
||||
stored_hash = obj_ru.at("password_hash").as_string();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -802,6 +814,14 @@ http::response<http::string_body> handleLogin(
|
||||
// std::cout << "Migrated password hash for " << username << " to PBKDF2" << std::endl;
|
||||
//}
|
||||
}
|
||||
else {
|
||||
/* 用户不存在:执行一次等价的 dummy PBKDF2 校验拉平响应时序,
|
||||
* 防止通过响应时间差异枚举有效用户名(用户不存在/密码错误一律 401) */
|
||||
static const std::string dummy_hash =
|
||||
"pbkdf2:100000:00000000000000000000000000000000:"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000";
|
||||
PBKDF2Hasher::verify_password(password, dummy_hash);
|
||||
}
|
||||
|
||||
if (!auth_ok) THROW_EX_CUSTOM(HttpException, 2101, "Invalid Username or Password", 401, "/login");
|
||||
|
||||
@@ -899,7 +919,7 @@ http::response<http::string_body> handleCaptchaImage(
|
||||
pmc::net::MsgClient client("127.0.0.1", 8009, "gateway");
|
||||
std::string resp;
|
||||
try {
|
||||
resp = client.call(R"({"_route":"image"})", 5.0);
|
||||
resp = client.call(R"({"_route":"image"})", _TIMEOUT_CAPTCHA);
|
||||
} catch (const std::exception& e) {
|
||||
THROW_EX_CUSTOM(HttpException, 2400, "Captcha service unavailable", 502, "/captcha/image");
|
||||
}
|
||||
@@ -947,7 +967,7 @@ http::response<http::string_body> handleRegister(
|
||||
client_ip = std::string(it_host->value());
|
||||
if (!_REGISTER_RATE.check_and_record(client_ip, 3, 600))
|
||||
THROW_EX_CUSTOM(HttpException, 3001, "Too many registration attempts. Please try again later.",
|
||||
403, "/register");
|
||||
429, "/register");
|
||||
|
||||
// parse the req body
|
||||
json::value jv;
|
||||
@@ -1041,7 +1061,7 @@ http::response<http::string_body> handleSendEmailCode(
|
||||
client_ip = std::string(it_host->value());
|
||||
if (!_EMAIL_CODE_RATE.check_and_record(client_ip, 5, 600))
|
||||
THROW_EX_CUSTOM(HttpException, 3101, "Too many code requests. Please try again later.",
|
||||
403, "/register/send_code");
|
||||
429, "/register/send_code");
|
||||
|
||||
// parse the req body
|
||||
json::value jv;
|
||||
@@ -1072,7 +1092,7 @@ http::response<http::string_body> handleSendEmailCode(
|
||||
{"_route", "send_email"},
|
||||
{"email", email}
|
||||
};
|
||||
std::string resp = client.call(json::serialize(jv_req), 20.0);
|
||||
std::string resp = client.call(json::serialize(jv_req), _TIMEOUT_SMTP_SEND);
|
||||
auto jv_r = json::parse(resp);
|
||||
auto& obj_r = jv_r.as_object();
|
||||
if (obj_r.at("success").as_bool())
|
||||
|
||||
Reference in new issue
Block a user