diff --git a/dnfs/dnfs.py b/dnfs/dnfs.py index 090d00d..11a4149 100755 --- a/dnfs/dnfs.py +++ b/dnfs/dnfs.py @@ -279,36 +279,6 @@ def get_notes(): return jsonify(result), 200 -# 当前用户身份查询:配合前端用户面板,返回 user / is_admin。 -# 与 get_notes 同款可选解析:无 Token 或畸形 Token 按未登录处理(不 500)。 -@app.route('/whoami', methods=["POST"]) -def whoami(): - info = None - auth: str = request.headers.get('Authorization', '') - if auth.startswith('Bearer '): - try: - info = decode_token(auth[7:]) - except Exception: - info = None - - if info is None: - return jsonify({'login': False}), 200 - - resp = { - 'login': True, - 'user': info.get('user'), - 'is_admin': bool(info.get('is_admin')) - } - # 附加 Token 生命周期信息,供前端 Token 管理器估算过期时间 - if info.get('iat'): - resp['iat'] = info['iat'] - if info.get('created_at'): - resp['created_at'] = info['created_at'] - if info.get('keep_time'): - resp['keep_time'] = info['keep_time'] - return jsonify(resp), 200 - - # eg: {"id": 1} @app.route('/delete_note', methods=['POST']) diff --git a/gateway/src/main.cpp b/gateway/src/main.cpp index 70a5be1..c989711 100644 --- a/gateway/src/main.cpp +++ b/gateway/src/main.cpp @@ -306,6 +306,11 @@ http::response handleRefreshToken( const http::request& req, const std::unordered_map& params); +/* 当前用户身份查询:验签 Token 后返回 user / is_admin / Token 生命周期信息 */ +http::response handleWhoami( + const http::request& req, + const std::unordered_map& params); + /* 代理 captcha 图形验证码:pymsg 调 captcha 服务,返回 PNG 图片 */ http::response handleCaptchaImage( const http::request& req, @@ -635,6 +640,11 @@ void registerRoutes(HttpServer &server) { [](const auto& req, const auto& params) { return handleRefreshToken(req, params); }); }); + server.post("/whoami", [](const auto& req, const auto& params) -> http::response { + return wrapperHttp(req, params, + [](const auto& req, const auto& params) { return handleWhoami(req, params); }); + }); + /* 代理 captcha 图形验证码(须在 get("*") 通配符之前注册) */ server.get("/captcha/image", [](const auto& req, const auto& params) -> http::response { return wrapperHttp(req, params, @@ -946,6 +956,132 @@ http::response handleLogout( return res; } +/* 当前用户身份查询 /whoami + * + * 验签 Token 后返回 user / is_admin / Token 生命周期信息(iat / created_at / keep_time)。 + * Token 来源:Authorization: Bearer 优先,回退 Cookie q4_token。 + * 无 Token 或验签失败返回 {"login": false},与 dnfs 旧接口契约一致。 */ +http::response handleWhoami( + const http::request& req, + const std::unordered_map& params) +{ + http::response res; + res.version(req.version()); + + auto _PUBL = MyRSA::Public_Key::from_pem(_KEYGEN->get_public_key_pem()); + + /* 获取 Token:Authorization 头优先,回退 Cookie */ + std::string raw; + auto it_auth = req.base().find("Authorization"); + if (it_auth != req.base().end()) { + raw = std::string(it_auth->value()); + } else { + auto cit = req.base().find("Cookie"); + if (cit != req.base().end()) { + std::string cookie_token = get_cookie_value(std::string(cit->value()), "q4_token"); + if (!cookie_token.empty()) + raw = "Bearer " + cookie_token; + } + } + + /* 无 Token 或格式错误:返回未登录 */ + if (raw.empty() || raw.substr(0, 7) != "Bearer ") { + res.set(http::field::content_type, "application/json"); + res.set("Connection", "close"); + res.result(http::status::ok); + res.body() = R"({"login": false})"; + res.prepare_payload(); + return res; + } + + raw = raw.substr(7); + size_t idx = raw.find("_"); + if (idx == std::string::npos) { + res.set(http::field::content_type, "application/json"); + res.set("Connection", "close"); + res.result(http::status::ok); + res.body() = R"({"login": false})"; + res.prepare_payload(); + return res; + } + + std::string data = raw.substr(0, idx); + std::string sign = raw.substr(idx + 1); + std::string auth; + try { auth = Base64::base64_decode_to_string(data); } + catch (const std::exception&) { + res.set(http::field::content_type, "application/json"); + res.set("Connection", "close"); + res.result(http::status::ok); + res.body() = R"({"login": false})"; + res.prepare_payload(); + return res; + } + + /* RSA 验签:失败按未登录处理 */ + if (!_PUBL.Verify(auth, sign)) { + res.set(http::field::content_type, "application/json"); + res.set("Connection", "close"); + res.result(http::status::ok); + res.body() = R"({"login": false})"; + res.prepare_payload(); + return res; + } + + /* 解析 Token payload */ + json::value jv_t; + try { jv_t = json::parse(auth); } + catch (const std::exception&) { + res.set(http::field::content_type, "application/json"); + res.set("Connection", "close"); + res.result(http::status::ok); + res.body() = R"({"login": false})"; + res.prepare_payload(); + return res; + } + if (!jv_t.is_object()) { + res.set(http::field::content_type, "application/json"); + res.set("Connection", "close"); + res.result(http::status::ok); + res.body() = R"({"login": false})"; + res.prepare_payload(); + return res; + } + + json::object& obj = jv_t.as_object(); + std::string username = json::value_to(obj.at("user")); + + /* is_admin:旧 Token 未携带时默认为 false */ + bool is_admin = false; + auto it_admin = obj.find("is_admin"); + if (it_admin != obj.end() && it_admin->value().is_bool()) + is_admin = it_admin->value().as_bool(); + + json::value jv_r = { + {"login", true}, + {"user", username}, + {"is_admin", is_admin} + }; + + /* 附加 Token 生命周期信息,供前端 Token 管理器估算过期时间 */ + auto it_iat = obj.find("iat"); + if (it_iat != obj.end() && it_iat->value().is_string()) + jv_r.as_object()["iat"] = it_iat->value(); + auto it_created = obj.find("created_at"); + if (it_created != obj.end() && it_created->value().is_string()) + jv_r.as_object()["created_at"] = it_created->value(); + auto it_keep = obj.find("keep_time"); + if (it_keep != obj.end() && it_keep->value().is_int64()) + jv_r.as_object()["keep_time"] = it_keep->value(); + + res.set(http::field::content_type, "application/json"); + res.set("Connection", "close"); + res.result(http::status::ok); + res.body() = json::serialize(jv_r); + res.prepare_payload(); + return res; +} + /* 刷新 Token:POST /refresh_token * * 接受当前有效 Token(即使已过期但在宽限期内),签发新 Token。 diff --git a/static/README.md b/static/README.md index 003ea13..0b94753 100644 --- a/static/README.md +++ b/static/README.md @@ -68,9 +68,10 @@ static :9200 (Flask + CORS) | `POST /add_note` | 新增笔记(代理至 dnfs:8006) | | `POST /delete_note` | 删除笔记(代理至 dnfs:8006) | | `POST /update_note` | 更新笔记(代理至 dnfs:8006) | -| `POST /whoami` | 当前用户身份查询(代理至 dnfs:8006) | -以上 5 个 dnfs 代理路由均支持 HttpOnly Cookie 模式:当请求携带 `q4_token` Cookie 时,自动转为 `Authorization: Bearer` 头转发。 +以上 4 个 dnfs 代理路由均支持 HttpOnly Cookie 模式:当请求携带 `q4_token` Cookie 时,自动转为 `Authorization: Bearer` 头转发。 + +> `/whoami` 已上移至 gateway,由 gateway 直接验签 Token 返回用户身份,不再经 static 代理到 dnfs。 ## 功能 diff --git a/static/fsys.py b/static/fsys.py index 3c90403..5220d5a 100644 --- a/static/fsys.py +++ b/static/fsys.py @@ -474,22 +474,6 @@ def proxy_update_note(): return _cors(make_response(_json.dumps({"error": str(e)}), 502, {"Content-Type": "application/json"})) -@app.route('/whoami', methods=['POST', 'OPTIONS']) -def proxy_whoami(): - if request.method == "OPTIONS": return _cors(make_response("", 204)) - try: - data = request.get_json(force=True, silent=True) or {} - 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}/whoami", 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"})) - except Exception as e: - logs.warning("static", "dnfs whoami 代理失败", extra={"error": str(e)}) - return _cors(make_response(_json.dumps({"error": str(e)}), 502, {"Content-Type": "application/json"})) - - @app.route('/api/proxy_send_code', methods=['POST']) def proxy_send_code(): from flask import request, jsonify