見出し画像

【kabuステーション】自動起動(パスキー認証)

はじめに

eスマート証券 kabuステーションで「パスキー認証」が導入されてログイン画面が変更されたので、自動起動コマンドを改修しました。

PINを自動入力するのに、少し苦労したのでポイントを書いておきます。

現状の運用(本番運用PC)

・所定時刻にPC電源ON(BIOS設定)
・kabuステーション自動起動コマンド実行(タスクスケジューラ)
・デイトレBot実行(タスクスケジューラ)
・kabuステーション終了&Shutdown実行(タスクスケジューラ)

このように、現状は完全に自動化しています。
PC電源ONでWindowsが起動されるとログインした状態まで進むようにしています。(電気代が気になるので24時間稼働にはしていない)

セキュリティ的に?という話はありますが、自分しか入らない自室で稼働させているので、そこは自己責任ですね。

パスキー認証

初日は、今まで通りメールによるワンタイム認証コードで逃げたのですが、せっかくなので「パスキー認証」を使うように改修しました。

パスキー登録方法などは割愛します。eスマート証券のサイトを参考にしてください。

画像
ログイン画面

「パスキーでログイン」をクリックするとダイアログが出ます。

画像
PIN入力ダイアログ

簡単そうに見えたのにコマンドからだとPINが入力できなくて、なかなか苦労しました。
結論から言うと「管理者として実行」することで入力できました。

コード抜粋

def _enter_passkey(self, dlg: WindowSpecification, pin: str) -> None:
    """
    パスキーでログイン
    """
    self.logger.debug("「パスキーでログイン」をクリック")
    element = dlg.child_window(title="パスキーでログイン", control_type="Button")
    element.click()

    #
    # これ以降の処理は「管理者権限」がないと動かない!
    #
    win = dlg.child_window(title="Windows セキュリティ", control_type="Window")
    win.wait("ready", timeout=self.wait_short)

    # 「パスキー」を入力
    self.logger.debug("「パスキー」を入力")
    element = win.child_window(auto_id="PasswordField_4", control_type="Edit")
    self.logger.debug("Password Edit=%s", element)
    element.wait("ready", timeout=self.wait_short)
    element.set_text(pin)

「パスキーでログイン」をクリック、「Windows セキュリティ」ダイアログからPIN入力エリアを取得してPIN入力。という流れになってます。

変更内容>
・time.sleep() を element.wait() へ変更
・key_board.send_keys() を element.set_text() へ変更

更新履歴

※PIN入力時は、セキュリティダイアログが確実に前面(フォーカスされた状態)にきている必要があります。

本当は「Windows セキュリティ」ウィンドウのオブジェクトを取得して、PIN入力エリアのオブジェクトを取得して・・・
という感じで色々と試してみたのですが、うまく出来ずに諦めました。
正解が分かる方がいれば、教えてください。

改めて試したら、すんなり出来ました。
管理者権限で悩んでた時に出来なかっただけのようです。

タスクスケジューラ

画像
タスク登録時のダイアログ

管理者権限が必要なコマンドをタスクスケジューラへ登録する時のポイントが、「最上位の特権で実行する」をチェックすることです。

私は出来上がったスクリプトを PyInstaller でEXE化していて、そのコマンドをBATファイルから呼び出すようにして、BATファイルをタスクスケジューラへ登録してます。

コマンド直接でも登録可能ですが、BATにしておくと作業フォルダーやコマンド引数などがわかりやすいのと登録手順が単純になるのでお勧めです。

@echo off

cd
cd /d %~dp0
cd

kabus_auto.exe --mode open


rem DEBUG用
pause

おまけ

メインモジュールのソースコードを掲載します。
これだけでは動きませんが、以前からの記事と組み合わせれば参考になると思います。

kabus_auto.py

import argparse
import ctypes
import time

from pywinauto import Desktop, WindowSpecification, keyboard

from kabus_context import KabusContext
from kabus_gmail_watcher import KabusGmailWatcher
from kabus_logging import KabusLogging
from kabus_settings import KabusSettings


