Gerenciador de callback de autorização de build

Este documento explica como implementar um manipulador de retorno de chamada de autorização do OAuth 2.0 usando servlets Java por meio de um aplicativo da Web de exemplo que exibirá as tarefas do usuário utilizando a API de tarefas do Google. O aplicativo de exemplo primeiro solicitará a autorização para acessar o Google Tarefas do usuário e, em seguida, exibirá as tarefas do usuário na lista de tarefas padrão.

Público-alvo

Este documento é voltado para pessoas familiarizadas com a arquitetura de aplicativos da Web Java e J2EE. É recomendável ter algum conhecimento sobre o fluxo de autorização do OAuth 2.0.

Índice

Para que esse exemplo funcione, você precisa seguir várias etapas:

Declarar mapeamentos de servlet no arquivo web.xml

Usaremos dois servlets em nosso aplicativo:

  • PrintTasksTitlesServlet (mapeado para /): o ponto de entrada do aplicativo que vai processar a autenticação do usuário e exibir as tarefas dele
  • OAuthCodeCallbackHandlerServlet (mapeado para /oauth2callback): a chamada de retorno do OAuth 2.0 que gerencia a resposta do endpoint de autorização do OAuth.

Confira abaixo o arquivo web.xml, que mapeia esses dois servlets para os URLs do aplicativo:

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

Autenticar os usuários no seu sistema e solicitar autorização para acessar as tarefas dele

O usuário entra no aplicativo pela raiz "/" URL que é mapeado para o servlet PrintTaskListsTitlesServlet. Nesse servlet, as seguintes tarefas são realizadas:

  • Verifica se o usuário está autenticado no sistema
  • Se o usuário não estiver autenticado, ele será redirecionado para a página de autenticação
  • Se o usuário estiver autenticado, verificamos se já existe um token de atualização em nosso armazenamento de dados, que é manipulado pelo OAuthTokenDao abaixo. Se não houver um token de atualização disponível para o usuário, isso significa que ele ainda não concedeu autorização do aplicativo para acessar suas tarefas. Nesse caso, o usuário é redirecionado para o endpoint de autorização do OAuth 2.0 do Google.
. Confira abaixo como fazer isso:

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;
  }
}
Arquivo PrintTasksTitlesservlet.java

Observação: a implementação acima usa algumas bibliotecas do App Engine, que são usadas para fins de simplificação. Se você estiver desenvolvendo para outra plataforma, fique à vontade para implementar novamente a interface UserService, que lida com a autenticação do usuário.

O aplicativo usa um DAO para persistir e acessar os tokens de autorização do usuário. Confira abaixo a interface OAuthTokenDao e uma implementação fictícia (na memória) OAuthTokenDaoMemoryImpl, que são usadas neste exemplo:

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

Além disso, as credenciais de OAuth 2.0 do aplicativo são armazenadas em um arquivo de propriedades. Como alternativa, você pode simplesmente tê-las como uma constante em algum lugar em uma das classes Java, embora aqui estejam a classe OAuthProperties e o arquivo oauth.properties que está sendo usado no exemplo:

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

Abaixo está o arquivo oauth.properties que contém as credenciais do OAuth 2.0 de seu aplicativo. Você precisa alterar os valores abaixo por conta própria.

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

O ID e a chave secreta do cliente OAuth 2.0 identificam seu aplicativo e permitem que a API de tarefas aplique filtros e regras de cota definidas para seu aplicativo. O ID do cliente e a chave secreta podem ser encontrados no Console de APIs do Google. No console, você vai precisar fazer o seguinte:

  • Crie ou selecione um projeto.
  • Ative a API Tasks alternando o status dela para ATIVADO na lista de serviços.
  • Em Acesso à API, crie um ID do cliente OAuth 2.0 se ainda não tiver feito isso.
  • Verifique se o URL do gerenciador de callback do código do OAuth 2.0 do projeto está registrado/colocado na lista de permissões nos URIs de redirecionamento. Por exemplo, neste exemplo de projeto, será necessário registrar https://www.example.com/oauth2callback se o aplicativo da Web for disponibilizado a partir do domínio https://www.example.com.

URI de redirecionamento no Console de APIs
URI de redirecionamento no Console de APIs

Detectar o código de autorização do endpoint de autorização do Google

No caso em que o usuário ainda não autorizou o aplicativo a acessar as tarefas e, portanto, foi redirecionado para o endpoint de autorização do OAuth 2.0 do Google, é exibida uma caixa de diálogo de autorização do Google pedindo que o usuário conceda ao seu aplicativo acesso às tarefas:

Caixa de diálogo de autorização do Google
Caixa de diálogo de autorização do Google

Depois de conceder ou negar o acesso, o usuário será redirecionado de volta para o gerenciador de callback do código do OAuth 2.0, que foi especificado como um redirecionamento/retorno de chamada ao construir o URL de autorização do Google:

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

