その日は突然やってきた。
2025/8/9(土)以降、リアルタイムフィッシングによる詐欺対策として、デバイス認証による認証手順を、現行の「認証コードをEメールで送信する方式」から、「認証コード入力画面のURLを送信する方式」に変更することをお知らせいたします。
ユーザーをバカにしとるのか!
5月に変更したばかりだろ……。何?あれじゃフィッシング詐欺に対処できなかったの?
折角、必死に自動ログイン化対応をしたのにさ……

今回の認証の流れは次の通り。
40秒の間に、メール開いて メール探して URL開いて IDを入力して認証ボタンを押す
さらに元のブラウザからチェックボタンを押してデバイス登録のボタンを押す
初見でクリアできた人はブラウザタブの職人www
タイムアタック職人YouTuberも登場しそうwww
© 異世界おじさん(一部改変)
おじいちゃんの私には初見クリアは無理だった。「えっ?えっ?」ってなったわww
自動的に認証を突破させる
残念ながら前回のように汎用的な実装は思いつかない。
Webメール宛てに届いた本文のURLをクリックして認証番号を入れる
人間がやることを、単にコンピュータにやらせるだけ……
Webメールサービスにログインが必要なので人によって実装は変わる。
私が使っているYahooメールはサービスへのログイン自体が二段階認証が必要だったので、「さくらインターネット」のメールサービスを使った。
GmailやHotmailなどの仕様は分からないが再利用できるかもしれないのでコードを最後に記載。呼び出し方法は次となる。
【main.py】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#coding: utf-8 from sbiauth import SbiAuthenticator import logging import os import re # ログの設定 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s [%(filename)s:%(lineno)d]') logger = logging.getLogger(__name__) if __name__ == '__main__': try: authenticator = SbiAuthenticator( sbi_username="SBI_USERNAME", # SBIユーザ名 sbi_password="SBI_PASSWORD", # SBIパスワード mail_username="MAIL_USERNAME", # WEBメールユーザ名 mail_password="MAIL_PASSWORD", # WEBメールパスワード headless=False # ヘッドレス設定(Trueで非表示、Falseで表示) ) playwright, browser, sbi_page = authenticator.authenticate() 好きな処理関数(sbi_page) authenticator.close() except ValueError as e: logger.error(f"認証エラー: {e}") raise except Exception as e: logger.error(f"予期しないエラー: {e}") raise |
エラー回避
SBI証券の自動操作スクリプトをPlaywrightで実装中、次のようなエラーが発生した。
playwright._impl._errors.Error: Page.goto: Navigation to “https://site1.sbisec.co.jp/ETGate/?OutSide=on&_ControlID=WPLETsmR001Control&_DataStoreID=DSWPLETsmR001Control&sw_page=Offer&cat1=home&cat2=none&getFlg=on&sw_param1=21&sw_param2=” is interrupted by another navigation to “https://site1.sbisec.co.jp/ETGate/WPLEThmR001Control/DefaultPID/DefaultAID/DSWPLEThmR001Control”
Call log:
Playwrightのpage.gotoは、指定したURLへの移動が完了する前に別の移動が発生すると、Navigation interruptedエラーを投げる。
このエラーを解決するには、サイトのリダイレクトが完了するまで待機し、その後に目的のページに移動させる。
1 2 3 4 |
redirect_url = "https://site1.sbisec.co.jp/ETGate/WPLEThmR001Control/DefaultPID/DefaultAID/DSWPLEThmR001Control" page.wait_for_url(redirect_url, timeout=20000) # 20秒以内にリダイレクトを待機 page.goto(account_url, wait_until="load", timeout=30000) # loadで待機、タイムアウト30秒 |
こちらは ページ末のソースコードに追加済。
おわりに
「スマホ認証」ではなくパソコンで完結する世界が残る限り、私は自動化を試みる。
これぞ、侍魂!
このブログは2000年から書いているため、テキストサイトの名残りを継いでる。
エンターテイメント性(ネタ)も重んじてるけど「情報」を大切にしてる。
アクセス数は10年前より1/3に。YouTubeが主流になり更に 1/3 に。そしてAI台頭により更に更に1/3に減った。
ソースコード
「sbiauth.py」という名前にして同じフォルダに置いておく。
Webメール処理の部分(mail_operation)は「さくらインターネット」のメールサービスしか利用できない。
だけどAIに「Gmail向けに対応して。不足している情報は取得方法を教えて」等と伝えれば実装できるかもしれない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
#coding: utf-8 from playwright.sync_api import sync_playwright from PIL import Image import pandas as pd import time import logging import re import os from bs4 import BeautifulSoup from io import StringIO # ログの設定 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s [%(filename)s:%(lineno)d]') logger = logging.getLogger(__name__) class SbiAuthenticator: def __init__( self, sbi_username=None, sbi_password=None, mail_username=None, mail_password=None, mail_url="https://secure.sakura.ad.jp/rs/mail/○○/folders/Lg%3D%3D", headless=False # ヘッドレス設定を追加 ): """ SBI証券のログイン・認証処理を行うクラス """ self.sbi_username = sbi_username or os.environ.get("SBI_USERNAME") self.sbi_password = sbi_password or os.environ.get("SBI_PASSWORD") self.mail_username = mail_username or os.environ.get("MAIL_USERNAME") self.mail_password = mail_password or os.environ.get("MAIL_PASSWORD") self.mail_url = mail_url self.headless = headless self.browser = None self.sbi_page = None self.context = None self.playwright = None if not self.sbi_username or not self.sbi_password: logger.error("SBI_USERNAME and SBI_PASSWORD must be provided via arguments or environment variables") raise ValueError("Missing SBI_USERNAME or SBI_PASSWORD") if not self.mail_username or not self.mail_password: logger.error("MAIL_USERNAME and MAIL_PASSWORD must be provided via arguments or environment variables") raise ValueError("Missing MAIL_USERNAME or MAIL_PASSWORD") logger.info("SbiAuthenticator initialized with provided credentials") def login_to_sbi(self, context): """ SBI証券にログインする """ page = context.new_page() page.goto("https://www.sbisec.co.jp/ETGate/") page.fill('input[name="user_id"]', self.sbi_username) page.fill('input[name="user_password"]', self.sbi_password) page.click('input[name="ACT_login"]') return page def click_to_emailbottom(self, page): """ 'Eメールを送信する' ボタンをクリックする """ time.sleep(3) page.click('button[name="ACT_deviceotpcall"]') logger.info("Clicked 'Eメールを送信する' button") def authenticate_sbi(self, page): """ SBI証券の認証キーを取得する """ time.sleep(3) auth_code = page.text_content('div#code-display') or "認証コードが見つかりませんでした" logger.info(f"Authentication code: {auth_code}") return auth_code def mail_operation(self, browser, auth_id): """ メール操作を行い、認証コードを入力する(さくらインターネットの場合) """ context = browser.new_context() page = context.new_page() page.goto(self.mail_url) page.fill('input[name="username"]', self.mail_username) page.fill('input[name="password"]', self.mail_password) page.click('button[type="submit"]') time.sleep(5) page.click('div.title._text-overflow-ellipsis_qghvh_1') time.sleep(2) content = page.text_content('div.body') or "本文が見つかりませんでした" url_match = re.search(r'https://m\.sbisec\.co\.jp/deviceAuthentication/input\?[^ \n]+', content) if not url_match: logger.error("Authentication URL not found in email") context.close() return auth_url = url_match.group(0) logger.info(f"Authentication URL: {auth_url}") page.goto(auth_url) time.sleep(3) page.fill('input#verifyCode', auth_id) logger.info(f"Entered authentication code: {auth_id}") page.click('button#verification') logger.info("Clicked '認証する' button") time.sleep(5) context.close() def click_to_certification(self, page): """ SBI証券ログイン側のブラウザで認証を進める """ page.check('input#device-checkbox') logger.info("Checked 'device-checkbox'") page.click('button#device-auth-otp') logger.info("Clicked 'デバイスを登録する' button") time.sleep(3) def authenticate(self): """ SBI証券にログインし、認証を完了してブラウザとページを返す 戻り値: (playwright, browser, sbi_page) """ self.playwright = sync_playwright().start() self.browser = self.playwright.chromium.launch(headless=headless=self.headless) self.context = self.browser.new_context(viewport={"width": 1500, "height": 1000}) self.context.set_default_timeout(180000) self.sbi_page = self.login_to_sbi(self.context) self.click_to_emailbottom(self.sbi_page) auth_id = self.authenticate_sbi(self.sbi_page) self.mail_operation(self.browser, auth_id) self.click_to_certification(self.sbi_page) time.sleep(5) redirect_url = "https://site1.sbisec.co.jp/ETGate/WPLEThmR001Control/DefaultPID/DefaultAID/DSWPLEThmR001Control" logger.info("Waiting for redirect after authentication") try: self.sbi_page.wait_for_url(redirect_url, timeout=20000) # リダイレクトを20秒待機 logger.info(f"Current page URL after redirect: {self.sbi_page.url}") except Exception as e: logger.warning(f"Redirect wait failed: {e}. Proceeding with current URL: {self.sbi_page.url}") return self.playwright, self.browser, self.sbi_page def close(self): """ ブラウザとPlaywrightを閉じる """ if self.browser: self.browser.close() self.browser = None self.sbi_page = None self.context = None logger.info("Browser closed") if self.playwright: self.playwright.stop() self.playwright = None logger.info("Playwright stopped") |