保证 MQL5 代码的安全:密码保护,钥匙生成器,时间限制,远程许可证及先进的 EA 许可证密钥加密技术·进阶篇
🔐

保证 MQL5 代码的安全:密码保护,钥匙生成器,时间限制,远程许可证及先进的 EA 许可证密钥加密技术·进阶篇

(2/3)·从单一密码到远程许可证,EA 防盗版链条还差这几环

实战向 第 2/3 篇
很多开发者以为加个密码输入框就万事大吉,结果序列号一泄露全网白送。进阶防护不是堆复杂度,而是把许可拆成可撤销、可限时的多层关卡。

「用账户号给EA上锁」

MT5 里每个经纪商终端的账户登录号都是唯一的,直接拿它做白名单就能限制 EA 只在指定账户跑。核心就两个调用:AccountInfoString(ACCOUNT_COMPANY) 取经纪商名,AccountInfoInteger(ACCOUNT_LOGIN) 取账户号,再和代码里写死的允许值比对。 这套做法实现成本低、防误装效果好,但硬伤也明显——每加一个授权账户号,就得重新编译发版,不适合大规模分发。 下面这段示例把允许经纪商写死为 MetaQuotes Software Corp.,账户号数组里放了 5 个:979890、436290、646490、225690、279260。OnInit 里比对通过就置 password_status=1,否则打印不允许运行。 别把经纪商名当万能钥匙 只校验 ACCOUNT_COMPANY 可能被同名的白标经纪商绕过,实盘部署前建议把登录号数组和经纪商名双重锁死,并自己在 MT5 用两个不同账户号测一遍通断。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                AccountProtectedEA.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"
const class="type">class="kw">string allowed_broker = "MetaQuotes Software Corp.";
const class="type">long allowed_accounts[] = { class="num">979890, class="num">436290, class="num">646490, class="num">225690, class="num">279260 };

class="type">int password_status = -class="num">1;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   class="type">class="kw">string broker = AccountInfoString(ACCOUNT_COMPANY);
   class="type">long account = AccountInfoInteger(ACCOUNT_LOGIN);

   printf("The name of the broker = %s", broker);
   printf("Account number =  %d", account);

   if (broker == allowed_broker)
      for (class="type">int i=class="num">0; i<ArraySize(allowed_accounts); i++)
      if (account == allowed_accounts[i]) {
         password_status = class="num">1;
         Print("EA account verified");
         class="kw">break;
      }
   if (password_status == -class="num">1) Print("EA is not allowed to run on this account.");

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">//---
  }
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">1)

授权校验的极简写法

在 EA 或指标里做基础权限控制,可以直接用字符串比对确认密码是否匹配。 下面这段 MQL5 片段展示了校验通过后的空分支处理:密码正确时仅留一个注释,不做额外动作,逻辑上把鉴权失败的交由外层拦截。

MQL5 / C++
  {
    class=class="str">"cmt">// password correct
  }
实盘加载前,建议把该比对逻辑放在 OnInit 或关键函数入口,避免每次 OnTick 都重复判断拖慢执行。外汇与贵金属品种波动剧烈,这类轻量校验不应成为性能瓶颈。

MQL5 / C++
  {
    class=class="str">"cmt">// password correct
  }

◍ 给 EA 和指标加时限锁

临时授权的场景在 MT5 里很常见:试用版 EA、按月或按年售卖的指标,都得靠时限保护来掐断过期后的使用权限。最直白的思路就是比对服务器时间,时间没到就放行功能,过了点就部分或完全禁用——这套逻辑对 EA 和指标都通用。 它的硬伤也明显:每发给一个客户,就得单独编译一份带特定到期日的 exe 或 ex5。没法像联网验证那样一套程序管所有人,分发成本随用户数线性上涨。 下面这段 MT5 代码演示了最朴素的时限锁。核心只有一个常量 allowed_until,以及 OnInit 和 OnTick 里对 TimeCurrent() 的比对。 //+------------------------------------------------------------------+

