W tym dokumencie wyjaśniamy, jak zaimplementować procedurę obsługi wywołania zwrotnego autoryzacji OAuth 2.0 za pomocą servletów Java w przykładowej aplikacji internetowej, która wyświetla zadania użytkownika przy użyciu Google Tasks API. Aplikacja przykładowa najpierw prosi o autoryzację dostępu do Zadań Google użytkownika, a następnie wyświetla zadania użytkownika na domyślnej liście zadań.
Odbiorcy
Ten dokument jest przeznaczony dla osób, które znają język Java i architekturę aplikacji internetowych J2EE. Zalecana jest pewna wiedza na temat przepływu autoryzacji OAuth 2.0.
Spis treści
Aby uzyskać w pełni działający przykład, musisz wykonać kilka czynności:
- Deklarowanie mapowań serwletów w pliku web.xml
- Uwierzytelnianie użytkowników w ich systemie i wysyłanie prośby o autoryzację dostępu do ich listy zadań
- Odsłuchaj kod autoryzacji z punktu końcowego autoryzacji Google
- Wymiana kodu autoryzacji na token odświeżania i token dostępu
- Odczytywanie i wyświetlanie zadań użytkownika
Deklarowanie mapowań serwletów w pliku web.xml
Ta aplikacja używa tych 2 serwletów:
- PrintTasksTitlesServlet (mapowany na
/): punkt wejścia aplikacji, która będzie obsługiwać uwierzytelnianie użytkowników i wyświetlać ich zadania. - OAuthCodeCallbackHandlerServlet (mapowany na
/oauth2callback): wywołanie zwrotne OAuth 2.0, które obsługuje odpowiedź z punktu końcowego autoryzacji OAuth.
Ten plik web.xml mapuje te 2 serwlety na adresy URL w naszej aplikacji:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<servlet>
<servlet-name>PrintTasksTitles</servlet-name>
<servlet-class>com.google.oauthsample.PrintTasksTitlesServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PrintTasksTitles</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name>
<servlet-class>com.google.oauthsample.OAuthCodeCallbackHandlerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name>
<url-pattern>/oauth2callback</url-pattern>
</servlet-mapping>
</web-app>Uwierzytelnianie użytkowników w ich systemie i wysyłanie próśb o autoryzację dostępu do ich zadań
Użytkownik wchodzi do aplikacji przez główny adres URL „/”, który jest mapowany na servlet PrintTaskListsTitlesServlet. W tym serwlecie są wykonywane te zadania:
- Sprawdza, czy użytkownik jest uwierzytelniony w systemie.
- Jeśli użytkownik nie jest uwierzytelniony, zostanie przekierowany na stronę uwierzytelniania.
- Jeśli użytkownik jest uwierzytelniony, sprawdzane jest, czy w pamięci danych jest już token odświeżania. Zajmuje się tym funkcja
OAuthTokenDao. Jeśli tokeny nie są dostępne w pamięci użytkownika, oznacza to, że nie przyznał on jeszcze aplikacji autoryzacji dostępu do swoich zadań. Użytkownik jest następnie przekierowywany do punktu końcowego autoryzacji OAuth 2.0 w Google.
Poniżej znajdziesz sposób na wdrożenie tej funkcji:
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. Additionally, a * simple memory implementation is used as a mock. Change the implementation to * using the user's own user/login implementation. */ 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 tokens are not available for this user if (accessTokenResponse == null) { OAuthProperties oauthProperties = new OAuthProperties(); // Redirects 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; } }
Uwaga: powyższa implementacja korzysta z niektórych bibliotek App Engine. Są one używane w celu uproszczenia. Jeśli tworzysz aplikację na inną platformę, ponownie zaimplementuj interfejs UserService, który obsługuje uwierzytelnianie użytkownika.
Aplikacja używa obiektu DAO do przechowywania tokenów autoryzacji użytkownika i uzyskiwania do nich dostępu.
Interfejs OAuthTokenDao i przykładowa (w pamięci) implementacja – OAuthTokenDaoMemoryImpl – użyte w tym przykładzie są pokazane w tych przykładach:
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); } }
Dane uwierzytelniające OAuth 2.0 aplikacji są przechowywane w pliku właściwości.
Możesz też przechowywać je jako stałą w jednej z klas Java.
Oto klasa OAuthProperties i plik oauth.properties używane w przykładzie:
package com.google.oauthsample; import ... /** * Object representation of an OAuth properties file. */ public class OAuthProperties { public static final String DEFAULT_OAUTH_PROPERTIES_FILE_NAME = "oauth.properties"; /** The OAuth 2.0 Client ID */ private String clientId; /** The OAuth 2.0 Client Secret */ private String clientSecret; /** The Google APIs scopes to access */ private String scopes; /** * Instantiates a new OauthProperties object reading its values from the * {@code OAUTH_PROPERTIES_FILE_NAME} properties file. * * @throws IOException IF there is an issue reading the {@code propertiesFile} * @throws OauthPropertiesFormatException If the given {@code propertiesFile} * is not of the right format (does not contains the keys {@code * clientId}, {@code clientSecret} and {@code scopes}) */ public OAuthProperties() throws IOException { this(OAuthProperties.class.getResourceAsStream(DEFAULT_OAUTH_PROPERTIES_FILE_NAME)); } /** * Instantiates a new OAuthProperties object, reading its values from the given * properties file. * * @param propertiesFile the InputStream to read an OAuth Properties file. The * file should contain the keys {@code clientId}, {@code * clientSecret} and {@code scopes} * @throws IOException if there is an issue reading the {@code propertiesFile} * @throws OAuthPropertiesFormatException If the given {@code propertiesFile} * is not in the correct format (does not contain 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 { } }
W tym przykładzie pokazujemy plik oauth.properties, który zawiera dane logowania OAuth 2.0 dla Twojej aplikacji.
Musisz zmienić wartości w tym pliku.
# 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
Identyfikator klienta OAuth 2.0 i tajny klucz klienta identyfikują aplikację i umożliwiają interfejsowi Tasks API stosowanie filtrów i reguł limitów zdefiniowanych dla aplikacji. Identyfikator klienta i klucz tajny znajdziesz w Konsoli interfejsów API Google. Po otwarciu konsoli użytkownik musi:
- Utwórz lub wybierz projekt.
- Włącz interfejs Tasks API, ustawiając jego stan na WŁĄCZONY na liście usług.
- W sekcji Dostęp do interfejsu API utwórz identyfikator klienta OAuth 2.0, jeśli nie został jeszcze utworzony.
- Upewnij się, że adres URL modułu obsługi wywołania zwrotnego kodu OAuth 2.0 projektu jest zarejestrowany lub znajduje się na liście dozwolonych w sekcji Identyfikatory URI przekierowania. Na przykład w tym przykładowym projekcie użytkownik musi zarejestrować
https://www.example.com/oauth2callback, jeśli aplikacja internetowa jest obsługiwana z domenyhttps://www.example.com.
Obsługa kodu autoryzacji z punktu końcowego autoryzacji Google
Jeśli użytkownik nie autoryzował jeszcze aplikacji do uzyskiwania dostępu do jego zadań i został przekierowany do punktu końcowego autoryzacji OAuth 2.0 Google, wyświetli mu się okno autoryzacji Google z prośbą o przyznanie aplikacji dostępu do jego zadań:
Po przyznaniu lub odmowie dostępu użytkownik jest przekierowywany z powrotem do obsługi wywołania zwrotnego kodu OAuth 2.0, która została określona jako przekierowanie/wywołanie zwrotne podczas tworzenia adresu URL autoryzacji Google:
new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
.getScopesAsString()).build()Obsługa wywołania zwrotnego kodu OAuth 2.0 – OAuthCodeCallbackHandlerServlet – obsługuje przekierowanie z punktu końcowego OAuth 2.0 Google. Należy rozpatrzyć 2 przypadki:
- Użytkownik przyznał dostęp: żądanie jest analizowane w celu uzyskania kodu OAuth 2.0 z parametrów adresu URL.
- Użytkownik odmówił dostępu: wyświetla się komunikat.
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; } }
Wymiana kodu autoryzacji na token odświeżania i token dostępu
Następnie OAuthCodeCallbackHandlerServlet wymienia kod Auth 2.0 na tokeny odświeżania i dostępu, zapisuje go w pamięci danych i przekierowuje użytkownika z powrotem na adres URL PrintTaskListsTitlesServlet:
Kod dodany do pliku jest wyróżniony.
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"; /** The URL to redirect the user to after handling the callback. Consider * saving this in a cookie before redirecting users to the Google * authorization URL if you have multiple possible URL to redirect people to. */ public static final String REDIRECT_URL = "/"; /** The OAuth Token DAO implementation. 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 "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 incoming request URL String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // Exchange the code for OAuth tokens AccessTokenResponse accessTokenResponse = exchangeCodeForAccessAndRefreshTokens(code[0], requestUrl); // Getting the current user // This is using App Engine's User Service, but the user should replace this // with their own user/login implementation UserService userService = UserServiceFactory.getUserService(); String email = userService.getCurrentUser().getEmail(); // Save the 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; } /** * Exchanges the given code for an exchange and a refresh token. * * @param code The code gotten back from the authorization service * @param currentUrl The URL of the callback * @param oauthProperties The object containing the OAuth configuration * @return The object containing both an access and refresh token * @throws IOException */ public AccessTokenResponse exchangeCodeForAccessAndRefreshTokens(String code, String currentUrl) throws IOException { HttpTransport httpTransport = new NetHttpTransport(); JacksonFactory jsonFactory = new JacksonFactory(); // Loading the oauth config file OAuthProperties oauthProperties = new OAuthProperties(); return new GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties .getClientId(), oauthProperties.getClientSecret(), code, currentUrl).execute(); } }
Uwaga: powyższa implementacja korzysta z niektórych bibliotek App Engine, które zostały użyte w celu uproszczenia kodu. Jeśli tworzysz aplikację na inną platformę, ponownie zaimplementuj interfejs UserService, który obsługuje uwierzytelnianie użytkownika.
Odczytywanie i wyświetlanie zadań użytkownika
Użytkownik przyznał aplikacji dostęp do swoich zadań. Aplikacja ma token odświeżania
zapisany w pamięci danych dostępnej za pomocą OAuthTokenDao. Serwlet PrintTaskListsTitlesServlet może teraz używać tych tokenów, aby uzyskać dostęp do zadań użytkownika i je wyświetlić:
Kod dodany do pliku jest wyróżniony.
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. Additionally, a * simple memory implementation is used as a mock. Change the implementation to * use your own 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; } // Prints the user's task list titles in the response resp.setContentType("text/plain"); resp.getWriter().append("Task Lists titles for user " + 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; } /** * Uses the Google Tasks API to retrieve a list of the user's tasks in the default * tasks list. * * @param accessTokenResponse The OAuth 2.0 AccessTokenResponse object * containing the access token and a refresh token. * @param output The output stream writer to write the task list titles to. * @return A list of the user's task titles in the default task list. * @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"); } } }
Wyświetlą się zadania użytkownika:
Przykładowa aplikacja
Możesz pobrać kod tej przykładowej aplikacji.