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

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

(3/3)·把密码、密钥生成、时限、远程许可与 PGP 加密串成一套可实战的 EA 防护链

案例拆解新手友好 第 3/3 篇
很多开发者以为加个密码框就算保护完成,结果序列号一泄露就只能发新版拉黑。其实 MQL5 里从本地校验到远程许可可以分层叠加,本篇把前两部分讲过的单点手段拼成完整防护网。

用 base64 给许可通道加一层壳

EA 走远程过程调用和许可服务器交换账户数据,明文 TCP 包用嗅探器就能抓,等于把注册校验裸奔在局域网上。把账户字段和回传信息先做 base64 编码再发,是最低成本的防嗅探手段,黑客截到包也只看到一串不可读的字符。 老手还会叠 PGP 或把核心逻辑塞进 DLL,但 base64 嵌套的思路像俄罗斯娃娃:XML-RPC 的有效负荷里包一个 base64 字符串,解码后还是 XML-RPC,再解回 MQL5 数据。这样即便第一层被破,里面还裹着一层。 MetaTrader 4 上 Renat 早写过 base64 实现,迁到 MQL5 只需改类型和数组声明。下面这段静态表就是编码用的 64 字符索引,长度固定 64,解码表则开到 256 以映射全部字节。 编码有效性可以用 Python 四行代码交叉验证,比如把 MQL5 吐出的串贴进 base64.b64decode 看能否还原。外汇和贵金属 EA 授权涉及真金白银,这类防护只能提高破解成本,无法绝对免疫,部署前建议在模拟盘环境先跑通。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Base64.mq4 |
class=class="str">"cmt">//|          Copyright © class="num">2006, MetaQuotes Software Corp. |
class=class="str">"cmt">//|       MT5 version © class="num">2012, Investeo.pl |
class=class="str">"cmt">//|                   https://www.metaquotes.net |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright © class="num">2006, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "https:class=class="str">"cmt">//www.metaquotes.net"
class="kw">static class="type">uchar ExtBase64Encode[class="num">64]={ &class="macro">#x27;A&class="macro">#x27;,&class="macro">#x27;B&class="macro">#x27;,&class="macro">#x27;C&class="macro">#x27;,&class="macro">#x27;D&class="macro">#x27;,&class="macro">#x27;E&class="macro">#x27;,&class="macro">#x27;F&class="macro">#x27;,&class="macro">#x27;G&class="macro">#x27;,&class="macro">#x27;H&class="macro">#x27;,&class="macro">#x27;I&class="macro">#x27;,&class="macro">#x27;J&class="macro">#x27;,&class="macro">#x27;K&class="macro">#x27;,&class="macro">#x27;L&class="macro">#x27;,&class="macro">#x27;M&class="macro">#x27;,
                                 &class="macro">#x27;N&class="macro">#x27;,&class="macro">#x27;O&class="macro">#x27;,&class="macro">#x27;P&class="macro">#x27;,&class="macro">#x27;Q&class="macro">#x27;,&class="macro">#x27;R&class="macro">#x27;,&class="macro">#x27;S&class="macro">#x27;,&class="macro">#x27;T&class="macro">#x27;,&class="macro">#x27;U&class="macro">#x27;,&class="macro">#x27;V&class="macro">#x27;,&class="macro">#x27;W&class="macro">#x27;,&class="macro">#x27;X&class="macro">#x27;,&class="macro">#x27;Y&class="macro">#x27;,&class="macro">#x27;Z&class="macro">#x27;,
                                 &class="macro">#x27;a&class="macro">#x27;,&class="macro">#x27;b&class="macro">#x27;,&class="macro">#x27;c&class="macro">#x27;,&class="macro">#x27;d&class="macro">#x27;,&class="macro">#x27;e&class="macro">#x27;,&class="macro">#x27;f&class="macro">#x27;,&class="macro">#x27;g&class="macro">#x27;,&class="macro">#x27;h&class="macro">#x27;,&class="macro">#x27;i&class="macro">#x27;,&class="macro">#x27;j&class="macro">#x27;,&class="macro">#x27;k&class="macro">#x27;,&class="macro">#x27;l&class="macro">#x27;,&class="macro">#x27;m&class="macro">#x27;,
                                 &class="macro">#x27;n&class="macro">#x27;,&class="macro">#x27;o&class="macro">#x27;,&class="macro">#x27;p&class="macro">#x27;,&class="macro">#x27;q&class="macro">#x27;,&class="macro">#x27;r&class="macro">#x27;,&class="macro">#x27;s&class="macro">#x27;,&class="macro">#x27;t&class="macro">#x27;,&class="macro">#x27;u&class="macro">#x27;,&class="macro">#x27;v&class="macro">#x27;,&class="macro">#x27;w&class="macro">#x27;,&class="macro">#x27;x&class="macro">#x27;,&class="macro">#x27;y&class="macro">#x27;,&class="macro">#x27;z&class="macro">#x27;,
                                 &class="macro">#x27;class="num">0&class="macro">#x27;,&class="macro">#x27;class="num">1&class="macro">#x27;,&class="macro">#x27;class="num">2&class="macro">#x27;,&class="macro">#x27;class="num">3&class="macro">#x27;,&class="macro">#x27;class="num">4&class="macro">#x27;,&class="macro">#x27;class="num">5&class="macro">#x27;,&class="macro">#x27;class="num">6&class="macro">#x27;,&class="macro">#x27;class="num">7&class="macro">#x27;,&class="macro">#x27;class="num">8&class="macro">#x27;,&class="macro">#x27;class="num">9&class="macro">#x27;,&class="macro">#x27;+&class="macro">#x27;,&class="macro">#x27;/&class="macro">#x27;      };
                                
class="kw">static class="type">uchar ExtBase64Decode[class="num">256]={

「索引矩阵的负值占位与连续段」

上面这串数字不是行情,而是一个 7 行 16 列的索引矩阵原始排布,用来映射指标缓冲区的逻辑位置。前 32 个格子全是 -1,代表该位置暂不参与计算,相当于占位空洞。 第 3 行末尾出现 62、63 两个正数,紧接着第 4 行是 52 到 61 的连续段,说明有效索引被拆成了不连续的块,中间用 -1 和 -2 隔开。 第 5 行从 0 到 14、第 6 行 15 到 25、第 7 行 26 到 40,是三段连续编号,每段尾部又回到 -1。开 MT5 把这类矩阵贴进缓冲区初始化函数,能直接看出哪段索引被激活、哪段被弃用。 外汇与贵金属杠杆高,这类底层映射写错一个偏移,面板显示就可能错位,实盘前务必在策略测试器单步核对。

◍ 缓冲区未覆盖区域的 -1 填充现象

上面这串数值来自指标缓冲区某段原始输出:前 11 个格位是 41 到 51 的连续整数,其后连续 96 个格位全部是 -1。 在 MT5 的自定义指标里,若某根 K 线尚未计算出有效值,缓冲区常会保留初值 -1 而非自动补零。当你用 CopyBuffer 读取超过已计算柱数范围的索引时,拿到的就是这类 -1。 开 MT5 把这段数据贴进脚本打印,或直接用 iCustom 取同长度缓冲区,能确认 -1 的占比是否随品种周期变化。外汇与贵金属属高风险品种,这类未初始化标记不代表任何方向信号,仅用于区分‘还没算’。

把字符串塞进 Base64 的编码回路

这段实现把任意字符串按 3 字节一组拆开,映射到一张 64 字符表上。表尾那 32 个 -1 是占位废项,对应 Base64 字母表之外的区间,解码时用来快速判错。 编码函数每次取 1~3 个字符:满 3 字节时算出 c1~c4 四个 6 位索引;只剩 1 字节则 pad=2 补两个 '=',剩 2 字节则 pad=1 补一个 '='。你在 MT5 里跑一段 16 字符的账号标识,输出长度会稳定是 24(含填充),这是 4/3 向上取整的结果。 下面这段是编码主体的原样代码,逐行看逻辑落点: void Base64Encode(string in,string &out) // 入参原串,out 用引用累加 { int i=0,pad=0,len=StringLen(in); // i 游标,pad 记录缺几字节 while(i<len) // 没吃完就继续 { int b3,b2,b1=StringGetCharacter(in,i); // 先拿第一个字节 i++; if(i>=len) { b2=0; b3=0; pad=2; } // 只剩1字节,后两个补0 else { b2=StringGetCharacter(in,i); // 拿第二个 i++; if(i>=len) { b3=0; pad=1; } // 只剩2字节 else { b3=StringGetCharacter(in,i); i++; } // 满3字节 } //---- int c1=(b1 >> 2); // 高6位 int c2=(((b1 & 0x3) << 4) | (b2 >> 4)); // 低2位+高4位 int c3=(((b2 & 0xf) << 2) | (b3 >> 6)); // 中4位+高2位 int c4=(b3 & 0x3f); // 低6位 out=out+CharToString(ExtBase64Encode[c1]); // 查表拼字符 out=out+CharToString(ExtBase64Encode[c2]); switch(pad) { case 0: // 满组 out=out+CharToString(ExtBase64Encode[c3]); out=out+CharToString(ExtBase64Encode[c4]); break; case 1: // 缺1字节 out=out+CharToString(ExtBase64Encode[c3]); out=out+"="; break; case 2: // 缺2字节 out=out+"=="; break; } } //---- } 解码函数开头只声明了游标和长度,说明它沿同样索引往回拆。外汇或贵金属 EA 里拿这套做配置串 obfuscation 时,注意 MT5 字符串最大长度受平台限制,超长 in 可能截断,属于高风险的小坑。

MQL5 / C++
class="type">void Base64Encode(class="type">class="kw">string in,class="type">class="kw">string &out)
  {
   class="type">int i=class="num">0,pad=class="num">0,len=StringLen(in);
   while(i<len)
     {
      class="type">int b3,b2,b1=StringGetCharacter(in,i);
      i++;
      if(i>=len) { b2=class="num">0; b3=class="num">0; pad=class="num">2; }
      else
        {
         b2=StringGetCharacter(in,i);
         i++;
         if(i>=len) { b3=class="num">0; pad=class="num">1; }
         else       { b3=StringGetCharacter(in,i); i++; }
        }
      class=class="str">"cmt">//----
      class="type">int c1=(b1 >> class="num">2);
      class="type">int c2=(((b1 & 0x3) << class="num">4) | (b2 >> class="num">4));
      class="type">int c3=(((b2 & 0xf) << class="num">2) | (b3 >> class="num">6));
      class="type">int c4=(b3 & 0x3f);
      out=out+CharToString(ExtBase64Encode[c1]);
      out=out+CharToString(ExtBase64Encode[c2]);
      class="kw">switch(pad)
        {
         case class="num">0:
           out=out+CharToString(ExtBase64Encode[c3]);
           out=out+CharToString(ExtBase64Encode[c4]);
           class="kw">break;
         case class="num">1:
           out=out+CharToString(ExtBase64Encode[c3]);
           out=out+"=";
           class="kw">break;
         case class="num">2:
           out=out+"==";
           class="kw">break;
        }
     }
class=class="str">"cmt">//----
  }
class="type">void Base64Decode(class="type">class="kw">string in,class="type">class="kw">string &out)
  {
   class="type">int i=class="num">0,len=StringLen(in);

「Base64 编解码在 MT5 里的落地验证」

把字符串塞进 MT5 做 Base64 编码,核心是把每 6 bit 拼回字节。下面这段解码循环就是典型实现:先取字符对应的 6 位值,左移累加器 6 位后并入,等移位计数到 8 以上就吐出一个完整字节。

MQL5 / C++
class="type">int shift=class="num">0,accum=class="num">0;
while(i<len)
  {
   class="type">int value=ExtBase64Decode[StringGetCharacter(in,i)];
   if(value<class="num">0 || value>class="num">63) class="kw">break;
   accum<<=class="num">6;
   shift+=class="num">6;
   accum|=value;
   if(shift>=class="num">8)
     {
      shift-=class="num">8;
      value=accum >> shift;
      out=out+CharToString((class="type">uchar)(value & 0xFF));
     }
   i++;
  }
逐行看:shift 和 accum 初始化为 0,用来记已攒的位数和中间值;循环里用 StringGetCharacter 拿输入字符再查解码表,值不在 0~63 就 break 退出;accum 每次左移 6 位给新值腾地方,shift 加 6 表示多攒了 6 bit;当 shift≥8 说明够一个字节,右移回退多余位后和 0xFF 掩码取低 8 位,CharToString 拼进结果串。 实测脚本把 <test>Abrakadabra</test> 丢进 Base64Encode,终端打印出 PHRlc3Q+QWJyYWthZGFicmE8L3Rlc3Q+;再 Base64Decode 回去,原串一字不差。外汇和贵金属 EA 里拿这招藏配置串有概率绕开明文嗅探,但 MT5 端高风险,参数泄露仍可能发生在日志或邮件里。 顺手用 Python 核对:base64.b64decode('PHRlc3Q+QWJyYWthZGFicmE8L3Rlc3Q+') 同样解回 <test>Abrakadabra</test>。开 MT5 建个 Script 引 Base64.mqh 跑一遍,比看文档直观。

MQL5 / C++
class="type">int shift=class="num">0,accum=class="num">0;
while(i<len)
  {
   class="type">int value=ExtBase64Decode[StringGetCharacter(in,i)];
   if(value<class="num">0 || value>class="num">63) class="kw">break;
   accum<<=class="num">6;
   shift+=class="num">6;
   accum|=value;
   if(shift>=class="num">8)
     {
      shift-=class="num">8;
      value=accum >> shift;
      out=out+CharToString((class="type">uchar)(value & 0xFF));
     }
   i++;
  }

◍ 用 XML-RPC 给 EA 做远程授权校验

把 EA 的激活逻辑放到外部 Python 服务,是脱离本地 DLL 校验的一种思路。下面这段服务端代码在 192.168.2.103:9099 起了个 SimpleXMLRPCServer,注册了 isValid 方法,EA 每次启动连过来问一次,服务端比对 broker 名和账户号,返回 base64 包裹的 XML 布尔值。 初始化时只放了两个测试授权:EA 名 RemoteProtectedEA,broker 为 MetaQuotes Software Corp.,账户号 1024221 与 1024223。也就是说,这两组账号在本地不写死也能过鉴权,换账号连会被 xmlrpc_isValid 里的 try/except 拦下,直接回 False。 注意 account_number 强制转 int,转失败就返回无效,这能挡掉大部分手填字符串的伪造请求。返回前把 XML 用 base64.b64encode 包一层,EA 侧解出来再读 boolean 节点,传输过程不直接露明文。 外汇与贵金属交易本身高风险,这种远程校验依赖网络与服务端存活,断网时 EA 可能直接锁死,实盘前务必在模拟环境跑通再决定要不要接。

MQL5 / C++
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
	
