English Русский Español Deutsch 日本語
preview
在 MQL5 中创建交易管理员面板(第五部分):双因素认证(2FA)

在 MQL5 中创建交易管理员面板(第五部分):双因素认证(2FA)

MetaTrader 5示例 |
331 0
Clemence Benjamin
Clemence Benjamin

内容:


引言

此前我们在管理员面板中探讨了密码认证的实现方法,这是保护管理员与交易者之间通信安全的首要且关键环节。尽管这种基本的安全形式对于初步保护至关重要,但由于其依赖于单一的验证因素,因此可能会使这些系统暴露于漏洞之中。引入双因素认证(2FA)成为了一项至关重要的升级措施,为我们的应用程序提供了更强大的安全框架。

这种方法显著降低了未经授权访问的风险,因为它要求用户除了知道密码外,还需要访问第二种验证方法。2FA确保所有通信均来自合法来源,从而保护系统免受数据泄露、虚假信息和市场操纵等潜在后果的影响。

通过将2FA集成到管理员面板中,我们可以为用户提供更高水平的信任和安全性。这种双重认证系统作为防范潜在入侵的强大威慑力量,让管理员和交易者在动态的金融环境中操作时能够安心。今天,我们将讨论一个我已经成功转化为实际解决方案的概念。这一举措加强了管理员面板的安全性,以应对之前系列文章中实现的,基于密码的安全措施所可能存在的漏洞。


什么是双因素认证(2FA)

双因素认证(2FA)是一种安全机制,要求用户在访问账户或系统之前提供两种不同形式的验证。它是多因素认证(MFA)的一个子集,MFA可以涉及两种或更多种验证因素。2FA的主要目标是在用户名和密码之外再增加一层安全保护,使未经授权的用户更难获得访问权限。

根据一些资料,2FA通常涉及以下三类认证因素中的两类:

  1. 您所知晓的(认知因素):这通常是用户为了访问其账户而必须输入的密码或个人识别码(PIN)。它作为第一道防线。
  2. 您所拥有的(拥有因素):这涉及用户拥有的物理设备或令牌,如安全令牌、智能卡或手机。许多系统使用Google Authenticator或Authy等应用程序来生成一次性密码(OTP),这些密码通常在短时间内有效(通常为15分钟)。
  3. 您所是的(生物特征因素):这可能包括指纹识别、面部识别或其他生物特征数据。虽然生物特征并不总是与前两个因素一起用于2FA,但它们可以提供额外的安全层。


用MQL5在管理员面板中实现双因素认证(2FA)

通过集成2FA,我们正在超越传统的单因素认证(SFA),后者通常仅依赖于用户名和密码。这种转变至关重要,因为尽管密码仍然是最常见的初始安全形式,但它们本质上容易受到各种类型的攻击,包括社会工程、暴力破解和字典攻击。2FA的多因素方法通过要求用户提供属于不同类别的两种不同形式的认证,有效地降低了这些风险,从而增加了对真正授权用户访问的信心。

为了在我们的管理员面板项目中实现2FA,我利用了Dialog库,它允许我们创建由特定逻辑控制的多个窗口层。在这个系列的开始,我们集成了Telegram通信,主要侧重于从管理员面板向Telegram上的用户频道或群组发送消息。然而,其潜力远不止于此;我们还旨在利用它来发送一次性密码(OTP)。

在这个项目中,我们将调整代码以生成一个随机的六位数代码,程序将安全地存储该代码,并随后将其发送到管理员的Telegram以进行验证。我注意到许多公司使用Telegram进行验证,尽管它们采用的方法与我们将在这里实现的方法有所不同。如在下面的图片中,您可以看到MQL5验证机器人,它就是这种用法的一个例子。

MQL5 Verifcation Bot

MQL5验证机器人


实现密码输入和双因素认证(2FA)代码验证的图形用户界面(GUI)元素

在实现密码输入对话框时,我们首先定义ShowAuthenticationPrompt()函数,该函数封装了创建密码输入用户界面所需的所有步骤。该过程从使用Create()方法实例化认证对话框开始,指定其在图表上的尺寸和位置。对于用户输入,我们创建一个passwordInputBox来安全地捕获用户的密码。随后,添加一个passwordPromptLabe以向用户提供清晰的指示,引导他们了解对话框的用途。为了处理用户反馈,特别是对于错误的输入,我们实现了一个feedbackLabel,错误消息将以红色文本显示在其中,从而增强用户理解出错原因的能力。接下来,我们设置两个按钮:

  • 一个loginButton,用户可以点击它来提交密码进行认证。 
  • 一个closeAuthButton,允许他们在决定不继续时退出对话框。 

最后,我们在认证对话框上调用show()以将其呈现给用户,并调用ChartRedraw()以确保所有组件在屏幕上正确显示。这种系统化的方法确保了在管理员面板中实现了一个安全且用户友好的密码输入界面。请参阅下一个段代码。

密码输入对话框创建

