このドキュメントでは、Google ToDo リスト API を使用してユーザーのタスクを表示するサンプル ウェブ アプリケーションで、Java サーブレットを使用して OAuth 2.0 認可コールバック ハンドラを実装する方法について説明します。サンプル アプリは、まずユーザーの Google タスクにアクセスするための認可をリクエストし、その後、ユーザーのタスクをデフォルトのタスクリストに表示します。
オーディエンス
このドキュメントは、Java と J2EE のウェブ アプリケーション アーキテクチャに精通しているユーザーを対象としています。OAuth 2.0 認可フローに関する一定の知識があることをおすすめします。
目次
このような完全に動作するサンプルを作成するには、次の手順が必要です。
- web.xml ファイルでサーブレット マッピングを宣言する
- システム上のユーザーを認証し、タスクへのアクセス権限をリクエストする
- Google 認証エンドポイントから認証コードをリッスンする
- 認可コードを更新トークンとアクセス トークンと交換する
- ユーザーのタスクを読み取って表示する
web.xml ファイルでサーブレット マッピングを宣言する
このアプリケーションでは、次の 2 つのサーブレットを使用します。
- PrintTasksTitlesServlet(/ にマッピング): ユーザー認証を処理し、ユーザーのタスクを表示するアプリケーションのエントリ ポイント
- OAuthCodeCallbackHandlerServlet(/oauth2callback にマッピング): OAuth 認可エンドポイントからのレスポンスを処理する OAuth 2.0 コールバック
次の web.xml ファイルは、これらの 2 つのサーブレットをアプリケーションの URL にマッピングします。
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <servlet> <servlet-name>PrintTasksTitles</servlet-name> <servlet-class>com.google.oauthsample.PrintTasksTitlesServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>PrintTasksTitles</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <servlet> <servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name> <servlet-class>com.google.oauthsample.OAuthCodeCallbackHandlerServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name> <url-pattern>/oauth2callback</url-pattern> </servlet-mapping> </web-app>
システム上のユーザーを認証し、タスクへのアクセス権限をリクエストする
ユーザーは、PrintTaskListsTitlesServlet サーブレットにマッピングされたルート URL「/」からアプリケーションに入ります。このサーブレットでは、次のタスクが実行されます。
- ユーザーがシステムで認証されているかどうかを確認します。
- ユーザーが認証されていない場合は、認証ページにリダイレクトされます。
- ユーザーが認証されると、データ ストレージに更新トークンがすでに存在するかどうかが確認されます。これは、下記の OAuthTokenDao によって処理されます。ユーザー用にストアに更新トークンがない場合、ユーザーがタスクにアクセスする権限をアプリに付与していないことを意味します。この場合、ユーザーは Google の OAuth 2.0 認可エンドポイントにリダイレクトされます。
package com.google.oauthsample; import ... /** * Simple sample Servlet which will display the tasks in the default task list of the user. */ @SuppressWarnings("serial") public class PrintTasksTitlesServlet extends HttpServlet { /** * The OAuth Token DAO implementation, used to persist the OAuth refresh token. * Consider injecting it instead of using a static initialization. Also we are * using a simple memory implementation as a mock. Change the implementation to * using your database system. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl(); public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the current user // This is using App Engine's User Service but you should replace this to // your own user/login implementation UserService userService = UserServiceFactory.getUserService(); User user = userService.getCurrentUser(); // If the user is not logged-in it is redirected to the login service, then back to this page if (user == null) { resp.sendRedirect(userService.createLoginURL(getFullRequestUrl(req))); return; } // Checking if we already have tokens for this user in store AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail()); // If we don't have tokens for this user if (accessTokenResponse == null) { OAuthProperties oauthProperties = new OAuthProperties(); // Redirect to the Google OAuth 2.0 authorization endpoint resp.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()); return; } } /** * Construct the request's URL without the parameter part. * * @param req the HttpRequest object * @return The constructed request's URL */ public static String getFullRequestUrl(HttpServletRequest req) { String scheme = req.getScheme() + "://"; String serverName = req.getServerName(); String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort(); String contextPath = req.getContextPath(); String servletPath = req.getServletPath(); String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo(); String queryString = (req.getQueryString() == null) ? "" : "?" + req.getQueryString(); return scheme + serverName + serverPort + contextPath + servletPath + pathInfo + queryString; } }
注: 上記の実装では、簡素化のために App Engine ライブラリが使用されています。別のプラットフォーム向けに開発している場合は、ユーザー認証を処理する UserService インターフェースを自由に再実装してください。
アプリケーションは DAO を使用して、ユーザーの認可トークンを保持し、アクセスします。以下に、このサンプルで使用されるインターフェース(OAuthTokenDao)とモック(インメモリ)実装(OAuthTokenDaoMemoryImpl)を示します。
package com.google.oauthsample; import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse; /** * Allows easy storage and access of authorization tokens. */ public interface OAuthTokenDao { /** * Stores the given AccessTokenResponse using the {@code username}, the OAuth * {@code clientID} and the tokens scopes as keys. * * @param tokens The AccessTokenResponse to store * @param userName The userName associated wit the token */ public void saveKeys(AccessTokenResponse tokens, String userName); /** * Returns the AccessTokenResponse stored for the given username, clientId and * scopes. Returns {@code null} if there is no AccessTokenResponse for this * user and scopes. * * @param userName The username of which to get the stored AccessTokenResponse * @return The AccessTokenResponse of the given username */ public AccessTokenResponse getKeys(String userName); }
package com.google.oauthsample; import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse; ... /** * Quick and Dirty memory implementation of {@link OAuthTokenDao} based on * HashMaps. */ public class OAuthTokenDaoMemoryImpl implements OAuthTokenDao { /** Object where all the Tokens will be stored */ private static Map<String, AccessTokenResponse> tokenPersistance = new HashMap<String, AccessTokenResponse>(); public void saveKeys(AccessTokenResponse tokens, String userName) { tokenPersistance.put(userName, tokens); } public AccessTokenResponse getKeys(String userName) { return tokenPersistance.get(userName); } }
また、アプリケーションの OAuth 2.0 認証情報はプロパティ ファイルに保存されます。または、Java クラスのどこかに定数として設定することもできます。ここでは、サンプルで使用されている OAuthProperties クラスと oauth.properties ファイルを示します。
package com.google.oauthsample; import ... /** * Object representation of an OAuth properties file. */ public class OAuthProperties { public static final String DEFAULT_OAUTH_PROPERTIES_FILE_NAME = "oauth.properties"; /** The OAuth 2.0 Client ID */ private String clientId; /** The OAuth 2.0 Client Secret */ private String clientSecret; /** The Google APIs scopes to access */ private String scopes; /** * Instantiates a new OauthProperties object reading its values from the * {@code OAUTH_PROPERTIES_FILE_NAME} properties file. * * @throws IOException IF there is an issue reading the {@code propertiesFile} * @throws OauthPropertiesFormatException If the given {@code propertiesFile} * is not of the right format (does not contains the keys {@code * clientId}, {@code clientSecret} and {@code scopes}) */ public OAuthProperties() throws IOException { this(OAuthProperties.class.getResourceAsStream(DEFAULT_OAUTH_PROPERTIES_FILE_NAME)); } /** * Instantiates a new OauthProperties object reading its values from the given * properties file. * * @param propertiesFile the InputStream to read an OAuth Properties file. The * file should contain the keys {@code clientId}, {@code * clientSecret} and {@code scopes} * @throws IOException IF there is an issue reading the {@code propertiesFile} * @throws OAuthPropertiesFormatException If the given {@code propertiesFile} * is not of the right format (does not contains the keys {@code * clientId}, {@code clientSecret} and {@code scopes}) */ public OAuthProperties(InputStream propertiesFile) throws IOException { Properties oauthProperties = new Properties(); oauthProperties.load(propertiesFile); clientId = oauthProperties.getProperty("clientId"); clientSecret = oauthProperties.getProperty("clientSecret"); scopes = oauthProperties.getProperty("scopes"); if ((clientId == null) || (clientSecret == null) || (scopes == null)) { throw new OAuthPropertiesFormatException(); } } /** * @return the clientId */ public String getClientId() { return clientId; } /** * @return the clientSecret */ public String getClientSecret() { return clientSecret; } /** * @return the scopes */ public String getScopesAsString() { return scopes; } /** * Thrown when the OAuth properties file was not at the right format, i.e not * having the right properties names. */ @SuppressWarnings("serial") public class OAuthPropertiesFormatException extends RuntimeException { } }
以下は、アプリケーションの OAuth 2.0 認証情報が含まれる oauth.properties ファイルです。以下の値は、ご自身で変更する必要があります。
# Client ID and secret. They can be found in the APIs console. clientId=1234567890.apps.googleusercontent.com clientSecret=aBcDeFgHiJkLmNoPqRsTuVwXyZ # API scopes. Space separated. scopes=https://www.googleapis.com/auth/tasks
OAuth 2.0 クライアント ID とクライアント シークレットはアプリケーションを識別し、Tasks API がアプリケーションに定義されたフィルタと割り当てルールを適用できるようにします。クライアント ID とシークレットは、Google API コンソールで確認できます。コンソールにアクセスしたら、次の操作を行う必要があります。
- プロジェクトを作成または選択します。
- サービスのリストで Tasks API のステータスを [オン] に切り替えて、Tasks API を有効にします。
- [API アクセス] で、OAuth 2.0 クライアント ID をまだ作成していない場合は作成します。
- プロジェクトの OAuth 2.0 コード コールバック ハンドラの URL が、[リダイレクト URI] に登録またはホワイトリストに登録されていることを確認します。たとえば、このサンプル プロジェクトでは、ウェブ アプリケーションが https://www.example.com ドメインから提供される場合は、https://www.example.com/oauth2callback を登録する必要があります。

