Gestionnaire de rappel d'autorisation de compilation

Ce document explique comment implémenter un gestionnaire de rappel d'autorisation OAuth 2.0 à l'aide de servlets Java via un exemple d'application Web qui affiche les tâches de l'utilisateur via l'API Google Tasks. L'application exemple demandera d'abord l'autorisation d'accéder aux tâches Google de l'utilisateur, puis affichera les tâches de l'utilisateur dans la liste des tâches par défaut.

Audience

Ce document s'adresse aux personnes familiarisées avec l'architecture des applications Web Java et J2EE. Il est recommandé de connaître le flux d'autorisation OAuth 2.0.

Sommaire

Pour obtenir un échantillon aussi fonctionnel que possible, plusieurs étapes sont nécessaires:

Déclarer les mappages de servlets dans le fichier web.xml

Nous allons utiliser deux servlets dans notre application:

  • PrintTasksTitlesServlet (mappé sur /): point d'entrée de l'application qui gère l'authentification de l'utilisateur et affiche les tâches de l'utilisateur.
  • OAuthCodeCallbackHandlerServlet (mappé à /oauth2callback): rappel OAuth 2.0 qui gère la réponse du point de terminaison de l'autorisation OAuth

Voici le fichier web.xml qui mappe ces deux servlets avec les URL de notre application:

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

Authentifier les utilisateurs sur votre système et demander l'autorisation d'accéder à ses tâches

L'utilisateur accède à l'application via l'URL racine "/" qui est mappée avec le servlet PrintTaskListsTitlesServlet. Ce servlet permet d'effectuer les tâches suivantes:

  • Vérifie si l'utilisateur est authentifié sur le système.
  • Si l'utilisateur n'est pas authentifié, il est redirigé vers la page d'authentification.
  • Si l'utilisateur est authentifié, nous vérifions si notre système de stockage de données dispose déjà d'un jeton d'actualisation. Cette opération est gérée par le protocole OAuthTokenDao ci-dessous. Si aucun jeton d'actualisation n'est enregistré pour l'utilisateur, cela signifie que celui-ci n'a pas encore autorisé l'application à accéder à ses tâches. Dans ce cas, l'utilisateur est redirigé vers le point de terminaison Autorisation OAuth 2.0 de Google.
Vous trouverez ci-dessous une façon de procéder:

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;
  }
}
Fichier PrintTasksTitlesserv.java

Remarque: L'implémentation ci-dessus utilise certaines bibliothèques App Engine pour des raisons de simplification. Si vous développez votre application pour une autre plate-forme, n'hésitez pas à réimplémenter l'interface UserService qui gère l'authentification des utilisateurs.

L'application utilise un DAO pour persister et accéder aux jetons d'autorisation de l'utilisateur. Vous trouverez ci-dessous l'interface OAuthTokenDao et une implémentation fictive (en mémoire) OAuthTokenDaoMemoryImpl, utilisées dans cet exemple:

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

De plus, les identifiants OAuth 2.0 de l'application sont stockés dans un fichier de propriétés. Vous pouvez également les avoir simplement en tant que constantes quelque part dans l'une de vos classes Java. Vous trouverez ci-dessous la classe OAuthProperties et le fichier oauth.properties utilisés dans l'exemple:

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 {
  }
}
Fichier OAuthProperties.java

Vous trouverez ci-dessous le fichier oauth.properties, qui contient les identifiants OAuth 2.0 de votre application. Vous devez modifier vous-même les valeurs ci-dessous.

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

L'ID et le code secret du client OAuth 2.0 identifient votre application et permettent à l'API Tasks d'appliquer les filtres et les règles de quota définis pour votre application. L'ID client et le code secret sont disponibles dans la console des API Google. Une fois dans la console, vous devrez:

  • Créez ou sélectionnez un projet.
  • Activez l'API Tasks en basculant son état sur ACTIVÉ dans la liste des services.
  • Sous Accès à l'API, créez un ID client OAuth 2.0 s'il n'en existe aucun.
  • Assurez-vous que l'URL du gestionnaire de rappel de code OAuth 2.0 du projet est enregistrée/sur la liste blanche dans les URI de redirection. Ainsi, dans cet exemple de projet, vous devriez enregistrer https://www.example.com/oauth2callback si votre application Web est diffusée à partir du domaine https://www.example.com.

URI de redirection dans la console des API
URI de redirection dans la console des API

Écouter le code d'autorisation depuis le point de terminaison Google Autorisation

Si l'utilisateur n'a pas encore autorisé l'application à accéder à ses tâches et qu'il a donc été redirigé vers le point de terminaison Autorisation OAuth 2.0 de Google, une boîte de dialogue d'autorisation de Google s'affiche pour lui demander d'autoriser votre application à accéder à ses tâches:

Boîte de dialogue d&#39;autorisation de Google
Boîte de dialogue d'autorisation de Google

Après avoir accordé ou refusé l'accès, l'utilisateur est redirigé vers le gestionnaire de rappel de code OAuth 2.0, qui a été spécifié en tant que redirection/rappel lors de la création de l'URL d'autorisation Google:

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

Le gestionnaire de rappel de code OAuth 2.0 OAuthCodeCallbackHandlerServlet gérer la redirection à partir du point de terminaison Google OAuth 2.0. Deux cas sont à traiter:

  • L'utilisateur a accordé l'accès: analyse la requête pour obtenir le code OAuth 2.0 à partir des paramètres d'URL.
  • L'utilisateur a refusé l'accès: affiche un message à l'attention de l'utilisateur.

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;
  }
}
Fichier OAuthCodeCallbackHandlerProvider.java

Échanger le code d'autorisation contre un jeton d'actualisation et d'accès

Ensuite, OAuthCodeCallbackHandlerServlet échange le code Auth 2.0 contre une actualisation et des jetons d'accès, le conserve dans le datastore et redirige l'utilisateur vers l'URL OAuthCodeCallbackHandlerServlet:

La syntaxe du code ajouté au fichier ci-dessous est mise en surbrillance. Le code déjà existant est grisé.

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";
/** L'URL vers laquelle rediriger l'utilisateur après la gestion du rappel. Envisagez de * enregistrer ceci dans un cookie avant de rediriger les internautes vers * l'URL d'autorisation Google si vous pouvez rediriger les utilisateurs vers plusieurs URL. */ publicstatic final String REDIRECT_URL = "/"; /** Implémentation DAO du jeton OAuth. Envisagez de l'injecter au lieu d'utiliser * une initialisation statique. Nous utilisons également une implémentation de mémoire simple * à titre d'exemple. Modifiez l'implémentation pour utiliser votre système de base de données. */ public statique 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;
    }
// Construire une URL de requête entrante String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // Remplacer le code pour les jetons OAuth
  /**
   * 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;
  }
* * code @param code Le code obtenu par le service d'autorisation * @param currentUrl The URL du rappel * @param oauthProperties L'objet contenant la configuration OAuth * @return L'objet contenant à la fois un jeton d'accès et d'actualisation * @throws IOException */ public AccessTokenResponseResponseFortcode d'accès-par-la-fabrique-OAuthFactory
Fichier OAuthCodeCallbackHandlerProvider.java

Remarque: L'implémentation ci-dessus utilise certaines bibliothèques App Engine pour des raisons de simplification. Si vous développez votre application pour une autre plate-forme, n'hésitez pas à réimplémenter l'interface UserService qui gère l'authentification des utilisateurs.

Lire les tâches de l'utilisateur et les afficher

L'utilisateur a autorisé l'application à accéder à ses tâches. L'application dispose d'un jeton d'actualisation enregistré dans le datastore accessible via OAuthTokenDao. Le servlet PrintTaskListsTitlesServlet peut désormais utiliser ces jetons pour accéder aux tâches de l'utilisateur et les afficher:

La syntaxe du code ajouté au fichier ci-dessous est mise en surbrillance. Le code déjà existant est grisé.

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;
    }
// Afficher les titres des listes de tâches de l'utilisateur dans la réponse resp.setContentType("text/plain'); resp.getWriter().append("Task numérots titres pour l'utilisateur " + user.getEmail() + ":\n\n'); printTasksTitles(accessTokenResponse, resp.getWriter( lorsque)
  }

  /**
   * 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;
  }
/** * Utilise la liste des tâches des utilisateurs dans l'API Google Tasks pour la récupération d'une liste de tâches par défaut. * * @param accessTokenResponse Objet AccessTokenResponse d'OAuth 2.0 * contenant le jeton d'accès et un jeton d'actualisation. * @param sortie du rédacteur du flux de sortie où lire les titres des tâches * @return Liste des titres des tâches de l'utilisateur dans la liste de tâches par défaut. * @throws IOException */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer output) throws IOException { // Initialisation du service Tasks HttpTransport transport = new NetHttpTransport() JsonFactory json.Factory = new JacksonFactory()
Fichier PrintTasksTitlesserv.java

L'utilisateur s'affiche avec ses tâches:

Les tâches de l&#39;utilisateur
Les tâches de l'utilisateur

Exemple d'application

Le code de cet exemple d'application peut être téléchargé ici. N'hésitez pas à la consulter.