OAuth 2.0 ve Java için Google OAuth İstemci Kitaplığı

Genel Bakış

Amaç: Bu dokümanda, Java için Google OAuth İstemci Kitaplığı tarafından sunulan genel OAuth 2.0 işlevleri açıklanmaktadır. Bu işlevleri, herhangi bir internet hizmeti için kimlik doğrulama ve yetkilendirme amacıyla kullanabilirsiniz.

Google hizmetlerinde OAuth 2.0 yetkilendirmesi yapmak için GoogleCredential kullanmayla ilgili talimatlar için Java için Google API İstemci Kitaplığı ile OAuth 2.0'ı kullanma başlıklı makaleyi inceleyin.

Özet: OAuth 2.0, son kullanıcıların istemci uygulamasının korunan sunucu tarafı kaynaklarına güvenli bir şekilde yetkilendirilmesine olanak tanıyan standart bir spesifikasyondur. Ayrıca, OAuth 2.0 taşıyıcı jetonu spesifikasyonunda, son kullanıcı yetkilendirme işlemi sırasında verilen bir erişim jetonu kullanılarak bu korumalı kaynaklara nasıl erişileceği açıklanmaktadır.

Ayrıntılar için aşağıdaki paketlerin Javadoc dokümanlarına bakın:

Müşteri kaydı

Java için Google OAuth İstemci Kitaplığı'nı kullanmadan önce muhtemelen, istemci kimliği ve istemci gizli anahtarı almak için uygulamanızı bir yetkilendirme sunucusuna kaydetmeniz gerekir. (Bu işlemle ilgili genel bilgiler için İstemci Kaydı spesifikasyonuna bakın.)

Kimlik bilgisi ve kimlik bilgisi deposu

Kimlik bilgisi, erişim jetonu kullanarak korumalı kaynaklara erişmek için iş parçacığı için güvenli bir OAuth 2.0 yardımcı sınıfıdır. Yenileme jetonu kullanıldığında Credential, erişim jetonunun süresi dolduğunda yenileme jetonunu kullanarak erişim jetonunu da yeniler. Örneğin, halihazırda bir erişim jetonunuz varsa aşağıdaki şekilde istek gönderebilirsiniz:

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

Çoğu uygulamanın, tarayıcıda yetkilendirme sayfasına yönlendirilmeyi önlemek için kimlik bilgisinin erişim jetonunu ve yenileme jetonunu koruması gerekir. Bu kitaplıktaki CredentialStore uygulamasının desteği sonlandırılmıştır ve gelecekteki sürümlerde kaldırılacaktır. Alternatif olarak, Java için Google HTTP İstemci Kitaplığı tarafından sağlanan DataStoreFactory ve DataStore arayüzlerini StoredCredential ile kullanabilirsiniz.

Kitaplık tarafından sağlanan aşağıdaki uygulamalardan birini kullanabilirsiniz:

Google App Engine kullanıcıları:

AppEngineCredentialStore desteği sonlandırıldı ve kaldırılıyor.

StoredCredential ile AppEngineDataStoreFactory kullanmanızı öneririz. Eski yöntemlerle depolanan kimlik bilgileriniz varsa taşıma işlemi için migrationTo(AppEngineDataStoreFactory) veya MigrateTo(DataStore) gibi yardımcı yöntemleri kullanabilirsiniz.

DataStoreCredentialRefreshListener'ı kullanın ve GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener) yöntemini kullanarak kimlik bilgisi için ayarlayın.

Yetkilendirme kodu akışı

Son kullanıcının, uygulamanıza korumalı verilerine erişim izni vermesine izin vermek için yetkilendirme kodu akışını kullanın. Bu akışa ilişkin protokol, Yetkilendirme Kodu Verme spesifikasyonunda belirtilmiştir.

Bu akış, AuthorizationCodeFlow kullanılarak uygulanır. Adımlar aşağıdaki gibidr:

Alternatif olarak, AuthorizationCodeFlow kullanmıyorsanız daha alt düzey sınıfları da kullanabilirsiniz:

Servlet yetkilendirme kodu akışı

