Java용 Google API 클라이언트 라이브러리에서 OAuth 2.0 사용

개요

용도: 이 문서에서는 GoogleCredential 유틸리티 클래스를 사용하여 Google 서비스에서 OAuth 2.0 승인을 실행하는 방법을 설명합니다. Google에서 제공하는 일반 OAuth 2.0 기능에 대한 자세한 내용은 OAuth 2.0 및 Java용 Google OAuth 클라이언트 라이브러리를 참조하세요.

요약: Google 서비스에 저장된 보호된 데이터에 액세스하려면 승인에 OAuth 2.0을 사용합니다. Google API는 다양한 유형의 클라이언트 애플리케이션에 대해 OAuth 2.0 흐름을 지원합니다. 이러한 모든 흐름에서 클라이언트 애플리케이션은 클라이언트 애플리케이션 및 액세스되는 보호된 데이터의 소유자와만 연결된 액세스 토큰을 요청합니다. 액세스 토큰은 클라이언트 애플리케이션에서 액세스할 수 있는 데이터의 종류를 정의하는 제한된 범위 (예: '작업 관리')와도 연결됩니다. OAuth 2.0의 중요한 목표는 액세스 토큰을 도난당할 경우 발생할 수 있는 영향을 최소화하면서 보호된 데이터에 안전하고 편리한 액세스를 제공하는 것입니다.

Java용 Google API 클라이언트 라이브러리의 OAuth 2.0 패키지는 범용 Java용 Google OAuth 2.0 클라이언트 라이브러리를 기반으로 빌드되었습니다.

자세한 내용은 다음 패키지에 대한 Javadoc 문서를 참고하세요.

Google API 콘솔

Google API에 액세스하려면 먼저 클라이언트가 설치된 애플리케이션, 모바일 애플리케이션, 웹 서버 또는 브라우저에서 실행되는 클라이언트 중 어느 것이든 Google API 콘솔에서 인증 및 결제 목적으로 프로젝트를 설정해야 합니다.

사용자 인증 정보를 올바르게 설정하는 방법은 API 콘솔 도움말을 참고하세요.

사용자 인증 정보

GoogleCredential

GoogleCredential은 액세스 토큰을 사용해 보호된 리소스에 액세스하기 위한 OAuth 2.0용 스레드 안전 도우미 클래스입니다. 예를 들어 이미 액세스 토큰이 있다면 다음과 같은 방법으로 요청할 수 있습니다.

GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
Plus plus = new Plus.builder(new NetHttpTransport(),
                             GsonFactory.getDefaultInstance(),
                             credential)
    .setApplicationName("Google-PlusSample/1.0")
    .build();

Google App Engine ID

이 대체 사용자 인증 정보는 Google App Engine App Identity Java API를 기반으로 합니다. 클라이언트 애플리케이션이 최종 사용자 데이터에 대한 액세스를 요청하는 사용자 인증 정보와 달리 App Identity API는 클라이언트 애플리케이션의 자체 데이터에 대한 액세스를 제공합니다.

(google-api-client-appengine에서) AppIdentityCredential을 사용합니다. 이 사용자 인증 정보는 Google App Engine이 모든 세부정보를 처리하므로 훨씬 간단합니다. 필요한 OAuth 2.0 범위만 지정하면 됩니다.

urlshortener-robots-appengine-sample에서 가져온 예시 코드:

static Urlshortener newUrlshortener() {
  AppIdentityCredential credential =
      new AppIdentityCredential(
          Collections.singletonList(UrlshortenerScopes.URLSHORTENER));
  return new Urlshortener.Builder(new UrlFetchTransport(),
                                  GsonFactory.getDefaultInstance(),
                                  credential)
      .build();
}

데이터 저장소

액세스 토큰의 만료일은 일반적으로 1시간이며, 그 이후에 사용하려고 하면 오류가 발생합니다. GoogleCredential은 토큰을 자동으로 '새로고침'합니다. 즉, 새 액세스 토큰을 가져오는 것뿐입니다. 이 작업은 일반적으로 승인 코드 흐름 중에 access_type=offline 매개변수를 사용하는 경우 액세스 토큰과 함께 수명이 긴 갱신 토큰을 통해 실행됩니다 (GoogleAuthorizationCodeFlow.Builder.setAccessType(String) 참고).

대부분의 애플리케이션은 사용자 인증 정보의 액세스 토큰 또는 갱신 토큰을 유지해야 합니다. 사용자 인증 정보의 액세스 또는 갱신 토큰을 유지하려면 StoredCredential과 함께 자체 DataStoreFactory 구현을 제공하거나 라이브러리에서 제공하는 다음 구현 중 하나를 사용하면 됩니다.

  • AppEngineDataStoreFactory: Google App Engine Data Store API를 사용하여 사용자 인증 정보를 유지합니다.
  • MemoryDataStoreFactory: 메모리에 사용자 인증 정보를 '유지'하며, 프로세스의 전체 기간 동안 단기 저장소로만 유용합니다.
  • FileDataStoreFactory: 파일에 사용자 인증 정보를 유지합니다.