// Show authentication input dialog
bool ShowAuthenticationPrompt()
{
    if (!authentication.Create(ChartID(), "Authentication", 0, 100, 100, 500, 300))
    {
        Print("Failed to create authentication dialog");
        return false;
    }

    // Create password input box
    if (!passwordInputBox.Create(ChartID(), "PasswordInputBox", 0, 20, 70, 260, 95))
    {
        Print("Failed to create password input box");
        return false;
    }
    authentication.Add(passwordInputBox);

    // Create password prompt label
    if (!passwordPromptLabel.Create(ChartID(), "PasswordPromptLabel", 0, 20, 20, 260, 40))
    {
        Print("Failed to create password prompt label");
        return false;
    }
    passwordPromptLabel.Text("Enter password: Access Admin Panel");
    authentication.Add(passwordPromptLabel);

    // Create feedback label for wrong attempts
    if (!feedbackLabel.Create(ChartID(), "FeedbackLabel", 0, 20, 140, 380, 40))
    {
        Print("Failed to create feedback label");
        return false;
    }
    feedbackLabel.Text("");
    feedbackLabel.Color(clrRed); // Set color for feedback
    authentication.Add(feedbackLabel);

    // Create login button
    if (!loginButton.Create(ChartID(), "LoginButton", 0, 20, 120, 100, 40))
    {
        Print("Failed to create login button");
        return false;
    }
    loginButton.Text("Login");
    authentication.Add(loginButton);

    // Create close button for authentication dialog
    if (!closeAuthButton.Create(ChartID(), "CloseAuthButton", 0, 120, 120, 200, 40))
    {
        Print("Failed to create close button for authentication");
        return false;
    }
    closeAuthButton.Text("Close");
    authentication.Add(closeAuthButton);

    authentication.Show();
    ChartRedraw();
    return true;
}


创建2FA认证对话框

为了创建双因素认证(2FA)代码验证对话框,我们定义了ShowTwoFactorAuthPrompt()函数,该函数负责处理验证用户双因素认证代码所需的所有组件。

我们首先使用Create()方法来创建twoFactorAuth对话框,并设置其相关属性。添加的第一个组件是twoFACodeInput,这是一个输入框,设计用于安全地捕获发送到用户Telegram的2FA代码。

为了引导用户正确操作,我们实现了一个twoFAPromptLabel,它清晰地指示用户输入他们所收到的2FA代码。为了进一步提升用户体验,我们加入了一个twoFAFeedbackLabel用于显示实时反馈;如果用户输入的代码与预期值不匹配,该标签将以红色显示消息,从而告知用户输入有误。

对于提交操作,我们创建了一个twoFALoginButton,使用户能够验证他们的代码,同时提供了一个close2FAButton,以便用户在必要时退出对话框。一旦所有组件设置完毕,我们就在twoFactorAuth对话框上调用Show()方法以使其对用户可见,并调用ChartRedraw()来刷新界面,确保所有组件正确显示。

这种结构化的方法确保了在管理员面板中验证2FA代码的安全性,并提供了简化的用户交互体验。以下代码片段可帮助更好地理解这一点。

// Show two-factor authentication input dialog
void ShowTwoFactorAuthPrompt()
{
    if (!twoFactorAuth.Create(ChartID(), "Two-Factor Authentication", 0, 100, 100, 500, 300))
    {
        Print("Failed to create 2FA dialog");
        return;
    }

    // Create input box for 2FA code
    if (!twoFACodeInput.Create(ChartID(), "TwoFACodeInput", 0, 20, 70, 260, 95))
    {
        Print("Failed to create 2FA code input box");
        return;
    }
    twoFactorAuth.Add(twoFACodeInput);

    // Create prompt label for 2FA
    if (!twoFAPromptLabel.Create(ChartID(), "TwoFAPromptLabel", 0, 20, 20, 380, 40))
    {
        Print("Failed to create 2FA prompt label");
        return;
    }
    twoFAPromptLabel.Text("Enter the 2FA code sent to your Telegram:");
    twoFactorAuth.Add(twoFAPromptLabel);

    // Create feedback label for wrong attempts
    if (!twoFAFeedbackLabel.Create(ChartID(), "TwoFAFeedbackLabel", 0, 20, 140, 380, 40))
    {
        Print("Failed to create 2FA feedback label");
        return;
    }
    twoFAFeedbackLabel.Text("");
    twoFAFeedbackLabel.Color(clrRed); // Set color for feedback
    twoFactorAuth.Add(twoFAFeedbackLabel);

    // Create login button for 2FA code submission
    if (!twoFALoginButton.Create(ChartID(), "TwoFALoginButton", 0, 20, 120, 100, 40))
    {
        Print("Failed to create 2FA login button");
        return;
    }
    twoFALoginButton.Text("Verify");
    twoFactorAuth.Add(twoFALoginButton);

    // Create close button for 2FA dialog
    if (!close2FAButton.Create(ChartID(), "Close2FAButton", 0, 120, 120, 200, 40))
    {
        Print("Failed to create close button for 2FA");
        return;
    }
    close2FAButton.Text("Close");
    twoFactorAuth.Add(close2FAButton);

    twoFactorAuth.Show();
    ChartRedraw();
}


认证代码生成算法

