build 授权回调处理程序

本文档介绍了如何通过一个使用 Google Tasks API 显示用户任务的示例 Web 应用,使用 Java WebView 实现 OAuth 2.0 授权回调处理程序。示例应用将首先请求授权以访问用户的 Google Tasks,然后会在默认任务列表中显示用户的任务。

受众群体

本文档适用于熟悉 Java 和 J2EE Web 应用架构的人员。建议您先了解 OAuth 2.0 授权流程。

目录

为获得此类完全正常运行的示例,您需要执行几个步骤:

在 web.xml 文件中声明 WebView 映射

我们将在应用中使用 2 个 WebView:

  • PrintTasksTitlesServlet(映射到 /):应用的入口点,将处理用户身份验证并显示用户任务
  • OAuthCodeCallbackHandlerServlet(映射到 /oauth2callback):处理来自 OAuth 授权端点的响应的 OAuth 2.0 回调

下面是 web.xml 文件,该文件将这 2 个 WebView 映射到应用中的网址:

<?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>
/WEB-INF/web.xml 文件

对系统上的用户进行身份验证,并请求授权以访问其任务

用户通过映射到 PrintTaskListsTitlesServlet Blob 的根“/”网址进入应用。在该 WebView 中,系统会执行以下任务:

  • 检查用户是否在系统中通过身份验证
  • 如果用户未通过身份验证,系统会将其重定向到身份验证页面
  • 如果相应用户已通过身份验证,我们会检查我们的数据存储中是否已经有了刷新令牌。该刷新令牌由下文的 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;
  }
}
PrintTasksTitlesSO.java 文件

注意: 上述实现使用了一些 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);
}
OAuthTokenDao.java 文件
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 tokenPersistance = new HashMap();

  public void saveKeys(AccessTokenResponse tokens, String userName) {
    tokenPersistance.put(userName, tokens);
  }

  public AccessTokenResponse getKeys(String userName) {
    return tokenPersistance.get(userName);
  }
}
OAuthTokenDaoMemoryImpl.java 文件

此外,应用的 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 {
  }
}
OAuthProperties.java 文件

以下是 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.properties 文件

OAuth 2.0 客户端 ID 和客户端密钥用于标识您的应用,并允许 Tasks API 为您的应用应用过滤器和配额规则。您可以在 Google API 控制台中找到客户端 ID 和密钥。打开控制台后,您必须执行以下操作:

  • 创建或选择一个项目。
  • 在服务列表中将 Tasks API 的状态切换为开启,以启用 Tasks API。
  • 如果尚未创建 OAuth 2.0 客户端 ID,请在 API 访问权限下创建一个。
  • 确保项目的 OAuth 2.0 代码回调处理程序网址已在重定向 URI 中注册/列入白名单。例如,在此示例项目中,如果您的 Web 应用是从 https://www.example.com 网域提供的,那么您必须注册 https://www.example.com/oauth2callback

API 控制台中的重定向 URI
API 控制台中的重定向 URI

监听 Google 授权端点中的授权代码

如果用户尚未授权应用访问其任务,并因此被重定向到 Google 的 OAuth 2.0 授权端点,则系统会向用户显示来自 Google 的授权对话框,要求用户授予您的应用访问其任务的权限:

Google 的授权对话框
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;
  }
}
OAuthCodeCallbackHandlerREST.java 文件

用授权代码换取刷新令牌和访问令牌

然后,OAuthCodeCallbackHandlerServlet 会使用 Auth 2.0 代码交换刷新令牌和访问令牌,将其保留在数据存储区中,并将用户重定向回 OAuthCodeCallbackHandlerServlet 网址:

添加到下方文件的代码突出显示了语法,现有代码显示为灰色。

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";
/** 处理回调后用户要重定向到的网址。如果您有多个可能将用户重定向到的网址,不妨考虑 * 先在 Cookie 中保存该网址,然后再将用户重定向到 Google * 授权网址。*/ public static final String REDIRECT_网址 = "/"; /** OAuth 令牌 DAO 实现。请考虑进行注入,而不是使用 * 静态初始化。此外,我们使用简单的内存实现 * 作为模拟。将实现更改为使用您的数据库系统。*/ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();
  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;
    }
// 构造传入请求网址 String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // Exchange the code for OAuth tokens AccessTokenResponse accessTokenResponse = exchangesCodeForAccessAndRefreshTokens(code[0], request you); //
  /**
   * 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;
  }
* * @param code 代码。从授权服务获取返回代码 * @param currentUrl 回调的网址 * @param oauthProperties 包含 OAuth 配置的对象 * @return 同时包含访问和刷新令牌的对象 * @throws IOException */ public AccessTokenResponse HttpUrlResponse HttpUrls newTypeCodeForAccessAndRefreshUrl() events IOException
OAuthCodeCallbackHandlerREST.java 文件

注意: 上述实现使用了一些 App Engine 库,为了简化起见,我们使用了这些库。如果您要为其他平台进行开发,请重新实现用于处理用户身份验证的 UserService 接口。

读取并显示用户的任务

用户已授权应用访问其任务。该应用有一个刷新令牌,该令牌保存在可通过 OAuthTokenDao 访问的数据存储区中。PrintTaskListsTitlesServlet SF 现在可以使用这些令牌来访问和显示用户的任务:

添加到下方文件的代码突出显示了语法,现有代码显示为灰色。

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;
    }
// 输出用户的任务列表,以列出响应中的标题 resp.setContentType("text/plain"); resp.getWriter().append("Task Listings for user " + user.getEmail() + ":\n\n"); printTasksTitles(accessTokenResponse, resp.getWriter());
  }

  /**
   * 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;
  }
/** * 使用该 API 中的默认任务列表。* * @param accessTokenResponse 包含访问令牌和刷新令牌的 OAuth 2.0 AccessTokenResponse 对象 *。 * @param 输出输出流写入器,在其中列出任务标题 * @return:默认任务列表中用户任务标题的列表。 * @throws IOException */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer output) throws IOException { // 初始化 Tasks 服务 HttpTransport transport = new NetHttp(); JsonFactory jsonFactory = new JacksonFactorys; OAuthProperties oauthProperties = new OAuthProperties() Google
PrintTasksTitlesSO.java 文件

系统会显示该用户及其任务:

用户的任务
用户的任务

示例应用

您可以在此处下载此示例应用的代码。欢迎随时了解详情。