security: Token 改为 HttpOnly Cookie 下发,消除 XSS 窃取面

登录时网关签发 HttpOnly+Secure+SameSite=Lax 的 q4_token Cookie,前端不再经手 Token;网关代理时支持从 Cookie 验签并向下游注入 Authorization(dnfs 零改动),新增 /logout 清除 Cookie,static 代理路由兼容 Cookie 转 Bearer。
This commit is contained in:
qing committed 2026-07-28 09:07:12 +08:00
1 parent 65cfaf97a1
commit e89578fa82
4 files changed
+102 -28

No files matched your search

+79 -3
View File
@@ -26,7 +26,7 @@ using namespace qing;
namespace json = boost::json;
namespace po = boost::program_options;
constexpr const char *_VER = "0.4.0";
constexpr const char *_VER = "0.4.1";
//////////////////////////////////
#include <sstream>
@@ -291,6 +291,13 @@ http::response<http::string_body> handleCaptcha(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params);
http::response<http::string_body> handleLogout(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params);
/* 从 Cookie 头中解析指定名称的值(找不到返回空串) */
std::string get_cookie_value(const std::string& cookie_header, const std::string& name);
/* headers can't processed */
bool is_special_header(const std::string &header);
@@ -532,6 +539,11 @@ void registerRoutes(HttpServer &server) {
[](const auto& req, const auto& params) { return handleRegister(req, params); });
});
server.post("/logout", [](const auto& req, const auto& params) -> http::response<http::string_body> {
return wrapperHttp(req, params,
[](const auto& req, const auto& params) { return handleLogout(req, params); });
});
server.get("*", [](const auto& req, const auto& params) -> http::response<http::string_body> {
return wrapperHttp(req, params,
[](const auto& req, const auto& params) { return handleProxy(req, params, "get"); });
@@ -776,12 +788,38 @@ http::response<http::string_body> handleLogin(
}
};
res.set("Connection", "close");
/* 签发 HttpOnly Cookie(防 XSS 窃取 Token;JS 不再经手 Token) */
{
std::string ck = "q4_token=" + token +
"; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=" +
std::to_string(_TOKEN_KEEP_TIME);
res.set("Set-Cookie", ck);
}
res.result(http::status::ok);
res.body() = json::serialize(jv_rr);
res.prepare_payload();
return res;
}
/* 退出登录 /logout:清除 q4_token Cookie */
http::response<http::string_body> handleLogout(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params)
{
http::response<http::string_body> res;
res.version(req.version());
res.set("Set-Cookie",
"q4_token=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0");
res.set("Connection", "close");
res.result(http::status::ok);
res.body() = R"({"success": true, "message": "logged out"})";
res.prepare_payload();
return res;
}
/* 处理注册 /register */
http::response<http::string_body> handleRegister(
const http::request<http::string_body>& req,
@@ -878,6 +916,7 @@ http::response<http::string_body> handleProxy(
std::string t_url = "";
std::string url = "";
std::string auth = "";
std::string cookie_token = ""; /* 从 Cookie 提取的 Token(用于向下游注入 Authorization) */
json::value jv_t;
{
auto flag = false;
@@ -912,10 +951,21 @@ http::response<http::string_body> handleProxy(
url += "/";
url += target_path;
std::string raw = "";
it = req.base().find("Authorization");
if (it == req.base().end() && !flag)
if (it != req.base().end()) {
raw = std::string(it->value());
} else {
/* 回退:从 HttpOnly Cookie 读取 q4_token(浏览器同源请求自动携带) */
auto cit = req.base().find("Cookie");
if (cit != req.base().end()) {
cookie_token = get_cookie_value(std::string(cit->value()), "q4_token");
if (!cookie_token.empty())
raw = "Bearer " + cookie_token;
}
}
if (raw.empty() && !flag)
THROW_EX_CUSTOM(HttpException, 2301, "Unauthorization", 401, "*");
std::string raw = req["Authorization"];
if (raw.substr(0, 7) == "Bearer ") {
raw = raw.substr(7, raw.size() - 7);
size_t idx = raw.find("_");
@@ -996,6 +1046,11 @@ http::response<http::string_body> handleProxy(
client.set_header("X-Forwarded-For: unknown");
}
/* Token 来自 Cookie 时,向下游补齐 Authorization 头(dnfs 等下游仍按 Bearer 解析,无需改动) */
if (!cookie_token.empty()) {
client.set_header("Authorization: Bearer " + cookie_token);
}
/* 转发请求 */
if (method == "get")
client.get_stream_sync(url, callback);
@@ -1031,6 +1086,27 @@ bool is_special_header(const std::string &header) {
|| header == "Forwarded";
}
std::string get_cookie_value(const std::string& cookie_header, const std::string& name) {
// Cookie 头格式:"k1=v1; k2=v2; q4_token=xxx"
size_t pos = 0;
while (pos <= cookie_header.size()) {
size_t end = cookie_header.find(';', pos);
if (end == std::string::npos) end = cookie_header.size();
std::string pair = cookie_header.substr(pos, end - pos);
size_t eq = pair.find('=');
if (eq != std::string::npos) {
std::string k = pair.substr(0, eq);
size_t b = k.find_first_not_of(" \t");
size_t e = k.find_last_not_of(" \t");
if (b != std::string::npos) k = k.substr(b, e - b + 1);
if (k == name) return pair.substr(eq + 1);
}
if (end == cookie_header.size()) break;
pos = end + 1;
}
return "";
}
bool is_safe_target(const std::string& target) {
// 请求路径安全性校验(P1-1)
// 防御目标:路径遍历、协议注入、空字节、非绝对路径形式