Java によるユーザー認証

Bookshelf の Java チュートリアルのこのパートでは、ユーザーのログインフローを作成する方法と、プロフィール情報を使用してユーザー固有の機能を提供する方法を説明します。

Google Identity Platform を使うと、アプリユーザーのログイン認証情報を安全に管理する部分は Google に任せつつ、ユーザーの情報に簡単にアクセスできます。 OAuth 2.0 により、アプリのすべてのユーザーのログインフローを簡単に提供できるだけでなく、認証済みユーザーの基本的なプロフィール情報にアプリからアクセスすることもできます。

このページは、複数ページからなるチュートリアルの一部です。最初からの説明や設定手順を確認するには、Java Bookshelf アプリに移動してください。

ウェブ アプリケーション クライアント ID の作成

ウェブ アプリケーション クライアント ID を使用すると、アプリケーションがユーザーを承認し、ユーザーの代わりにアプリケーションが Google API にアクセスできます。

  1. Google Cloud Platform Console の認証情報セクションに進みます。

  2. [OAuth 同意画面] をクリックします。プロダクト名に「Java Bookshelf App」と入力し、オプションのフィールドにも該当する事項を入力します。[保存] をクリックします。

  3. [認証情報を作成] > [OAuth クライアント ID] をクリックします。

  4. [アプリケーションの種類] で、[ウェブ アプリケーション] を選択します。

  5. [名前] に「Java Bookshelf Client」と入力します。

  6. [承認済みのリダイレクト URI] の下に、以下の URL を 1 つずつ入力します。[YOUR_PROJECT_ID] は実際のプロジェクト ID で置き換えます。

    http://localhost:8080/oauth2callback
    http://[YOUR_PROJECT_ID].appspot.com/oauth2callback
    https://[YOUR_PROJECT_ID].appspot.com/oauth2callback
    http://[YOUR_PROJECT_ID].appspot-preview.com/oauth2callback
    https://[YOUR_PROJECT_ID].appspot-preview.com/oauth2callback

  7. [作成] をクリックします。

  8. クライアント IDクライアント シークレットをコピーし、後で使用するために保存します。

設定の構成

1. getting-started-java/bookshelf/4-auth ディレクトリで、pom.xml を開いて編集します。

  1. <properties> セクションで、callback.host の値として [YOUR_PROJECT_ID].appspot.com を設定します。

  2. bookshelf.clientID の値として、以前作成したクライアント ID を設定します。

  3. bookshelf.clientSecret の値として、以前作成したクライアント シークレットを設定します。

  4. pom.xml を保存して閉じます。

ローカルマシンでのアプリの実行

アプリをローカルで実行するには:

  1. getting-started-java/bookshelf/4-auth ディレクトリで以下のコマンドを入力して、ローカル ウェブサーバーを起動します。

    mvn -Plocal clean jetty:run-exploded -DprojectID=[YOUR-PROJECT-ID]
  2. ウェブブラウザで http://localhost:8080 に移動します。

これで、アプリのウェブページを閲覧できるようになります。Google アカウントでログインして書籍を追加し、追加した書籍を上部のナビゲーション バーにある [My Books] リンクから確認できます。

App Engine フレキシブル環境へのアプリのデプロイ

  1. 次のコマンドを入力してアプリをデプロイします。

    mvn appengine:deploy -DprojectID=YOUR-PROJECT-ID
    
  2. ウェブブラウザで、次のアドレスを入力します。[YOUR_PROJECT_ID] は実際のプロジェクト ID で置き換えます。

    https://[YOUR_PROJECT_ID].appspot-preview.com
    

アプリを更新する場合は、最初にデプロイしたときと同じコマンドを使って、更新バージョンをデプロイできます。デプロイを行うと、アプリの新しいバージョンが作成され、それがデフォルトのバージョンに設定されます。古いバージョンはそのまま残り、関連付けられた VM インスタンスも同様に残ります。すべてのアプリ バージョンと VM インスタンスが課金対象のリソースとなるのでご注意ください。

アプリのデフォルト以外のバージョンを削除することで、コストを削減できます。

アプリのバージョンを削除する手順は次のとおりです。

  1. GCP Console の [App Engine のバージョン] ページに移動します。

    [バージョン] ページに移動

  2. 削除したい、デフォルト以外のアプリのバージョンの横にあるチェックボックスをクリックします。
  3. ページ上部にある [削除] ボタンをクリックし、アプリのバージョンを削除します。

課金対象のリソースをクリーンアップする方法の詳細については、このチュートリアルの最後のステップにあるクリーンアップ セクションを参照してください。

アプリケーションの構造

次の図は、アプリケーションを構成するコンポーネントと、それらの接続関係を示しています。

サンプル Auth の構造

コードを理解する

このセクションでは、アプリケーションのコードとその動作を、順を追って説明します。

ユーザーが [Login] をクリックすると、LoginServlet が呼び出されます。この処理内容は次のとおりです。

  1. リクエスト偽造を防止するために、いくつかのランダムな state を保存する。

  2. ログイン後の行き先を保存する。

  3. Google API クライアント ライブラリ(具体的には GoogleAuthorizationCodeFlow)を使用して Google へのコールバック リクエストを生成し、Google アカウントのログイン処理を依頼する。このアプリでは、スコープとして "email" と "profile" が指定されているので、各ページにユーザーのメールアドレスと画像を表示することができます。