AppEngine 사용자: AppEngineCredentialStore는 지원 중단되었으며 곧 삭제됩니다. StoredCredential과 함께 AppEngineDataStoreFactory를 사용하는 것이 좋습니다. 사용자 인증 정보가 이전 방식으로 저장된 경우 추가된 도우미 메서드인 migrateTo(AppEngineDataStoreFactory) 또는 migrateTo(DataStore)를 사용하여 이전할 수 있습니다.

DataStoreCredentialRefreshListener를 사용하고 GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener)를 사용하여 사용자 인증 정보로 설정할 수 있습니다.

승인 코드 흐름

승인 코드 흐름을 사용하여 최종 사용자가 애플리케이션에 Google API의 보호된 데이터에 대한 액세스 권한을 부여할 수 있도록 합니다. 이 흐름의 프로토콜은 승인 코드 부여에 지정되어 있습니다.

이 흐름은 GoogleAuthorizationCodeFlow를 사용하여 구현됩니다. 단계는 다음과 같습니다.

  • 최종 사용자가 애플리케이션에 로그인합니다. 이 사용자를 애플리케이션별로 고유한 사용자 ID와 연결해야 합니다.
  • 사용자 ID에 따라 AuthorizationCodeFlow.loadCredential(String)을 호출하여 최종 사용자의 사용자 인증 정보가 이미 알려져 있는지 확인합니다. 그렇다면 완료된 것입니다.
  • 그렇지 않은 경우 AuthorizationCodeFlow.newAuthorizationUrl()을 호출하고 최종 사용자의 브라우저를 승인 페이지로 안내하여 애플리케이션이 보호된 데이터에 액세스할 수 있도록 합니다.
  • 그러면 Google 승인 서버가 code 쿼리 매개변수와 함께 브라우저를 애플리케이션에서 지정한 리디렉션 URL로 다시 리디렉션합니다. code 매개변수를 사용하여 AuthorizationCodeFlow.newTokenRequest(String)을 사용하여 액세스 토큰을 요청합니다.
  • AuthorizationCodeFlow.createAndStoreCredential(TokenResponse, String)을 사용하여 보호된 리소스에 액세스하기 위한 사용자 인증 정보를 저장하고 가져옵니다.

또는 GoogleAuthorizationCodeFlow를 사용하지 않는 경우, 하위 수준의 클래스를 사용할 수도 있습니다.

Google API 콘솔에서 프로젝트를 설정할 때는 사용 중인 흐름에 따라 다양한 사용자 인증 정보 중에서 선택합니다. 자세한 내용은 OAuth 2.0 설정OAuth 2.0 시나리오를 참고하세요. 각 흐름의 코드 스니펫은 아래와 같습니다.

웹 서버 애플리케이션

이 흐름의 프로토콜은 웹 서버 애플리케이션용 OAuth 2.0 사용에 설명되어 있습니다.

이 라이브러리는 서블릿 도우미 클래스를 제공하여 기본 사용 사례의 승인 코드 흐름을 크게 단순화합니다. AbstractAuthorizationCodeServletAbstractAuthorizationCodeCallbackServlet(google-oauth-client-servlet에서)의 구체적인 서브클래스를 제공하고 이를 web.xml 파일에 추가하면 됩니다. 여전히 웹 애플리케이션의 사용자 로그인을 처리하고 사용자 ID를 추출해야 합니다.

public class CalendarServletSample extends AbstractAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new GoogleAuthorizationCodeFlow.Builder(
        new NetHttpTransport(), GsonFactory.getDefaultInstance(),
        "[[ENTER YOUR CLIENT ID]]", "[[ENTER YOUR CLIENT SECRET]]",
        Collections.singleton(CalendarScopes.CALENDAR)).setDataStoreFactory(
        DATA_STORE_FACTORY).setAccessType("offline").build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

public class CalendarServletCallbackSample extends AbstractAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new GoogleAuthorizationCodeFlow.Builder(
        new NetHttpTransport(), GsonFactory.getDefaultInstance()
        "[[ENTER YOUR CLIENT ID]]", "[[ENTER YOUR CLIENT SECRET]]",
        Collections.singleton(CalendarScopes.CALENDAR)).setDataStoreFactory(
        DATA_STORE_FACTORY).setAccessType("offline").build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

Google App Engine 애플리케이션

