MQL5 代码防护基础:从密码到密钥生成器的防盗逻辑(基础篇)
(1/3)· 多数 EA 作者只加个密码框就以为安全,盗版序列号一上网防护即刻失效
给 EA 加一道授权锁
在 MT5 上分发的 EA 若直接裸奔,很容易被二次打包倒卖。常见的防护手段包括本地密码校验、按机器指纹生成钥匙、限定运行期限,以及把验证请求发到远程服务器做许可证核对。 以社区里 2014 年 1 月的一则示例为例,该帖累计被查看 7496 次、收到 51 条讨论,说明这类需求在实盘开发者里一直有市场。外汇与贵金属杠杆高,未经授权的 EA 流传还可能夹带恶意逻辑,风险不小。 下面这段 MQL5 片段演示了最基础的本地时间限制思路,到期即禁止初始化: [int Year, Month, Day;] [datetime expiry = D'2024.12.31';] [if(TimeCurrent() > expiry) { Print("License expired"); return INIT_FAILED; }] 第一行取系统日期分量备用;第二行写死一个过期时间点;第三行在 EA 初始化时比对当前时间,超期直接返回失败状态,MT5 就不会加载策略。实际部署可把 expiry 换成远程下发的字段,降低被改本地时间绕过的概率。
class="type">int Year, Month, Day; class="type">class="kw">datetime expiry = D&class="macro">#x27;class="num">2024.12.class="num">31&class="macro">#x27;; if(TimeCurrent() > expiry) { Print("License expired"); class="kw">return INIT_FAILED; }
「EA 防护从密码到远程许可的演进路径」
做 MQL5 开发的都绕不开一个问题:写出来的 EA、脚本或指标,怎么防止被人白嫖。本文这条线从最朴素的密码校验起步,逐步叠加密钥生成器、按经纪账户授权、时限锁,再到远程许可服务器的思路。 文中示例虽都挂在 EA 上,但同一套法则对脚本和指标同样成立。远程许可那块会直接复用 MQL5-RPC 框架里的 XML-RPC 调用能力,把 MetaTrader 5 当成客户端去问许可服务器拿授权。 在基础方案之上,还会引入 base64 编码做传输混淆,并给出 PGP 支持的建议,把 EA 和指标的保护等级再抬一档。MetaQuotes 官方市场自带的授权机制和本文思路不冲突,两者叠用反而让盗版成本更高。
◍ EA 的密码框怎么落地
给 MT5 上的 EA 加一层最基础的授权,通常就是弹个输入框让用户填序列号,逻辑和 Windows 或 Office 的激活码差不多。第一次加载后若密码不匹配,就别让策略跑单,这是多数小众指标作者的第一道防线。 下面这段存根代码用 CChartObjectEdit 在图表上画了一个可编辑文本框,并预置了 3 个允许密码,例如 "863H-6738725-JG76364"。密码在图表事件 CHARTEVENT_OBJECT_ENDEDIT 触发后于 OnChartEvent 中比对,这里只给了 OnInit 里建框、OnDeinit 里删框、OnTick 里看状态的骨架。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| PasswordProtectedEA.mq5 | class=class="str">"cmt">//| Copyright class="num">2012, Investeo.pl | class=class="str">"cmt">//| http://www.investeo.pl | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2012, Investeo.pl" class="macro">#class="kw">property link "http:class=class="str">"cmt">//www.investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <ChartObjects/ChartObjectsTxtControls.mqh> CChartObjectEdit password_edit; const class="type">class="kw">string allowed_passwords[] = { "863H-class="num">6738725-JG76364", "145G-class="num">8927523-JG76364", "263H-class="num">7663233-JG76364" }; class="type">int password_status = -class="num">1; class="type">class="kw">string password_message[] = { "WRONG PASSWORD. Trading not allowed.", "EA PASSWORD verified." }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- password_edit.Create(class="num">0, "password_edit", class="num">0, class="num">10, class="num">10, class="num">260, class="num">25); password_edit.BackColor(White); password_edit.BorderColor(Black); password_edit.SetInteger(OBJPROP_SELECTED, class="num">0, true); class=class="str">"cmt">//--- class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- password_edit.Delete(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- if (password_status>class="num">0) { class=class="str">"cmt">// password correct } }
CChartObjectEdit password_edit; 声明图表编辑框对象;第 9–11 行 allowed_passwords 是写死在程序里的白名单,共 3 条;第 14 行 password_status 初始为 -1 表示未验证;OnInit 里 Create(0,"password_edit",0,10,10,260,25) 在左上角 (10,10) 画了宽 260 高 25 的框;OnTick 仅判断 password_status>0 才视作通过。
别把白名单当铁壁
这种方案的软肋很实在:只要有一个人把可用序列号发到论坛上,防护就破了。作者能做的只有发新版本、把被盗码塞进黑名单,但在外汇和贵金属这种高杠杆市场里,未授权拷贝跑起来的风控漏洞可能比少卖几份授权更麻烦。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| PasswordProtectedEA.mq5 | class=class="str">"cmt">//| Copyright class="num">2012, Investeo.pl | class=class="str">"cmt">//| http://www.investeo.pl | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2012, Investeo.pl" class="macro">#class="kw">property link "http:class=class="str">"cmt">//www.investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <ChartObjects/ChartObjectsTxtControls.mqh> CChartObjectEdit password_edit; const class="type">class="kw">string allowed_passwords[] = { "863H-class="num">6738725-JG76364", "145G-class="num">8927523-JG76364", "263H-class="num">7663233-JG76364" }; class="type">int password_status = -class="num">1; class="type">class="kw">string password_message[] = { "WRONG PASSWORD. Trading not allowed.", "EA PASSWORD verified." }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- password_edit.Create(class="num">0, "password_edit", class="num">0, class="num">10, class="num">10, class="num">260, class="num">25); password_edit.BackColor(White); password_edit.BorderColor(Black); password_edit.SetInteger(OBJPROP_SELECTED, class="num">0, true); class=class="str">"cmt">//--- class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- password_edit.Delete(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- if (password_status>class="num">0) { class=class="str">"cmt">// password correct } }
图表输入框里的密码校验逻辑
在 MT5 的 EA 或指标里,用 OnChartEvent 捕获用户在编辑框里的回车动作,是做本地轻量鉴权的常用手法。上面这段代码挂在一个名为 password_edit 的图表对象上,只在对象结束编辑且标识名为 "password_edit" 时才触发。 核心判断是拿编辑框当前文本和预存的 allowed_passwords 数组逐个比对。匹配成功就把 password_status 设成对应下标并跳出循环,失败则保持 -1。 随后根据 status 改写编辑框显示内容:status 为 -1 时塞入 password_message[0](通常是错误提示),否则塞入 password_message[1](可能是欢迎语)。这种写法不涉及网络请求,纯前端字符串比对,外汇或贵金属脚本里拿来做多账户分权可能够用,但明文比对本身高风险,别直接用于实盘资金相关校验。 想验证的话,在 MT5 新建指标工程,把 allowed_passwords 和 password_message 先写成字符串数组常量,拖一个 OBJ_EDIT 到图表并命名 password_edit,贴入下面代码看回显即可。
class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- if(id == CHARTEVENT_OBJECT_ENDEDIT && sparam == "password_edit") { password_status = -class="num">1; for(class="type">int i=class="num">0; i<ArraySize(allowed_passwords); i++) if(password_edit.GetString(OBJPROP_TEXT) == allowed_passwords[i]) { password_status = i; class="kw">break; } if(password_status == -class="num">1) password_edit.SetString(OBJPROP_TEXT, class="num">0, password_message[class="num">0]); else password_edit.SetString(OBJPROP_TEXT, class="num">0, password_message[class="num">1]); } }
「用规则化密钥锁死 EA 运行权限」
密钥生成器本质是一套按预设规则产出密码的机制,用来限制 EA 只在授权环境跑。最简单的形式是三段数字用连字符隔开,格式为 XXXXX-XXXXX-XXXXX,例如 3-4-5、18000-20000-20000,甚至 3708-102792-2844770 这类组合。 规则可以设定为第一段被 3 整除、第二段被 4 整除、第三段被 5 整除。位数和算法都能加码,比如把硬盘序列号或 CPU ID 揉进计算,让密码只认某一台硬件。 这种绑硬件的做法有边界:换机器或用 VPS 跑 EA 会失效,通常留两三个备用有效密码来兜底。MT5 社区市场里售卖的受保护 EA 就是这套逻辑。 下面这段存根演示了密码输入框与校验状态骨架。OnInit 里画出编辑框,OnTick 里仅当 password_status==3 才视作通过,其余情况不交易。 [CODE] 部分逐行看:前几行 property 声明版权与版本;包含两个系统头文件用于处理图表文本控件和字符串。password_edit 是输入框对象,divisor_sequence 数组存了 3.0/4.0/5.0 三个除数,对应三段校验。password_status 初始 -1 表示未验证,password_message 存了错误与通过两种提示。OnInit 在坐标(10,10)到(260,25)画白底黑框输入框并取消选中;OnDeinit 销毁它;OnTick 判断状态为 3 才放行,否则空转。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| KeyGeneratorProtectedEA.mq5 | class=class="str">"cmt">//| Copyright class="num">2012, Investeo.pl | class=class="str">"cmt">//| http://www.investeo.pl | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2012, Investeo.pl" class="macro">#class="kw">property link "http:class=class="str">"cmt">//www.investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <ChartObjects/ChartObjectsTxtControls.mqh> class="macro">#include <Strings/String.mqh> CChartObjectEdit password_edit; CString user_pass; const class="type">class="kw">double divisor_sequence[] = { class="num">3.0, class="num">4.0, class="num">5.0 }; class="type">int password_status = -class="num">1; class="type">class="kw">string password_message[] = { "WRONG PASSWORD. Trading not allowed.", "EA PASSWORD verified." }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- password_edit.Create(class="num">0, "password_edit", class="num">0, class="num">10, class="num">10, class="num">260, class="num">25); password_edit.BackColor(White); password_edit.BorderColor(Black); password_edit.SetInteger(OBJPROP_SELECTED, class="num">0, true); class=class="str">"cmt">//--- class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- password_edit.Delete(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- if (password_status==class="num">3) { class=class="str">"cmt">// password correct } } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 图表输入框里的分段校验逻辑
在 MT5 自定义指标或 EA 里,用 OnChartEvent 捕获 CHARTEVENT_OBJECT_ENDEDIT 事件,可以监听用户在图表对象(如密码框 password_edit)里敲完回车那一刻。上面这段把用户输入的字符串按两个连字符 '-' 拆成三段,再分别做整除校验。 代码先找首次和末次出现 '-' 的位置,若缺任一个或两位置重合,直接把提示语 password_message[0] 写回输入框并退出。之后用 Mid 切出三段转成整数,对 divisor_sequence[0~2] 三个除数做 MathMod 取模,全为 0 才让 password_status 累加到 3。 只有三段全部通过取模(password_status == 3),界面才显示 password_message[1],否则维持错误提示。你在 MT5 里把 divisor_sequence 设成例如 {3,7,11},就能逼着用户输入类似 '12-14-33' 这种满足模运算的串,外汇或贵金属脚本加这层交互校验,能挡掉大部分手滑输错,但任何终端脚本都只是辅助,实盘杠杆品种风险依旧很高。
class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- if (id == CHARTEVENT_OBJECT_ENDEDIT && sparam == "password_edit" ) { password_status = class="num">0; user_pass.Assign(password_edit.GetString(OBJPROP_TEXT)); class="type">int hyphen_1 = user_pass.Find(class="num">0, "-"); class="type">int hyphen_2 = user_pass.FindRev("-"); if (hyphen_1 == -class="num">1 || hyphen_2 == -class="num">1 || hyphen_1 == hyphen_2) { password_edit.SetString(OBJPROP_TEXT, class="num">0, password_message[class="num">0]); class="kw">return; } ; class="type">long pass_1 = StringToInteger(user_pass.Mid(class="num">0, hyphen_1)); class="type">long pass_2 = StringToInteger(user_pass.Mid(hyphen_1 + class="num">1, hyphen_2)); class="type">long pass_3 = StringToInteger(user_pass.Mid(hyphen_2 + class="num">1, StringLen(user_pass.Str()))); class=class="str">"cmt">// PrintFormat("%d : %d : %d", pass_1, pass_2, pass_3); if (MathIsValidNumber(pass_1) && MathMod((class="type">class="kw">double)pass_1, divisor_sequence[class="num">0]) == class="num">0.0) password_status++; if (MathIsValidNumber(pass_2) && MathMod((class="type">class="kw">double)pass_2, divisor_sequence[class="num">1]) == class="num">0.0) password_status++; if (MathIsValidNumber(pass_3) && MathMod((class="type">class="kw">double)pass_3, divisor_sequence[class="num">2]) == class="num">0.0) password_status++; if (password_status != class="num">3) password_edit.SetString(OBJPROP_TEXT, class="num">0, password_message[class="num">0]); else password_edit.SetString(OBJPROP_TEXT, class="num">0, password_message[class="num">1]); } } class=class="str">"cmt">//+------------------------------------------------------------------+