	# additional xml encoded with base64
	xml_response = "<?xml version=&class="macro">#x27;class="num">1.0&class="macro">#x27;?><methodResponse><params><param><value><boolean>%d</boolean></value></param></params></methodResponse>"
	
	retval = xml_response % class="type">int(isValidLicense)
	
	class="kw">return base64.b64encode(retval)

if __name__ == &class="macro">#x27;__main__&class="macro">#x27;:
	server = RemoteLicenseExampleBase64()
	server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", class="num">1024221)
	server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", class="num">1024223)
	
	server.listLicenses()
	server.serve_forever()

用局域网 RPC 给 EA 做远程授权校验

想在 MT5 里给 EA 加一道远程授权,不依赖本地破解,可以走 XML-RPC 让本机 EA 去连一台内网许可服务器要授权。下面这段代码在 OnInit 里连了 192.168.2.103:9099,连不上就直接打印未连接,连上了才继续验票。 校验时先抓当前账户信息:用 AccountInfoString(ACCOUNT_COMPANY) 拿券商名、AccountInfoInteger(ACCOUNT_LOGIN) 拿账号数字,然后塞进三个数组(EA名、券商、账号)作为 RPC 参数,调用远端方法 isValid。返回结果是一段 Base64 字符串,需要 Base64Decode 解开再当成内嵌的 XML-RPC 结果二次解析,最终取出第一个布尔值写进 license_status。 实测中若服务器返回结构正常,终端会先后打印 encoded license 和 decoded license 两段;若解码后首元素不是 TYPE_STRING,license_status 会被硬性置为 false,EA 后续逻辑可据此决定停跑。外汇与贵金属交易杠杆高,这类授权机制只解决版权管控,不改善任何胜率,部署前请在模拟盘验证网络连通性。 别把内网地址写死当万能 生产环境里 192.168.x.x 是随路由器分配的,换网段就连不上。建议把 IP:端口提为外部输入参数,小布盯盘跑回测时改一处就能切测试服与生产服。

MQL5 / C++
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="macro">#include <Base64.mqh>
class="type">bool license_status=false;
class="type">int OnInit()
  {
   CXMLRPCServerProxy s("class="num">192.168.class="num">2.103:class="num">9099");
   if(s.isConnected()==true)
     {
      CXMLRPCResult *result;
      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);
      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_STRING)
        {
         CArrayString *stats=resultArray.At(class="num">0);
         class="type">class="kw">string license_encoded=stats.At(class="num">0);
         printf("encoded license: %s",license_encoded);
         class="type">class="kw">string license_decoded;
         Base64Decode(license_encoded,license_decoded);
         printf("decoded license: %s",license_decoded);
         CXMLRPCResult license(license_decoded);
         resultArray=license.getResults();
         CArrayBool *bstats=resultArray.At(class="num">0);
         license_status=bstats.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="kw">return(class="num">0);
  }

「EA 退出与心跳里的授权校验」

EA 骨架里 OnDeinit 通常为空实现,仅占位供清场用;真正的逻辑入口在 OnTick。下面这段裸结构显示了最小可编译形态: void OnDeinit(const int reason) { } // 卸载时什么都不做 void OnTick() { if(license_status==true) { /* license valid */ } } // 每跳先判授权再跑策略 实盘日志能反推校验链路。EURUSD H1 下 19:47:57 一帧打出 broker=MetaQuotes Software Corp.、account=1024223,随后远端返回 Base64 串 PD94bWwg...PjwvbWV0aG9kUmVzcG9uc2U+,解码后为标准 XML-RPC:<methodResponse><params><param><value><boolean>1</boolean></value></param></params></methodResponse>,即 license valid。 把授权状态塞进全局布尔再在 OnTick 首行判,是低成本防裸奔做法;外汇与贵金属杠杆高,这类远程校验若超时或返回 0,策略应倾向直接空转而非强行下单。

MQL5 / C++
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(license_status==true)
     {
      class=class="str">"cmt">// license valid
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用参数加密堵住反编译的口子

MQL5 程序一旦落地成 ex5 被逆向,再厚的壳也挡不住有经验的逆向人员。网上能搜到号称做 MQL5 反编译的站点,多数更像盯准想白嫖代码的人群敛财,没实测过不敢断言绝对假,但风险信号很明显。 真正能抬高的门槛不在代码混淆,而在运行期参数。把 EA 或指标的关键输入用加密通道下发,黑客即便反编译出骨架,拿不到匹配账户的明文参数,跑出来的逻辑也是废的。 一种可行做法是绑定账户 ID:校验通过才发真参,不匹配就丢一组假参数进去,让逆向结果自相矛盾。更硬一点可上 PGP,用私钥加密参数,仅当账户 ID 与密钥对得上时才可能解密——代码被扒光,数据依旧锁着。 外汇与贵金属品种波动剧烈、杠杆风险高,这类保护只解决知识产权,不替你扛任何交易亏损,上生产前务必在 MT5 策略测试器里用不同账户 ID 验证真假参分支。

把防护思路落到自己的 EX5 工程里

前面几节把 MQL5 代码保护的几条路径都拆开了:本地混淆、MQL5-RPC 远程许可校验、base64 编解码支撑。实际工程里没有一种能彻底防住反编译,讨论区一条高赞留言说得很直白——核心问题只是「在产品被破解前,还能赢得多少时间」,目标是把破解成本堆到不值得动手。 附带的两个压缩包给出了可跑的雏形:simpleremotelicenseservers.zip 仅 2.01 KB,是轻量许可服务端;license_examples_mql5.zip 24.45 KB,含调用示例。想验证就直接下下来,在 MT5 里挂上 RPC 调用看返回逻辑,比读文字快。 外汇与贵金属 EA 涉及杠杆交易,高风险,任何保护手段都不构成盈利保证;代码安全只是减少被白嫖的概率,别指望它替你挡住所有逆向。把示例里的标识符改名、把校验点藏进服务器响应,先在自己机器上跑通再说。

把重复校验交给小布盯盘
这些本地密码与远程许可的诊断逻辑,小布盯盘的 AIGC 已内置,打开对应 EA 品种页即可看到校验状态,你只需专注策略本身。

常见问题

整除规则容易被逆向推导,破解者写个脚本就能批量产出合法密钥,建议叠加随机盐值或远端校验。
可以,小布盯盘的品种页会读取时限与远程许可返回状态,临近失效时给出提示,省去你手动查日志。
不算,base64 只是编码,肉眼不可读但随时可解,仅适合做传输包装,核心仍需 PGP 或对称加密。
取决于你写的容错逻辑,倾向在离线时降级到本地时限校验,否则可能误杀正常用户。
不冲突,两者可并行,市场管分发你管高级权限,组合后对抗盗版的概率更高。