//TimeLimitProtectedEA.mq5
//Copyright 2012, Investeo.pl
//http://www.investeo.pl

//+------------------------------------------------------------------+ #property copyright "Copyright 2012, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" datetime allowed_until = D'2012.02.11 00:00'; int password_status = -1; //+------------------------------------------------------------------+

//Expert initialization function

//+------------------------------------------------------------------+ int OnInit() { //--- printf("This EA is valid until %s", TimeToString(allowed_until, TIME_DATE|TIME_MINUTES)); datetime now = TimeCurrent(); if (now < allowed_until) Print("EA time limit verified, EA init time : " + TimeToString(now, TIME_DATE|TIME_MINUTES)); //--- return(0); } //+------------------------------------------------------------------+

//Expert deinitialization function

//+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+

//Expert tick function

//+------------------------------------------------------------------+ void OnTick() { //--- if (TimeCurrent() < allowed_until) { } else Print("EA expired."); } 逐行拆解: #property version "1.00" 定义程序版本号,纯元数据。 datetime allowed_until = D'2012.02.11 00:00'; 设定到期时刻,D 前缀是 MQL5 的日期字面量语法,这里写死为 2012-02-11 零点。 int password_status = -1; 预留的状态变量,本例未实际使用。 OnInit()TimeCurrent() 取经纪商服务器当前时间;若早于 allowed_until 就打印验证通过,否则静默(未在初始化阻断)。 OnTick() 每笔报价触发,仍用 TimeCurrent() < allowed_until 判断;过期后只 Print("EA expired."),没有强制停止交易——真要锁功能得在里面加 ExpertRemove() 或跳过下单逻辑。 你在 MT5 里把 allowed_until 改成本月月底,编译发给朋友试跑,就能直观看到过期那天终端里开始刷 "EA expired."。外汇和贵金属杠杆高,这类时限 EA 仅作授权技术验证,实盘前务必自测风险。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                TimeLimitProtectedEA.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="type">class="kw">datetime allowed_until = D&class="macro">#x27;class="num">2012.02.class="num">11 class="num">00:class="num">00&class="macro">#x27;;

 class="type">int password_status = -class="num">1;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   printf("This EA is valid until %s", TimeToString(allowed_until, TIME_DATE|TIME_MINUTES));
   class="type">class="kw">datetime now = TimeCurrent();

   if (now < allowed_until)
       Print("EA time limit verified, EA init time : " + TimeToString(now, TIME_DATE|TIME_MINUTES));

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">//---
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
  if (TimeCurrent() < allowed_until)
    {
    }
   else Print("EA expired.");
  }

「用 RPC 把 EA 授权捏在远端手里」

想按用户逐个开关 EA 权限、或拖长试用期限,不必把逻辑写死在 MT5 端。走一次 MQL5-RPC 调用,把账户名丢给远端服务,它回你一个值:按试用跑、还是直接禁用。 下面这段 Python 起的 XML-RPC 服务,预置了两个 MetaTrader 5 许可,绑定默认 MetaQuotes 演示服务器(access.metatrader5.com:443)上账号 1024221 与 1024223 的 RemoteProtectedEA。生产环境大概率会接 Postgresql 或别的库,但示例足够看清远程许可的运转方式。 EA 侧只消在初始化时打一个远程调用到 isValid(),拿到 true 才放行。你也能把许可做成限时型:给人 10 天试用,不满意远端撤激活,满意就续任意时长。外汇与贵金属杠杆高,这类远程控制若网络抖动可能误杀实盘,上之前先在模拟盘验链路。 代码逐行拆解: from SimpleXMLRPCServer import SimpleXMLRPCServer — 引入 XML-RPC 服务基类。 from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler — 引入请求处理器。 class RequestHandler(SimpleXMLRPCRequestHandler): rpc_path = ('/RPC2',) — 限定 RPC 访问路径为 /RPC2。 class RemoteLicenseExample(SimpleXMLRPCServer): — 自定义许可服务类。 def __init__(self): SimpleXMLRPCServer.__init__(self, ('192.168.2.103', 9099), requestHandler=RequestHandler, logRequests=False) — 在 192.168.2.103:9099 起服务,关请求日志。 self.register_introspection_functions() — 注册内省函数。 self.register_function(self.xmlrpc_isValid, 'isValid') — 把校验方法暴露为 isValid。 self.licenses = {} — 初始化许可字典。 def addLicense(self, ea_name, broker_name, account_number): — 添加一条 EA+券商+账号的许可。 if ea_name in self.licenses: 已存在则追加,否则新建列表。 def listLicenses(self): print self.licenses — 打印当前许可。 def xmlrpc_isValid(self, ea_name, broker_name, account_number): — 远端校验入口。 isValidLicense = False — 默认无效。 try: account_number = int(account_number) except ValueError: return isValidLicense — 账号转整型失败直接拒。 if ea_name in self.licenses: 遍历比对券商名与账号,全中才置 True。 if __name__ == '__main__': 预置两个账号许可并永久服务。