App Engine의 승인 코드 흐름은 Google App Engine의 Users Java API를 활용할 수 있다는 점을 제외하면 서블릿 승인 코드 흐름과 거의 동일합니다. Users Java API를 사용 설정하려면 사용자가 로그인해야 합니다. 아직 로그인하지 않은 사용자를 로그인 페이지로 리디렉션하는 방법에 대한 자세한 내용은 보안 및 인증(web.xml)을 참조하세요.

서블릿 사례와 다른 점은 AbstractAppEngineAuthorizationCodeServletAbstractAppEngineAuthorizationCodeCallbackServlet(google-oauth-client-appengine)의 구체적인 서브클래스를 제공한다는 점입니다. 추상 서블릿 클래스를 확장하고 Users Java API를 사용하여 getUserId 메서드를 구현합니다. AppEngineDataStoreFactory(google-http-client-appengine에서)는 Google App Engine Data Store API를 사용하여 사용자 인증 정보를 유지하기 위한 좋은 옵션입니다.

calendar-appengine-sample에서 가져온 예 (약간 수정됨)

public class CalendarAppEngineSample extends AbstractAppEngineAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    return Utils.getRedirectUri(req);
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return Utils.newFlow();
  }
}

class Utils {
  static String getRedirectUri(HttpServletRequest req) {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  static GoogleAuthorizationCodeFlow newFlow() throws IOException {
    return new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY,
        getClientCredential(), Collections.singleton(CalendarScopes.CALENDAR)).setDataStoreFactory(
        DATA_STORE_FACTORY).setAccessType("offline").build();
  }
}

public class OAuth2Callback extends AbstractAppEngineAuthorizationCodeCallbackServlet {

  private static final long serialVersionUID = 1L;

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    String nickname = UserServiceFactory.getUserService().getCurrentUser().getNickname();
    resp.getWriter().print("<h3>" + nickname + ", why don't you want to play with me?</h1>");
    resp.setStatus(200);
    resp.addHeader("Content-Type", "text/html");
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    return Utils.getRedirectUri(req);
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return Utils.newFlow();
  }
}

추가 샘플은 storage-serviceaccount-appengine-sample을 참조하세요.

서비스 계정

GoogleCredential서비스 계정도 지원합니다. 클라이언트 애플리케이션이 최종 사용자 데이터에 대한 액세스를 요청하는 사용자 인증 정보와 달리 서비스 계정은 클라이언트 애플리케이션의 자체 데이터에 대한 액세스를 제공합니다. 클라이언트 애플리케이션은 Google API 콘솔에서 다운로드한 비공개 키를 사용하여 액세스 토큰 요청에 서명합니다.

plus-serviceaccount-cmdline-sample에서 가져온 코드 예:

HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
JsonFactory jsonFactory = GsonFactory.getDefaultInstance();
...
// Build service account credential.

GoogleCredential credential = GoogleCredential.fromStream(new FileInputStream("MyProject-1234.json"))
    .createScoped(Collections.singleton(PlusScopes.PLUS_ME));
// Set up global Plus instance.
plus = new Plus.Builder(httpTransport, jsonFactory, credential)
    .setApplicationName(APPLICATION_NAME).build();
...

추가 샘플은 storage-serviceaccount-cmdline-sample을 참조하세요.

명의 도용

서비스 계정 흐름을 사용하여 소유한 도메인에서 사용자를 가장할 수도 있습니다. 이는 위의 서비스 계정 흐름과 매우 유사하지만 GoogleCredential.Builder.setServiceAccountUser(String)를 추가로 호출합니다.

설치된 애플리케이션

다음은 설치된 애플리케이션에 OAuth 2.0 사용에 설명된 명령줄 인증 코드 흐름입니다.

plus-cmdline-sample의 스니펫 예는 다음과 같습니다.

public static void main(String[] args) {
  try {
    httpTransport = GoogleNetHttpTransport.newTrustedTransport();
    dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR);
    // authorization
    Credential credential = authorize();
    // set up global Plus instance
    plus = new Plus.Builder(httpTransport, JSON_FACTORY, credential).setApplicationName(
        APPLICATION_NAME).build();
   // ...
}

private static Credential authorize() throws Exception {
  // load client secrets
  GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY,
      new InputStreamReader(PlusSample.class.getResourceAsStream("/client_secrets.json")));
  // set up authorization code flow
  GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
      httpTransport, JSON_FACTORY, clientSecrets,
      Collections.singleton(PlusScopes.PLUS_ME)).setDataStoreFactory(
      dataStoreFactory).build();
  // authorize
  return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
}

클라이언트 측 애플리케이션

