Este documento explica como implementar um gerenciador de callback de autorização OAuth 2.0 usando servlets Java em um exemplo de aplicativo da Web que mostra as tarefas do usuário usando a API Google Tasks. O aplicativo de exemplo primeiro solicita autorização para acessar as Tarefas do Google do usuário e depois mostra as tarefas na lista padrão.
Público-alvo
Este documento é adaptado para pessoas que conhecem a arquitetura de aplicativos da Web Java e J2EE. Recomendamos algum conhecimento do fluxo de autorização do OAuth 2.0.
Conteúdo
Para ter um exemplo totalmente funcional, várias etapas são necessárias:
- Declarar mapeamentos de servlet no arquivo web.xml
- Autenticar os usuários no seu sistema e solicitar autorização para acessar as tarefas
- Detectar o código de autorização do endpoint de autorização do Google
- Trocar o código de autorização por um token de atualização e de acesso
- Ler e mostrar as tarefas do usuário
Declarar mapeamentos de servlet no arquivo web.xml
Vamos usar dois servlets no aplicativo:
- PrintTasksTitlesServlet (mapeado para /): o ponto de entrada do aplicativo que processa a autenticação do usuário e exibe as tarefas dele.
- OAuthCodeCallbackHandlerServlet (mapeado para /oauth2callback): o callback OAuth 2.0 que processa a resposta do endpoint de autorização do OAuth.
Confira abaixo o arquivo web.xml, que mapeia esses dois servlets para URLs no nosso 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>
Autenticar os usuários no seu sistema e solicitar autorização para acessar as tarefas
O usuário entra no aplicativo pelo URL raiz '/', 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 for autenticado, vamos verificar se já temos um token de atualização no armazenamento de dados, que é processado pelo OAuthTokenDao abaixo. Se não houver um token de atualização armazenado para o usuário, isso significa que ele ainda não concedeu a autorização do aplicativo para acessar as tarefas. Nesse caso, o usuário é redirecionado para o endpoint de autorização do OAuth 2.0 do Google.
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; } }
Observação: a implementação acima usa algumas bibliotecas do App Engine, que são usadas para simplificar. Se você estiver desenvolvendo para outra plataforma, implemente novamente a interface UserService, que processa 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 simulada (na memória) OAuthTokenDaoMemoryImpl 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); }
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); } }
Além disso, as credenciais do OAuth 2.0 para o aplicativo são armazenadas em um arquivo de propriedades. Como alternativa, você pode simplesmente ter esses valores como uma constante em uma das suas classes Java, mas aqui está 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 { } }
Confira abaixo o arquivo oauth.properties, que contém as credenciais do OAuth 2.0 do seu aplicativo. Você precisa mudar 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
O ID e a chave secreta do cliente OAuth 2.0 identificam seu aplicativo e permitem que a API Tasks aplique filtros e regras de cota definidos para ele. O ID do cliente e o segredo podem ser encontrados no Console de APIs do Google. No console, você vai precisar:
- Crie ou selecione um projeto.
- Ative a API Tasks definindo o status dela como ATIVADO na lista de serviços.
- Em Acesso à API, crie um ID do cliente do OAuth 2.0, se ainda não tiver sido criado.
- Verifique se o URL do gerenciador de callback de código OAuth 2.0 do projeto está registrado/na lista de permissões nos URIs de redirecionamento. Por exemplo, neste projeto de exemplo, você precisa registrar https://www.example.com/oauth2callback se o aplicativo da Web for veiculado no domínio https://www.example.com.

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 OAuth 2.0 do Google, uma caixa de diálogo de autorização do Google é mostrada ao usuário solicitando que ele conceda ao aplicativo acesso às tarefas:

Depois de conceder ou negar o acesso, o usuário será redirecionado de volta para o gerenciador de callback de código OAuth 2.0 especificado como um redirecionamento/callback ao criar o URL de autorização do Google:
new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()
O gerenciador de callback de código OAuth 2.0 (OAuthCodeCallbackHandlerServlet) processa o redirecionamento do endpoint OAuth 2.0 do Google. Há dois casos a serem processados:
- O usuário concedeu acesso: analisa a solicitação para receber o código OAuth 2.0 dos parâmetros do URL
- O usuário negou o acesso: mostra uma mensagem ao 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; } }
Trocar o código de autorização por um token de atualização e de acesso
Em seguida, o OAuthCodeCallbackHandlerServlet troca o código Auth 2.0 por tokens de atualização e de acesso, o persistindo no repositório de dados e redirecionando o usuário de volta ao URL PrintTaskListsTitlesServlet:
O código adicionado ao arquivo abaixo tem a sintaxe destacada, e o código já existente está esmaecido.
/** O URL para redirecionar o usuário depois de processar o callback. Considere * salvar isso em um cookie antes de redirecionar os usuários para o URL de autorização * do Google 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. Considere injetá-lo em vez de usar * uma inicialização estática. Também estamos usando uma implementação de memória simples * como um modelo. Mude a implementação para usar seu sistema de banco de dados. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl(); 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";// Cria o URL de solicitação de entrada String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // Troca o código por tokens OAuth AccessTokenResponse accessTokenResponse = exchangeCodeForAccessAndRefreshTokens(code[0], requestUrl); // Recupera o usuário atual // Isso usa o serviço de usuário do App Engine, mas você precisa substituí-lo pela // sua própria implementação de usuário/login UserService userService = UserServiceFactory.getUserService(); String email = userService.getCurrentUser().getEmail(); // Salva os tokens oauthTokenDao.saveKeys(accessTokenResponse, email); resp.sendRedirect(REDIRECT_URL); } 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; }/** * Troca o código fornecido por uma troca e um token de atualização. * * @param code O código recebido do serviço de autorização * @param currentUrl O URL do callback * @param oauthProperties O objeto que contém a configuração do OAuth * @return O objeto que contém um token de acesso e de atualização * @throws IOException */ public AccessTokenResponse exchangeCodeForAccessAndRefreshTokens(String code, String currentUrl) throws IOException { HttpTransport httpTransport = new NetHttpTransport(); JacksonFactory jsonFactory = new JacksonFactory(); // Carrega o arquivo de configuração do OAuth OAuthProperties oauthProperties = new OAuthProperties(); return new GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties .getClientId(), oauthProperties.getClientSecret(), code, currentUrl).execute(); } } /** * 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.javaObservação: a implementação acima usa algumas bibliotecas do App Engine, que são usadas para simplificar. Se você estiver desenvolvendo para outra plataforma, implemente novamente a interface UserService, que processa a autenticação do usuário.
Ler e mostrar as tarefas do usuário
O usuário concedeu ao aplicativo acesso às tarefas. O aplicativo tem um token de atualização salvo no repositório de dados acessível pelo OAuthTokenDao. O servlet PrintTaskListsTitlesServlet agora pode usar esses tokens para acessar e mostrar as tarefas do usuário:
O código adicionado ao arquivo abaixo tem a sintaxe destacada, e o código já existente está esmaecido.
// Mostra os títulos das listas de tarefas do usuário na resposta resp.setContentType("text/plain"); resp.getWriter().append("Títulos de listas de tarefas para o usuário " + user.getEmail() + ":\n\n"); printTasksTitles(accessTokenResponse, resp.getWriter()); 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; }/** * Usa a API Google Tasks para extrair uma lista de tarefas dos usuários na lista * padrão. * * @param accessTokenResponse O objeto AccessTokenResponse do OAuth 2.0 * que contém o token de acesso e um token de atualização. * @param output o gravador de stream de saída em que gravar os títulos das listas de tarefas * @return Uma lista dos títulos das tarefas dos usuários na lista de tarefas padrão. * @throws IOException */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer output) throws IOException { // Initializing the Tasks service HttpTransport transport = 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); // Using the initialized Tasks API service to query the list of tasks lists com.google.api.services.tasks.model.Tasks tasks = service.tasks.list("@default").execute(); for (Task task : tasks.items) { output.append(task.title + "\n"); } } } } /** * 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.javaO usuário vai aparecer com as tarefas:
As tarefas do usuárioExemplo de aplicativo
O código para este aplicativo de exemplo pode ser baixado aqui. Confira.