Moduł obsługi wywołania zwrotnego autoryzacji kompilacji

W tym dokumencie opisujemy, jak wdrożyć moduł obsługi wywołania zwrotnego autoryzacji OAuth 2.0 za pomocą serwletów Java w przykładowej aplikacji internetowej, która wyświetli zadania użytkownika przy użyciu interfejsu Google Tasks API. Przykładowa aplikacja najpierw zażąda autoryzacji dostępu do Listy zadań Google użytkownika, a następnie wyświetli zadania tego użytkownika na domyślnej liście zadań.

Odbiorcy

Ten dokument jest przeznaczony dla osób zaznajomionych z architekturą aplikacji internetowych J2EE i Java. Zalecamy zapoznanie się z procedurą autoryzacji protokołu OAuth 2.0.

Spis treści

Aby w pełni działająca próbka była w pełni działająca, musisz wykonać kilka czynności:

Zadeklaruj mapowania serwletów w pliku web.xml

Wykorzystamy 2 serwlety w naszej aplikacji:

  • PrintTasksTitlesServlet (zmapowana na /): punkt wejścia aplikacji, który będzie obsługiwać uwierzytelnianie użytkownika i wyświetlać zadania użytkownika
  • OAuthCodeCallbackHandlerServlet (zmapowana na /oauth2callback): wywołanie zwrotne OAuth 2.0, które obsługuje odpowiedź z punktu końcowego autoryzacji OAuth;

Poniżej znajduje się plik web.xml, który mapuje te 2 serwlety na adresy URL w naszej aplikacji:

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

Uwierzytelniaj użytkowników w systemie i poproś o autoryzację, aby uzyskać dostęp do jego zadań

Użytkownik wprowadza aplikację przy użyciu głównego adresu URL „/”, który jest zmapowany na serwlet PrintTaskListsTitlesServlet. W tym serwlecie wykonywane są następujące zadania:

  • Sprawdza, czy użytkownik jest uwierzytelniony w systemie
  • Jeśli użytkownik nie jest uwierzytelniony, zostaje przekierowany na stronę uwierzytelniania.
  • Jeśli użytkownik jest uwierzytelniony, sprawdzamy, czy w magazynie danych znajduje się już token odświeżania, który jest obsługiwany przez zasadę OAuthTokenDao poniżej. Jeśli dla użytkownika nie ma zapisanego tokena odświeżania, oznacza to, że użytkownik nie przyznał jeszcze aplikacji autoryzacji dostępu do zadań. W takim przypadku użytkownik zostanie przekierowany do punktu końcowego autoryzacji OAuth 2.0 Google.
Poniżej znajdziesz sposób wdrożenia takiego rozwiązania:

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;
  }
}
Plik PrintTasksTitlesServlet.java

Uwaga: powyższa implementacja korzysta z niektórych bibliotek App Engine, które są używane dla uproszczenia. Jeśli tworzysz aplikację na inną platformę, możesz ponownie wdrożyć interfejs UserService, który obsługuje uwierzytelnianie użytkowników.

Aplikacja używa DAO do utrzymywania i uzyskiwania dostępu do tokenów autoryzacji użytkownika. Poniżej przedstawiamy interfejs – OAuthTokenDao – oraz przykładową implementację (w pamięci) – OAuthTokenDaoMemoryImpl, które wykorzystano w tym przykładzie:

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);
}
Plik 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);
  }
}
Plik OAuthTokenDaoMemoryImpl.java

Dane uwierzytelniające protokołu OAuth 2.0 aplikacji są przechowywane w pliku właściwości. Możesz też ustawić je jako stałą w jednej z klas Java, ale oto klasa OAuthProperties i plik oauth.properties używany w przykładzie:

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 {
  }
}
Plik OAuthProperty.java

Poniżej znajduje się plik oauth.properties zawierający dane uwierzytelniające protokołu OAuth 2.0 Twojej aplikacji. Musisz samodzielnie zmienić poniższe wartości.

# 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
Plik oauth.properties

Identyfikatory klienta OAuth 2.0 i tajny klucz klienta identyfikują aplikację oraz umożliwiają interfejsowi Tasks API stosowanie filtrów i reguł limitów zdefiniowanych dla aplikacji. Identyfikator klienta i tajny klucz znajdziesz w Konsoli interfejsów API Google. Po uruchomieniu konsoli musisz:

  • Utwórz lub wybierz projekt.
  • Włącz interfejs Tasks API, zmieniając stan interfejsu Tasks API na WŁ. na liście usług.
  • W sekcji Dostęp do API utwórz identyfikator klienta OAuth 2.0, jeśli nie został jeszcze utworzony.
  • Sprawdź, czy adres URL modułu obsługi wywołania zwrotnego kodu OAuth 2.0 projektu jest zarejestrowany/dodany do białej listy w sekcji Identyfikatory URI przekierowania. W tym przykładowym projekcie musisz na przykład zarejestrować adres https://www.example.com/oauth2callback, jeśli aplikacja internetowa jest udostępniana z domeny https://www.example.com.