在成功输入用于访问管理员面板的密码后,我们建立了一个涉及一次性密码(OTP)生成的第二层保护。该代码被安全存储并转发到与 Telegram 应用程序(无论是移动设备还是桌面端)关联的唯一硬编码聊天 ID,合法所有者可以在那里获取该代码,并进一步在提示中输入。如果输入的代码匹配,应用程序将授予对管理员面板的完整功能访问权限,以便进行操作和通信。

关于代码生成算法,我将在下面的代码片段中解释各个组成部分:

下面的代码行是我们在程序中管理双因素身份验证(2FA)的关键变量声明。

string twoFACode = "";

此行代码将变量 twoFACode 初始化为空字符串,它将用于存储随机生成的 6 位双因素身份验证代码。在整个身份验证过程中,这个变量在用户成功输入正确密码以访问管理员面板后,通过 Telegram 将实际代码发送给用户时,发挥着关键作用。

当用户通过初始密码检查后,变量 twoFACode 将被 GenerateRandom6DigitCode() 函数生成的新值填充,该函数生成一个 6 位数字字符串。然后通过 SendMessageToTelegram() 函数将该值发送到用户的 Telegram。

稍后,当用户被提示输入他们的 2FA 代码 时,程序会将用户的输入与存储在 twoFACode 中的值进行比较。如果用户的输入与存储在 twoFACode 中的值匹配,则授予对管理员面板的访问权限;否则,将显示错误消息。

1. 密码验证:

string Password = "2024"; // Hardcoded password

// Handle login button click
void OnLoginButtonClick()
{
    string enteredPassword = passwordInputBox.Text();
    if (enteredPassword == Password)
    {
        twoFACode = GenerateRandom6DigitCode();
        SendMessageToTelegram("Your 2FA code is: " + twoFACode, Hardcoded2FAChatId, Hardcoded2FABotToken);
        authentication.Destroy();
        ShowTwoFactorAuthPrompt();
        Print("Password authentication successful. A 2FA code has been sent to your Telegram.");
    }
    else
    {
        feedbackLabel.Text("Wrong password. Try again.");
        passwordInputBox.Text("");
    }
}

一个硬编码的密码("2024")作为管理员面板的访问控制机制。当用户通过指定的输入框提交输入的密码时,代码会检查输入是否与硬编码的密码匹配。如果匹配,将使用 GenerateRandom6DigitCode() 函数生成一个随机的 6 位代码,并通过 SendMessageToTelegram() 函数发送到用户的 Telegram。这表明身份验证成功,并提示应用程序进入双因素身份验证阶段。如果密码错误,错误消息会提示用户重新尝试。

2. 双因素身份验证代码生成与交互:

// Generate a random 6-digit code for 2FA
string GenerateRandom6DigitCode()
{
    int code = MathRand() % 1000000; // Produces a 6-digit number
    return StringFormat("%06d", code); // Ensures leading zeros
}

// Handle 2FA login button click
void OnTwoFALoginButtonClick()
{
    string enteredCode = twoFACodeInput.Text();
    if (enteredCode == twoFACode)
    {
        twoFactorAuth.Destroy();
        adminPanel.Show();
        Print("2FA authentication successful.");
    }
    else
    {
        twoFAFeedbackLabel.Text("Wrong code. Try again.");
        twoFACodeInput.Text("");
    }
}

此部分通过生成一个独特的 6 位代码并验证用户输入与该代码的匹配情况来管理双因素身份验证(2FA)。GenerateRandom6DigitCode() 函数通过使用 MathRand() 函数生成一个 6 位数字,确保即使存在前导零,也能保持所需的格式。在初始密码验证通过后,该 6 位代码会通过 Telegram 发送到用户指定的聊天中,以增强安全性。在后续的 OnTwoFALoginButtonClick() 函数中,会检查用户的输入是否与生成的代码匹配。如果输入的代码与发送到 Telegram 的代码一致,则授予对管理员面板的访问权限;否则,会通知用户代码错误,并提示其重新尝试。


MathRand()函数的说明

在 MQL5 中,这个函数用于生成伪随机整数。在我们的项目中,MathRand() 函数生成一个范围在 0MathRandMax() 之间的随机整数,通常定义为 0 到 32767。 

为了生成一个随机的 6 位数字,可以通过应用取模运算符(%)来限制输出。这个运算符计算除法的余数,允许您将随机数字的范围限制在有效的 6 位数字值内,范围从 000000 (0)999999

具体来说,使用表达式 MathRand() % 1000000 将会得到一个介于 0999999 之间的结果,确保所有可能的 6 位数字组合(包括前导零)都被涵盖。


2FA认证的Telegram API

SendMessageToTelegram() 函数对于确保我们的 2FA 验证码和其他消息安全地发送到用户的 Telegram 聊天中至关重要。该函数构建一个 HTTP POST 请求,发送到 Telegram Bot API,包括 bot token 和目标 chat ID。消息以 JSON 格式化,以符合 API 的要求。

WebRequest() 函数执行请求并设置指定的超时时间,该函数会检查响应代码以确认消息是否成功发送(HTTP 状态码 200)。