class KabusAuto:
    """
    kabuSTATIONの自動操作を行うクラス。

    --mode open
        1. アプリケーションのショートカット起動
        2. パスワード入力(又は、PINコード入力)
        3. ワンタイム認証コードのメール送信依頼
        4. ワンタイム認証コードの取得 (GMail API)
        5. ワンタイム認証コードの入力

    --mode close
        1. kabuSTATIONを閉じる
        2. 確認ダイアログのYesを選択
    """

    def __init__(self, context_file: str = "kabus_context.toml"):
        """
        コンストラクタ

        Args:
            context_file (str, optional): 設定ファイルパス. Defaults to "kabus_context.toml".
        """
        self.context = KabusContext(context_file)
        self.logger = KabusLogging.get_logger(__class__.__name__)

        self.wait_long = self.context.get("kabuSTATION.WaitTimeLong", 30.0)
        self.wait_short = self.context.get("kabuSTATION.WaitTimeShort", 5.0)


    def is_admin(self):
        try:
            return ctypes.windll.shell32.IsUserAnAdmin() == 1
        except:
            return False


    def _launch(self) -> WindowSpecification:
        """
        kabuSTATIONを起動する。
        """
        self.logger.info("kabuSTATIONを起動します。")

        # 「Windowsキー + 数字キー」でアプリを起動
        short_cut_key = self.context.get(
            "kabuSTATION.ExecuteToolShortcut",
            "{LWIN down}1{LWIN up}",
        )
        keyboard.send_keys(short_cut_key, pause=0.5)

        desktop = Desktop(backend="uia")
        win = desktop.window(
            title="ログイン",
            auto_id="WebAuthentication",
            control_type="Window",
        )
        win.wait(wait_for="ready", timeout=self.wait_long)
        return win


    def _login(self, dlg: WindowSpecification) -> bool:
        """
        パスキー認証、又は、口座番号+パスワード認証を行う。
        """

        pin = self.context.get("kabuSTATION.PIN")
        account_no = self.context.get("kabuSTATION.AccountNo", "your_account_no_here")
        password = self.context.get("kabuSTATION.Password", "your_password_here")

        # データベース使用有無
        use_database = self.context.get("Settings.UseDatabase", False)
        if use_database:
            db_path = self.context.get("Settings.Database", "database/settings.db")
            settings = KabusSettings(db_path)
            account_no = settings.get_value(account_no)
            password = settings.get_value(password)
            if pin:
                pin = settings.get_value(pin)

        # 管理者権限チェック
        self.logger.info("管理者権限チェックを行います。")
        if self.is_admin() is False:
            self.logger.error("管理者権限で実行されていないため、パスキー認証は使用できません。")
            pin = None

        if pin:
            self.logger.info("パスキーでログインします。")
            self._enter_passkey(dlg, pin)

        else:
            self.logger.info("口座番号+パスワードでログインします。")
            self._enter_password(dlg, account_no, password)

        return pin is None


    def _enter_passkey(self, dlg: WindowSpecification, pin: str) -> None:
        """
        パスキーでログイン
        """
        self.logger.debug("「パスキーでログイン」をクリック")
        element = dlg.child_window(title="パスキーでログイン", control_type="Button")
        element.click()

        #
        # これ以降の処理は「管理者権限」がないと動かない!
        #
        win = dlg.child_window(title="Windows セキュリティ", control_type="Window")
        win.wait("ready", timeout=self.wait_short)

        # 「パスキー」を入力
        self.logger.debug("「パスキー」を入力")
        element = win.child_window(auto_id="PasswordField_4", control_type="Edit")
        self.logger.debug("Password Edit=%s", element)
        element.wait("ready", timeout=self.wait_short)
        element.set_text(pin)


    def _enter_password(self, dlg: WindowSpecification, account_no: str, password: str) -> None:
        """
        パスワードでログイン
        """
        # 口座番号入力
        self.logger.debug("口座番号を入力します。")
        element = dlg.child_window(auto_id="username", control_type="Edit")
        element.wait("ready", timeout=self.wait_short)
        element.set_text(account_no)

        # 「次へ」クリック
        self.logger.debug("「次へ」をクリックします。")
        element = dlg.child_window(title="次へ", control_type="Button")
        element.click()

        # パスワード入力
        self.logger.debug("パスワードを入力します。")
        element = dlg.child_window(auto_id="password", control_type="Edit")
        element.wait("ready", timeout=self.wait_short)
        element.set_text(password)

        # ログインボタン・クリック
        self.logger.debug("ログインボタンをクリックします。")
        element = dlg.child_window(title="ログイン", control_type="Button")
        element.click()

    def _handle_2fa(self, win: WindowSpecification) -> None:
        """
        二要素認証を処理する。
        """
        # 「別の方法を試す」を選択
        element = win.child_window(title="別の方法を試す", control_type="Button")
        element.wait("ready", timeout=self.wait_short)
        element.click()

        # 「メール」を選択
        element = win.child_window(title="メール", control_type="Button")
        element.wait("ready", timeout=self.wait_short)
        element.click()

        # GMail のワンタイム認証コードを取得
        self.logger.info("Gmail でワンタイム認証コードを取得します。")
        one_time_code = None
        gmail_watcher = KabusGmailWatcher(context=self.context)
        if gmail_watcher.service:
            one_time_code = gmail_watcher.watch_for_email(
                duration_minutes=5, interval_seconds=10
            )
            self.logger.debug("ワンタイム認証コード: [%s]", one_time_code)

        if one_time_code:
            element = win.child_window(auto_id="code", control_type="Edit")
            element.wait("ready", timeout=self.wait_short)
            element.set_text(one_time_code)

            element = win.child_window(title="続ける", control_type="Button")
            element.wait("ready", timeout=self.wait_short)
            element.click()
        else:
            self.logger.error("ワンタイム認証コードの取得に失敗しました。")


    def open(self):
        """
        kabuSTATION を起動してログインを行う。
        パスキー認証でない場合は、メールによるワンタイム認証コードの二要素認証を行う。
        """
        win = self._launch()
        if self._login(win):
            self._handle_2fa(win)


    def close(self):
        """
        kabuSTATION を終了する。
        """
        self.logger.info("kabuSTATION を終了します。")

        desktop = Desktop(backend="uia")
        win = desktop.window(title="kabuステーション", control_type="Window")
        win.wait(wait_for="ready", timeout=self.wait_short)

        # 閉じるボタン
        win.set_focus()
        win.close()

        # 確認ダイアログのYesを選択
        self.logger.info("確認ダイアログのYesを選択します。")
        time.sleep(self.wait_short)
        keyboard.send_keys("{ENTER}")

        self.logger.info("kabuSTATIONを終了しました。")