Identyfikator URI przekierowania w konsoli interfejsów API
Identyfikator URI przekierowania w konsoli interfejsów API

Nasłuchiwanie kodu autoryzacji z punktu końcowego autoryzacji Google

Jeśli użytkownik nie autoryzował jeszcze aplikacji dostępu do zadań i został przekierowany do punktu końcowego autoryzacji OAuth 2.0 Google, użytkownik zobaczy okno autoryzacji od Google z prośbą o przyznanie aplikacji dostępu do jej zadań:

Okno autoryzacji Google
Okno autoryzacji Google

Po przyznaniu lub odmowie dostępu użytkownik zostanie przekierowany z powrotem do modułu obsługi wywołania zwrotnego kodu OAuth 2.0, który został określony jako przekierowanie/wywołanie zwrotne podczas tworzenia adresu URL autoryzacji Google:

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

Moduł obsługi wywołań zwrotnych w kodzie OAuth 2.0 – OAuthCodeCallbackHandlerServlet – obsługuje przekierowanie z punktu końcowego Google OAuth 2.0. Rozpatrujemy 2 sprawy:

  • Użytkownik przyznał dostęp: analizuje żądanie, aby uzyskać kod OAuth 2.0 z parametrów adresu URL.
  • Użytkownik odmówił dostępu: wyświetla wiadomość.

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;
  }
}
Plik OAuthCodeCallbackHandlerServlet.java

Wymiana kodu autoryzacji na token odświeżania i dostępu

Następnie OAuthCodeCallbackHandlerServlet wymienia kod Auth 2.0 na tokeny odświeżania i dostępu, utrzymuje go w magazynie danych i przekierowuje użytkownika z powrotem na adres URL PrintTaskListsTitlesServlet:

Kod dodany do poniższego pliku jest wyróżniony składnią, a istniejący kod jest wyszarzony.

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";
/** Adres URL, na który ma zostać przekierowany użytkownik po obsłudze wywołania zwrotnego. Jeśli masz wiele adresów URL, na które możesz przekierowywać użytkowników, * rozważ zapisanie go w pliku cookie przed przekierowaniem użytkowników na URL autoryzacji Google *. */ public static final String REDIRECT_URL = "/"; /** Implementacja funkcji DAO tokena OAuth. Rozważ jego wstrzyknięcie, zamiast korzystać ze statycznego inicjowania: *. Ponadto jako przykład korzystamy z prostej implementacji pamięci. *. Zmień implementację na używającą Twojego systemu bazy danych.
  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;
  }
* * @param code; kod otrzymany z usługi autoryzacji, o tej samej nazwie * @param currentUrl The URL of the callback * @param oauthMethod The obiekt zawierający konfigurację OAuth * @return The obiekt zawierający zarówno dostęp, jak i token odświeżania * @throws TransportWyjątek
Plik OAuthCodeCallbackHandlerServlet.java

Uwaga: powyższa implementacja korzysta z niektórych bibliotek App Engine, które są używane dla uproszczenia. Jeśli tworzysz aplikację na inną platformę, możesz ponownie wdrożyć interfejs UserService, który obsługuje uwierzytelnianie użytkowników.

Odczytywanie i wyświetlanie zadań użytkownika

Użytkownik zezwolił aplikacji na dostęp do zadań. Aplikacja ma token odświeżania zapisany w magazynie danych dostępnym przez OAuthTokenDao. Serwlet PrintTaskListsTitlesServlet może teraz używać tych tokenów, aby uzyskiwać dostęp do zadań użytkownika i je wyświetlać:

Kod dodany do poniższego pliku jest wyróżniony składnią, a istniejący kod jest wyszarzony.

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;
    }
// Wydrukowanie tytułów list zadań użytkownika w odpowiedzi resp.setContentType("text/plain"); resp.getWriter().append("Task Lists Title for user " + user.getEmail() + ":\n\n"); printTasksTitles(accessTokenResponse, resp.getWriter(renewal)
  }

  /**
   * 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;
  }
/** * Wykorzystuje listę zadań użytkownika w interfejsie Google Tasks. * * @param accessTokenResponse Obiekt OAuth 2.0 AccessTokenResponse * zawierający token dostępu i token odświeżania. * @param generuje dane wyjściowe zapisującego strumień danych wyjściowych, w którym ma być weryfikowane tytuły zadań. * @return Lista tytułów zadań użytkownika na domyślnej liście zadań.
Plik PrintTasksTitlesServlet.java

Użytkownikowi zostaną wyświetlone zadania, takie jak:

Zadania użytkownika
Zadania użytkownika

Przykładowa aplikacja

Kod tej przykładowej aplikacji można pobrać tutaj. Zachęcamy do zapoznania się z tą usługą.