如果消息发送失败,函数会记录错误,包括响应代码、错误消息以及任何相关的响应内容,帮助识别发送消息时可能出现的问题,以便进一步诊断。

关于 Telegram API 的更多信息,您可以访问官网,并阅读我们之前关于它的介绍文章。

bool SendMessageToTelegram(string message, string chatId, string botToken)
{
    string url = "https://api.telegram.org/bot" + botToken + "/sendMessage";
    string jsonMessage = "{\"chat_id\":\"" + chatId + "\", \"text\":\"" + message + "\"}";

    char postData[];
    ArrayResize(postData, StringToCharArray(jsonMessage, postData) - 1);

    int timeout = 5000;
    char result[];
    string responseHeaders;
    int responseCode = WebRequest("POST", url, "Content-Type: application/json\r\n", timeout, postData, result, responseHeaders);

    if (responseCode == 200)
    {
        Print("Message sent successfully: ", message);
        return true;
    }
    else
    {
        Print("Failed to send message. HTTP code: ", responseCode, " Error code: ", GetLastError());
        Print("Response: ", CharArrayToString(result));
        return false;
    }
}

作为 Telegram API 的一部分,我们在项目中引入了以下常量,以便通过 Telegram 促进双因素身份验证(2FA)过程。 

// Constants for 2FA
const string Hardcoded2FAChatId = "REPLACE WITH YOUR CHAT ID";
const string Hardcoded2FABotToken = "REPLACE WITH YOUR ACTUAL BOT TOKEN";

在这段代码中,变量 Hardcoded2FAChatId 表示将接收身份验证消息的 Telegram 聊天的唯一聊天标识符,而 Hardcoded2FABotToken 则存储用于发送消息的 Telegram 机器人的令牌。

机器人令牌对于验证向 Telegram API 发起的请求至关重要,它确保只有具有正确权限的合法机器人可以向指定的聊天发送消息。通过将这些常量硬编码到程序中,发送 2FA 验证码的过程得以简化,因为每次都会使用相同的聊天 ID 和机器人令牌,无需用户输入或配置。

然而,需要注意的是,硬编码敏感信息(如机器人令牌)可能会带来安全风险,尤其是当代码被泄露时。因此,在生产环境中应考虑使用更安全的存储方法。

以下是完整的代码,我们可以看到,随着新功能的实现,代码量正在显著增加。

//+------------------------------------------------------------------+
//|                                             Admin Panel.mq5      |
//|                           Copyright 2024, Clemence Benjamin      |
//|        https://www.mql5.com/en/users/billionaire2024/seller      |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, Clemence Benjamin"
#property link      "https://www.mql5.com/en/users/billionaire2024/seller"
#property description "A secure and  responsive Admin Panel. Send messages to your telegram clients without leaving MT5"
#property version   "1.20"

#include <Trade\Trade.mqh>
#include <Controls\Dialog.mqh>
#include <Controls\Button.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Label.mqh>

// Input parameters for quick messages
input string QuickMessage1 = "Updates";
input string QuickMessage2 = "Close all";
input string QuickMessage3 = "In deep profits";
input string QuickMessage4 = "Hold position";
input string QuickMessage5 = "Swing Entry";
input string QuickMessage6 = "Scalp Entry";
input string QuickMessage7 = "Book profit";
input string QuickMessage8 = "Invalid Signal";
input string InputChatId = "YOUR_CHAT_ID";
input string InputBotToken = "YOUR_BOT_TOKEN";

// Constants for 2FA
const string Hardcoded2FAChatId = "ENTER YOUR REAL CHAT ID";
const string Hardcoded2FABotToken = "ENTER YOUR Telegram Bot Token";

// Global variables
CDialog adminPanel;
CDialog authentication, twoFactorAuth;
CButton sendButton, clearButton, changeFontButton, toggleThemeButton;
CButton loginButton, closeAuthButton, twoFALoginButton, close2FAButton;
CButton quickMessageButtons[8], minimizeButton, maximizeButton, closeButton;
CEdit inputBox, passwordInputBox, twoFACodeInput;
CLabel charCounter, passwordPromptLabel, feedbackLabel, twoFAPromptLabel, twoFAFeedbackLabel;
bool minimized = false;
bool darkTheme = false;
int MAX_MESSAGE_LENGTH = 4096;
string availableFonts[] = { "Arial", "Courier New", "Verdana", "Times New Roman" };
int currentFontIndex = 0;
string Password = "2024"; // Hardcoded password
string twoFACode = "";

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    if (!ShowAuthenticationPrompt())
    {
        Print("Authorization failed. Exiting...");
        return INIT_FAILED;
    }

    if (!adminPanel.Create(ChartID(), "Admin Panel", 0, 30, 30, 500, 500))
    {
        Print("Failed to create admin panel dialog");
        return INIT_FAILED;
    }

    if (!CreateControls())
    {
        Print("Control creation failed");
        return INIT_FAILED;
    }

    adminPanel.Hide();
    Print("Initialization complete");
    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Show authentication input dialog                                 |