def main():
    """
    メイン処理
    """
    parser = argparse.ArgumentParser(description="kabuSTATION の自動操作ツール")
    parser.add_argument(
        "-m",
        "--mode",
        help="操作モード: open - 起動&ログイン, close - 終了",
        type=str,
        choices=["open", "close"],
        required=True,
    )
    args = parser.parse_args()

    automator = KabusAuto()

    if args.mode == "open":
        automator.open()
    elif args.mode == "close":
        automator.close()


if __name__ == "__main__":
    main()

管理者権限チェックを行い、管理者権限がない場合は、従来のメールによるワンタイム認証コードでログインする仕組みになってます。

以前の記事は、以下になります。
やはり、PyWinAutoへ変更してメンテナンス性が良くなったと思います。

もし参考になったら「いいね」よろしくお願いします。


いいなと思ったら応援しよう!

ピックアップされています

Python アプリ開発

  • 10本

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
IT企業を定年退職したオヤジが興味本位で競馬AI予想プログラムを作ってます。 JRA-VAN Data Lab.のデータをpython+lightGBMで分析。最終的には全自動で寝ててもPCが稼いでくれる。 そんなことを夢見てます。ちなみに競馬は素人です。
【kabuステーション】自動起動(パスキー認証)|HRAPS@競馬AI予想
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1