From 3a351442439aa99121a6ab900bf6ec41daeccb38 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Thu, 19 Feb 2026 22:56:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Ecookie=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=89=B4=E6=9D=83=E6=A8=A1=E5=BC=8F=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?secret=E7=A1=AC=E7=BC=96=E7=A0=81=E9=89=B4=E6=9D=83=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E7=BC=BA=E9=99=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conf/config.ini | 3 + postman/ZLMediaKit.postman_collection.json | 47 +++++++- server/WebApi.cpp | 119 +++++++++++++++++---- server/WebApi.h | 47 ++++---- server/pyinvoker.cpp | 19 ++-- src/Http/HttpCookieManager.cpp | 5 + src/Http/HttpCookieManager.h | 5 + src/Http/HttpFileManager.cpp | 2 +- 8 files changed, 201 insertions(+), 46 deletions(-) diff --git a/conf/config.ini b/conf/config.ini index 907f7882..70d8f23d 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -17,6 +17,9 @@ snapRoot=./www/snap/ defaultSnap=./www/logo.png #downloadFile http接口可访问文件的根目录,支持多个目录,不同目录通过分号(;)分隔 downloadRoot=./www +#是否采用传统secret硬编码鉴权模式,默认开启,开启后每次http接口请求都需要传递secret +#关闭传统鉴权模式后,需要先调用/index/api/login接口登录,成功后将设置cookie,在cookie有效期内访问所有接口都将放行。 +legacyAuth=1 [ffmpeg] #FFmpeg可执行程序路径,支持相对路径/绝对路径 diff --git a/postman/ZLMediaKit.postman_collection.json b/postman/ZLMediaKit.postman_collection.json index 66c96c8c..e7ddec35 100644 --- a/postman/ZLMediaKit.postman_collection.json +++ b/postman/ZLMediaKit.postman_collection.json @@ -2950,7 +2950,52 @@ } }, "response": [] - } + }, + { + "name": "登录(login)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ZLMediaKit_URL}}/index/api/login?digest=d00414822dfd8eabed87c5e24ffcdca7", + "host": [ + "{{ZLMediaKit_URL}}" + ], + "path": [ + "index", + "api", + "login" + ], + "query": [ + { + "key": "digest", + "value": "", + "description": "MD5(\"zlmediakit:\"+${secret}+\":\" +${cookie})" + } + ] + } + }, + "response": [] + }, + { + "name": "登出(logout)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ZLMediaKit_URL}}/index/api/logout", + "host": [ + "{{ZLMediaKit_URL}}" + ], + "path": [ + "index", + "api", + "logout" + ] + } + }, + "response": [] + } ], "event": [ { diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 26ac4401..7bfcc085 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -86,6 +86,7 @@ const string kSecret = API_FIELD"secret"; const string kSnapRoot = API_FIELD"snapRoot"; const string kDefaultSnap = API_FIELD"defaultSnap"; const string kDownloadRoot = API_FIELD"downloadRoot"; +const string kLegacyAuth = API_FIELD"legacyAuth"; static onceToken token([]() { mINI::Instance()[kApiDebug] = "1"; @@ -93,6 +94,7 @@ static onceToken token([]() { mINI::Instance()[kSnapRoot] = "./www/snap/"; mINI::Instance()[kDefaultSnap] = "./www/logo.png"; mINI::Instance()[kDownloadRoot] = "./www"; + mINI::Instance()[kLegacyAuth] = 1; }); }//namespace API @@ -101,18 +103,20 @@ using HttpApi = function s_map_api; -static void responseApi(const Json::Value &res, const HttpSession::HttpResponseInvoker &invoker){ - GET_CONFIG(string, charSet, Http::kCharSet); - HttpSession::KeyValue headerOut; - headerOut["Content-Type"] = string("application/json; charset=") + charSet; - invoker(200, headerOut, res.toStyledString()); -}; - -static void responseApi(int code, const string &msg, const HttpSession::HttpResponseInvoker &invoker){ +static void responseApi(int code, const string &msg, const HttpSession::HttpResponseInvoker &invoker, ApiRetException *ex = nullptr){ Json::Value res; + HttpSession::KeyValue headerOut; + if (ex) { + res = ex->getBody(); + headerOut = ex->getHeaders(); + } res["code"] = code; res["msg"] = msg; - responseApi(res, invoker); + + GET_CONFIG(string, charSet, Http::kCharSet); + headerOut["Content-Type"] = string("application/json; charset=") + charSet; + + invoker(200, headerOut, res.toStyledString()); } static HttpApi toApi(const function &cb) { @@ -304,12 +308,12 @@ static inline void addHttpListener(){ try { it->second(parser, invoker, *helper); } catch (ApiRetException &ex) { - responseApi(ex.code(), ex.what(), invoker); + responseApi(ex.code(), ex.what(), invoker, &ex); helper->getPoller()->async([helper, ex]() { helper->shutdown(SockException(Err_shutdown, ex.what())); }, false); } #ifdef ENABLE_MYSQL catch (SqlException &ex) { - responseApi(API::SqlFailed, StrPrinter << "操作数据库失败:" << ex.what() << ":" << ex.getSql(), invoker); + responseApi(API::SqlFailed, StrPrinter << "操作数据库失败:" << ex.what() << ":" << ex.getSql(), invoker, &ex); } #endif // ENABLE_MYSQL catch (std::exception &ex) { @@ -363,7 +367,7 @@ Value ToJson(const PusherProxy::Ptr& p) { item["url"] = p->getUrl(); item["status"] = p->getStatus(); item["liveSecs"] = p->getLiveSecs(); - item["rePublishCount"] = p->getRePublishCount(); + item["rePublishCount"] = p->getRePublishCount(); item["bytesSpeed"] = (Json::UInt64) p->getSendSpeed(); item["totalBytes"] =(Json::UInt64) p->getSendTotalBytes(); @@ -708,6 +712,38 @@ void getThreadsLoad(TaskExecutorGetterImp &getter, API_ARGS_MAP_ASYNC) { }); } +static constexpr char kLoginCookiePath[] = "/"; +static constexpr char kUnLoginCookieName[] = "ZLM_UNLOGIN"; +static constexpr char kLoginedCookieName[] = "ZLM_LOGINED"; +static constexpr size_t kUnLoginCookieLifeSeconds = 60; +static constexpr size_t kLoginedCookieLifeSeconds = 24 * 3600; + +void check_secret(toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const ArgsMap &allArgs, Json::Value &val) { + GET_CONFIG(bool, legacy_auth , API::kLegacyAuth); + GET_CONFIG(std::string, api_secret, API::kSecret); + + auto ip = sender.get_peer_ip(); + if (!HttpFileManager::isIPAllowed(ip)) { + throw AuthException("Your ip is not allowed to access the service."); + } + if (legacy_auth) { + CHECK_ARGS("secret"); + if (api_secret != allArgs["secret"]) { + throw AuthException("Incorrect secret"); + } + } else { + auto logined_cookie = HttpCookieManager::Instance().getCookie(kLoginedCookieName, allArgs.getParser().getHeader()); + if (!logined_cookie) { + auto unlogin_cookie = HttpCookieManager::Instance().getCookie(kUnLoginCookieName, allArgs.getParser().getHeader()); + if (!unlogin_cookie) { + unlogin_cookie = HttpCookieManager::Instance().addCookie(kUnLoginCookieName, "", kUnLoginCookieLifeSeconds); + headerOut["Set-Cookie"] = unlogin_cookie->getCookie(kLoginCookiePath); + } + val["cookie"] = unlogin_cookie->getCookie(); + throw AuthException("Please login first", headerOut, val); + } + } +} /** * 安装api接口 * 所有api都支持GET和POST两种方式 @@ -720,7 +756,6 @@ void getThreadsLoad(TaskExecutorGetterImp &getter, API_ARGS_MAP_ASYNC) { */ void installWebApi() { addHttpListener(); - GET_CONFIG(string,api_secret,API::kSecret); // 获取线程负载 [AUTO-TRANSLATED:3b0ece5c] // Get thread load @@ -2346,10 +2381,6 @@ void installWebApi() { string subnet_prefix = allArgs["subnet_prefix"]; - // if (subnet_prefix.empty()) { - // subnet_prefix = "192.168.1"; //default ip prefix - // } - auto result = std::make_shared(std::move(val)); auto complete_token = std::make_shared(nullptr, [result, headerOut, invoker]() { invoker(200, headerOut, result->toStyledString()); }); auto lam_search = [complete_token, result](const std::map &device_info, const std::string &onvif_url) { @@ -2362,7 +2393,7 @@ void installWebApi() { //继续等待扫描 return true; }; - OnvifSearcher::Instance().sendSearchBroadcast(move(subnet_prefix), std::move(lam_search), allArgs["timeout_ms"]); + OnvifSearcher::Instance().sendSearchBroadcast(std::move(subnet_prefix), std::move(lam_search), allArgs["timeout_ms"]); }); api_regist("/index/api/getStreamUrl", [](API_ARGS_MAP_ASYNC) { @@ -2389,6 +2420,58 @@ void installWebApi() { }); }); + api_regist("/index/api/login", [](API_ARGS_MAP) { + auto logined_cookie = HttpCookieManager::Instance().getCookie(kLoginedCookieName, allArgs.getParser().getHeader()); + if (logined_cookie) { + // 已经登录成功 + val["code"] = API::Success; + val["msg"] = "You are already logined"; + return; + } + CHECK_ARGS("digest"); + GET_CONFIG(std::string, api_secret, API::kSecret); + + auto unlogin_cookie = HttpCookieManager::Instance().getCookie(kUnLoginCookieName, allArgs.getParser().getHeader()); + // MD5("zlmediakit:"+${secret}+":" +${cookie}) + auto digest_ok = unlogin_cookie ? MD5("zlmediakit:" + api_secret + ":" + unlogin_cookie->getCookie()).hexdigest() : ""; + if (!unlogin_cookie || digest_ok != allArgs["digest"]) { + if (!unlogin_cookie) { + unlogin_cookie = HttpCookieManager::Instance().addCookie(kUnLoginCookieName, "", kUnLoginCookieLifeSeconds); + headerOut["Set-Cookie"] = unlogin_cookie->getCookie(kLoginCookiePath); + } + val["cookie"] = unlogin_cookie->getCookie(); + throw AuthException("Digest does not match, incorrect secret?", headerOut, val); + } + // 登录成功, cookie保持24小时 + logined_cookie = HttpCookieManager::Instance().addCookie(kLoginedCookieName, "", kLoginedCookieLifeSeconds); + headerOut["Set-Cookie"] = logined_cookie->getCookie(kLoginCookiePath); + + // 删除未登录状态的cookie + unlogin_cookie->setExpired(); + HttpCookieManager::Instance().delCookie(unlogin_cookie); + headerOut.emplace_force("Set-Cookie", unlogin_cookie->getCookie(kLoginCookiePath)); + + val["code"] = API::Success; + }); + + api_regist("/index/api/logout", [](API_ARGS_MAP) { + auto logined_cookie = HttpCookieManager::Instance().getCookie(kLoginedCookieName, allArgs.getParser().getHeader()); + if (logined_cookie) { + // 已经登录成功, 删除cookie + logined_cookie->setExpired(); + HttpCookieManager::Instance().delCookie(logined_cookie); + headerOut["Set-Cookie"] = logined_cookie->getCookie(kLoginCookiePath); + } else { + val["msg"] = "You are not logined"; + } + auto unlogin_cookie = HttpCookieManager::Instance().getCookie(kUnLoginCookieName, allArgs.getParser().getHeader()); + if (!unlogin_cookie) { + unlogin_cookie = HttpCookieManager::Instance().addCookie(kUnLoginCookieName, "", kUnLoginCookieLifeSeconds); + headerOut["Set-Cookie"] = unlogin_cookie->getCookie(kLoginCookiePath); + } + val["cookie"] = unlogin_cookie->getCookie(); + }); + #if defined(ENABLE_VIDEOSTACK) && defined(ENABLE_X264) && defined(ENABLE_FFMPEG) VideoStackManager::Instance().loadBgImg("novideo.yuv"); NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastStreamNoneReader, [](BroadcastStreamNoneReaderArgs) { diff --git a/server/WebApi.h b/server/WebApi.h index dc7ee6ed..a573e494 100755 --- a/server/WebApi.h +++ b/server/WebApi.h @@ -23,6 +23,8 @@ #include "webrtc/WebRtcTransport.h" #endif +#include "Http/HttpCookieManager.h" + // 配置文件路径 [AUTO-TRANSLATED:8a373c2f] // Configuration file path extern std::string g_ini_file; @@ -53,31 +55,45 @@ typedef enum { } ApiErr; extern const std::string kSecret; -}//namespace API +extern const std::string kLegacyAuth; +} // namespace API -class ApiRetException: public std::runtime_error { +class ApiRetException : public std::runtime_error { public: - ApiRetException(const char *str = "success" ,int code = API::Success):runtime_error(str){ + ApiRetException(const char *str = "success", int code = API::Success, mediakit::StrCaseMap headers = {}, Json::Value body = {}) + : runtime_error(str) { _code = code; + _headers = std::move(headers); + _body = std::move(body); } - int code(){ return _code; } + int code() { return _code; } + + mediakit::StrCaseMap &getHeaders() { return _headers; } + + Json::Value &getBody() { return _body; } + private: int _code; + mediakit::StrCaseMap _headers; + Json::Value _body; }; class AuthException : public ApiRetException { public: - AuthException(const char *str):ApiRetException(str,API::AuthFailed){} + AuthException(const char *str, mediakit::StrCaseMap headers = {}, Json::Value body = {}) + : ApiRetException(str, API::AuthFailed, std::move(headers), std::move(body)) {} }; -class InvalidArgsException: public ApiRetException { +class InvalidArgsException : public ApiRetException { public: - InvalidArgsException(const char *str):ApiRetException(str,API::InvalidArgs){} + InvalidArgsException(const char *str, mediakit::StrCaseMap headers = {}, Json::Value body = {}) + : ApiRetException(str, API::InvalidArgs, std::move(headers), std::move(body)) {} }; -class SuccessException: public ApiRetException { +class SuccessException : public ApiRetException { public: - SuccessException():ApiRetException("success",API::Success){} + SuccessException(mediakit::StrCaseMap headers = {}, Json::Value body = {}) + : ApiRetException("success", API::Success, std::move(headers), std::move(body)) {} }; using ApiArgsType = std::map; @@ -218,17 +234,8 @@ bool checkArgs(Args &args, const Key &key, const KeyTypes &...keys) { // Check whether the http parameters contain the secret key, the ip of 127.0.0.1 does not check the key // 同时检测是否在ip白名单内 [AUTO-TRANSLATED:d12f963d] // Check whether it is in the ip whitelist at the same time -#define CHECK_SECRET() \ - do { \ - auto ip = sender.get_peer_ip(); \ - if (!HttpFileManager::isIPAllowed(ip)) { \ - throw AuthException("Your ip is not allowed to access the service."); \ - } \ - CHECK_ARGS("secret"); \ - if (api_secret != allArgs["secret"]) { \ - throw AuthException("Incorrect secret"); \ - } \ - } while(false); +void check_secret(toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const ArgsMap &allArgs, Json::Value &val); +#define CHECK_SECRET() check_secret(sender, headerOut, allArgs, val) void installWebApi(); void unInstallWebApi(); diff --git a/server/pyinvoker.cpp b/server/pyinvoker.cpp index ff43cf9b..a4123639 100644 --- a/server/pyinvoker.cpp +++ b/server/pyinvoker.cpp @@ -123,18 +123,25 @@ void handle_http_request(const py::object &check_route, const py::object &submit } consumed = true; + Json::Value val; + HttpSession::KeyValue headerOut; // http api被python拦截了,再api统一鉴权 try { auto args = getAllArgs(parser); auto allArgs = ArgsMap(parser, args); - GET_CONFIG(std::string, api_secret, API::kSecret); - // TODO python http api暂不开启secret鉴权 - // CHECK_SECRET(); // 检测secret + GET_CONFIG(bool, legacy_auth , API::kLegacyAuth); + if (!legacy_auth) { + // 非传统secret鉴权模式,Python接口强制要求登录鉴权 + CHECK_SECRET(); + } } catch (std::exception &ex) { - Json::Value val; - val["code"] = API::Exception; + auto ex1 = dynamic_cast(&ex); + if (ex1) { + val["code"] = ex1->code(); + } else { + val["code"] = API::Exception; + } val["msg"] = ex.what(); - HttpSession::KeyValue headerOut; headerOut["Content-Type"] = "application/json"; invoker(200, headerOut, val.toStyledString()); return; diff --git a/src/Http/HttpCookieManager.cpp b/src/Http/HttpCookieManager.cpp index 1b3d4c35..7fbae80b 100644 --- a/src/Http/HttpCookieManager.cpp +++ b/src/Http/HttpCookieManager.cpp @@ -61,6 +61,11 @@ bool HttpServerCookie::isExpired() { return _ticker.elapsedTime() > _max_elapsed * 1000; } +void HttpServerCookie::setExpired() { + _ticker.resetTime(); + _max_elapsed = 0; +} + void HttpServerCookie::setAttach(toolkit::Any attach) { _attach = std::move(attach); } diff --git a/src/Http/HttpCookieManager.h b/src/Http/HttpCookieManager.h index e9846198..64868302 100644 --- a/src/Http/HttpCookieManager.h +++ b/src/Http/HttpCookieManager.h @@ -118,6 +118,11 @@ public: */ bool isExpired(); + /** + * 使cookie过期作废 + */ + void setExpired(); + /** * 设置附加数据 * Set additional data diff --git a/src/Http/HttpFileManager.cpp b/src/Http/HttpFileManager.cpp index 1883c181..d6f535df 100644 --- a/src/Http/HttpFileManager.cpp +++ b/src/Http/HttpFileManager.cpp @@ -31,7 +31,7 @@ namespace mediakit { // If the player does not access the cookie within 60 seconds, the hls playback authentication will be triggered again. static size_t kHlsCookieSecond = 60; static size_t kFindSrcIntervalSecond = 3; -static const string kCookieName = "ZL_COOKIE"; +static const string kCookieName = "ZLM_HTTP_COOKIE"; static const string kHlsSuffix = "/hls.m3u8"; static const string kHlsFMP4Suffix = "/hls.fmp4.m3u8";