빌드 승인 콜백 핸들러

이 문서에서는 Google Tasks API를 사용하여 사용자의 작업을 표시하는 샘플 웹 애플리케이션을 통해 Java 서블릿을 사용하여 OAuth 2.0 승인 콜백 핸들러를 구현하는 방법을 설명합니다. 샘플 애플리케이션은 먼저 사용자의 Google Tasks에 액세스할 수 있는 권한을 요청한 다음 기본 할 일 목록에 사용자의 할 일을 표시합니다.

대상

이 문서는 Java 및 J2EE 웹 애플리케이션 아키텍처에 익숙한 사용자를 대상으로 합니다. OAuth 2.0 승인 흐름에 관해 어느 정도 알고 있는 것이 좋습니다.

목차

완벽하게 작동하는 샘플을 만들려면 다음과 같은 여러 단계가 필요합니다.

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>
/WEB-INF/web.xml 파일

시스템의 사용자를 인증하고 작업에 액세스할 수 있는 권한을 요청합니다.

사용자는 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;
  }
}
PrintTasksTitlesServlet.java 파일

참고: 위 구현은 단순화를 위해 일부 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);
}
OAuthTokenDao.java 파일
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 tokenPersistance = new HashMap();

  public void saveKeys(AccessTokenResponse tokens, String userName) {
    tokenPersistance.put(userName, tokens);
  }

  public AccessTokenResponse getKeys(String userName) {
    return tokenPersistance.get(userName);
  }
}
OAuthTokenDaoMemoryImpl.java 파일

또한 애플리케이션의 OAuth 2.0 사용자 인증 정보가 속성 파일에 저장됩니다. 또는 샘플에서 사용되는 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 {
  }
}
OAuthProperties.java 파일

다음은 애플리케이션의 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.properties 파일

OAuth 2.0 클라이언트 ID와 클라이언트 비밀번호를 사용하여 애플리케이션을 식별하고 Tasks API가 애플리케이션에 정의된 필터 및 할당량 규칙을 적용할 수 있도록 합니다. 클라이언트 ID와 비밀번호는 Google API 콘솔에서 확인할 수 있습니다. 콘솔에서 다음 작업을 수행해야 합니다.

  • 프로젝트를 만들거나 선택합니다.
  • 서비스 목록에서 Tasks API 상태를 사용으로 전환하여 Tasks API를 사용 설정합니다.
  • OAuth 2.0 클라이언트 ID가 아직 생성되지 않았다면 API 액세스에서 만듭니다.
  • 프로젝트의 OAuth 2.0 코드 콜백 핸들러 URL이 리디렉션 URI에 등록/허용 목록에 포함되어 있는지 확인합니다. 예를 들어 이 샘플 프로젝트에서는 웹 애플리케이션이 https://www.example.com 도메인에서 제공되는 경우 https://www.example.com/oauth2callback을 등록해야 합니다.

API 콘솔의 리디렉션 URI
API 콘솔의 리디렉션 URI

Google 승인 엔드포인트에서 승인 코드 리슨

사용자가 애플리케이션에 해당 작업에 액세스하도록 애플리케이션을 승인하지 않아서 Google의 OAuth 2.0 승인 엔드포인트로 리디렉션된 경우 사용자에게 작업에 대한 애플리케이션 액세스 권한을 부여하도록 요청하는 Google의 승인 대화상자가 사용자에게 표시됩니다.

Google의 승인 대화상자
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.java 파일

승인 코드를 갱신 및 액세스 토큰으로 교환

그런 다음 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 Legacy 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); 사용자 정보를 저장합니다.
  /**
   * 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 oauthProperties 콜백의 URL * @param oauthProperties 객체입니다. * @return 액세스 및 갱신 토큰을 모두 포함하는 개체입니다. * @throws IOException */ public AccessTokenResponse exchangeCodeForAccessAndRefreshToken Grant oauth. String currentProperties() json Transport oauthClientResponse exchangeCodeForAccessAndRefreshToken Grant oauth. String currentProperties json IOException {
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 lists 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;
  }
/** * API에서 * Google Tasks의 작업 목록을 가져옵니다. * Google Tasks의 목록을 사용합니다. * * @param accessTokenResponse: * 액세스 토큰과 갱신 토큰을 포함하는 OAuth 2.0 AccessTokenResponse 객체입니다. * @param은 작업 목록을 검색할 출력 스트림 작성기를 출력합니다. * @return 기본 작업 목록에 있는 사용자의 작업 제목 목록입니다.
PrintTasksTitlesServlet.java 파일

사용자가 할 일과 함께 표시됩니다.

사용자의 작업
사용자의 작업

샘플 애플리케이션

이 샘플 애플리케이션의 코드는 여기에서 다운로드할 수 있습니다. 자유롭게 확인해 보세요.