//+------------------------------------------------------------------+
bool ShowAuthenticationPrompt()
{
    if (!authentication.Create(ChartID(), "Authentication", 0, 100, 100, 500, 300))
    {
        Print("Failed to create authentication dialog");
        return false;
    }

    if (!passwordInputBox.Create(ChartID(), "PasswordInputBox", 0, 20, 70, 260, 95))
    {
        Print("Failed to create password input box");
        return false;
    }
    authentication.Add(passwordInputBox);

    if (!passwordPromptLabel.Create(ChartID(), "PasswordPromptLabel", 0, 20, 20, 260, 40))
    {
        Print("Failed to create password prompt label");
        return false;
    }
    passwordPromptLabel.Text("Enter password: Access Admin Panel");
    authentication.Add(passwordPromptLabel);

    if (!feedbackLabel.Create(ChartID(), "FeedbackLabel", 0, 20, 140, 380, 160))
    {
        Print("Failed to create feedback label");
        return false;
    }
    feedbackLabel.Text("");
    feedbackLabel.Color(clrRed); // Red color for incorrect attempts
    authentication.Add(feedbackLabel);

    if (!loginButton.Create(ChartID(), "LoginButton", 0, 20, 120, 100, 140))
    {
        Print("Failed to create login button");
        return false;
    }
    loginButton.Text("Login");
    authentication.Add(loginButton);

    if (!closeAuthButton.Create(ChartID(), "CloseAuthButton", 0, 120, 120, 200, 140))
    {
        Print("Failed to create close button for authentication");
        return false;
    }
    closeAuthButton.Text("Close");
    authentication.Add(closeAuthButton);

    authentication.Show();
    ChartRedraw();

    return true;
}

//+------------------------------------------------------------------+
//| Show two-factor authentication input dialog                      |
//+------------------------------------------------------------------+
void ShowTwoFactorAuthPrompt()
{
    if (!twoFactorAuth.Create(ChartID(), "Two-Factor Authentication", 0, 100, 100, 500, 300))
    {
        Print("Failed to create 2FA dialog");
        return;
    }

    if (!twoFACodeInput.Create(ChartID(), "TwoFACodeInput", 0, 20, 70, 260, 95))
    {
        Print("Failed to create 2FA code input box");
        return;
    }
    twoFactorAuth.Add(twoFACodeInput);

    if (!twoFAPromptLabel.Create(ChartID(), "TwoFAPromptLabel", 0, 20, 20, 380, 40))
    {
        Print("Failed to create 2FA prompt label");
        return;
    }
    twoFAPromptLabel.Text("Enter the 2FA code sent to your Telegram:");
    twoFactorAuth.Add(twoFAPromptLabel);

    if (!twoFAFeedbackLabel.Create(ChartID(), "TwoFAFeedbackLabel", 0, 20, 140, 380, 160))
    {
        Print("Failed to create 2FA feedback label");
        return;
    }
    twoFAFeedbackLabel.Text("");
    twoFAFeedbackLabel.Color(clrRed); // Red color for incorrect 2FA attempts
    twoFactorAuth.Add(twoFAFeedbackLabel);

    if (!twoFALoginButton.Create(ChartID(), "TwoFALoginButton", 0, 20, 120, 100, 140))
    {
        Print("Failed to create 2FA login button");
        return;
    }
    twoFALoginButton.Text("Verify");
    twoFactorAuth.Add(twoFALoginButton);

    if (!close2FAButton.Create(ChartID(), "Close2FAButton", 0, 120, 120, 200, 140))
    {
        Print("Failed to create close button for 2FA");
        return;
    }
    close2FAButton.Text("Close");
    twoFactorAuth.Add(close2FAButton);

    twoFactorAuth.Show();
    ChartRedraw();
}

//+------------------------------------------------------------------+
//| Handle chart events                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
    if (id == CHARTEVENT_OBJECT_CLICK)
    {
        if (sparam == "LoginButton")
        {
            OnLoginButtonClick();
        }
        else if (sparam == "CloseAuthButton")
        {
            OnCloseAuthButtonClick();
        }
        else if (sparam == "TwoFALoginButton")
        {
            OnTwoFALoginButtonClick();
        }
        else if (sparam == "Close2FAButton")
        {
            OnClose2FAButtonClick();
        }
    }

    switch (id)
    {
        case CHARTEVENT_OBJECT_CLICK:
            if (sparam == "SendButton") OnSendButtonClick();
            else if (sparam == "ClearButton") OnClearButtonClick();
            else if (sparam == "ChangeFontButton") OnChangeFontButtonClick();
            else if (sparam == "ToggleThemeButton") OnToggleThemeButtonClick();
            else if (sparam == "MinimizeButton") OnMinimizeButtonClick();
            else if (sparam == "MaximizeButton") OnMaximizeButtonClick();
            else if (sparam == "CloseButton") OnCloseButtonClick();
            else if (StringFind(sparam, "QuickMessageButton") != -1)
            {
                long index = StringToInteger(StringSubstr(sparam, 18));
                OnQuickMessageButtonClick(index - 1);
            }
            break;

        case CHARTEVENT_OBJECT_ENDEDIT:
            if (sparam == "InputBox") OnInputChange();
            break;
    }
}

