外部サーバーでユーザー認証を行い口座縛りを実装する

EAの使用を特定のユーザーのみに制限したいという場面。
取引サーバーにアクセスするためのアカウント固有の情報としては”口座番号”があり、これを用いて使用制限をかけるのが一般的だ。

口座番号をEAの中に直接書く方法では、多数のユーザーへの配布が非常に面倒くさいことや一度配布した後に強制的に使用を停止させることができない点がデメリットになる。
そこで、ユーザー認証の仕組みをEAと分離することで柔軟に対応できるようにする。

具体的には、以下のようなものを用意する必要がある。

  • 口座番号等のユーザー情報を登録しておくデータベース等を用意。
    (例:情報が保存されている=使用権限を持つものとして扱う)
  • GETリクエストで口座番号を受け付けて、ユーザーの登録確認を行うAPIエンドポイントを用意。

実現するためには色んなサービスが使用できる。
Googleが提供するFirebase FirestoreとFirebase Cloud Functionsを使っても良いし、本サイトが稼働しているような一般的なレンタルサーバーを使っても良い。

どちらがより馴染みが合って分かりやすいか悩むところだが、今回は一般的なレンタルサーバーを使用するパターンで書いておく。
月々100円程度から使える安いレンタルサーバーでも大丈夫。(さくらインターネットとか)

レンタルサーバーを契約すると初期ドメインが与えられる。(ここでは例として example.com とする)
ユーザー認証するためだけであれば、独自ドメインを使用する必要はない。

ユーザー情報を格納する場所を用意する(データベースやCSVファイル等)

レンタルサーバーの場合、MySQLというデータベースを使えることが多い。
例えば以下のようなテーブルを作り、ユーザー情報を登録しておく。

CREATE TABLE `users` (
   `account_number` VARCHAR(10) NOT NULL ,
   `server` TEXT NOT NULL ,
   `name` TEXT NOT NULL ,
PRIMARY KEY (`account_number`, `server`));

account_number(口座番号)だけでも良いが、業者違いによる口座番号の重複が心配な場合は取引サーバー情報を合わせて使っても良い。
またユーザーの情報がどれかを把握しやすくするために氏名などの情報を合わせて保存しておくと良い。

なおMySQLデータベースの利用には、データベースを操作するためのSQL(MQLと響きは似てるが全く別物)という言語が必要になる。

また安いレンタルサーバーでは、データベースそのものが使えない場合もあるが、その場合はCSVファイル等にまとめて書き込んでおいても良い。
例えば次のようなCSVファイルを用意してサーバーにアップロードしておく。ユーザーの追加や削除はエクセルやテキストエディタで行う。

account_number, server, name
12345678, test, 田中
23456789, test, 鈴木

使用を許可したい口座番号を上記に登録する。
強制的に使用停止したい場合は登録されているデータを消す。

ユーザー情報を検索して結果を返すページを作成

サーバーサイドで動作する言語を使い、リクエストを受け付けて認証結果を返すファイルを作成する。
ここでは例としてPHP言語を使用し、サーバーは以下の構成にする。

public_html (公開ディレクトリのルート)
   └auth
      ├index.php
      └users.csv (CSVを使用する場合)

以下のように口座番号を付加したGETリクエストを受け付ける想定。
https://example.com/auth/?account_number=12345678

【データベースを使う場合に用意する index.php の例】

<?php
// データベース接続情報
$host = '127.0.0.1'; // データベースサーバーのIPまたはホスト名
$db   = 'your_database_name'; // データベース名
$user = 'your_username'; // ユーザー名
$pass = 'your_password'; // パスワード
$charset = 'utf8mb4';

// DSN(Data Source Name)の設定
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";

// オプションの設定
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
    // PDOインスタンスの作成
    $pdo = new PDO($dsn, $user, $pass, $options);

    // 検索するaccount_numberをGETリクエストから取得
    $accountNumber = $_GET['account_number'];

    // SQL文の準備
    $stmt = $pdo->prepare("SELECT * FROM USERS WHERE ACCOUNT_NUMBER = ?");
    $stmt->execute([$accountNumber]);

    // 結果の取得
    if ($stmt->rowCount() > 0) {
        echo "ok"; //ユーザー認証成功
    } else {
        echo "ng"; //ユーザー認証失敗
    }

} catch (\PDOException $e) {
    // エラー発生時の処理
    echo "err";
}
?>

【CSVを使う場合に用意する index.php の例】

<?php
$filename = 'users.csv'; // CSVファイルのパス
$accountNumber = $_GET['account_number']; // 検索するaccount_numberをGETリクエストから取得
$found = false; // フラグ