Google 認可エンドポイントから認証コードをリッスンする
ユーザーがタスクへのアクセスをアプリに許可しておらず、Google の OAuth 2.0 認可エンドポイントにリダイレクトされた場合、Google の認可ダイアログが表示され、アプリにタスクへのアクセスを許可するよう求められます。

アクセスを許可または拒否すると、ユーザーは、Google 認可 URL の作成時にリダイレクト/コールバックとして指定された OAuth 2.0 コード コールバック ハンドラにリダイレクトされます。
new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()
OAuth 2.0 コード コールバック ハンドラ(OAuthCodeCallbackHandlerServlet)が、Google OAuth 2.0 エンドポイントからのリダイレクトを処理します。処理するケースは次の 2 つです。
- ユーザーがアクセスを許可している場合: リクエストを解析して、URL パラメータから OAuth 2.0 コードを取得します。
- ユーザーがアクセスを拒否している: ユーザーにメッセージを表示します
package com.google.oauthsample; import ... /** * Servlet handling the OAuth callback from the authentication service. We are * retrieving the OAuth code, then exchanging it for a refresh and an access * token and saving it. */ @SuppressWarnings("serial") public class OAuthCodeCallbackHandlerServlet extends HttpServlet { /** The name of the Oauth code URL parameter */ public static final String CODE_URL_PARAM_NAME = "code"; /** The name of the OAuth error URL parameter */ public static final String ERROR_URL_PARAM_NAME = "error"; /** The URL suffix of the servlet */ public static final String URL_MAPPING = "/oauth2callback"; public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the "error" URL parameter String[] error = req.getParameterValues(ERROR_URL_PARAM_NAME); // Checking if there was an error such as the user denied access if (error != null && error.length > 0) { resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "There was an error: \""+error[0]+"\"."); return; } // Getting the "code" URL parameter String[] code = req.getParameterValues(CODE_URL_PARAM_NAME); // Checking conditions on the "code" URL parameter if (code == null || code.length == 0) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "The \"code\" URL parameter is missing"); return; } } /** * Construct the OAuth code callback handler URL. * * @param req the HttpRequest object * @return The constructed request's URL */ public static String getOAuthCodeCallbackHandlerUrl(HttpServletRequest req) { String scheme = req.getScheme() + "://"; String serverName = req.getServerName(); String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort(); String contextPath = req.getContextPath(); String servletPath = URL_MAPPING; String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo(); return scheme + serverName + serverPort + contextPath + servletPath + pathInfo; } }
認可コードを更新トークンとアクセス トークンと交換する
次に、OAuthCodeCallbackHandlerServlet が Auth 2.0 コードを更新トークンとアクセス トークンと交換し、データストアに保存して、ユーザーを PrintTaskListsTitlesServlet URL にリダイレクトします。
以下のファイルに追加されたコードは構文がハイライト表示され、既存のコードはグレー表示されています。
/** コールバックの処理後にユーザーをリダイレクトする URL。リダイレクト先の URL が複数ある場合は、ユーザーを Google 認可 URL にリダイレクトする前に、* これを Cookie に保存することを検討してください。*/ public static final String REDIRECT_URL = "/"; /** OAuth Token DAO の実装。静的初期化を使用する代わりに、挿入することを検討してください。また、モックとしてシンプルなメモリ実装を使用しています。実装を変更して、データベース システムを使用します。*/ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl(); package com.google.oauthsample; import ... /** * Servlet handling the OAuth callback from the authentication service. We are * retrieving the OAuth code, then exchanging it for a refresh and an access * token and saving it. */ @SuppressWarnings("serial") public class OAuthCodeCallbackHandlerServlet extends HttpServlet { /** The name of the Oauth code URL parameter */ public static final String CODE_URL_PARAM_NAME = "code"; /** The name of the OAuth error URL parameter */ public static final String ERROR_URL_PARAM_NAME = "error"; /** The URL suffix of the servlet */ public static final String URL_MAPPING = "/oauth2callback";// 受信リクエスト URL を構築します。 String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // コードを OAuth トークンと交換します。 AccessTokenResponse accessTokenResponse = exchangeCodeForAccessAndRefreshTokens(code[0], requestUrl); // 現在のユーザーを取得します。 // これは App Engine のユーザー サービスを使用していますが、これは独自のユーザー / ログイン実装に置き換える必要があります。 UserService userService = UserServiceFactory.getUserService(); String email = userService.getCurrentUser().getEmail(); // トークンを保存します。 oauthTokenDao.saveKeys(accessTokenResponse, email); resp.sendRedirect(REDIRECT_URL); } public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the "error" URL parameter String[] error = req.getParameterValues(ERROR_URL_PARAM_NAME); // Checking if there was an error such as the user denied access if (error != null && error.length > 0) { resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "There was an error: \""+error[0]+"\"."); return; } // Getting the "code" URL parameter String[] code = req.getParameterValues(CODE_URL_PARAM_NAME); // Checking conditions on the "code" URL parameter if (code == null || code.length == 0) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "The \"code\" URL parameter is missing"); return; }/** * 指定されたコードを交換トークンと更新トークンと交換します。 * * @param code 認可サービスから返されたコード * @param currentUrl コールバックの URL * @param oauthProperties OAuth 構成を含むオブジェクト * @return アクセス トークンと更新トークンの両方を含むオブジェクト * @throws IOException */ public AccessTokenResponse exchangeCodeForAccessAndRefreshTokens(String code, String currentUrl) throws IOException { HttpTransport httpTransport = new NetHttpTransport(); JacksonFactory jsonFactory = new JacksonFactory(); // Loading the oauth config file OAuthProperties oauthProperties = new OAuthProperties(); return new GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties .getClientId(), oauthProperties.getClientSecret(), code, currentUrl).execute(); } } /** * Construct the OAuth code callback handler URL. * * @param req the HttpRequest object * @return The constructed request's URL */ public static String getOAuthCodeCallbackHandlerUrl(HttpServletRequest req) { String scheme = req.getScheme() + "://"; String serverName = req.getServerName(); String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort(); String contextPath = req.getContextPath(); String servletPath = URL_MAPPING; String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo(); return scheme + serverName + serverPort + contextPath + servletPath + pathInfo; }OAuthCodeCallbackHandlerServlet.java ファイル注: 上記の実装では、簡素化のために App Engine ライブラリが使用されています。別のプラットフォーム向けに開発している場合は、ユーザー認証を処理する UserService インターフェースを自由に再実装してください。
ユーザーのタスクを読み取って表示する
ユーザーが、タスクへのアクセスをアプリに許可している。アプリケーションには、OAuthTokenDao からアクセスできるデータストアに保存されている更新トークンがあります。PrintTaskListsTitlesServlet サーブレットは、これらのトークンを使用してユーザーのタスクにアクセスし、タスクを表示できるようになりました。
以下のファイルに追加されたコードは構文がハイライト表示され、既存のコードはグレー表示されています。
// レスポンスでユーザーのタスクリストのタイトルを出力します resp.setContentType("text/plain"); resp.getWriter().append("ユーザー " + user.getEmail() + " のタスクリストのタイトル:&n\n"); printTasksTitles(accessTokenResponse, resp.getWriter()); package com.google.oauthsample; import ... /** * Simple sample Servlet which will display the tasks in the default task list of the user. */ @SuppressWarnings("serial") public class PrintTasksTitlesServlet extends HttpServlet { /** * The OAuth Token DAO implementation, used to persist the OAuth refresh token. * Consider injecting it instead of using a static initialization. Also we are * using a simple memory implementation as a mock. Change the implementation to * using your database system. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl(); public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the current user // This is using App Engine's User Service but you should replace this to // your own user/login implementation UserService userService = UserServiceFactory.getUserService(); User user = userService.getCurrentUser(); // If the user is not logged-in it is redirected to the login service, then back to this page if (user == null) { resp.sendRedirect(userService.createLoginURL(getFullRequestUrl(req))); return; } // Checking if we already have tokens for this user in store AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail()); // If we don't have tokens for this user if (accessTokenResponse == null) { OAuthProperties oauthProperties = new OAuthProperties(); // Redirect to the Google OAuth 2.0 authorization endpoint resp.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()); return; }/** * Google Tasks API を使用して、デフォルトのタスクリスト内のユーザーのタスクのリストを取得します。 * * @param accessTokenResponse アクセス トークンと更新トークンを含む OAuth 2.0 AccessTokenResponse オブジェクト。 * @param output タスクリストのタイトルを書き込む出力ストリーム ライター * @return デフォルトのタスクリスト内のユーザーのタスクのタイトルのリスト。 * @throws IOException */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer output) throws IOException { // Tasks サービスを初期化する HttpTransport transport = new NetHttpTransport(); JsonFactory jsonFactory = new JacksonFactory(); OAuthProperties oauthProperties = new OAuthProperties(); GoogleAccessProtectedResource accessProtectedResource = new GoogleAccessProtectedResource( accessTokenResponse.accessToken, transport, jsonFactory, oauthProperties.getClientId(), oauthProperties.getClientSecret(), accessTokenResponse.refreshToken); Tasks service = new Tasks(transport, accessProtectedResource, jsonFactory); // 初期化された Tasks API サービスを使用して、タスクリストのリストをクエリします com.google.api.services.tasks.model.Tasks tasks = service.tasks.list("@default").execute(); for (Task task : tasks.items) { output.append(task.title + "\n"); } } } } /** * Construct the request's URL without the parameter part. * * @param req the HttpRequest object * @return The constructed request's URL */ public static String getFullRequestUrl(HttpServletRequest req) { String scheme = req.getScheme() + "://"; String serverName = req.getServerName(); String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort(); String contextPath = req.getContextPath(); String servletPath = req.getServletPath(); String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo(); String queryString = (req.getQueryString() == null) ? "" : "?" + req.getQueryString(); return scheme + serverName + serverPort + contextPath + servletPath + pathInfo + queryString; }PrintTasksTitlesServlet.java ファイルユーザーにタスクが表示されます。
ユーザーのタスクサンプル アプリケーション
このサンプル アプリケーションのコードは、こちらからダウンロードできます。ぜひご覧ください。