//+------------------------------------------------------------------+
//| Handle login button click                                        |
//+------------------------------------------------------------------+
void OnLoginButtonClick()
{
    string enteredPassword = passwordInputBox.Text();
    if (enteredPassword == Password)
    {
        twoFACode = GenerateRandom6DigitCode();
        SendMessageToTelegram("A login attempt was made on the Admin Panel. Please use this code to verify your identity. " + twoFACode, Hardcoded2FAChatId, Hardcoded2FABotToken);

        authentication.Destroy();
        ShowTwoFactorAuthPrompt();
        Print("Password authentication successful. A 2FA code has been sent to your Telegram.");
    }
    else
    {
        feedbackLabel.Text("Wrong password. Try again.");
        passwordInputBox.Text("");
    }
}

//+------------------------------------------------------------------+
//| Handle 2FA login button click                                    |
//+------------------------------------------------------------------+
void OnTwoFALoginButtonClick()
{
    string enteredCode = twoFACodeInput.Text();
    if (enteredCode == twoFACode)
    {
        twoFactorAuth.Destroy();
        adminPanel.Show();
        Print("2FA authentication successful.");
    }
    else
    {
        twoFAFeedbackLabel.Text("Wrong code. Try again.");
        twoFACodeInput.Text("");
    }
}

//+------------------------------------------------------------------+
//| Handle close button for authentication                           |
//+------------------------------------------------------------------+
void OnCloseAuthButtonClick()
{
    authentication.Destroy();
    ExpertRemove(); // Exit the expert
    Print("Authentication dialog closed.");
}

//+------------------------------------------------------------------+
//| Handle close button for 2FA                                      |
//+------------------------------------------------------------------+
void OnClose2FAButtonClick()
{
    twoFactorAuth.Destroy();
    ExpertRemove();
    Print("2FA dialog closed.");
}

//+------------------------------------------------------------------+
//| Create necessary UI controls                                     |
//+------------------------------------------------------------------+
bool CreateControls()
{
    long chart_id = ChartID();

    if (!inputBox.Create(chart_id, "InputBox", 0, 5, 25, 460, 95))
    {
        Print("Failed to create input box");
        return false;
    }
    adminPanel.Add(inputBox);

    if (!charCounter.Create(chart_id, "CharCounter", 0, 380, 5, 460, 25))
    {
        Print("Failed to create character counter");
        return false;
    }
    charCounter.Text("0/" + IntegerToString(MAX_MESSAGE_LENGTH));
    adminPanel.Add(charCounter);

    if (!clearButton.Create(chart_id, "ClearButton", 0, 235, 95, 345, 125))
    {
        Print("Failed to create clear button");
        return false;
    }
    clearButton.Text("Clear");
    adminPanel.Add(clearButton);

    if (!sendButton.Create(chart_id, "SendButton", 0, 350, 95, 460, 125))
    {
        Print("Failed to create send button");
        return false;
    }
    sendButton.Text("Send");
    adminPanel.Add(sendButton);

    if (!changeFontButton.Create(chart_id, "ChangeFontButton", 0, 95, 95, 230, 115))
    {
        Print("Failed to create change font button");
        return false;
    }
    changeFontButton.Text("Font<>");
    adminPanel.Add(changeFontButton);

    if (!toggleThemeButton.Create(chart_id, "ToggleThemeButton", 0, 5, 95, 90, 115))
    {
        Print("Failed to create toggle theme button");
        return false;
    }
    toggleThemeButton.Text("Theme<>");
    adminPanel.Add(toggleThemeButton);

    if (!minimizeButton.Create(chart_id, "MinimizeButton", 0, 375, -22, 405, 0))
    {
        Print("Failed to create minimize button");
        return false;
    }
    minimizeButton.Text("_");
    adminPanel.Add(minimizeButton);

    if (!maximizeButton.Create(chart_id, "MaximizeButton", 0, 405, -22, 435, 0))
    {
        Print("Failed to create maximize button");
        return false;
    }
    maximizeButton.Text("[ ]");
    adminPanel.Add(maximizeButton);

    if (!closeButton.Create(chart_id, "CloseButton", 0, 435, -22, 465, 0))
    {
        Print("Failed to create close button");
        return false;
    }
    closeButton.Text("X");
    adminPanel.Add(closeButton);

    return CreateQuickMessageButtons();
}

//+------------------------------------------------------------------+
//| Create quick message buttons                                     |
//+------------------------------------------------------------------+
bool CreateQuickMessageButtons()
{
    string quickMessages[] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 };
    int startX = 5, startY = 160, width = 222, height = 65, spacing = 5;

    for (int i = 0; i < ArraySize(quickMessages); i++)
    {
        bool created = quickMessageButtons[i].Create(ChartID(), "QuickMessageButton" + IntegerToString(i + 1), 0,
            startX + (i % 2) * (width + spacing), startY + (i / 2) * (height + spacing), 
            startX + (i % 2) * (width + spacing) + width, startY + (i / 2) * (height + spacing) + height);

        if (!created)
        {
            Print("Failed to create quick message button ", i + 1);
            return false;
        }
        quickMessageButtons[i].Text(quickMessages[i]);
        adminPanel.Add(quickMessageButtons[i]);
    }
    return true;
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    adminPanel.Destroy();
    Print("Deinitialization complete");
}

