OAuth 2.0 및 Java용 Google OAuth 클라이언트 라이브러리

개요

용도: 이 문서에서는 Java용 Google OAuth 클라이언트 라이브러리에서 제공하는 일반적인 OAuth 2.0 기능을 설명합니다. 이러한 함수는 모든 인터넷 서비스의 인증과 승인에 사용할 수 있습니다.

GoogleCredential를 사용하여 Google 서비스에서 OAuth 2.0 승인을 실행하는 방법은 Java용 Google API 클라이언트 라이브러리에서 OAuth 2.0 사용을 참고하세요.

요약: OAuth 2.0은 최종 사용자가 클라이언트 애플리케이션이 보호되는 서버 측 리소스에 액세스할 수 있도록 안전하게 승인할 수 있는 표준 사양입니다. 또한 OAuth 2.0 Bearer 토큰 사양에서는 최종 사용자 승인 프로세스 중에 부여된 액세스 토큰을 사용하여 이러한 보호된 리소스에 액세스하는 방법을 설명합니다.

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

클라이언트 등록

Java용 Google OAuth 클라이언트 라이브러리를 사용하려면 우선 승인 서버에 애플리케이션을 등록하여 클라이언트 ID와 클라이언트 보안 비밀번호를 수신해야 할 수 있습니다. 이 프로세스에 대한 일반적인 정보는 클라이언트 등록 사양을 참고하세요.

사용자 인증 정보 및 사용자 인증 정보 저장소

사용자 인증 정보는 액세스 토큰을 사용하여 보호된 리소스에 액세스하기 위한 스레드 안전 OAuth 2.0 도우미 클래스입니다. 갱신 토큰을 사용하면 Credential은 액세스 토큰이 갱신 토큰을 사용하여 만료되면 액세스 토큰을 새로고침합니다. 예를 들어 이미 액세스 토큰이 있다면 다음과 같은 방법으로 요청할 수 있습니다.

  public static HttpResponse executeGet(
      HttpTransport transport, JsonFactory jsonFactory, String accessToken, GenericUrl url)
      throws IOException {
    Credential credential =
        new Credential(BearerToken.authorizationHeaderAccessMethod()).setAccessToken(accessToken);
    HttpRequestFactory requestFactory = transport.createRequestFactory(credential);
    return requestFactory.buildGetRequest(url).execute();
  }

대부분의 애플리케이션은 향후 브라우저에서 승인 페이지로 리디렉션되는 것을 방지하기 위해 사용자 인증 정보의 액세스 토큰과 갱신 토큰을 유지해야 합니다. 이 라이브러리의 CredentialStore 구현은 지원 중단되었으며 향후 출시에서 삭제될 예정입니다. 대안은 Java용 Google HTTP 클라이언트 라이브러리에서 제공하는 StoredCredential과 함께 DataStoreFactoryDataStore 인터페이스를 사용하는 것입니다.

라이브러리에서 제공하는 다음 구현 중 하나를 사용할 수 있습니다.

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

Google App Engine 사용자:

AppEngineCredentialStore는 지원 중단되었으며 삭제될 예정입니다.

AppEngineDataStoreFactoryStoredCredential와 함께 사용하는 것이 좋습니다. 이전 방식으로 사용자 인증 정보를 저장한 경우 추가된 도우미 메서드인 migrateTo(AppEngineDataStoreFactory) 또는 migrateTo(DataStore)를 사용하여 이전할 수 있습니다.

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

승인 코드 흐름

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

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

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

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

서블릿 승인 코드 흐름

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

샘플 코드:

public class ServletSample 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 AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

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

public class ServletCallbackSample 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 AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .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 메서드를 구현합니다. Java용 Google HTTP 클라이언트 라이브러리AppEngineDataStoreFactory는 Google App Engine Data Store API를 사용하여 사용자 인증 정보를 유지하기 위한 좋은 옵션입니다.

샘플 코드:

public class AppEngineSample extends AbstractAppEngineAuthorizationCodeServlet {

  @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 AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

public class AppEngineCallbackSample extends AbstractAppEngineAuthorizationCodeCallbackServlet {

