diff --git a/gateway/src/main.cpp b/gateway/src/main.cpp index d663e47..dc0e1ac 100644 --- a/gateway/src/main.cpp +++ b/gateway/src/main.cpp @@ -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 @@ -291,6 +291,13 @@ http::response handleCaptcha( const http::request& req, const std::unordered_map& params); +http::response handleLogout( + const http::request& req, + const std::unordered_map& 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 { + return wrapperHttp(req, params, + [](const auto& req, const auto& params) { return handleLogout(req, params); }); + }); + server.get("*", [](const auto& req, const auto& params) -> http::response { return wrapperHttp(req, params, [](const auto& req, const auto& params) { return handleProxy(req, params, "get"); }); @@ -776,12 +788,38 @@ http::response 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 handleLogout( + const http::request& req, + const std::unordered_map& params) +{ + http::response 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 handleRegister( const http::request& req, @@ -878,6 +916,7 @@ http::response 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 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 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) // 防御目标:路径遍历、协议注入、空字节、非绝对路径形式 diff --git a/static/fsys.py b/static/fsys.py index 17b9ff9..9c5bdc7 100644 --- a/static/fsys.py +++ b/static/fsys.py @@ -411,6 +411,7 @@ def proxy_get_notes(): data = request.get_json(force=True) h = {"Content-Type": "application/json"} if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"] + elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer req = _urlreq.Request(f"{DNFS_URL}/get_notes", data=_json.dumps(data).encode(), headers=h, method="POST") with _urlreq.urlopen(req, timeout=15) as r: return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"})) @@ -426,6 +427,7 @@ def proxy_add_note(): data = request.get_json(force=True) h = {"Content-Type": "application/json"} if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"] + elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer req = _urlreq.Request(f"{DNFS_URL}/add_note", data=_json.dumps(data).encode(), headers=h, method="POST") with _urlreq.urlopen(req, timeout=15) as r: return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"})) @@ -440,6 +442,7 @@ def proxy_delete_note(): data = request.get_json(force=True) h = {"Content-Type": "application/json"} if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"] + elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer req = _urlreq.Request(f"{DNFS_URL}/delete_note", data=_json.dumps(data).encode(), headers=h, method="POST") with _urlreq.urlopen(req, timeout=15) as r: return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"})) @@ -454,6 +457,7 @@ def proxy_update_note(): data = request.get_json(force=True) h = {"Content-Type": "application/json"} if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"] + elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer req = _urlreq.Request(f"{DNFS_URL}/update_note", data=_json.dumps(data).encode(), headers=h, method="POST") with _urlreq.urlopen(req, timeout=15) as r: return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"})) diff --git a/static/public/js/dnfs.js b/static/public/js/dnfs.js index 87a38e1..5fb4497 100644 --- a/static/public/js/dnfs.js +++ b/static/public/js/dnfs.js @@ -1,11 +1,10 @@ -// Session restore from localStorage -if(!sessionStorage.getItem('q4_token')){var t=localStorage.getItem('q4_token');if(t)sessionStorage.setItem('q4_token',t);} +// 认证改为 HttpOnly Cookie(gateway 下发 q4_token),浏览器同源请求自动携带,JS 不再经手 Token /* by qing. * * 用于获取一个节点下的所有子节点信息,0为根节点。 */ -async function getNode(token, id) { +async function getNode(id) { try { obj = { "pid": id } jsonBody = JSON.stringify(obj) @@ -14,7 +13,6 @@ async function getNode(token, id) { method: 'POST', headers: { 'Content-Type': 'application/json', // 告诉服务器内容为json - 'Authorization': `Bearer ${token}`, 'Target-Url': 'http://127.0.0.1:8006' }, body: jsonBody @@ -54,7 +52,7 @@ async function getNode(token, id) { * * 用于向dnfs添加节点 */ -async function addNode(token, pid, title, content) { +async function addNode(pid, title, content) { try { obj = { "pid": pid, @@ -67,7 +65,6 @@ async function addNode(token, pid, title, content) { method: 'POST', headers: { 'Content-Type': 'application/json', // 告诉服务器内容为json - 'Authorization': `Bearer ${token}`, 'Target-Url': 'http://127.0.0.1:8006' }, body: jsonBody @@ -89,8 +86,8 @@ async function addNode(token, pid, title, content) { input_note_title.value = ""; input_note_content.value = ""; - getNode(token, stack[stack.length - 1]).then(load_notes); - getNode(token, stack[stack.length - 1]).then(loadMap) + getNode(stack[stack.length - 1]).then(load_notes); + getNode(stack[stack.length - 1]).then(loadMap) //if (result.success) { alert(`添加节点成功`, result); @@ -108,7 +105,7 @@ async function addNode(token, pid, title, content) { } } -async function deleteNode(token, id) { +async function deleteNode(id) { if (confirm("确认删除此节点?") == false) { return; } @@ -121,7 +118,6 @@ async function deleteNode(token, id) { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, 'Target-Url': 'http://127.0.0.1:8006' }, body: jsonBody @@ -142,8 +138,8 @@ async function deleteNode(token, id) { } stack.pop(); - getNode(token, stack[stack.length - 1]).then(load_notes); - getNode(token, stack[stack.length - 1]).then(loadMap) + getNode(stack[stack.length - 1]).then(load_notes); + getNode(stack[stack.length - 1]).then(loadMap) console.log(`删除节点成功`); return result @@ -182,7 +178,7 @@ input_note_submit.addEventListener('click', function(event) { var pid = (stack.length == 0) ? 0 : stack[stack.length - 1]; //alert(stack.length) //alert(pid) - addNode(token, pid, title, content).then(result => { + addNode(pid, title, content).then(result => { //alert(result.message); }) }) @@ -199,7 +195,7 @@ delete_note.addEventListener('click', (event) => { console.log("节点未删除"); } else { - deleteNode(token, id); + deleteNode(id); } }) @@ -232,8 +228,8 @@ function load_notes(main_node) { card.addEventListener('click', function(event) { var id = Number(item.id) stack.push(id) - getNode(token, id).then(load_notes) - getNode(token, id).then(loadMap) + getNode(id).then(load_notes) + getNode(id).then(loadMap) }); let ttl = document.createElement('center'); let ttl1 =document.createElement('h3'); @@ -274,10 +270,10 @@ function loadMap(children_json) { fold_div.addEventListener("click", () => { if (is_expand == true) { is_expand = false; - getNode(token, stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap); + getNode(stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap); } else { is_expand = true; - getNode(token, stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap); + getNode(stack.length == 0 ? 0 : stack[stack.length - 1]).then(loadMap); } }); if (need_return == true) { @@ -290,8 +286,8 @@ function loadMap(children_json) { back.addEventListener("click", () => { stack.pop(); var id = (stack.length >= 0) ? stack[stack.length - 1] : 0; - getNode(token, id).then(load_notes); - getNode(token, id).then(loadMap); + getNode(id).then(load_notes); + getNode(id).then(loadMap); }); minimap.appendChild(back); } @@ -309,6 +305,5 @@ function loadMap(children_json) { } } -var token = sessionStorage.getItem('q4_token') -getNode(token, 0).then(load_notes); -getNode(token, 0).then(loadMap) +getNode(0).then(load_notes); +getNode(0).then(loadMap) diff --git a/static/public/js/login.js b/static/public/js/login.js index fe5d10a..0dc621c 100644 --- a/static/public/js/login.js +++ b/static/public/js/login.js @@ -76,8 +76,7 @@ form.addEventListener('submit', async function(e) { try { result = JSON.parse(responseText); } catch { throw new Error('服务器响应格式异常'); } if (!res.ok) throw new Error(result.message || '登录失败'); if (result.success) { - localStorage.setItem('q4_token', result.data.token); - sessionStorage.setItem('q4_token', result.data.token); + // Token 已由网关通过 HttpOnly Cookie 下发,JS 不再经手 alert('登录成功'); location.href = '/dnfs.html'; } else {