feat(smtp): 同一邮箱 60 秒发送冷却,gateway 429 透传 + 前端重发倒计时(gateway v0.5.3)

This commit is contained in:
qing committed 2026-07-29 20:54:56 +08:00
1 parent f4a7aca2fa
commit c5dd8d198d
5 files changed
+77 -12

No files matched your search

+4 -3
View File
@@ -90,7 +90,7 @@ UniServe Framework 的应用层网关,C++20 实现。
{ "email": "user@example.com" }
```
限流 5 次/10 分钟/IP。错误码:`400`(邮箱格式错误)、`429`(速率超限)、`502`(smtp 服务不可用)、`500`(发送失败)。
限流 5 次/10 分钟/IP;同一邮箱 60 秒发送冷却(由 smtp 服务执行,冷却期拒绝时返回 `retry_after` 剩余秒数与提示文案,gateway 按 429 透传;已发出且在有效期内的验证码不受影响,发送失败不计入冷却)。错误码:`400`(邮箱格式错误)、`429`(IP 速率超限或同邮箱发送冷却)、`502`(smtp 服务不可用)、`500`(发送失败)。
### `GET/POST *`(反向代理)
@@ -226,14 +226,15 @@ gateway/
## 版本
`0.5.2`(见 [src/main.cpp:29](src/main.cpp#L29))。启动时打印:
`0.5.3`(见 [src/main.cpp:30](src/main.cpp#L30))。启动时打印:
```
Gateway 0.5.2 started
Gateway 0.5.3 started
```
近期变更:
- `0.5.3`:配合 smtp 同邮箱 60 秒发送冷却——`/register/send_code` 在 smtp 返回 `retry_after` 时按 429 透传其提示文案(其余发送失败仍用通用 500 文案,SMTP 内部错误细节不向外暴露)。
- `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,与其他步骤一致)。
+23 -4
View File
@@ -27,7 +27,7 @@ using namespace qing;
namespace json = boost::json;
namespace po = boost::program_options;
constexpr const char *_VER = "0.5.2";
constexpr const char *_VER = "0.5.3";
//////////////////////////////////
#include <sstream>
@@ -1108,6 +1108,8 @@ http::response<http::string_body> handleSendEmailCode(
// 调 smtp 服务(pymsg/TCP :8008)发送验证码
bool sent = false;
int retry_after = 0; // smtp 同邮箱发送冷却的剩余秒数,0 表示非冷却拒绝
std::string smtp_err; // 冷却拒绝时 smtp 返回的提示文案(仅冷却场景透传客户端)
try {
pmc::net::MsgClient client("127.0.0.1", 8008, "gateway");
json::value jv_req = {
@@ -1117,14 +1119,31 @@ http::response<http::string_body> handleSendEmailCode(
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())
sent = obj_r.at("data").as_object().at("sent").as_bool();
if (obj_r.at("success").as_bool()) {
auto& data = obj_r.at("data").as_object();
sent = data.at("sent").as_bool();
if (!sent) {
auto it_ra = data.find("retry_after");
if (it_ra != data.end() && it_ra->value().is_int64())
retry_after = static_cast<int>(it_ra->value().as_int64());
auto it_err = data.find("error");
if (it_err != data.end() && it_err->value().is_string())
smtp_err = std::string(it_err->value().as_string());
}
}
} catch (const std::exception&) {
THROW_EX_CUSTOM(HttpException, 3102, "Mail service unavailable", 502, "/register/send_code");
}
if (!sent)
if (!sent) {
// 同邮箱发送冷却:按限流语义返回 429 并透传 smtp 提示文案;
// 其余发送失败(含 SMTP 内部错误细节)仍用通用文案,不向外暴露
if (retry_after > 0)
THROW_EX_CUSTOM(HttpException, 3103,
smtp_err.empty() ? "Too many code requests. Please try again later." : smtp_err,
429, "/register/send_code");
THROW_EX_CUSTOM(HttpException, 3103, "Failed to send verification code", 500, "/register/send_code");
}
logs().info("gateway", "验证码邮件发送成功: " + email, std::nullopt,
json::serialize(json::value{{"ip", client_ip}}));