//+------------------------------------------------------------------+
//| Handle custom message send button click                          |
//+------------------------------------------------------------------+
void OnSendButtonClick()
{
    string message = inputBox.Text();
    if (StringLen(message) > 0)
    {
        if (SendMessageToTelegram(message, InputChatId, InputBotToken))
            Print("Custom message sent: ", message);
        else
            Print("Failed to send custom message.");
    }
    else
    {
        Print("No message entered.");
    }
}
//+------------------------------------------------------------------+
//| Handle clear button click                                        |
//+------------------------------------------------------------------+
void OnClearButtonClick()
{
    inputBox.Text("");
    OnInputChange();
    Print("Input box cleared.");
}

//+------------------------------------------------------------------+
//| Handle quick message button click                                |
//+------------------------------------------------------------------+
void OnQuickMessageButtonClick(long index)
{
    string quickMessages[] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 };
    string message = quickMessages[(int)index];

    if (SendMessageToTelegram(message, InputChatId, InputBotToken))
        Print("Quick message sent: ", message);
    else
        Print("Failed to send quick message.");
}

//+------------------------------------------------------------------+
//| Update character counter                                         |
//+------------------------------------------------------------------+
void OnInputChange()
{
    int currentLength = StringLen(inputBox.Text());
    charCounter.Text(IntegerToString(currentLength) + "/" + IntegerToString(MAX_MESSAGE_LENGTH));
    ChartRedraw();
}

//+------------------------------------------------------------------+
//| Handle toggle theme button click                                 |
//+------------------------------------------------------------------+
void OnToggleThemeButtonClick()
{
    darkTheme = !darkTheme;
    UpdateThemeColors();
    Print("Theme toggled: ", darkTheme ? "Dark" : "Light");
}

