Обработчик обратного вызова авторизации сборки

В этом документе объясняется, как реализовать обработчик обратного вызова авторизации OAuth 2.0 с помощью сервлетов Java с помощью примера веб-приложения, которое будет отображать задачи пользователя с помощью API задач Google . Пример приложения сначала запросит авторизацию для доступа к задачам Google пользователя, а затем отобразит задачи пользователя в списке задач по умолчанию.

Аудитория

Этот документ предназначен для людей, знакомых с архитектурой веб-приложений Java и J2EE. Рекомендуется иметь некоторые знания о процессе авторизации OAuth 2.0 .

Содержание

Чтобы получить такой полностью рабочий образец, необходимо выполнить несколько шагов:

Объявите сопоставления сервлетов в файле web.xml.

В нашем приложении мы будем использовать два сервлета:

  • PrintTasksTitlesServlet (сопоставленный с / ): точка входа приложения, которая будет обрабатывать аутентификацию пользователя и отображать задачи пользователя.
  • OAuthCodeCallbackHandlerServlet (сопоставленный с /oauth2callback ): обратный вызов OAuth 2.0, который обрабатывает ответ от конечной точки авторизации OAuth.

Ниже приведен файл web.xml , который сопоставляет эти два сервлета с 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>
Файл /WEB-INF/web.xml

Аутентифицируйте пользователей в вашей системе и запросите авторизацию для доступа к ее задачам.

Пользователь входит в приложение через корневой URL-адрес «/», который сопоставлен с сервлетом PrintTaskListsTitlesServlet . В этом сервлете выполняются следующие задачи:

  • Проверяет, аутентифицирован ли пользователь в системе.
  • Если пользователь не аутентифицирован, он перенаправляется на страницу аутентификации.
  • Если пользователь аутентифицирован, мы проверяем, есть ли у нас токен обновления в нашем хранилище данных, который обрабатывается 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;
  }
}
Файл PrintTasksTitlesServlet.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<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);
  }
}
Файл 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 и секрет клиента идентифицируют ваше приложение и позволяют API задач применять фильтры и правила квот, определенные для вашего приложения. Идентификатор и секрет клиента можно найти в консоли Google API . Оказавшись на консоли, вам придется:

  • Создайте или выберите проект.
  • Включите API задач, переключив статус API задач на ВКЛ в списке служб.
  • В разделе «Доступ к API» создайте идентификатор клиента OAuth 2.0, если он еще не создан.
  • Убедитесь, что URL-адрес обработчика обратного вызова кода OAuth 2.0 проекта зарегистрирован или внесен в белый список в URI перенаправления . Например, в этом примере проекта вам придется зарегистрировать https://www.example.com/oauth2callback , если ваше веб-приложение обслуживается из домена https://www.example.com .

URI перенаправления в консоли API
URI перенаправления в консоли API

Прослушайте код авторизации от конечной точки авторизации Google.

В случае, если пользователь еще не разрешил приложению доступ к его задачам и поэтому был перенаправлен на конечную точку авторизации Google OAuth 2.0, пользователю отображается диалоговое окно авторизации от Google с просьбой предоставить вашему приложению доступ к его задачам:

Диалог авторизации Google
Диалог авторизации Google

После предоставления или отказа в доступе пользователь будет перенаправлен обратно к обработчику обратного вызова кода OAuth 2.0, который был указан как перенаправление/обратный вызов при создании URL-адреса авторизации Google:

new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
      OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
          .getScopesAsString()).build()

Обработчик обратного вызова кода OAuth 2.0 — OAuthCodeCallbackHandlerServlet — обрабатывает перенаправление из конечной точки Google OAuth 2.0. Есть 2 случая, которые нужно обработать:

  • Пользователь предоставил доступ: анализирует запрос, чтобы получить код OAuth 2.0 из параметров URL.
  • Пользователь запретил доступ: показывает сообщение пользователю.

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.java

Обменяйте код авторизации на токен обновления и доступа.

Затем OAuthCodeCallbackHandlerServlet обменивает код Auth 2.0 на токены обновления и доступа, сохраняет его в хранилище данных и перенаправляет пользователя обратно на URL-адрес PrintTaskListsTitlesServlet :

