Selenium WebDriverでクライアント証明書を使う時の問題

selenium-2.46.0、IE8を使用。

Selenium WebDriverでクライアント証明書を使う時の問題を解決する。

  1. IEではWindows セキュリティのポップアップを制御できない問題
  2. ブラウザなしで動作するHtmlUnitDriverでは、クライアント証明書を設定する機能がない問題

解決策

  1. AWTを使って力ずくでEnterを押す。

    WebDriverがポップアップを認識できないという問題、また画面上でOKを押すまでWebDriverから通信の応答が返らない(= new InternetExplorerDriver()が終わらない)という問題に対応するため、非同期にAWTを使ってEnterキーを押下する。
    ソースコードのpressEnterAsynchronously()を参照。

  2. HtmlUnitDriverクラスを拡張して、SSLクライアント認証の機能をつける。

    HtmlUnitDriverはWebClientというブラウザを模したオブジェクトを所有しており、WebClientOptionsというオプションによってJavaScriptを有効にするなど、ブラウザの設定が制御できる。
    WebClientOptionsは元々setSSLClientCertificate()というクライアント証明書を設定するメソッドを持っているため、HtmlUnitDriverからsetSSLClientCertificate()を呼べるようにするだけで良い。
    今回はコンストラクタで設定できるように拡張した。

import java.awt.AWTException;
import java.awt.Robot;
import java.awt.event.KeyEvent;
import java.io.File;
import java.net.MalformedURLException;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.ie.InternetExplorerDriverService;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import com.gargoylesoftware.htmlunit.WebClientOptions;

public class WebDriverSample {

    static WebDriver driver;

    /*------------ 実行環境設定 ---------------*/
    static final String IE_DRIVER_EXE = "C:\\IEDriverServer.exe";
    static final String CLIENT_CERT = "C:\\client.p12";
    static final String CLIENT_CERT_PASS = "password";
    static final String CERTIFICATE_TYPE = "pkcs12";

    /*------------ サイト設定 ---------------*/
    static final String SITE_URL = "https://example.com/test/";

    /*------------ ログインページ ---------------*/
    static final String $LOGIN_ID = "user_id";
    static final String $PASSWORD = "password";
    static final String $BTN_LOGIN = "#btn_login";

    static final String LOGIN_ID = "user_a";
    static final String LOGIN_PASS = "mypassword";

    /*------------ ポータルページ ---------------*/
    static final By BY_P_FORM = By.name("portalForm");

    public static void main(String[] args) {
        String browser = args.length > 0 ? args[0] : "";
        createDriver(browser);

        try {
            $($LOGIN_ID).sendKeys(LOGIN_ID);
            WebElement password = $($PASSWORD);
            // パスワードの自動補完がされた場合を考えて、一旦クリア
            password.clear();
            password.sendKeys(LOGIN_PASS);

            $($BTN_LOGIN).click();

            // 第二引数で最大の待機時間(秒)を設定
            WebDriverWait wait = new WebDriverWait(driver, 10);
            // 条件を満たすまで待つ
            wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(BY_P_FORM));

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(driver.getPageSource());
            driver.quit();
        }
    }

    private static void createDriver(String browser) {
        switch (browser) {
        case "IE":
            createIEDriver(SITE_URL);
            break;
        case "HEADLESS":
            createHeadlessDriver(SITE_URL);
            break;
        default :
            throw new IllegalArgumentException("args[0] requires browser type: IE, HEADLESS");
        }
    }


    /*------------ IE用 ---------------*/
    private static void createIEDriver(String url) {
        // IEDriverServer.exeの指定
        System.setProperty(
                InternetExplorerDriverService.IE_DRIVER_EXE_PROPERTY,
                IE_DRIVER_EXE);

        DesiredCapabilities caps = DesiredCapabilities.internetExplorer();

        // 以下、対策をしていないときに発生するSessionNotFoundExceptionの抑制
        // ・「保護モードを有効にする」の設定をすべてのゾーン(インターネット、ローカルイントラネット、信頼済みサイト及び制限付きサイト)で統一する。
        // ・「信頼済みサイト」ゾーンにテスト対象のサイトを追加する。
        caps.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS, true);

        // ブラウザ起動時のURL
        caps.setCapability(InternetExplorerDriver.INITIAL_BROWSER_URL, url);

        // クライアント証明書の選択を行う「Windows セキュリティ」ポップアップに対して、AWTを使って力ずくでEnterを押す。
        // Threadで非同期実行している理由はRemoteWebDriver#startSessionの以下コードが、画面上でOKを押すまで応答しないから。
        // Response response = execute(DriverCommand.NEW_SESSION, parameters);
        pressEnterAsynchronously();

        driver = new InternetExplorerDriver(caps);
        initialized = true;
    }

    static boolean initialized = false;
    static final int delay = 8000;
    static final int interval = 2000;
    private static void pressEnterAsynchronously() {
        (new Thread() {
            @Override
            public void run() {
                Robot robot = new Robot();
                robot.delay(delay);
                while (true) {
                    try {
                        System.out.println("press enter");
                        robot.keyPress(KeyEvent.VK_ENTER);
                        if (initialized) {
                            return;
                        }
                        robot.delay(interval);
                    } catch (AWTException e) {
                        e.printStackTrace();
                        return;
                    }
                }
            }
        }).start();
    }


    /*------------ ブラウザなし用 ---------------*/
    private static void createHeadlessDriver(String url) {
        driver = new HtmlUnitDriverEx(CLIENT_CERT, CLIENT_CERT_PASS, CERTIFICATE_TYPE);
        driver.get(url);
    }


    /*------------ Utility ---------------*/
    private static WebElement $(String name) {
        if (name.startsWith("#")) {
            return driver.findElement(By.id(name.substring(1)));
        }
        return driver.findElement(By.name(name));
    }
}

/**
 * <pre>
 * HtmlUnitDriverの拡張
 * SSLクライアント認証(SSLClientCertificate)を使えるようにした。
 * </pre>
 */
class HtmlUnitDriverEx extends HtmlUnitDriver {
    public HtmlUnitDriverEx(String certificatePath, String certificatePassword, String certificateType) {
        super(true);
        // turn off htmlunit warnings
        java.util.logging.Logger.getLogger("com.gargoylesoftware.htmlunit").setLevel(java.util.logging.Level.OFF);
        java.util.logging.Logger.getLogger("org.apache.http").setLevel(java.util.logging.Level.OFF);

        WebClientOptions options = super.getWebClient().getOptions();
        try {
            options.setSSLClientCertificate(new File(certificatePath).toURI().toURL(), certificatePassword, certificateType);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
}