if (($handle = fopen($filename, "r")) !== FALSE) {
    // CSVファイルを1行ずつ読み込む
    while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
        // ヘッダー行をスキップ
        if ($data[0] == 'account_number') {
            continue;
        }
        // account_numberをチェック
        if ($data[0] == $accountNumber) {
            $found = true;
            break;
        }
    }
    fclose($handle);
}

// 結果の表示
if ($found) {
    echo "ok";  //ユーザー認証成功
} else {
    echo "ng";  //ユーザー認証失敗
}
?>

作成したファイルをサーバーに設置する。
ブラウザから、
https://example.com/auth/?account_number=12345678
のようにアクセスすると、指定した口座番号と登録状況に応じて ”ok” / ”ng” と表示される。

EA側の記述

EAに前述のURLに口座番号を付加してアクセスさせるよう記述する。
MQL言語が用意しているWebRequest関数は、事前にMT4/5の設定から通信先のURLを登録しておく必要があり少し使いづらいため、DLLを使ったインターネット通信を用いる。

後からユーザーが使用しているEAを強制的に停止させたい場合、OnInit時に1度だけユーザー認証を行うのでは少々機会が少ないように思われる。
OnTickの度に認証を行うことが理想と言えるが、認証にはインターネットアクセスを伴うためEAのパフォーマンスを著しく損なう可能性が高い。

また、用意したエンドポイントへのアクセスが行われるため、OnTickごとにアクセスが行われると、サーバー負荷がとんでもない事になってしまう。
ここでは例として1時間おきにユーザー認証を行うソースコード(以下はMQL5の例)を書く。
(1時間おきに認証する場合、強制停止してもそこから最大1時間使用される可能性があるということになる。)

#property copyright ""
#property link ""
#property version ""
#property strict

#import "wininet.dll"
int InternetOpenW(string agent, int accessType, string proxyName, string proxyByPass, int flags);
int InternetConnectW(int internet, string serverName, int port, string userName, string password, int service, int flags, int context);
int HttpOpenRequestW(int connect, string verb, string objectName, string version, string referer, int acceptType, uint flags, int context);
int HttpAddRequestHeadersW(int, string, int, int);
bool HttpSendRequestW(int hRequest, string &lpszHeaders, int dwHeadersLength, uchar &lpOptional[], int dwOptionalLength);
bool HttpQueryInfoW(int request, int infoLevel, string &buffer, int &size, int &index);
int InternetOpenUrlW(int internetSession, string url, string header, int headerLength, int flags, int context);
int InternetReadFile(int, uchar &arr[], int, int &byte);
int InternetCloseHandle(int winINet);
#import

// ユーザー認証を行うエンドポイントのURL
string authUrl = "https://example.com/auth/";

// ユーザー認証を行った時間
datetime authTime = 0;

int OnInit()
{
    return (INIT_SUCCEEDED);
}

void OnTick()
{
    // 1時間に1度ユーザー認証を行う
    if (authTime != iTime(NULL, PERIOD_H1, 0))
    {
        if (!auth(IntegerToString((int)AccountInfoInteger(ACCOUNT_LOGIN))))
        {
            Print("口座番号が認証されていません。EAを削除します。");
            ExpertRemove();
        }
        else
        {
            // ユーザー認証を行った時間を記録する
            authTime = iTime(NULL, PERIOD_H1, 0);
        }
    }
}

// ユーザー認証を行う関数
bool auth(string accountNumber)
{
    Print(authUrl + "?account_number=" + accountNumber);
    string data = AccessToInternetGetMethod(authUrl + "?account_number=" + accountNumber);
    if (data == "ok")
    {
        return true;
    }
    else
    {
        return false;
    }
}

// URLにGETメソッドでアクセスし、結果を文字列で返す関数
string AccessToInternetGetMethod(string url)
{
    string result = "";
    int byte_size = 0;
    uchar receive[1024];
    int inet = InternetOpenW("MetaTrader 5 Terminal", 0, "0", "0", 0);
    if (inet == 0)
        return (NULL);
    int handle = InternetOpenUrlW(inet, url, NULL, 0, 0, 0);
    while (InternetReadFile(handle, receive, 1024, byte_size))
    {
        if (byte_size <= 0)
            break;
        result += CharArrayToString(receive, 0, byte_size, CP_UTF8);
    }
    InternetCloseHandle(handle);
    InternetCloseHandle(inet);
    return (result);
}

認証する頻度は一度決めたらEAを書き換えない限り変更できないため、上記のような構成の場合、見込まれる同時稼働数から考えてどの程度の頻度でユーザー認証を行うか慎重に判断しよう。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

コメントする