迁移/whoami视图

This commit is contained in:
qing committed 2026-07-30 22:05:12 +08:00
1 parent 3f97dd92ef
commit 5839172dae
4 files changed
+139 -48

No files matched your search

-30
View File
@@ -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'])
+136
View File
@@ -306,6 +306,11 @@ http::response<http::string_body> handleRefreshToken(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params);
/* 当前用户身份查询:验签 Token 后返回 user / is_admin / Token 生命周期信息 */
http::response<http::string_body> handleWhoami(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params);
/* 代理 captcha 图形验证码:pymsg 调 captcha 服务,返回 PNG 图片 */
http::response<http::string_body> handleCaptchaImage(
const http::request<http::string_body>& 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<http::string_body> {
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<http::string_body> {
return wrapperHttp(req, params,
@@ -946,6 +956,132 @@ http::response<http::string_body> 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<http::string_body> handleWhoami(
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());
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<std::string>(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。
+3 -2
View File
@@ -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。
## 功能
-16
View File
@@ -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