이 문서에서는 Google Tasks API를 사용하여 사용자의 할 일을 표시하는 샘플 웹 애플리케이션을 통해 Java Servlet을 사용하여 OAuth 2.0 승인 콜백 핸들러를 구현하는 방법을 설명합니다. 샘플 애플리케이션은 먼저 사용자의 Google Tasks에 액세스할 수 있는 권한을 요청한 다음 기본 할 일 목록에 사용자의 할 일을 표시합니다.
잠재고객
이 문서는 Java 및 J2EE 웹 애플리케이션 아키텍처에 익숙한 사용자를 대상으로 합니다. OAuth 2.0 승인 흐름에 대한 지식이 있으면 좋습니다.
목차
완전히 작동하는 샘플을 만들려면 다음 단계를 따라야 합니다.
- web.xml 파일에서 서블릿 매핑 선언
- 시스템의 사용자를 인증하고 Tasks에 액세스할 수 있는 권한을 요청합니다.
- Google 승인 엔드포인트에서 승인 코드 리슨
- 승인 코드를 갱신 및 액세스 토큰으로 교환
- 사용자의 할 일을 읽고 표시
web.xml 파일에서 서블릿 매핑 선언
애플리케이션에서는 다음과 같은 두 가지 servlet을 사용합니다.
- PrintTasksTitlesServlet (/에 매핑됨): 사용자 인증을 처리하고 사용자의 작업을 표시하는 애플리케이션의 진입점입니다.
- OAuthCodeCallbackHandlerServlet (/oauth2callback에 매핑됨): OAuth 승인 엔드포인트의 응답을 처리하는 OAuth 2.0 콜백
다음은 이러한 두 서블릿을 애플리케이션의 URL에 매핑하는 web.xml 파일입니다.
<?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>
시스템의 사용자를 인증하고 작업에 액세스할 수 있는 권한을 요청합니다.
사용자는 PrintTaskListsTitlesServlet 서블릿에 매핑된 루트 '/' URL을 통해 애플리케이션에 진입합니다. 이 servlet에서는 다음 작업이 실행됩니다.
- 사용자가 시스템에서 인증되었는지 확인합니다.
- 사용자가 인증되지 않은 경우 인증 페이지로 리디렉션됩니다.
- 사용자가 인증되면 데이터 저장소에 이미 새로고침 토큰이 있는지 확인합니다. 이는 아래의 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 2.0 사용자 인증 정보가 포함된 oauth.properties 파일입니다. 아래 값은 직접 변경해야 합니다.
# 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 클라이언트 ID 및 클라이언트 비밀번호는 애플리케이션을 식별하고 Tasks API가 애플리케이션에 정의된 필터 및 할당량 규칙을 적용할 수 있도록 허용합니다. 클라이언트 ID와 보안 비밀은 Google API 콘솔에서 확인할 수 있습니다. 콘솔에 있으면 다음을 실행해야 합니다.
- 프로젝트를 만들거나 선택합니다.
- 서비스 목록에서 Tasks API 상태를 사용으로 전환하여 Tasks API를 사용 설정합니다.
- API 액세스에서 아직 OAuth 2.0 클라이언트 ID가 없는 경우 만듭니다.
- 프로젝트의 OAuth 2.0 코드 콜백 핸들러 URL이 리디렉션 URI에 등록/허용 목록에 추가되어 있는지 확인합니다. 예를 들어 이 샘플 프로젝트에서 웹 애플리케이션이 https://www.example.com 도메인에서 제공되는 경우 https://www.example.com/oauth2callback을 등록해야 합니다.

Google 승인 엔드포인트에서 승인 코드 리슨
사용자가 아직 애플리케이션이 할 일 목록에 액세스하도록 승인하지 않았으므로 Google의 OAuth 2.0 승인 엔드포인트로 리디렉션된 경우 사용자에게 애플리케이션이 할 일 목록에 액세스하도록 승인해 달라는 Google의 승인 대화상자가 표시됩니다.

