powershell-icon
uto
uto

PowerShellでIEの自動ログイン

2020年6月21日

background

PC起動時にIEを自動で立ち上げログインまで済ませる必要があり、PowerShellを初めてまともに扱った。 今回の自動ログインで苦労させられた点は、ログイン画面にアクセスすると別ウィンドウで正規のログイン画面が表示される点だ。このよく分からん仕様のせいでPowerShellから呼び出したIEのウィンドウにログイン画面がなく、PowerShellからログインができなくなってしまう。 そこでページ読み込み後、本来のログインページが表示されているウィンドウを検索し、それを操作対象とした。

コードに一部不具合があったため修正と改良を行いました。 一部解説内容が旧コードのままになっています。 ID、パスワードを暗号化して保存する機能を追加したバージョンを別記事で紹介しています。 よければご覧ください。

PowerShellで文字列を暗号化してJSON化する

deliverables

諸々は後で書くとして今回の成果物は次のコードである。上の変数の値を書き換えることで同じようなサイトであれば大体動くだろう。

$url = "URL"#目的のURL$login_url = ""#リダイレクトされたログインページのURL
$user_name = "ID"#ログインID
$password = "PassWord"#ログインパスワード
$site_name = "Site_name"#ページのタイトルである場合が多い
$user_name_id = "user_name_id"#HTMLのid入力欄のid
$password_id = "password_id"#HTMLのpass入力欄のid
$button_id = "login_button_id"
#HTMLのログインボタンのid#上の項目は状況に合わせて変更必須#初期設定
function OverrideMethod ([mshtml.HTMLDocumentClass]$Document) {
    $doc = $Document | Add-Member -MemberType ScriptMethod -Name "getElementById" -Value {
        param($Id)
        [System.__ComObject].InvokeMember(
            "getElementById",
            [System.Reflection.BindingFlags]::InvokeMethod,
            $null,
            $this,
            $Id
        ) | ? {$_ -ne [System.DBNull]::Value}
    } -Force -PassThru
    $doc | Add-Member -MemberType ScriptMethod -Name "getElementsByClassName" -Value {
        param($ClassName)
        [System.__ComObject].InvokeMember(
            "getElementsByClassName",
            [System.Reflection.BindingFlags]::InvokeMethod,
            $null,
            $this,
            $ClassName
        ) | ? {$_ -ne [System.DBNull]::Value}
    } -Force
    $doc | Add-Member -MemberType ScriptMethod -Name "getElementsByTagName" -Value {
        param($TagName)
        [System.__ComObject].InvokeMember(
            "getElementsByTagName",
            [System.Reflection.BindingFlags]::InvokeMethod,
            $null,
            $this,
            $TagName
        ) | ? {$_ -ne [System.DBNull]::Value}
    } -Force
    return $doc
}#初期設定終わり
[string]"IE起動"
$ie = New-Object -ComObject InternetExplorer.Application  # IE起動
$ie.Visible = $false
$ie.Navigate($url,4)#ページを開く
# ページが読み込まれるまで待機
for ($i=0; $i -lt 100; $i++)
{
  if($ie.Busy -or $ie.readyState -ne 4)
  {
    break  }
  Start-Sleep -Milliseconds 10}
[string]"ログインページ取得中"#$ieの再設定$ie = $null
$jud = $false#ページ情報の取得
for ($i=0; $i -lt 100; $i++)
{
  #シェルを取得  $shell = New-Object -ComObject Shell.Application
  #ieで開いているページ一覧を取得  
$ie_list = @($shell.Windows() | where { $_.Name -match "Internet Explorer" })
  #ページタイトルを用いてオブジェクトを取得  
$ie = @($ie_list | where { $_.LocationURL -match $login_url})
  if ($ie)
  {
    $jud = $true
    break  }elseif($ie_list | where { $_.LocationName -match $site_name})
  {
    [string]"ログイン不要`r`n処理終了"    exit  }
  Start-Sleep -Milliseconds 100}#エラーチェックif($jud -eq $false)
{
  $wsobj = new-object -comobject wscript.shell
  $wsobj.popup("自動ログインに失敗しました。`r`n手動でログインを行なってください。")
  exit}# ページが読み込まれるまで待機for ($i=0; $i -lt 100; $i++)
{
  if($ie.Busy -or $ie.readyState -ne 4)
  {
    Start-Sleep -Milliseconds 10  }else  {
    break  }
}
  [string]"ログイン実行中"  #最新のログイン画面を取得 
 $doc = OverrideMethod($ie[-1].Document)
