이 문서에서는 Google Tasks API를 사용하여 사용자의 작업을 표시하는 샘플 웹 애플리케이션을 통해 자바 서블릿을 사용하여 OAuth 2.0 승인 콜백 핸들러를 구현하는 방법을 설명합니다. 샘플 애플리케이션은 먼저 사용자의 Google Tasks에 액세스할 수 있는 권한을 요청한 다음 기본 작업 목록에 사용자의 작업을 표시합니다.
잠재고객
이 문서는 Java 및 J2EE 웹 애플리케이션 아키텍처에 익숙한 사용자를 대상으로 합니다. OAuth 2.0 승인 흐름에 대해 알고 있는 것이 좋습니다.
목차
이처럼 완전히 작동하는 샘플을 여러 단계로 실행하려면 다음과 같이 해야 합니다.
- web.xml 파일에서 서블릿 매핑 선언
- 시스템에서 사용자를 인증하고 Tasks에 액세스할 수 있는 권한을 요청합니다.
- Google 승인 엔드포인트에서 승인 코드 수신 대기
- 승인 코드를 갱신 및 액세스 토큰으로 교환
- 사용자의 작업을 읽고 표시합니다.
web.xml 파일에서 서블릿 매핑 선언
이 애플리케이션에서는 2개의 서블릿을 사용할 것입니다.
- PrintTasksTitlesServlet (/에 매핑됨): 사용자 인증을 처리할 애플리케이션의 진입점으로, 사용자의 작업을 표시합니다.
- OAuthCodeCallbackHandlerServlet (/oauth2callback에 매핑됨): OAuth 승인 엔드포인트의 응답을 처리하는 OAuth 2.0 콜백입니다.
다음은 이러한 2개의 서블릿을 애플리케이션의 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입니다. 해당 서블릿에서 다음 태스크가 수행됩니다.
- 사용자가 시스템에서 인증되었는지 확인합니다.
- 사용자가 인증되지 않은 경우 인증 페이지로 리디렉션됩니다.
- 사용자가 인증되면 Google에서 데이터 저장소에 갱신 토큰이 있는지 확인합니다. 이 토큰은 아래 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 MaptokenPersistance = new HashMap (); public void saveKeys(AccessTokenResponse tokens, String userName) { tokenPersistance.put(userName, tokens); } public AccessTokenResponse getKeys(String userName) { return tokenPersistance.get(userName); } }
또한 애플리케이션의 OAuth 2.0 사용자 인증 정보가 속성 파일에 저장됩니다. 또는 샘플에서 사용되는 OAuthProperties 클래스와 oauth.properties 파일이 여기에 있지만, Java 클래스 중 하나의 어딘가에 해당 속성을 상수로 지정할 수도 있습니다.
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을 등록해야 합니다.
![API 콘솔의 리디렉션 URI](https://developers.google.cn/static/tasks/images/Google_APIs_Console_Redirect_URI.png?hl=ko)
Google 승인 엔드포인트에서 승인 코드를 수신 대기합니다.
사용자가 아직 애플리케이션의 작업 액세스를 승인하지 않아 Google의 OAuth 2.0 승인 엔드포인트로 리디렉션된 경우, 사용자에게 애플리케이션에 대한 작업 액세스 권한을 부여하도록 요청하는 Google의 승인 대화상자가 표시됩니다.
![Google의 승인 대화상자](https://developers.google.cn/static/tasks/images/Google_Auth_Dialog.png?hl=ko)
액세스 권한을 부여하거나 거부하면 사용자는 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은 인증 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입니다. 고려 사항 * Google로 리디렉션하기 전에 쿠키에 저장합니다. * 승인 URL(사용자를 리디렉션할 수 있는 URL이 여러 개 있는 경우) */ public static end String REDIRECT_URL = "/"; /** OAuth 토큰 DAO 구현입니다. 인코더-디코더 아키텍처를 * 정적 초기화입니다. 또한 간단한 메모리 구현을 사용하여 * 를 예시로 참고하세요. 데이터베이스 시스템을 사용하도록 구현을 변경합니다. */ 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; }// 수신 요청 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); }/** * 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; }/** * 지정된 코드를 교환 및 갱신 토큰으로 교환합니다. * * @param code 승인 서비스에서 반환된 코드 * @param currentUrl 콜백의 URL * @param oauthProperties OAuth 구성을 포함하는 객체 * @return 액세스 및 갱신 토큰을 모두 포함하는 객체 * @IOException 발생 */ public AccessTokenResponse exchangeCodeForAccessAndRefreshTokens(String code, String currentUrl) throws IOException { HttpTransport httpTransport = new NetHttpTransport(); JacksonFactory jsonFactory = new JacksonFactory(); // OAuth 구성 파일 로드 OAuthProperties oauthProperties = new OAuthProperties(); 새로운 GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties)를 반환합니다. .getClientId(), oauthProperties.getClientSecret(), code, currentUrl).execute(); } }OAuthCodeCallbackHandlerServlet.java 파일참고: 위의 구현에서는 일부 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; }// 사용자의 작업 인쇄는 응답의 제목 목록 resp.setContentType("text/plain"); resp.getWriter().append("Task List title 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; }/** * Google Tasks API를 사용하여 기본 작업 환경에서 사용자의 할 일 목록을 가져옵니다. * 작업 목록. * * @param accessTokenResponse OAuth 2.0 AccessTokenResponse 객체 * <start> 토큰으로 대체합니다. * @param 출력 * @return 기본 할 일 목록에 있는 사용자의 할 일 제목 목록입니다. * @IOException 발생 */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer output) throws IOException { // Tasks 서비스 초기화 HttpTransportTransport = new NetHttpTransport(); JsonFactory jsonFactory = new JacksonFactory(); OAuthProperties oauthProperties = new OAuthProperties(); GoogleAccessProtectedResource accessProtectedResource = new GoogleAccessProtectedResource( accessTokenResponse.accessToken, transfer, 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"); } } }PrintTasksTitlesServlet.java 파일사용자가 할 일과 함께 표시됩니다.
사용자의 작업샘플 애플리케이션
이 샘플 애플리케이션의 코드는 여기에서 다운로드할 수 있습니다. 자유롭게 확인해 보세요.