新增cookie登录鉴权模式,避免secret硬编码鉴权安全缺陷
Some checks failed
Android / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Docker / build (push) Has been cancelled
Linux / build (push) Has been cancelled
Linux_Python / build (push) Has been cancelled
macOS / build (push) Has been cancelled
macOS_Python / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Windows_Python / build (push) Has been cancelled

This commit is contained in:
xia-chu 2026-02-19 22:56:23 +08:00
parent 22dede5a18
commit 3a35144243
8 changed files with 201 additions and 46 deletions

View File

@ -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可执行程序路径,支持相对路径/绝对路径

View File

@ -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": [
{

View File

@ -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<void(const Parser &parser, const HttpSession::HttpRespo
// http api list
static map<string, HttpApi, StrCaseCompare> 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<void(API_ARGS_MAP_ASYNC)> &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<Value>(std::move(val));
auto complete_token = std::make_shared<onceToken>(nullptr, [result, headerOut, invoker]() { invoker(200, headerOut, result->toStyledString()); });
auto lam_search = [complete_token, result](const std::map<string, string> &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) {

View File

@ -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<std::string, std::string, mediakit::StrCaseCompare>;
@ -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();

View File

@ -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<ApiRetException *>(&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;

View File

@ -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);
}

View File

@ -118,6 +118,11 @@ public:
*/
bool isExpired();
/**
* 使cookie过期作废
*/
void setExpired();
/**
*
* Set additional data

View File

@ -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";