本文件說明如何透過範例網頁應用程式,使用 Google Tasks API 顯示使用者的工作,以使用 Java CR 實作 OAuth 2.0 授權回呼處理常式。範例應用程式會先要求授權,才能存取使用者的 Google Tasks,然後在預設工作清單中顯示使用者的工作。
目標對象
本文件的目標讀者是熟悉 Java 和 J2EE 網頁應用程式架構的人員。建議具備 OAuth 2.0 授權流程的基本知識。
目錄
如要取得如此完整的範例步驟,您必須:
- 在 web.xml 檔案中宣告 JDK 對應
- 驗證系統中的使用者,並要求授權存取其 Tasks
- 透過 Google Authorization 端點監聽授權碼
- 交換更新和存取權杖的授權碼
- 閱讀並顯示使用者工作
在 web.xml 檔案中宣告 JDK 對應
我們會在應用程式中使用 2 個 CSP:
- PrintTasksTitlesServlet (對應至 /):會處理使用者驗證的應用程式進入點,並顯示使用者的工作
- OAuthCodeCallbackHandlerServlet (對應至 /oauth2callback):OAuth 2.0 回呼,會處理 OAuth 授權端點的回應
以下 web.xml 檔案會將這 2 個 UDP 對應至應用程式中的網址:
<?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 WhatsApp 的網址。在該 Protocol 中,系統會執行以下工作:
- 檢查使用者是否在系統上進行驗證
- 如果使用者未進行驗證,系統會將其重新導向至驗證頁面
- 如果使用者已通過驗證,我們會檢查資料儲存空間中是否已有更新憑證 (由下方的 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
此外,應用程式的 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.properties 檔案,其中包含應用程式的 OAuth 2.0 憑證。您必須自行變更下列值。
# 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 套用您為應用程式定義的篩選器和配額規則。您可以在 Google API 控制台中找到這個用戶端 ID 與密鑰。進入控制台後,您必須執行下列操作:
- 建立或選取所需專案。
- 在服務清單中將 Tasks API 狀態切換為「開啟」,即可啟用 Tasks API。
- 如果您尚未建立 OAuth 2.0 用戶端 ID,請在「API 存取權」下方建立。
- 請確認專案的 OAuth 2.0 程式碼回呼處理常式網址已在重新導向 URI 中註冊/加入許可清單。舉例來說,在這個範例專案中,如果網頁應用程式是從 https://www.example.com 網域提供,就必須註冊 https://www.example.com/oauth2callback。

監聽 Google Authorization 端點的授權碼
如果使用者尚未授權應用程式存取其工作,因此已重新導向至 Google 的 OAuth 2.0 授權端點,則 Google 會顯示授權對話方塊,要求使用者授予應用程式工作存取權:

授予或拒絕存取權後,使用者會將重新導回 OAuth 2.0 程式碼回呼處理常式,在建構 Google 授權網址時指定為重新導向/回呼。
new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
.getScopesAsString()).build()
OAuth 2.0 程式碼回呼處理常式 - OAuthCodeCallbackHandlerServlet 會處理 Google OAuth 2.0 端點的重新導向。有 2 個案件需要處理:
- 使用者已授予存取權:剖析請求,以便透過網址參數取得 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 會交換驗證 2.0 程式碼來更新和存取權杖,在資料儲存庫中保留該程式碼,並將使用者重新導向回 OAuthCodeCallbackHandlerServlet 網址:
新增至下列檔案的程式碼會醒目顯示語法,且現有的程式碼會顯示為灰色。
/** 處理回呼後,將使用者導向至這個網址的網址。您可以考慮使用
* 請先將此位置儲存在 Cookie 中,再將使用者重新導向至 Google
* 授權網址 (如果您有多個可能將使用者重新導向至該網址)。*/
public 靜態最終字串 REDIRECT_URL = "/";
/** OAuth 權杖 DAO 實作。建議您將其插入
* 靜態初始化。並使用簡單的記憶體實作
* 為模擬內容。將實作變更為使用資料庫系統。*/
public static OAuthTokenDao oauthTokenDao = 新的 OAuthTokenDaoMemoryImpl();
// 建構傳入要求網址
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);
}
/**
* 交換指定代碼來交換交換和更新權杖。
。
* @param code 代碼從授權服務收回
* @param currentUrl 回呼網址
* @param oauthProperties 包含 OAuth 設定的物件
* @return 物件同時包含存取權和更新權杖
* @傳回 IOException
*/
public AccessTokenResponse ExchangeCodeForAccessAndRefreshTokens(String code, String currentUrl)
throws IOException {
HttpTransport httpTransport = 新的 NetHttpTransport();
JacksonFactory jsonFactory = 新的 JacksonFactory();
// 載入 OAuth 設定檔
OAuthProperties oauthProperties = 新的 OAuthProperties();
傳回新的 GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties
.getClientId()、oauthProperties.getClientSecret()、code、currentUrl).execute();
}
}OAuthCodeCallbackHandlerHandler.java 檔案注意: 上述實作使用一些 App Engine 程式庫,這些程式庫是用來簡化作業。如果您正針對其他平台開發應用程式,您可以重新導入處理使用者驗證的 UserService 介面。
讀取並顯示使用者的工作
使用者已授權應用程式存取其工作。透過 OAuthTokenDao 存取資料儲存庫,應用程式擁有儲存更新憑證並儲存在資料儲存庫中。PrintTaskListsTitlesServlet WhatsApp 用詞現在可以使用這些符記存取使用者的工作,並顯示這些憑證:
新增至下列檔案的程式碼會醒目顯示語法,且現有的程式碼會顯示為灰色。