O gerenciador de callback do código do OAuth 2.0 (OAuthCodeCallbackHandlerServlet) gerencia o redirecionamento do endpoint do Google OAuth 2.0. Há dois casos a serem processados:

  • O usuário concedeu acesso: analisa a solicitação para obter o código OAuth 2.0 dos parâmetros de URL.
  • O usuário negou acesso: mostra uma mensagem para o usuário.

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;
  }
}
Arquivo OAuthCodeCallbackHandlerservlet.java

Troque o código de autorização por um token de acesso e atualização

Em seguida, o OAuthCodeCallbackHandlerServlet troca o código do Auth 2.0 por tokens de acesso e atualização, persiste-o no armazenamento de dados e redireciona o usuário de volta para o URL OAuthCodeCallbackHandlerServlet:

O código adicionado ao arquivo abaixo está com a sintaxe destacada, o código já existente está esmaecido.

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";
/** O URL para redirecionar o usuário após processar a chamada de retorno. Considere * salvando em um cookie antes de redirecionar os usuários para o servidor * URL de autorização se você tiver vários URLs possíveis para redirecionar as pessoas. */ public Static final String REDIRECT_URL = "/"; /** A implementação do DAO do token OAuth. Injetá-lo em vez de usar * uma inicialização estática. Também estamos usando uma implementação simples de memória * como simulação. Mudar a implementação para usar o sistema de banco de dados. */ 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;
    }
// Construir URL de solicitação recebida String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // Trocar o código por tokens OAuth AccessTokenResponse accessTokenResponse = conversionCodeForAccessAndRefreshTokens(code[0], requestUrl); // Obter o usuário atual // Isto está usando o serviço de usuário do App Engine, mas você deve substituí-lo para // sua própria implementação de usuário/login UserService userService = UserServiceFactory.getUserService() String email = userService.getCurrentUser().getEmail() // Salve os tokens 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;
  }
/** * Troca o código fornecido por uma troca e um token de atualização. * * @param code O código retornado pelo serviço de autorização * @param currentUrl O URL do retorno de chamada * @param oauthProperties O objeto que contém a configuração de OAuth * @return O objeto que contém um token de acesso e de atualização * @gera uma IOException */ public AccessTokenResponse conversionCodeForAccessAndRefreshTokens(String code, String currentUrl) throws IOException { HttpTransport httpTransport = new NetHttpTransport() JacksonFactory jsonFactory = new JacksonFactory() // Carregar o arquivo de configuração OAuth OAuthProperties oauthProperties = new OAuthProperties() return new GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties .getClientId(), oauthProperties.getClientSecret(), code, currentUrl).execute() } }
Arquivo OAuthCodeCallbackHandlerservlet.java

Observação: a implementação acima usa algumas bibliotecas do App Engine, que são usadas para fins de simplificação. Se você estiver desenvolvendo para outra plataforma, fique à vontade para implementar novamente a interface UserService, que lida com a autenticação do usuário.

Ler e mostrar as tarefas do usuário

O usuário concedeu ao aplicativo acesso às tarefas dele. O aplicativo tem um token de atualização que é salvo no armazenamento de dados, que pode ser acessado por meio de OAuthTokenDao. O servlet PrintTaskListsTitlesServlet agora pode usar esses tokens para acessar as tarefas do usuário e exibi-las:

O código adicionado ao arquivo abaixo está com a sintaxe destacada, o código já existente está esmaecido.

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;
    }
// Imprimir os títulos das listas de tarefas do usuário na resposta resp.setContentType(&quot;text/plain&quot;); resp.getWriter().append("Task Lista títulos para o usuário " + 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;
  }
/** * Usa a API Google Tasks para recuperar uma lista das tarefas dos usuários no arquivo * lista de tarefas. * * @param accessTokenResponse O objeto AccessTokenResponse do OAuth 2.0 * contendo o token de acesso e um token de atualização. * @param gera como saída o gravador do stream de saída onde distribuir os títulos das listas de tarefas * @return Uma lista dos títulos das tarefas dos usuários na lista de tarefas padrão. * @gera uma IOException */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer output) throws IOException { // Inicializar o serviço Tarefas Transporte HttpTransport = new NetHttpTransport() JsonFactory jsonFactory = new JacksonFactory() OAuthProperties oauthProperties = new OAuthProperties() GoogleAccessProtectedResource accessProtectedResource = new GoogleAccessProtectedResource( accessTokenResponse.accessToken, transport, jsonFactory, oauthProperties.getClientId(), oauthProperties.getClientSecret(), accessTokenResponse.refreshToken); Tasks service = new Tasks(transport, accessProtectedResource, jsonFactory); // Usar o serviço inicializado da API Tasks para consultar a lista de listas de tarefas com.google.api.services.tasks.model.Tasks tasks = service.tasks.list(&quot;@default&quot;).execute(); for (Task task : tasks.items) { output.append(task.title + "\n"); } } }
Arquivo PrintTasksTitlesservlet.java

O usuário vai aparecer com as tarefas dele:

As tarefas do usuário
As tarefas do usuário

Exemplo de aplicativo

Faça o download do código desse aplicativo de exemplo aqui. Confira.