В этом документе объясняется, как реализовать обработчик обратного вызова авторизации OAuth 2.0 с помощью сервлетов Java с помощью примера веб-приложения, которое будет отображать задачи пользователя с помощью API задач Google . Пример приложения сначала запросит авторизацию для доступа к задачам Google пользователя, а затем отобразит задачи пользователя в списке задач по умолчанию.
Аудитория
Этот документ предназначен для людей, знакомых с архитектурой веб-приложений Java и J2EE. Рекомендуется иметь некоторые знания о процессе авторизации OAuth 2.0 .
Содержание
Чтобы получить такой полностью рабочий образец, необходимо выполнить несколько шагов:
- Объявите сопоставления сервлетов в файле web.xml.
- Аутентифицируйте пользователей в вашей системе и запросите авторизацию для доступа к ее задачам.
- Прослушайте код авторизации от конечной точки авторизации Google.
- Обменяйте код авторизации на токен обновления и доступа.
- Чтение задач пользователя и отображение их
Объявите сопоставления сервлетов в файле web.xml.
В нашем приложении мы будем использовать два сервлета:
- PrintTasksTitlesServlet (сопоставленный с / ): точка входа приложения, которая будет обрабатывать аутентификацию пользователя и отображать задачи пользователя.
- OAuthCodeCallbackHandlerServlet (сопоставленный с /oauth2callback ): обратный вызов OAuth 2.0, который обрабатывает ответ от конечной точки авторизации OAuth.
Ниже приведен файл web.xml , который сопоставляет эти два сервлета с URL-адресами в нашем приложении:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <servlet> <servlet-name>PrintTasksTitles</servlet-name> <servlet-class>com.google.oauthsample.PrintTasksTitlesServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>PrintTasksTitles</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <servlet> <servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name> <servlet-class>com.google.oauthsample.OAuthCodeCallbackHandlerServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name> <url-pattern>/oauth2callback</url-pattern> </servlet-mapping> </web-app>
Аутентифицируйте пользователей в вашей системе и запросите авторизацию для доступа к ее задачам.
Пользователь входит в приложение через корневой URL-адрес «/», который сопоставлен с сервлетом PrintTaskListsTitlesServlet . В этом сервлете выполняются следующие задачи:
- Проверяет, аутентифицирован ли пользователь в системе.
- Если пользователь не аутентифицирован, он перенаправляется на страницу аутентификации.
- Если пользователь аутентифицирован, мы проверяем, есть ли у нас токен обновления в нашем хранилище данных, который обрабатывается OAuthTokenDao ниже. Если для пользователя нет токена обновления, это означает, что пользователь еще не предоставил приложению авторизацию для доступа к своим задачам. В этом случае пользователь перенаправляется на конечную точку авторизации Google OAuth 2.0.
package com.google.oauthsample; import ... /** * Simple sample Servlet which will display the tasks in the default task list of the user. */ @SuppressWarnings("serial") public class PrintTasksTitlesServlet extends HttpServlet { /** * The OAuth Token DAO implementation, used to persist the OAuth refresh token. * Consider injecting it instead of using a static initialization. Also we are * using a simple memory implementation as a mock. Change the implementation to * using your database system. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl(); public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the current user // This is using App Engine's User Service but you should replace this to // your own user/login implementation UserService userService = UserServiceFactory.getUserService(); User user = userService.getCurrentUser(); // If the user is not logged-in it is redirected to the login service, then back to this page if (user == null) { resp.sendRedirect(userService.createLoginURL(getFullRequestUrl(req))); return; } // Checking if we already have tokens for this user in store AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail()); // If we don't have tokens for this user if (accessTokenResponse == null) { OAuthProperties oauthProperties = new OAuthProperties(); // Redirect to the Google OAuth 2.0 authorization endpoint resp.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()); return; } } /** * Construct the request's URL without the parameter part. * * @param req the HttpRequest object * @return The constructed request's URL */ public static String getFullRequestUrl(HttpServletRequest req) { String scheme = req.getScheme() + "://"; String serverName = req.getServerName(); String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort(); String contextPath = req.getContextPath(); String servletPath = req.getServletPath(); String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo(); String queryString = (req.getQueryString() == null) ? "" : "?" + req.getQueryString(); return scheme + serverName + serverPort + contextPath + servletPath + pathInfo + queryString; } }
Примечание. В приведенной выше реализации используются некоторые библиотеки App Engine, они используются для упрощения. Если вы разрабатываете приложение для другой платформы, не стесняйтесь повторно реализовать интерфейс UserService , который обрабатывает аутентификацию пользователей.
Приложение использует DAO для сохранения и доступа к токенам авторизации пользователя. Ниже приведен интерфейс OAuthTokenDao и макет реализации (в памяти) OAuthTokenDaoMemoryImpl , которые используются в этом примере:
package com.google.oauthsample; import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse; /** * Allows easy storage and access of authorization tokens. */ public interface OAuthTokenDao { /** * Stores the given AccessTokenResponse using the {@code username}, the OAuth * {@code clientID} and the tokens scopes as keys. * * @param tokens The AccessTokenResponse to store * @param userName The userName associated wit the token */ public void saveKeys(AccessTokenResponse tokens, String userName); /** * Returns the AccessTokenResponse stored for the given username, clientId and * scopes. Returns {@code null} if there is no AccessTokenResponse for this * user and scopes. * * @param userName The username of which to get the stored AccessTokenResponse * @return The AccessTokenResponse of the given username */ public AccessTokenResponse getKeys(String userName); }
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); } }
Кроме того, учетные данные OAuth 2.0 для приложения хранятся в файле свойств. В качестве альтернативы вы можете просто разместить их как константу где-нибудь в одном из ваших классов Java, хотя вот класс OAuthProperties и файл oauth.properties , который используется в примере:
package com.google.oauthsample; import ... /** * Object representation of an OAuth properties file. */ public class OAuthProperties { public static final String DEFAULT_OAUTH_PROPERTIES_FILE_NAME = "oauth.properties"; /** The OAuth 2.0 Client ID */ private String clientId; /** The OAuth 2.0 Client Secret */ private String clientSecret; /** The Google APIs scopes to access */ private String scopes; /** * Instantiates a new OauthProperties object reading its values from the * {@code OAUTH_PROPERTIES_FILE_NAME} properties file. * * @throws IOException IF there is an issue reading the {@code propertiesFile} * @throws OauthPropertiesFormatException If the given {@code propertiesFile} * is not of the right format (does not contains the keys {@code * clientId}, {@code clientSecret} and {@code scopes}) */ public OAuthProperties() throws IOException { this(OAuthProperties.class.getResourceAsStream(DEFAULT_OAUTH_PROPERTIES_FILE_NAME)); } /** * Instantiates a new OauthProperties object reading its values from the given * properties file. * * @param propertiesFile the InputStream to read an OAuth Properties file. The * file should contain the keys {@code clientId}, {@code * clientSecret} and {@code scopes} * @throws IOException IF there is an issue reading the {@code propertiesFile} * @throws OAuthPropertiesFormatException If the given {@code propertiesFile} * is not of the right format (does not contains the keys {@code * clientId}, {@code clientSecret} and {@code scopes}) */ public OAuthProperties(InputStream propertiesFile) throws IOException { Properties oauthProperties = new Properties(); oauthProperties.load(propertiesFile); clientId = oauthProperties.getProperty("clientId"); clientSecret = oauthProperties.getProperty("clientSecret"); scopes = oauthProperties.getProperty("scopes"); if ((clientId == null) || (clientSecret == null) || (scopes == null)) { throw new OAuthPropertiesFormatException(); } } /** * @return the clientId */ public String getClientId() { return clientId; } /** * @return the clientSecret */ public String getClientSecret() { return clientSecret; } /** * @return the scopes */ public String getScopesAsString() { return scopes; } /** * Thrown when the OAuth properties file was not at the right format, i.e not * having the right properties names. */ @SuppressWarnings("serial") public class OAuthPropertiesFormatException extends RuntimeException { } }
Ниже приведен файл oauth.properties , содержащий учетные данные OAuth 2.0 вашего приложения. Вам необходимо изменить значения ниже самостоятельно.
# Client ID and secret. They can be found in the APIs console. clientId=1234567890.apps.googleusercontent.com clientSecret=aBcDeFgHiJkLmNoPqRsTuVwXyZ # API scopes. Space separated. scopes=https://www.googleapis.com/auth/tasks
Идентификатор клиента OAuth 2.0 и секрет клиента идентифицируют ваше приложение и позволяют API задач применять фильтры и правила квот, определенные для вашего приложения. Идентификатор и секрет клиента можно найти в консоли Google API . Оказавшись на консоли, вам придется:
- Создайте или выберите проект.
- Включите API задач, переключив статус API задач на ВКЛ в списке служб.
- В разделе «Доступ к API» создайте идентификатор клиента OAuth 2.0, если он еще не создан.
- Убедитесь, что URL-адрес обработчика обратного вызова кода OAuth 2.0 проекта зарегистрирован или внесен в белый список в URI перенаправления . Например, в этом примере проекта вам придется зарегистрировать https://www.example.com/oauth2callback , если ваше веб-приложение обслуживается из домена https://www.example.com .
Прослушайте код авторизации от конечной точки авторизации Google.
В случае, если пользователь еще не разрешил приложению доступ к его задачам и поэтому был перенаправлен на конечную точку авторизации Google OAuth 2.0, пользователю отображается диалоговое окно авторизации от Google с просьбой предоставить вашему приложению доступ к его задачам:
После предоставления или отказа в доступе пользователь будет перенаправлен обратно к обработчику обратного вызова кода OAuth 2.0, который был указан как перенаправление/обратный вызов при создании URL-адреса авторизации Google:
new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()
Обработчик обратного вызова кода OAuth 2.0 — OAuthCodeCallbackHandlerServlet — обрабатывает перенаправление из конечной точки Google OAuth 2.0. Есть 2 случая, которые нужно обработать:
- Пользователь предоставил доступ: анализирует запрос, чтобы получить код OAuth 2.0 из параметров URL.
- Пользователь запретил доступ: показывает сообщение пользователю.
package com.google.oauthsample; import ... /** * Servlet handling the OAuth callback from the authentication service. We are * retrieving the OAuth code, then exchanging it for a refresh and an access * token and saving it. */ @SuppressWarnings("serial") public class OAuthCodeCallbackHandlerServlet extends HttpServlet { /** The name of the Oauth code URL parameter */ public static final String CODE_URL_PARAM_NAME = "code"; /** The name of the OAuth error URL parameter */ public static final String ERROR_URL_PARAM_NAME = "error"; /** The URL suffix of the servlet */ public static final String URL_MAPPING = "/oauth2callback"; public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the "error" URL parameter String[] error = req.getParameterValues(ERROR_URL_PARAM_NAME); // Checking if there was an error such as the user denied access if (error != null && error.length > 0) { resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "There was an error: \""+error[0]+"\"."); return; } // Getting the "code" URL parameter String[] code = req.getParameterValues(CODE_URL_PARAM_NAME); // Checking conditions on the "code" URL parameter if (code == null || code.length == 0) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "The \"code\" URL parameter is missing"); return; } } /** * Construct the OAuth code callback handler URL. * * @param req the HttpRequest object * @return The constructed request's URL */ public static String getOAuthCodeCallbackHandlerUrl(HttpServletRequest req) { String scheme = req.getScheme() + "://"; String serverName = req.getServerName(); String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort(); String contextPath = req.getContextPath(); String servletPath = URL_MAPPING; String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo(); return scheme + serverName + serverPort + contextPath + servletPath + pathInfo; } }
Обменяйте код авторизации на токен обновления и доступа.
Затем OAuthCodeCallbackHandlerServlet обменивает код Auth 2.0 на токены обновления и доступа, сохраняет его в хранилище данных и перенаправляет пользователя обратно на URL-адрес PrintTaskListsTitlesServlet :
Код, добавленный в файл ниже, выделен синтаксисом, а уже существующий код выделен серым цветом.
package com.google.oauthsample; import ... /** * Servlet handling the OAuth callback from the authentication service. We are * retrieving the OAuth code, then exchanging it for a refresh and an access * token and saving it. */ @SuppressWarnings("serial") public class OAuthCodeCallbackHandlerServlet extends HttpServlet { /** The name of the Oauth code URL parameter */ public static final String CODE_URL_PARAM_NAME = "code"; /** The name of the OAuth error URL parameter */ public static final String ERROR_URL_PARAM_NAME = "error"; /** The URL suffix of the servlet */ public static final String URL_MAPPING = "/oauth2callback";
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; }
Примечание. В приведенной выше реализации используются некоторые библиотеки App Engine, они используются для упрощения. Если вы разрабатываете приложение для другой платформы, не стесняйтесь повторно реализовать интерфейс UserService , который обрабатывает аутентификацию пользователей.
Чтение задач пользователя и отображение их
Пользователь предоставил приложению доступ к его задачам. Приложение имеет токен обновления, который сохраняется в хранилище данных, доступном через OAuthTokenDao . Сервлет PrintTaskListsTitlesServlet теперь может использовать эти токены для доступа к задачам пользователя и их отображения:
Код, добавленный в файл ниже, выделен синтаксисом, а уже существующий код выделен серым цветом.
package com.google.oauthsample; import ... /** * Simple sample Servlet which will display the tasks in the default task list of the user. */ @SuppressWarnings("serial") public class PrintTasksTitlesServlet extends HttpServlet { /** * The OAuth Token DAO implementation, used to persist the OAuth refresh token. * Consider injecting it instead of using a static initialization. Also we are * using a simple memory implementation as a mock. Change the implementation to * using your database system. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl(); public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the current user // This is using App Engine's User Service but you should replace this to // your own user/login implementation UserService userService = UserServiceFactory.getUserService(); User user = userService.getCurrentUser(); // If the user is not logged-in it is redirected to the login service, then back to this page if (user == null) { resp.sendRedirect(userService.createLoginURL(getFullRequestUrl(req))); return; } // Checking if we already have tokens for this user in store AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail()); // If we don't have tokens for this user if (accessTokenResponse == null) { OAuthProperties oauthProperties = new OAuthProperties(); // Redirect to the Google OAuth 2.0 authorization endpoint resp.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()); return; }
} /** * 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; }
Отобразится пользователь со своими задачами:
Образец заявления
Код этого примера приложения можно скачать здесь . Не стесняйтесь проверить это.