OAuth 2.0 i biblioteka klienta Google OAuth dla Javy

Omówienie

Cel: ten dokument zawiera opis ogólnych funkcji OAuth 2.0 oferowanych przez bibliotekę klienta OAuth Google dla języka Java. Możesz używać tych funkcji do uwierzytelniania i autoryzacji w dowolnych usługach internetowych.

Instrukcje dotyczące używania GoogleCredential do autoryzacji OAuth 2.0 w usługach Google znajdziesz w artykule Używanie OAuth 2.0 z biblioteką klienta interfejsu API Google dla języka Java.

Podsumowanie: OAuth 2.0 to standardowa specyfikacja umożliwiająca użytkownikom autoryzowanie aplikacji klienckiej w celu bezpiecznego uzyskiwania dostępu do zasobów chronionych po stronie serwera. Dodatkowo specyfikacja tokena uprawnień OAuth 2.0 wyjaśnia, jak uzyskać dostęp do tych zasobów za pomocą tokena dostępu przyznanego podczas procesu autoryzacji użytkownika.

Szczegółowe informacje znajdziesz w dokumentacji Javadoc dotyczącej tych pakietów:

Rejestracja klienta

Zanim zaczniesz korzystać z biblioteki klienta Google OAuth dla Javy, prawdopodobnie musisz zarejestrować aplikację na serwerze autoryzacji, by otrzymywać identyfikator klienta i tajny klucz klienta. (Ogólne informacje o tym procesie znajdziesz w specyfikacji rejestracji klienta).

Dane logowania i magazyn danych logowania

Credential to bezpieczna w wątku klasa pomocnicza OAuth 2.0, która umożliwia dostęp do zasobów chronionych za pomocą tokena dostępu. W przypadku użycia tokena odświeżania Credential odświeża też token dostępu po jego wygaśnięciu. Jeśli na przykład masz już token dostępu, możesz wysłać żądanie w ten sposób:

  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();
  }

Większość aplikacji musi zachowywać token dostępu i token odświeżania danych logowania, aby w przyszłości uniknąć przekierowania na stronę autoryzacji w przeglądarce. Implementacja CredentialStore w tej bibliotece została wycofana i zostanie usunięta w przyszłych wersjach. Możesz też użyć interfejsów DataStoreFactory i DataStore z StoredCredential, które są udostępniane przez bibliotekę klienta HTTP Google dla Javy.

Możesz użyć jednej z tych implementacji udostępnionych przez bibliotekę:

Użytkownicy Google App Engine:

Interfejs AppEngineCredentialStore został wycofany i zostanie usunięty.

Zalecamy użycie AppEngineDataStoreFactoryStoredCredential. Jeśli masz dane logowania zapisane w stary sposób, możesz użyć do migracji dodanych metod pomocniczych migrateTo(AppEngineDataStoreFactory) lub migrateTo(DataStore).

Użyj interfejsu DataStoreCredentialRefreshListener i ustaw go dla danych uwierzytelniających za pomocą metody GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener).

Przepływ kodu autoryzacji

Użyj procesu autoryzacji za pomocą kodu, aby umożliwić użytkownikowi przyznanie aplikacji dostępu do jego chronionych danych. Protokół dla tego przepływu jest określony w specyfikacji kodu autoryzacji.

Ten proces jest realizowany za pomocą AuthorizationCodeFlow. Kroki:

Jeśli nie używasz klasy AuthorizationCodeFlow, możesz użyć klas niskiego poziomu:

Przepływ kodu autoryzacji serwera Servlet

Ta biblioteka udostępnia pomocnicze klasy serwletów, które znacznie upraszczają przepływ kodu autoryzacji w przypadku podstawowych zastosowań. Wystarczy, że podasz konkretne podklasy AbstractAuthorizationCodeServlet i AbstractAuthorizationCodeCallbackServlet (z google-oauth-client-servlet) i dodasz je do pliku web.xml. Pamiętaj, że nadal musisz zadbać o logowanie użytkownika w aplikacji internetowej i wyodrębnienie identyfikatora użytkownika.

Przykładowy kod:

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
  }
}

Przepływ kodu autoryzacji Google App Engine