Bu kitaplık, temel kullanım alanları için yetkilendirme kodu akışını önemli ölçüde basitleştirmek amacıyla servlet yardımcı sınıfları sağlar. AbstractAuthorizationCodeServlet ve AbstractAuthorizationCodeCallbackServlet (google-oauth-client-servlet'ten) sınıflarının somut alt sınıflarını sağlayıp web.xml dosyanıza eklemeniz yeterlidir. Web uygulamanız için kullanıcı girişini halletmeniz ve bir kullanıcı kimliği çıkarmanız gerektiğini unutmayın.

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

Google App Engine yetkilendirme kodu akışı

App Engine'deki yetkilendirme kodu akışı, Google App Engine'in Users Java API'sinden yararlanabilmemiz dışında servlet yetkilendirme kodu akışıyla neredeyse aynıdır. Users Java API'nin etkinleştirilmesi için kullanıcının giriş yapmış olması gerekir. Henüz giriş yapmamış kullanıcıları bir giriş sayfasına yönlendirme hakkında daha fazla bilgi edinmek için Güvenlik ve Kimlik Doğrulama sayfasına (web.xml'de) göz atın.

Servlet örneğiyle arasındaki temel fark, AbstractAppEngineAuthorizationCodeServlet ve AbstractAppEngineAuthorizationCodeCallbackServlet (google-oauth-client-appengine'dan) sınıflarının somut alt sınıflarını sağlamanızdır. Soyut servlet sınıflarını genişletir ve Users Java API'sini kullanarak getUserId yöntemini sizin için uygular. AppEngineDataStoreFactory (Java için Google HTTP İstemci Kitaplığı'ndan), Google App Engine Data Store API'yi kullanarak kimlik bilgisini kalıcı hale getirmek için iyi bir seçenektir.

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

Komut satırı yetkilendirme kod akışı

dailymotion-cmdline-sample'dan alınmış basitleştirilmiş örnek kod:

/** 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);
  ...
}

Tarayıcı tabanlı istemci akışı

Dolaysız Atama spesifikasyonunda belirtilen tarayıcı tabanlı istemci akışının tipik adımları şunlardır:

  • BrowserClientRequestUrl'yi kullanarak son kullanıcının tarayıcısını, son kullanıcının uygulamanızın korunan verilerine erişmesine izin verebileceği yetkilendirme sayfasına yönlendirin.
  • Yetkilendirme sunucusuna kayıtlı yönlendirme URI'sindeki URL parçasında bulunan erişim jetonunu işlemek için bir JavaScript uygulaması kullanın.

Web uygulaması için örnek kullanım:

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

Süresi dolmuş erişim jetonlarını algılama

OAuth 2.0 taşıyıcı spesifikasyonuna göre, süresi dolmuş bir erişim jetonuyla korunan bir kaynağa erişmek için sunucu çağrıldığında sunucu genellikle aşağıdaki gibi bir HTTP 401 Unauthorized durum koduyla yanıt verir:

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

Ancak spesifikasyonda büyük bir esneklik olduğu görülüyor. Ayrıntılar için OAuth 2.0 sağlayıcısının dokümanlarını inceleyin.

Alternatif bir yaklaşım da erişim jetonu yanıtındaki expires_in parametresini kontrol etmektir. Bu, verilen erişim jetonunun saniye cinsinden geçerlilik süresini belirtir. Bu süre genellikle bir saattir. Ancak erişim jetonunun süresi bu sürenin sonunda sona ermeyebilir ve sunucu, erişime izin vermeye devam edebilir. Bu nedenle, genellikle geçen süreye göre jetonun süresinin dolduğunu varsaymak yerine 401 Unauthorized durum kodunu beklemenizi öneririz. Alternatif olarak, erişim jetonunu süresi dolmadan kısa bir süre önce yenilemeyi deneyebilir ve jeton sunucusu kullanılamıyorsa 401 alana kadar erişim jetonunu kullanmaya devam edebilirsiniz. Bu, Kimlik Bilgileri'nde varsayılan olarak kullanılan stratejidir.

Diğer bir seçenek de her istekten önce yeni bir erişim jetonu almaktır. Ancak bu, her seferinde jeton sunucusuna ek bir HTTP isteği gönderilmesini gerektirir. Bu nedenle, hız ve ağ kullanımı açısından muhtemelen kötü bir seçimdir. İdeal olarak, uygulamaların yeni erişim jetonu isteklerini en aza indirmek için erişim jetonunu güvenli ve kalıcı depolama alanında depolayın. (Ancak yüklü uygulamalar için güvenli depolama, zor bir sorundur.)

Erişim jetonunun geçerlilik bitişi dışındaki nedenlerle de geçersiz olabileceğini unutmayın (örneğin, kullanıcı jetonu açıkça iptal ettiyse). Bu nedenle, hata işleme kodunuzun sağlam olduğundan emin olun. Bir jetonun artık geçerli olmadığını tespit ettiğinizde (ör. süresi dolmuş veya iptal edilmişse) erişim jetonunu depolama alanınızdan kaldırmanız gerekir. Örneğin, Android'de AccountManager.invalidateAuthToken'ı çağırmanız gerekir.