@WebServlet(name = "login", value = "/login")
@SuppressWarnings("serial")
public class LoginServlet extends HttpServlet {

  private static final Collection<String> SCOPES = Arrays.asList("email", "profile");
  private static final JsonFactory JSON_FACTORY = new JacksonFactory();
  private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();

  private GoogleAuthorizationCodeFlow flow;

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws IOException, ServletException {

    String state = new BigInteger(130, new SecureRandom()).toString(32);  // prevent request forgery
    req.getSession().setAttribute("state", state);

    if (req.getAttribute("loginDestination") != null) {
      req
          .getSession()
          .setAttribute("loginDestination", (String) req.getAttribute("loginDestination"));
    } else {
      req.getSession().setAttribute("loginDestination", "/books");
    }

    flow = new GoogleAuthorizationCodeFlow.Builder(
        HTTP_TRANSPORT,
        JSON_FACTORY,
        getServletContext().getInitParameter("bookshelf.clientID"),
        getServletContext().getInitParameter("bookshelf.clientSecret"),
        SCOPES)
        .build();

    // Callback url should be the one registered in Google Developers Console
    String url =
        flow.newAuthorizationUrl()
            .setRedirectUri(getServletContext().getInitParameter("bookshelf.callback"))
            .setState(state)            // Prevent request forgery
            .build();
    resp.sendRedirect(url);
  }
}

Google がユーザーを URL /oauth2callback にリダイレクトします。ユーザーのログインが成功すると、Oauth2CallbackServletdoGet メソッドにより次の処理が実行されます。

  1. 偽造されたリクエストでないことを確認するために、現在の state を保存されている state と比較する。

  2. 保存されていたセッション state を削除する。

  3. レスポンス tokenResponse を取得する。

  4. tokenResponse を使用して Credential を取得する。

  5. credential を使用して requestFactory を作成する。

  6. request を使用して jsonIdentity を取得する。

  7. メールアドレス、画像、ID を抽出する。

  8. 前のステップで保存した loginDestination にリダイレクトする。

@WebServlet(name = "oauth2callback", value = "/oauth2callback")
@SuppressWarnings("serial")
public class Oauth2CallbackServlet extends HttpServlet {

  private static final Collection<String> SCOPES = Arrays.asList("email", "profile");
  private static final String USERINFO_ENDPOINT
      = "https://www.googleapis.com/plus/v1/people/me/openIdConnect";
  private static final JsonFactory JSON_FACTORY = new JacksonFactory();
  private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();

  private GoogleAuthorizationCodeFlow flow;

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException,
      ServletException {

    // Ensure that this is no request forgery going on, and that the user
    // sending us this connect request is the user that was supposed to.
    if (req.getSession().getAttribute("state") == null
        || !req.getParameter("state").equals((String) req.getSession().getAttribute("state"))) {
      resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
      resp.sendRedirect("/books");
      return;
    }

    req.getSession().removeAttribute("state");     // Remove one-time use state.

    flow = new GoogleAuthorizationCodeFlow.Builder(
        HTTP_TRANSPORT,
        JSON_FACTORY,
        getServletContext().getInitParameter("bookshelf.clientID"),
        getServletContext().getInitParameter("bookshelf.clientSecret"),
        SCOPES).build();

    final TokenResponse tokenResponse =
        flow.newTokenRequest(req.getParameter("code"))
            .setRedirectUri(getServletContext().getInitParameter("bookshelf.callback"))
            .execute();

    req.getSession().setAttribute("token", tokenResponse.toString()); // Keep track of the token.
    final Credential credential = flow.createAndStoreCredential(tokenResponse, null);
    final HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(credential);

    final GenericUrl url = new GenericUrl(USERINFO_ENDPOINT);      // Make an authenticated request.
    final HttpRequest request = requestFactory.buildGetRequest(url);
    request.getHeaders().setContentType("application/json");

    final String jsonIdentity = request.execute().parseAsString();
    @SuppressWarnings("unchecked")
    HashMap<String, String> userIdResult =
        new ObjectMapper().readValue(jsonIdentity, HashMap.class);
    // From this map, extract the relevant profile info and store it in the session.
    req.getSession().setAttribute("userEmail", userIdResult.get("email"));
    req.getSession().setAttribute("userId", userIdResult.get("sub"));
    req.getSession().setAttribute("userImageUrl", userIdResult.get("picture"));
    resp.sendRedirect((String) req.getSession().getAttribute("loginDestination"));
  }
}

LogoutServlet によって session が削除され、新しいものが作成されます。

@WebServlet(name = "logout", value = "/logout")
@SuppressWarnings("serial")
public class LogoutServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws IOException, ServletException {
    // you can also make an authenticated request to logout, but here we choose to
    // simply delete the session variables for simplicity
    HttpSession session =  req.getSession(false);
    if (session != null) {
      session.invalidate();
    }
    // rebuild session
    req.getSession();
  }
}
このページは役立ちましたか?評価をお願いいたします。