클라이언트 측 애플리케이션용 OAuth 2.0 사용에 설명된 브라우저 기반 클라이언트 흐름을 사용하려면 일반적으로 다음 단계를 따릅니다.

  1. GoogleBrowserClientRequestUrl을 사용하여 브라우저의 최종 사용자를 승인 페이지로 리디렉션하여 브라우저 애플리케이션에 최종 사용자의 보호된 데이터에 대한 액세스 권한을 부여합니다.
  2. JavaScript용 Google API 클라이언트 라이브러리를 사용하여 Google API 콘솔에 등록된 리디렉션 URI의 URL 프래그먼트에서 찾은 액세스 토큰을 처리합니다.

웹 애플리케이션의 사용 예:

public void doGet(HttpServletRequest request, HttpServletResponse response)throws IOException {
  String url = new GoogleBrowserClientRequestUrl("812741506391.apps.googleusercontent.com",
      "https://oauth2.example.com/oauthcallback", Arrays.asList(
          "https://www.googleapis.com/auth/userinfo.email",
          "https://www.googleapis.com/auth/userinfo.profile")).setState("/profile").build();
  response.sendRedirect(url);
}

Android

@베타

Android에서 사용할 라이브러리:

Android용으로 개발 중이며 사용하려는 Google API가 Google Play 서비스 라이브러리에 포함되어 있는 경우 최상의 성능과 환경을 위해 해당 라이브러리를 사용하세요. Android에서 사용하려는 Google API가 Google Play 서비스 라이브러리에 포함되어 있지 않으면 여기에 설명된 Android 4.0(Ice Cream Sandwich) 이상을 지원하는 Java용 Google API 클라이언트 라이브러리를 사용할 수 있습니다. Java용 Google API 클라이언트 라이브러리에서 Android 지원은 @Beta입니다.

배경 정보

Eclair (SDK 2.1)부터 계정 관리자를 사용하여 Android 기기에서 사용자 계정을 관리합니다. 모든 Android 애플리케이션 승인은 AccountManager를 사용하여 SDK가 중앙에서 관리합니다. 애플리케이션에 필요한 OAuth 2.0 범위를 지정하면 사용할 액세스 토큰이 반환됩니다.

OAuth 2.0 범위는 authTokenType 매개변수를 통해 oauth2:와 범위를 함께 지정합니다. 예를 들면 다음과 같습니다.

oauth2:https://www.googleapis.com/auth/tasks

Google Tasks API에 대한 읽기/쓰기 액세스 권한을 지정합니다. OAuth 2.0 범위가 여러 개 필요한 경우 공백으로 구분된 목록을 사용합니다.

일부 API에는 함께 작동하는 특수한 authTokenType 매개변수가 있습니다. 예를 들어 '작업 관리'는 위에 표시된 authtokenType 예의 별칭입니다.

Google API 콘솔에서 API 키도 지정해야 합니다. 그렇지 않으면 AccountManager가 제공하는 토큰이 익명 할당량만 제공하며 이는 일반적으로 매우 낮습니다. 반면 API 키를 지정하면 더 많은 무료 할당량을 받게 되며 선택적으로 이를 초과하는 사용량에 대한 결제를 설정할 수 있습니다.

tasks-android-sample에서 가져온 코드 스니펫의 예는 다음과 같습니다.

com.google.api.services.tasks.Tasks service;

@Override
public void onCreate(Bundle savedInstanceState) {
  credential =
      GoogleAccountCredential.usingOAuth2(this, Collections.singleton(TasksScopes.TASKS));
  SharedPreferences settings = getPreferences(Context.MODE_PRIVATE);
  credential.setSelectedAccountName(settings.getString(PREF_ACCOUNT_NAME, null));
  service =
      new com.google.api.services.tasks.Tasks.Builder(httpTransport, jsonFactory, credential)
          .setApplicationName("Google-TasksAndroidSample/1.0").build();
}

private void chooseAccount() {
  startActivityForResult(credential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  switch (requestCode) {
    case REQUEST_GOOGLE_PLAY_SERVICES:
      if (resultCode == Activity.RESULT_OK) {
        haveGooglePlayServices();
      } else {
        checkGooglePlayServicesAvailable();
      }
      break;
    case REQUEST_AUTHORIZATION:
      if (resultCode == Activity.RESULT_OK) {
        AsyncLoadTasks.run(this);
      } else {
        chooseAccount();
      }
      break;
    case REQUEST_ACCOUNT_PICKER:
      if (resultCode == Activity.RESULT_OK && data != null && data.getExtras() != null) {
        String accountName = data.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME);
        if (accountName != null) {
          credential.setSelectedAccountName(accountName);
          SharedPreferences settings = getPreferences(Context.MODE_PRIVATE);
          SharedPreferences.Editor editor = settings.edit();
          editor.putString(PREF_ACCOUNT_NAME, accountName);
          editor.commit();
          AsyncLoadTasks.run(this);
        }
      }
      break;
  }
}