//+------------------------------------------------------------------+
//| Update theme colors for the panel                                |
//+------------------------------------------------------------------+
void UpdateThemeColors()
{
    color textColor = darkTheme ? clrWhite : clrBlack;
    color buttonBgColor = darkTheme ? clrDarkSlateGray : clrGainsboro;
    color borderColor = darkTheme ? clrSlateGray : clrGray;
    color bgColor = darkTheme ? clrDarkBlue : clrWhite;

    
    UpdateButtonTheme(clearButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(sendButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(toggleThemeButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(changeFontButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(minimizeButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(maximizeButton, textColor, buttonBgColor, borderColor);
    UpdateButtonTheme(closeButton, textColor, buttonBgColor, borderColor);

    for (int i = 0; i < ArraySize(quickMessageButtons); i++)
    {
        UpdateButtonTheme(quickMessageButtons[i], textColor, buttonBgColor, borderColor);
    }

    ChartRedraw();
}

//+------------------------------------------------------------------+
//| Apply theme settings to a button                                 |
//+------------------------------------------------------------------+
void UpdateButtonTheme(CButton &button, color textColor, color bgColor, color borderColor)
{
    button.SetTextColor(textColor);
    button.SetBackgroundColor(bgColor);
    button.SetBorderColor(borderColor);
}

//+------------------------------------------------------------------+
//| Handle change font button click                                  |
//+------------------------------------------------------------------+
void OnChangeFontButtonClick()
{
    currentFontIndex = (currentFontIndex + 1) % ArraySize(availableFonts);
    SetFontForAll(availableFonts[currentFontIndex]);
    Print("Font changed to: ", availableFonts[currentFontIndex]);
    ChartRedraw();
}

//+------------------------------------------------------------------+
//| Set font for all input boxes and buttons                         |
//+------------------------------------------------------------------+
void SetFontForAll(string fontName)
{
    inputBox.Font(fontName);
    clearButton.Font(fontName);
    sendButton.Font(fontName);
    toggleThemeButton.Font(fontName);
    changeFontButton.Font(fontName);
    minimizeButton.Font(fontName);
    maximizeButton.Font(fontName);
    closeButton.Font(fontName);

    for (int i = 0; i < ArraySize(quickMessageButtons); i++)
    {
        quickMessageButtons[i].Font(fontName);
    }
}
//+------------------------------------------------------------------+
//| Generate a random 6-digit code for 2FA                           |
//+------------------------------------------------------------------+
string GenerateRandom6DigitCode()
{
    int code = MathRand() % 1000000; // Produces a 6-digit number
    return StringFormat("%06d", code); // Ensures leading zeros
}

//+------------------------------------------------------------------+
//| Handle minimize button click                                     |
//+------------------------------------------------------------------+
void OnMinimizeButtonClick()
{
    minimized = true;
    adminPanel.Hide();
    minimizeButton.Hide();
    maximizeButton.Show();
    closeButton.Show();
    Print("Panel minimized.");
}

//+------------------------------------------------------------------+
//| Handle maximize button click                                     |
//+------------------------------------------------------------------+
void OnMaximizeButtonClick()
{
    if (minimized)
    {
        adminPanel.Show();
        minimizeButton.Show();
        maximizeButton.Hide();
        closeButton.Hide();
        minimized = false;
        Print("Panel maximized.");
    }
}

//+------------------------------------------------------------------+
//| Handle close button click for admin panel                        |
//+------------------------------------------------------------------+
void OnCloseButtonClick()
{
    ExpertRemove();
    Print("Admin panel closed.");
}

//+------------------------------------------------------------------+
//| Send the message to Telegram                                     |
//+------------------------------------------------------------------+
bool SendMessageToTelegram(string message, string chatId, string botToken)
{
    string url = "https://api.telegram.org/bot" + botToken + "/sendMessage";
    string jsonMessage = "{\"chat_id\":\"" + chatId + "\", \"text\":\"" + message + "\"}";

    char postData[];
    ArrayResize(postData, StringToCharArray(jsonMessage, postData) - 1);

    int timeout = 5000;
    char result[];
    string responseHeaders;
    int responseCode = WebRequest("POST", url, "Content-Type: application/json\r\n", timeout, postData, result, responseHeaders);

    if (responseCode == 200)
    {
        Print("Message sent successfully: ", message);
        return true;
    }
    else
    {
        Print("Failed to send message. HTTP code: ", responseCode, " Error code: ", GetLastError());
        Print("Response: ", CharArrayToString(result));
        return false;
    }
}

//+------------------------------------------------------------------+


测试结果

最终,我们成功为管理员面板实现了强大的密码安全和双因素身份验证(2FA)。以下是展示响应的图片。

尝试错误的密码

尝试错误的密码

尝试错误的验证码。

尝试错误的验证码

以下图片展示了完整的登录和验证过程。我们还成功地将 Telegram 应用程序与登录流程同步,以便捕获验证码的发送过程。

完整的登录测试,包括密码和 2FA。

完整的登录测试,包括密码和 2FA。

Telegram 上的 2FA 验证码发送

上述登录尝试的 Telegram 2FA 验证码发送。


结论

在本 MQL5 项目中引入双因素身份验证(2FA)功能,显著增强了管理员面板的安全性,为用户访问增加了一个关键的验证层。利用 Telegram 发送验证码确保了用户能够实时收到通知。实现中包括了对错误输入的处理,提示用户重新输入密码或 2FA 验证码,这在最小化未经授权访问的同时,也让用户了解了任何错误。

然而,必须承认风险仍然存在,特别是如果 Telegram 应用程序安装并登录在存在风险的同一台计算机上。入侵者可能会利用此类设置中的漏洞,尤其是如果设备被入侵,或者未经授权的用户获得了系统管理员用于 Telegram 的手机的访问权限。因此,保持严格的安全实践——例如在不使用时退出应用程序并确保设备安全——成为保护敏感数据和通信的重中之重。

希望您通过本项目对 MQL5 的 2FA 实现有了深入的了解,特别是在实时验证码生成和安全消息传递的场景下。理解这些概念将增强您应用程序的安全性,并突出主动措施在保护应用程序免受潜在威胁中的重要性。以下是最终的作品。祝您开发顺利!交易者们。

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16142

附加的文件 |
Admin__Panel.mq5 (21.85 KB)
创建 MQL5-Telegram 集成 EA 交易(第 6 部分):添加响应式内联按钮 创建 MQL5-Telegram 集成 EA 交易(第 6 部分):添加响应式内联按钮
在本文中,我们将交互式内联按钮集成到 MQL5 EA 交易中,允许通过 Telegram 进行实时控制。每次按下按钮都会触发特定的操作,并将响应发送回用户。我们还模块化了函数,以便有效地处理 Telegram 消息和回调查询。
Connexus助手(第五部分):HTTP方法和状态码 Connexus助手(第五部分):HTTP方法和状态码
在本文中,我们将了解HTTP方法和状态码,这是网络上客户端与服务器之间通信的两个非常重要的部分。了解每种方法的作用,可以让您更精确地发出请求,告知服务器您想要执行的操作,从而提高效率。
您应当知道的 MQL5 向导技术(第 39 部分):相对强度指数 您应当知道的 MQL5 向导技术(第 39 部分):相对强度指数
RSI 是一款流行的动量震荡指标,衡量证券近期价格变化的速度和规模,从而评估证券价格中被高估和低估的情况。这些对速度和幅度的洞察是定义反转点的关键。我们将这个振荡器放入另一个自定义信号类中工作,并验证其信号的一些特征。不过,我们先从总结我们之前在布林带的内容开始。
交易中的神经网络:探索局部数据结构 交易中的神经网络:探索局部数据结构
在嘈杂的条件下有效识别和预存市场数据的局部结构是交易中的一项关键任务。运用自注意力机制在处理这类数据方面展现出可喜的结果;不过,经典方式并未考虑底层结构的局部特征。在本文中,我将引入一种能够协同这些结构依赖关系的算法。