$jud = $false#画面内に指定の要素があるかチェックfor ($i=0; $i -lt 100; $i++)
{
  $user_name_box=$doc.getElementById($user_name_id)
  $password_box=$doc.getElementById($password_id)
  $button = ($doc.getElementsByTagName("input") | where {$_.Value -eq $button_id})
  if ([string]::IsNullOrEmpty($user_name_box) -eq $false) 
  {
    if ([string]::IsNullOrEmpty($password_box) -eq $false) 
    {
      if ([string]::IsNullOrEmpty($button) -eq $false) 
      {
        $jud = $true
        break      }
    }
  }
  Start-Sleep -Milliseconds 100}#チェック内容に応じて処理を実行if($jud -eq $true)
{
  $user_name_box.value = $user_name
  $password_box.value = $password
  $button.click()
}else{
  $wsobj = new-object -comobject wscript.shell
  $wsobj.popup("自動ログインに失敗しました。`r`n手動でログインを行なってください。")
  exit}
[string]"処理終了"

問題点 ログイン用URLにアクセスすると別ウィンドウでログインページが表示されるため、実際のログインページの情報をPowerShellが所持しておらずそのまま操作しても正常に動かない。

解決策 適切なログインページが表示されているかをIEのプロセスから検索をかけ、表示されている場合そのページをPowerShellで取得する。

コードの解説

DOM周りのメソッドをオーバーライド

function OverrideMethod ([mshtml.HTMLDocumentClass]$Document) {
    $doc = $Document | Add-Member -MemberType ScriptMethod -Name "getElementById" -Value {
        param($Id)
        [System.__ComObject].InvokeMember(
            "getElementById",
            [System.Reflection.BindingFlags]::InvokeMethod,
            $null,
            $this,
            $Id
        ) | ? {$_ -ne [System.DBNull]::Value}
    } -Force -PassThru

    $doc | Add-Member -MemberType ScriptMethod -Name "getElementsByClassName" -Value {
        param($ClassName)
        [System.__ComObject].InvokeMember(
            "getElementsByClassName",
            [System.Reflection.BindingFlags]::InvokeMethod,
            $null,
            $this,
            $ClassName
        ) | ? {$_ -ne [System.DBNull]::Value}
    } -Force

    $doc | Add-Member -MemberType ScriptMethod -Name "getElementsByTagName" -Value {
        param($TagName)
        [System.__ComObject].InvokeMember(
            "getElementsByTagName",
            [System.Reflection.BindingFlags]::InvokeMethod,
            $null,
            $this,
            $TagName
        ) | ? {$_ -ne [System.DBNull]::Value}
    } -Force

    return $doc
}

標準のメソッドをそのまま利用するとエラーが発生することがあるため、メソッドをオーバーライドして利用しています。

URLアクセス

$ie = 
[object Object]
-Object -ComObject InternetExplorer.Application  # IE起動
$ie.Visible = $true #IE表示
$ie.Navigate($url,4)#初期ログインページを開く

PowerShellでIEを呼び出し、ログインページを呼び出すための元のページを開く。 $ie.Visible = $trueはバックグラウンド実行状態のIEをGUI表示するためのコードだ。ここではなく、実際のログインページが表示された後に実行しても良いと考えている。

ページ表示まで待機

for ($i=0; $i -lt 100; $i++) #10は繰り返しの回数{
  if($ie.Busy -or $ie.readyState -ne 4)
  {
    Start-Sleep -Milliseconds 10 #100は一回の待機時間(ミリビョウ)  }else  {
    break  }
}

他のサイトではページ読み込みをWhile文で行っていることが多かったが、個人的にはページ読み込みが失敗する可能性などを考えるとあまり良いとは思えなかった。 なので今回はfor文でタイムアウトを実装した。 上記コードはfor文の回数とSleepの秒数を変更することでタイムアウトまでの時間を設定できる。