Код, добавленный в файл ниже, выделен синтаксисом, а уже существующий код выделен серым цветом.

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-адрес, на который перенаправляется пользователь после обработки обратного вызова. Рассмотрите возможность * сохранить это в файле cookie, прежде чем перенаправлять пользователей на URL-адрес авторизации Google, * если у вас есть несколько возможных URL-адресов для перенаправления людей. */ public static Final String REDIRECT_URL = "/"; /** Реализация DAO токена OAuth. Рассмотрите возможность его внедрения вместо использования * статической инициализации. Также мы используем простую реализацию памяти * в качестве макета. Измените реализацию на использование вашей системы баз данных. */ 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;
    }
// Создание URL-адреса входящего запроса String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // Обмен кода на токены OAuth AccessTokenResponse accessTokenResponse = ExchangeCodeForAccessAndRefreshTokens(code[0], requestUrl); // Получение текущего пользователя // Здесь используется служба пользователей App Engine, но вам следует заменить ее на // собственную реализацию пользователя/входа UserService userService = UserServiceFactory.getUserService(); Строка электронной почты = userService.getCurrentUser().getEmail(); // Сохраняем токены oauthTokenDao.saveKeys(accessTokenResponse, email); resp.sendRedirect(REDIRECT_URL); }
  /**
   * 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 URL-адрес обратного вызова * @param oauthProperties Объект, содержащий конфигурацию OAuth * @return Объект, содержащий токен доступа и обновления * @throws IOException */ public AccessTokenResponse ExchangeCodeForAccessAndRefreshTokens (String code, String currentUrl) выдает IOException {HttpTransport httpTransport = new NetHttpTransport (); JacksonFactory jsonFactory = новый JacksonFactory(); // Загрузка файла конфигурации oauth OAuthProperties oauthProperties = new OAuthProperties(); вернуть новый GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties.getClientId(), oauthProperties.getClientSecret(), code, currentUrl).execute(); } }
Файл OAuthCodeCallbackHandlerServlet.java

Примечание. В приведенной выше реализации используются некоторые библиотеки App Engine, они используются для упрощения. Если вы разрабатываете приложение для другой платформы, не стесняйтесь повторно реализовать интерфейс UserService , который обрабатывает аутентификацию пользователей.

Чтение задач пользователя и отображение их

Пользователь предоставил приложению доступ к его задачам. Приложение имеет токен обновления, который сохраняется в хранилище данных, доступном через OAuthTokenDao . Сервлет PrintTaskListsTitlesServlet теперь может использовать эти токены для доступа к задачам пользователя и их отображения:

Код, добавленный в файл ниже, выделен синтаксисом, а уже существующий код выделен серым цветом.

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("Названия списков задач для пользователя " + 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 задач Google для получения списка задач пользователей из списка задач по умолчанию. * * @param accessTokenResponse Объект OAuth 2.0 AccessTokenResponse, * содержащий токен доступа и токен обновления. * @param выводит средство записи выходного потока, в котором следует записать заголовки списков задач * @return Список заголовков пользовательских задач в списке задач по умолчанию. * @throws IOException */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer output) бросает IOException { // Инициализация службы задач HttpTransport Transport = new NetHttpTransport(); JsonFactory jsonFactory = новый JacksonFactory(); OAuthProperties oauthProperties = новый OAuthProperties(); GoogleAccessProtectedResource accessProtectedResource = новый GoogleAccessProtectedResource( accessTokenResponse.accessToken, транспорт, jsonFactory, oauthProperties.getClientId(), oauthProperties.getClientSecret(), accessTokenResponse.refreshToken); Служба задач = новые задачи (транспорт, accessProtectedResource, jsonFactory); // Использование инициализированной службы API задач для запроса списка списков задач com.google.api.services.tasks.model.Tasks Tasks = service.tasks.list("@default").execute(); for (Task Task: Tasks.items) {output.append(task.title + "\n"); } } }
Файл PrintTasksTitlesServlet.java

Отобразится пользователь со своими задачами:

Задачи пользователя'
Задачи пользователя

Образец заявления

Код этого примера приложения можно скачать здесь . Не стесняйтесь проверить это.