📝

【JavaFX】WebViewを使用してGoogle APIのOAuth同意画面を表示する方法

2024/07/18に公開

JavaFX の WebView を使用して Google API の OAuth 同意画面を表示する方法をまとめました。

1. 動作環境

  • macOS 14.5
  • Java 21.0.3
  • JavaFX 21.0.3
  • google-api-client 2.6.0
  • google-oauth-client-jetty 1.36.0
  • google-api-services-drive v3-rev20240628-2.0.0

2. サンプルプロジェクト

GitHub でサンプルプロジェクトを公開しています。

https://github.com/iwazou-dev/javafxgoogleapi/tree/v1.0.0

サンプルプロジェクトの主なファイル構成は以下の通りです。

ファイル名 概要
App.java JavaFX メインクラス
CustomBrowser.java カスタムブラウザクラス
GoogleApiService.java Google API インターフェース
GoogleApiServiceImpl.java GoogleApiService.java の実装クラス
JavaFXGoogleApiMainController.java JavaFXGoogleApiMain.fxml のコントローラークラス
OAuthConsentController.java OAuthConsent.fxmlのコントローラークラス
JavaFXGoogleApiMain.fxml メインウィンドウの FXML ファイル
OAuthConsent.fxml OAuth 同意画面ウィンドウの FXML ファイル
error.html OAuth エラー時のランディングページ
success.html OAuth 正常時のランディングページ

3. 事前準備

事前準備として Google Drive API クイックスタートにしたがって 認証情報をダウンロードし src/main/resources に格納します。ファイル名はcredentials.jsonとします。

https://developers.google.com/drive/api/quickstart/java

4. プロジェクト概要

Google Drive のファイル一覧を表示する際の OAuth 同意画面を JavaFX の WebView を使用して表示します。

OAuth 同意画面ウィンドウ

ベースとした Google Drive API クイックスタートのサンプルコードに対する主な変更点は以下の通りです。

  • OAuth 同意画面ウィンドウの作成
  • AuthorizationCodeInstalledApp.Browser を実装したカスタムブラウザクラスの作成
  • AuthorizationCodeInstalledApp へのカスタムブラウザの設定
  • カスタムブラウザが閉じられた場合に発生する NullPointerException の対処
  • LocalServerReceiver のランディングページ[1]の指定

4.1. OAuth 同意画面ウィンドウの作成

OAuth 同意画面ウィンドウの FXML(OAuthConsent.fxml)とコントローラークラス(OAuthConsentController.java)を作成します。コントローラークラスには WebView の getter メソッドを用意します。

OAuthConsent.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.web.WebView?>

<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="800.0" prefWidth="800.0" spacing="10.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.javafxgoogleapi.OAuthConsentController">
   <children>
      <WebView fx:id="fxWebView" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS" />
      <Button fx:id="fxCloseButton" mnemonicParsing="false" text="閉じる" />
   </children>
   <padding>
      <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
</VBox>
OAuthConsentController.java
package org.javafxgoogleapi;

import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class OAuthConsentController {

    @FXML
    private WebView fxWebView;

    @FXML
    private Button fxCloseButton;

    public void initialize() {
        /*
         * 閉じるボタンのアクションを設定
         */
        fxCloseButton.setOnAction(event -> {
            /*
             * ウィンドウを閉じる
             */
            Node source = (Node) event.getSource();
            Stage stage = (Stage) source.getScene().getWindow();
            stage.close();
        });
    }

    /*
     * WebViewのgetter
     */
    WebView getFxWebView() {
        return this.fxWebView;
    }

}

4.2. AuthorizationCodeInstalledApp.Browser を実装したカスタムブラウザクラスの作成

コンストラクターには OAuth 同意画面ウィンドウの WebView と Google API の認証コードの受信に使用するLocalServerReceiverを渡します。LocalServerReceiver はローカルホスト上にサーバーを起動して認証コードの受信を待ちますが、受信が行われないとサーバーが起動したままになってしまうため OAuth 同意画面ウィンドウが閉じられた場合にはそのサーバーを停止させます。

CustomBrowser.java
package org.javafxgoogleapi;

import java.io.IOException;
import java.io.UncheckedIOException;

import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;

import javafx.scene.web.WebView;
import javafx.stage.Stage;

/*
 * カスタムブラウザクラス
 */
class CustomBrowser implements AuthorizationCodeInstalledApp.Browser {

    private final WebView webView;
    private final Stage stage;

    CustomBrowser(WebView webView, LocalServerReceiver localServerReceiver) {
        this.webView = webView;
        this.stage = (Stage) webView.getScene().getWindow();
        this.stage.setOnHidden(event -> {
            /*
             * ウィンドウが閉じられた場合にLocalServerReceiverをstopする
             */
            try {
                localServerReceiver.stop();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
    }

    @Override
    public void browse(String url) throws IOException {
        /*
         * WebViewでブラウズする
         */
        webView.getEngine().load(url);
        stage.showAndWait();
    }

    boolean isShowing() {
        /*
         * ウィンドウを表示しているか否かを返す
         */
        return stage.isShowing();
    }
}

4.3. AuthorizationCodeInstalledApp へのカスタムブラウザの設定

AuthorizationCodeInstalledAppのコンストラクターの引数にカスタムブラウザを指定します。

GoogleApiServiceImpl.java抜粋
CustomBrowser customBrowser = new CustomBrowser(webView, localServerReceiver);
AuthorizationCodeInstalledApp authorizationCodeInstalledApp = new AuthorizationCodeInstalledApp(
        googleAuthorizationCodeFlow, localServerReceiver,
        customBrowser); // カスタムブラウザの指定

4.4. カスタムブラウザが閉じられた場合に発生する NullPointerException の対処

OAuth 同意画面ウィンドウが閉じられて LocalServerReceiverが停止すると認証コードが null になるためAuthorizationCodeInstalledAppauthorizeメソッドでNullPointerExceptionが発生しますがその場合は正常系として処理します。

GoogleApiServiceImpl.java抜粋
Credential credential;
try {
    credential = authorizationCodeInstalledApp.authorize("user");
} catch (NullPointerException e) {
    if (!customBrowser.isShowing()) {
        /*
         * カスタムブラウザが閉じられた場合の処理
         */
        return null;
    }
    throw e;
}

4.5. LocalServerReceiver のランディングページの指定

デフォルトでは LocalServerReceiver が内部で起動したサーバーが HTTP レスポンスボディ(OutputStreamWriter)にランディングページを書き込んでいますが、 WebView ではその表示がうまくいかなかったので正常終了時(success.html)とエラー時(error.html)のランディングページを作成してLocalServerReceiverに指定します。

GoogleApiServiceImpl.java抜粋
LocalServerReceiver localServerReceiver = new LocalServerReceiver.Builder()
        .setPort(-1) // 未使用のポートを使う
        .setLandingPages(
                getClass().getResource("/success.html").toString(), // OAuth 2.0認証成功時の表示ページ
                getClass().getResource("/error.html").toString()) // OAuth 2.0認証失敗時の表示ページ
        .build();

正常時とエラー時のランディングページの例は以下の通りです。

success.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <title>OAuth 2.0 Authentication</title>
    <meta charset="UTF-8">
</head>

<body>
    <h2>認証コードを受け取りました。このウィンドウを閉じてください。</h2>
</body>

</html>
error.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <title>OAuth 2.0 Authentication</title>
    <meta charset="UTF-8">
</head>

<body>
    <h2>認証がエラーになりました。</h2>
</body>

</html>
脚注
  1. 認証プロセス完了後にウィンドウに表示するページ ↩︎

Discussion