目的のログインページが表示されているかを確認、取得

[string]"ログインページ取得中"#$ieの再設定
$ie = $null$jud = $false#ページ情報の取得
for ($i=0; $i -lt 100; $i++)
{#シェルを取得 
 $shell = New-Object -ComObject Shell.Application
  #ieで開いているページ一覧を取得  
$ie_list = @($shell.Windows() | where { $_.Name -match "Internet Explorer" })
  #ページタイトルを用いてオブジェクトを取得  
$ie = @($ie_list | where { $_.LocationURL -match $login_url})
if ($ie)
  {
    $jud = $true    break  }elseif($ie_list | where { $_.LocationName -match $site_name})
  {
    [string]"ログイン不要`r`n処理終了"    exit  }
  Start-Sleep -Milliseconds 100
}

ログインページを表示する前に利用していた$ieをnullで初期化し、そこに目的のログインページを格納する。 windowsのプロセスからIEで開いているページを取得し、そのページの中にログインページのURLが存在するかを確認している。以前はLocationNameで取得していたがページタイトルが同じ場合などに対応できないため修正した。 ここでもfor文で正常に読み込みがされていない場合取得し直しをするようにしている。

最新のログイン画面を選択

$doc=$ie[-1].Document

一つ前の検索の際に同一タイトルのページが複数見つかった場合$ieは配列のように複数のページ情報が格納される。そのため、最新のログイン画面を選択する必要がある。 PowerShellでは配列の番号に[-1]を指定することで配列の一番最後の情報を受け取れる。 これを行わないと、Dom操作(ページ内の操作)を行う際に正常に動作しない。

HTML内の操作したい項目を検索

for ($i=0; $i -lt 100; $i++)
{
  $user_name_box=$doc.getElementById($user_name_id)
  $password_box=$doc.getElementById($password_id)
  $button = $doc.getElementById($button_id)
 #button = ($doc.getElementsByTagName("input") | where {$_.Value -eq "Login"})  if ([string]::IsNullOrEmpty($user_name_box) -eq $false) 
  {
    if ([string]::IsNullOrEmpty($password_box) -eq $false) 
    {
      if ([string]::IsNullOrEmpty($button) -eq $false) 
      {
        $jud = $true
        break      }
    }
  }
  Start-Sleep -Milliseconds 10}

ID、パスワードの入力欄、ログインボタン等が取得したページ内に存在するかを検索し、存在する場合はその情報を取得する。 今回はHTML内のidで検索しているがコード内のgetElementByIdを書き換えることで別の条件で検索をかけられる。 コメントに別の検索条件を使った場合のコードを書いてある。実行されるとLoginと表示されているログインボタンを「inputタグかつvalueの値がLogin」という条件で検索し取得する。

入力等の実行

  $user_name_box.value = $user_name
  $password_box.value = $password
  $button.click()

一つ前のコードで取得した入力欄に指定の文字を入力し、ログインボタンをおす(実行)処理をしている。

エラーチェック

if($jud -eq $false)
{
  $wsobj = new-object -comobject wscript.shell
  $wsobj.popup("自動ログインに失敗しました。`r`n手動でログインを行なってください。")
  exit}

複数回登場するが、エラーチェックが必要な処理に成功した場合$judに$trueを返している。この$judの値を確認し$falseの場合ダイアログを表示しプログラムを終了する。

briefSummary

今回初めてPowerShellを利用したが、エラーチェックを意識してしっかり書かないと異常動作のままどんどん処理が進んでしまう。古い言語と新しい言語を混ぜたような?独特な印象だった。 個人的には標準機能以外使えない等特殊な理由がなければ無理に使わなくて良いと感じた。しかし、現実問題社用PCなど勝手に実行ファイルなどを入れられない環境も存在するため覚える価値はあると感じた。 今回のコードは実際の運用を想定して作ったため、多重起動などが起きても対応できるようになっている。想定外の動作としては連打されるとエラーを吐く。 実際の運用ではタスクスケジューラーでPC起動時に呼び出している。また、手動でも実行できるようにスクリプトを呼び出すバッチファイルを作成し扱いやすくしている。