액세스 권한을 부여하거나 거부한 후 사용자는 Google 승인 URL을 구성할 때 리디렉션/콜백으로 지정된 OAuth 2.0 코드 콜백 핸들러로 다시 리디렉션됩니다.
new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()
OAuth 2.0 코드 콜백 핸들러인 OAuthCodeCallbackHandlerServlet은 Google OAuth 2.0 엔드포인트의 리디렉션을 처리합니다. 처리해야 할 두 가지 사례가 있습니다.
- 사용자가 액세스를 승인함: 요청을 파싱하여 URL 매개변수에서 OAuth 2.0 코드를 가져옵니다.
- 사용자가 액세스를 거부함: 사용자에게 메시지를 표시합니다.
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 코드를 갱신 및 액세스 토큰으로 교환하고, 데이터 스토어에 유지한 후 사용자를 PrintTaskListsTitlesServlet 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";/** 콜백을 처리한 후 사용자를 리디렉션할 URL입니다. 리디렉션할 수 있는 URL이 여러 개인 경우 사용자를 Google 승인 URL로 리디렉션하기 전에 이를 쿠키에 저장하는 것이 좋습니다. */ public static final String REDIRECT_URL = "/"; /** OAuth 토큰 DAO 구현입니다. 정적 초기화를 사용하는 대신 삽입하는 것이 좋습니다. 또한 간단한 메모리 구현을 * 모의로 사용합니다. 데이터베이스 시스템을 사용하도록 구현을 변경합니다. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();// 수신 요청 URL 생성 String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // 코드를 OAuth 토큰으로 교환합니다. AccessTokenResponse accessTokenResponse = exchangeCodeForAccessAndRefreshTokens(code[0], requestUrl); // 현재 사용자 가져오기 // App Engine의 사용자 서비스를 사용하지만 이를 자체 사용자/로그인 구현으로 대체해야 합니다. UserService userService = UserServiceFactory.getUserService(); String email = userService.getCurrentUser().getEmail(); // 토큰 저장 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; }/** * 주어진 코드를 교환 및 갱신 토큰으로 교환합니다. * * @param code 승인 서비스에서 다시 가져온 코드입니다. * @param currentUrl 콜백의 URL입니다. * @param oauthProperties OAuth 구성을 포함하는 객체입니다. * @return 액세스 토큰과 새로고침 토큰을 모두 포함하는 객체입니다. * @throws IOException */ public AccessTokenResponse exchangeCodeForAccessAndRefreshTokens(String code, String currentUrl) throws IOException { HttpTransport httpTransport = new NetHttpTransport(); JacksonFactory jsonFactory = new JacksonFactory(); // 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; }OAuthCodeCallbackHandlerServlet.java 파일참고: 위의 구현은 일부 App Engine 라이브러리를 사용합니다. 이는 단순화를 위해 사용됩니다. 다른 플랫폼용으로 개발하는 경우 사용자 인증을 처리하는 UserService 인터페이스를 자유롭게 다시 구현할 수 있습니다.
사용자의 할 일을 읽고 표시
사용자가 애플리케이션에 작업에 대한 액세스 권한을 부여했습니다. 애플리케이션에는 OAuthTokenDao를 통해 액세스할 수 있는 데이터 저장소에 저장된 갱신 토큰이 있습니다. 이제 PrintTaskListsTitlesServlet servlet은 이러한 토큰을 사용하여 사용자의 작업에 액세스하고 표시할 수 있습니다.
아래 파일에 추가된 코드는 구문 강조 표시되어 있고 이미 있는 코드는 비활성화되어 있습니다.
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; }// 응답에 사용자의 할 일 목록 제목을 출력합니다. resp.setContentType("text/plain"); resp.getWriter().append("사용자 '+ user.getEmail() + '의 할 일 목록 제목:\n\n"); printTasksTitles(accessTokenResponse, resp.getWriter());/** * Google Tasks API를 사용하여 기본 할 일 목록에서 사용자의 할 일 목록을 가져옵니다. * * @param accessTokenResponse 액세스 토큰과 갱신 토큰이 포함된 OAuth 2.0 AccessTokenResponse 객체입니다. * @param output 할 일 목록 제목을 작성할 출력 스트림 작성자 * @return 기본 할 일 목록에 있는 사용자의 할 일 제목 목록입니다. * @throws IOException */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer output) throws IOException { // Tasks 서비스를 초기화합니다. 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); // 초기화된 Tasks API 서비스를 사용하여 할 일 목록 목록을 쿼리합니다. 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; }PrintTasksTitlesServlet.java 파일사용자에게 작업이 표시됩니다.
사용자의 할 일샘플 애플리케이션
이 샘플 애플리케이션의 코드는 여기에서 다운로드할 수 있습니다. 언제든지 확인해 보세요.