  @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 AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

명령줄 승인 코드 흐름

dailymotion-cmdline-sample에서 가져온 단순화된 코드 예:

/** Authorizes the installed application to access user's protected data. */
private static Credential authorize() throws Exception {
  OAuth2ClientCredentials.errorIfNotSpecified();
  // set up authorization code flow
  AuthorizationCodeFlow flow = new AuthorizationCodeFlow.Builder(BearerToken
      .authorizationHeaderAccessMethod(),
      HTTP_TRANSPORT,
      JSON_FACTORY,
      new GenericUrl(TOKEN_SERVER_URL),
      new ClientParametersAuthentication(
          OAuth2ClientCredentials.API_KEY, OAuth2ClientCredentials.API_SECRET),
      OAuth2ClientCredentials.API_KEY,
      AUTHORIZATION_SERVER_URL).setScopes(Arrays.asList(SCOPE))
      .setDataStoreFactory(DATA_STORE_FACTORY).build();
  // authorize
  LocalServerReceiver receiver = new LocalServerReceiver.Builder().setHost(
      OAuth2ClientCredentials.DOMAIN).setPort(OAuth2ClientCredentials.PORT).build();
  return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
}

private static void run(HttpRequestFactory requestFactory) throws IOException {
  DailyMotionUrl url = new DailyMotionUrl("https://api.dailymotion.com/videos/favorites");
  url.setFields("id,tags,title,url");

  HttpRequest request = requestFactory.buildGetRequest(url);
  VideoFeed videoFeed = request.execute().parseAs(VideoFeed.class);
  ...
}

public static void main(String[] args) {
  ...
  DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR);
  final Credential credential = authorize();
  HttpRequestFactory requestFactory =
      HTTP_TRANSPORT.createRequestFactory(new HttpRequestInitializer() {
        @Override
        public void initialize(HttpRequest request) throws IOException {
          credential.initialize(request);
          request.setParser(new JsonObjectParser(JSON_FACTORY));
        }
      });
  run(requestFactory);
  ...
}

브라우저 기반 클라이언트 흐름

다음은 암시적 권한 부여 사양에 지정된 브라우저 기반 클라이언트 흐름의 일반적인 단계입니다.

  • BrowserClientRequestUrl을 사용하여 최종 사용자의 브라우저를 최종 사용자가 애플리케이션에 보호된 데이터에 액세스할 수 있는 권한을 부여할 수 있는 승인 페이지로 리디렉션합니다.
  • JavaScript 애플리케이션을 사용하여 승인 서버에 등록된 리디렉션 URI의 URL 프래그먼트에 있는 액세스 토큰을 처리합니다.

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

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
  String url = new BrowserClientRequestUrl(
      "https://server.example.com/authorize", "s6BhdRkqt3").setState("xyz")
      .setRedirectUri("https://client.example.com/cb").build();
  response.sendRedirect(url);
}

만료된 액세스 토큰 감지

OAuth 2.0 Bearer 사양에 따라, 만료된 액세스 토큰으로 보호된 리소스에 액세스하기 위해 서버를 호출하면 서버는 일반적으로 다음과 같은 HTTP 401 Unauthorized 상태 코드로 응답합니다.

   HTTP/1.1 401 Unauthorized
   WWW-Authenticate: Bearer realm="example",
                     error="invalid_token",
                     error_description="The access token expired"

하지만 사양에는 많은 유연성이 있는 것으로 보입니다. 자세한 내용은 OAuth 2.0 제공업체의 문서를 참고하세요.

또 다른 방법은 액세스 토큰 응답에서 expires_in 매개변수를 확인하는 것입니다. 이는 부여된 액세스 토큰의 수명을 초 단위로 지정하며 일반적으로 1시간입니다. 그러나 액세스 토큰이 이 기간이 끝날 때 실제로 만료되지 않을 수도 있으며 서버에서 계속 액세스를 허용할 수도 있습니다. 따라서 경과된 시간에 따라 토큰이 만료되었다고 가정하지 말고 401 Unauthorized 상태 코드를 기다리는 것이 좋습니다. 또는 액세스 토큰이 만료되기 직전에 새로고침하고 토큰 서버를 사용할 수 없는 경우에는 401를 수신할 때까지 액세스 토큰을 계속 사용할 수 있습니다. 이는 사용자 인증 정보에서 기본적으로 사용되는 전략입니다.

또 다른 옵션은 모든 요청 전에 새 액세스 토큰을 가져오는 것인데, 매번 토큰 서버에 추가 HTTP 요청을 해야 하므로 속도 및 네트워크 사용량 측면에서 좋지 않을 수 있습니다. 액세스 토큰을 안전한 영구 저장소에 저장하여 애플리케이션의 새로운 액세스 토큰 요청을 최소화하는 것이 이상적입니다. (하지만 설치된 애플리케이션의 경우 보안 저장소는 어려운 문제입니다.)

만료가 아닌 다른 이유로 액세스 토큰이 무효화될 수 있으므로(예: 사용자가 명시적으로 토큰을 취소한 경우) 오류 처리 코드가 강력해야 합니다. 토큰이 더 이상 유효하지 않음을 감지하면(예: 토큰이 만료되었거나 취소된 경우) 스토리지에서 액세스 토큰을 삭제해야 합니다. 예를 들어 Android에서는 AccountManager.invalidateAuthToken을 호출해야 합니다.