Przepływ kodu autoryzacji w App Engine jest prawie identyczny jak w przypadku servletów, z tym wyjątkiem, że możemy korzystać z interfejsu Users Java API w Google App Engine. Aby umożliwić korzystanie z interfejsu Users Java API, użytkownik musi się zalogować. Informacje o przekierowywaniu użytkowników na stronę logowania, jeśli nie są jeszcze zalogowani, znajdziesz w sekcji Bezpieczeństwo i uwierzytelnianie (w pliku web.xml).

Podstawowa różnica w stosunku do przypadku serwletu polega na tym, że podajesz konkretne podklasy klasy AbstractAppEngineAuthorizationCodeServlet i AbstractAppEngineAuthorizationCodeCallbackServlet (z google-oauth-client-appengine). Rozszerzają one abstrakcyjne klasy serwera servleta i zaimplementują metodę getUserId za pomocą interfejsu Users Java API. AppEngineDataStoreFactory (z biblioteki klienta HTTP Google dla języka Java) to dobre rozwiązanie do trwałego przechowywania danych logowania za pomocą interfejsu Google App Engine Data Store API.

Przykładowy kod:

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();
  }
}

Przepływ kodu autoryzacji w wierszu poleceń

Uproszczony przykładowy kod z 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);
  ...
}

Przepływ klienta w przeglądarce

Oto typowe czynności związane z przetwarzaniem danych klienta w przeglądarce zgodnie ze specyfikacją Implicit Grant:

  • Za pomocą parametru BrowserClientRequestUrl przekieruj przeglądarkę użytkownika na stronę autoryzacji, na której użytkownik może zezwolić aplikacji na dostęp do jego chronionych danych.
  • Użyj aplikacji JavaScript, aby przetworzyć token dostępu znaleziony w fragmentach adresu URL w identyfikatorze URI przekierowania zarejestrowanym na serwerze autoryzacji.

Przykładowe zastosowanie w przypadku aplikacji internetowej:

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);
}

Wykrywanie wygasłego tokena dostępu

Zgodnie ze specyfikacją nośnika protokołu OAuth 2.0 gdy serwer jest wywoływany w celu uzyskania dostępu do chronionego zasobu z wygasłym tokenem dostępu, serwer zwykle w odpowiedzi przesyła kod stanu HTTP 401 Unauthorized, taki jak:

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

Wygląda jednak, że specyfikacja jest bardzo elastyczna. Więcej informacji znajdziesz w dokumentacji dostawcy usługi OAuth 2.0.

Innym sposobem jest sprawdzenie parametru expires_inodpowiedzi na żądanie tokena dostępu. Określa okres ważności przyznanego tokena dostępu w sekundach, który wynosi zazwyczaj godzinę. Token dostępu może jednak nie wygasnąć z końcem tego okresu i serwer może nadal zezwalać na dostęp. Dlatego zazwyczaj zalecamy czekanie na kod stanu 401 Unauthorized, a nie zakładanie, że token wygasł na podstawie upływu czasu. Możesz też spróbować odświeżyć token dostępu tuż przed jego wygaśnięciem. Jeśli serwer tokenów będzie niedostępny, używaj go do momentu otrzymania 401. Jest to strategia używana domyślnie w danych uwierzytelniających.

Inną opcją jest pobieranie nowego tokena dostępu przed każdym żądaniem, ale wymaga to dodatkowego żądania HTTP do serwera tokenów za każdym razem, więc jest to prawdopodobnie zły wybór pod względem szybkości i korzystania z sieci. Token dostępu należy przechowywać w bezpiecznej, trwałej pamięci masowej, aby zminimalizować liczbę żądań wysyłanych przez aplikację o nowe tokeny dostępu. (W przypadku zainstalowanych aplikacji bezpieczne przechowywanie jest trudnym problemem).

Pamiętaj, że token dostępu może stać się nieważny z powodów innych niż upłynięcie jego ważności, na przykład jeśli użytkownik wyraźnie go cofnie. Dlatego zadbaj o to, aby kod obsługi błędów był niezawodny. Gdy wykryjesz, że token nie jest już ważny, na przykład gdy wygasł lub został cofnięty, musisz usunąć go ze swojej pamięci. Na przykład w przypadku Androida musisz wywołać metodę AccountManager.invalidateAuthToken.