本文介绍了如何通过一个示例 Web 应用(使用 Google Tasks API 显示用户任务)使用 Java Servlet 实现 OAuth 2.0 授权回调处理程序。示例应用将首先请求授权以访问用户的 Google Tasks,然后在默认任务列表中显示用户的任务。
受众群体
本文档专为熟悉 Java 和 J2EE 网络应用架构的人员而设计。建议您对 OAuth 2.0 授权流程有所了解。
目录
为了获得这样一个完全正常运行的示例,您需要执行以下几个步骤:
- 在 web.xml 文件中声明 servlet 映射
- 对您系统上的用户进行身份验证,并请求授权以访问其任务
- 监听来自 Google 授权端点的授权代码
- 用授权代码换取刷新和访问令牌
- 读取并显示用户的任务
在 web.xml 文件中声明 servlet 映射
我们将在应用程序中使用 2 个 servlet:
- PrintTasksTitlesServlet(映射到 /):应用的入口点,将处理用户身份验证并显示用户的任务
- OAuthCodeCallbackHandlerServlet(映射到 /oauth2callback):OAuth 2.0 回调,用于处理来自 OAuth 授权端点的响应
以下是 web.xml 文件,该文件将这两个 servlet 映射到应用中的网址:
<?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 Servlet 的网址。在该 Servlet 中,将执行以下任务:
- 检查用户是否在系统上通过了身份验证
- 如果用户未通过身份验证,系统会将其重定向到身份验证页面
- 如果用户已通过身份验证,我们会检查我们的数据存储中是否已有刷新令牌(由下面的 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。
- 在 API 访问权限下,创建一个 OAuth 2.0 客户端 ID(如果尚未创建)。
- 请确保项目的 OAuth 2.0 代码回调处理程序网址已在重定向 URI 中注册/列入白名单。例如,在此示例项目中,如果您的 Web 应用是从 https://www.example.com 网域提供的,那么您必须注册 https://www.example.com/oauth2callback。

监听来自 Google 授权端点的授权代码
如果用户尚未授权应用访问其任务并因此被重定向到 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 端点的重定向。您需要处理以下两种情况:
- 用户已授予访问权限:解析请求,以从网址参数中获取 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 网址:
添加到以下文件的代码突出显示了语法,现有代码显示为灰色。
/** 处理回调后将用户重定向到的网址。考虑
* 在将用户重定向到 Google
* 授权网址。*/
public static final String REDIRECT_网址 = "/";
/** OAuth 令牌 DAO 实现。请考虑注入它,而不是使用
* 静态初始化。此外,我们还使用简单的内存实现
* 作为模拟。将实现更改为使用您的数据库系统。*/
public static OAuthTokenDao oauthTokenDao = new 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 = new NetHttpTransport();
JacksonFactory jsonFactory = new JacksonFactory();
// 加载 OAuth 配置文件
OAuthProperties oauthProperties = new OAuthProperties();
返回新的 GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties
.getClientId(), oauthProperties.getClientSecret(), code, currentUrl).execute();
}
}OAuthCodeCallbackHandlerServlet.java 文件注意: 上述实现使用了一些 App Engine 库,这些库只是为了简化而使用。如果您要针对其他平台进行开发,则可随时重新实现用于处理用户身份验证的 UserService 接口。
读取并显示用户的任务
用户已授予应用访问其任务的权限。该应用有一个刷新令牌,该令牌保存在可通过 OAuthTokenDao 访问的数据存储区中。PrintTaskListsTitlesServlet 网络服务器现在可以使用这些令牌访问并显示用户的任务:
添加到以下文件的代码突出显示了语法,现有代码显示为灰色。