MQL5 / C++
from SimpleXMLRPCServer class="kw">import SimpleXMLRPCServer
from SimpleXMLRPCServer class="kw">import SimpleXMLRPCRequestHandler
class RequestHandler( SimpleXMLRPCRequestHandler ):
    rpc_path = ( &class="macro">#x27;/RPC2&class="macro">#x27;, )
    
class RemoteLicenseExample(SimpleXMLRPCServer):
    def __init__(self):
        SimpleXMLRPCServer.__init__( self, ("class="num">192.168.class="num">2.103", class="num">9099), requestHandler=RequestHandler, logRequests = False)
        
        self.register_introspection_functions()
        self.register_function( self.xmlrpc_isValid, "isValid" )        
        
        self.licenses = {}
        
    def addLicense(self, ea_name, broker_name, account_number):
        if ea_name in self.licenses:
            self.licenses[ea_name].append({ &class="macro">#x27;broker_name&class="macro">#x27;: broker_name, &class="macro">#x27;account_number&class="macro">#x27; : account_number })
        else:
            self.licenses[ea_name] = [ { &class="macro">#x27;broker_name&class="macro">#x27;: broker_name, &class="macro">#x27;account_number&class="macro">#x27; : account_number } ]
            
    def listLicenses(self):
        print self.licenses
        
    def xmlrpc_isValid(self, ea_name, broker_name, account_number):
        isValidLicense = False
        
        ea_name = str(ea_name)
        broker_name = str(broker_name)
        
        print "Request for license", ea_name, broker_name, account_number
        
        try:
            account_number = class="type">int(account_number)
        except ValueError as error:
            class="kw">return isValidLicense
    
        if ea_name in self.licenses:
            for license in self.licenses[ea_name]:
                if license[&class="macro">#x27;broker_name&class="macro">#x27;] == broker_name and license[&class="macro">#x27;account_number&class="macro">#x27;] == account_number:
                    isValidLicense = True
                    class="kw">break
                
            print "License valid:", isValidLicense
            
            class="kw">return isValidLicense
    
if __name__ == &class="macro">#x27;__main__&class="macro">#x27;:
    server = RemoteLicenseExample()
    server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", class="num">1024221)
    server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", class="num">1024223)
    
    server.listLicenses()
    server.serve_forever()

EA 远程授权校验的初始化写法

把 EA 绑死到具体账户和券商,靠本地写死密码不现实,远程 license 校验是更稳的做法。下面这段初始化逻辑在 OnInit 里直连内网 RPC 服务(示例地址 192.168.2.103:9099),拿到 broker 名和登录号再去问服务端这个 EA 是否合法。 核心动作就三步:先取 AccountInfoString(ACCOUNT_COMPANY) 和 AccountInfoInteger(ACCOUNT_LOGIN),再把 "RemoteProtectedEA"、broker、account 塞进 XML-RPC 的 isValid 查询,最后从返回的 CArrayBool 里读第一位赋值给 license_status。若服务端连不上,直接打印 "License server not connected." 并保留默认 false。 实盘前你该把 192.168.2.103:9099 换成自己的验证机,且注意外汇/贵金属杠杆高,授权失效时 EA 应停止下单而非裸奔——这段代码只管标记 license_status,下单守卫得你自己补。

MQL5 / C++
class=class="str">"cmt">//|                                                                 RemoteProtectedEA.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 <MQL5-RPC.mqh>
class="macro">#include <Arrays\ArrayObj.mqh>
class="macro">#include <Arrays\ArrayInt.mqh>
class="macro">#include <Arrays\ArrayString.mqh>
class="macro">#include <Arrays\ArrayBool.mqh>
class="type">bool license_status=false;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
class=class="str">"cmt">/* License proxy server */
   CXMLRPCServerProxy s("class="num">192.168.class="num">2.103:class="num">9099");
   if(s.isConnected()==true)
     {
      CXMLRPCResult *result;
class=class="str">"cmt">/* Get account data */
      class="type">class="kw">string broker= AccountInfoString(ACCOUNT_COMPANY);
      class="type">long account = AccountInfoInteger(ACCOUNT_LOGIN);
      printf("The name of the broker = %s",broker);
      printf("Account number =  %d",account);
class=class="str">"cmt">/* Get remote license status */
      CArrayObj* params= new CArrayObj;
      CArrayString* ea = new CArrayString;
      CArrayString* br = new CArrayString;
      CArrayInt *ac=new CArrayInt;
      ea.Add("RemoteProtectedEA");
      br.Add(broker);
      ac.Add((class="type">int)account);
      params.Add(ea); params.Add(br); params.Add(ac);
      CXMLRPCQuery query("isValid",params);
      result=s.execute(query);
      CArrayObj *resultArray=result.getResults();
      if(resultArray!=NULL && resultArray.At(class="num">0).Type()==TYPE_BOOL)
        {
         CArrayBool *stats=resultArray.At(class="num">0);
         license_status=stats.At(class="num">0);
        }
      else license_status=false;
      if(license_status==true) printf("License valid.");
      else printf("License invalid.");
      class="kw">delete params;
      class="kw">delete result;
     }
   else Print("License server not connected.");
class=class="str">"cmt">//---
   class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把授权校验接进行情 tick

EA 的生命周期里,OnDeinit 负责退出清理,但上面这段模板里它是空壳,只留了 //--- 注释位,没做任何资源释放或指标句柄删除。真要落地,至少该在退出时把创建过的 iMA、iRSI 句柄用 IndicatorRelease 清掉,否则反复加载卸载可能漏句柄。 OnTick 才是行情驱动核心。示例里先判断 license_status==true 才进主逻辑,等于把「授权有效」作为一切交易的门禁。你在 MT5 里跑第三方 EA 时,可以故意把 license_status 初始值改成 false,观察图表是否彻底不交易——这能快速验证作者有没有把校验做成摆设。 外汇与贵金属杠杆高,这类带授权开关的 EA 若校验被绕过,风险不只亏本金,还可能触发经纪商风控。建议每次拿到新 EA 先单 tick 单步跟一遍 OnTick 入口。

MQL5 / C++
class=class="str">"cmt">//| Expert deinitialization function                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   if(license_status==true)
     {
      class=class="str">"cmt">// license valid
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
把许可校验交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到远程许可状态与到期提醒,你专注策略逻辑而非防盗细节。

常见问题

整除规则只是存根示例,实战中可替换为哈希校验或机器指纹绑定,规则越不透明越难批量破解。
在 OnInit 读取账户号并比对许可列表,不匹配直接 return INIT_FAILED,且远程黑名单可实时封禁。
可以,小布盯盘的 AIGC 模块能轮询许可服务器返回结果,在到期前推送通知,不用你手动查表。
倾向结合远程服务器下发剩余时长,避免本地时间被篡改导致防护失效,概率上更稳。
需借助 DLL 或 RPC 转发到外部程序处理,纯 MQL5 做 base64 混淆够用,超安全场景再上 PGP。