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 載體權杖規格說明如何使用在使用者授權程序中授予的存取權杖存取這些受保護的資源。

詳情請參閱下列套件的 Javadoc 說明文件:

用戶端註冊

使用 Java 適用的 Google OAuth 用戶端程式庫前,您可能需要向授權伺服器註冊應用程式,才能取得用戶端 ID 和用戶端密碼。(如需這項程序的一般資訊,請參閱用戶端註冊規格)。

憑證和憑證儲存空間

Credential 是執行緒安全的 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 實作已淘汰,並將在日後的版本中移除。另一種方法是將 DataStoreFactoryDataStore 介面與 StoredCredential 搭配使用,這是由 Java 適用的 Google HTTP 用戶端程式庫提供的。

您可以使用程式庫提供的下列其中一種實作方式:

Google App Engine 使用者:

AppEngineCredentialStore 已淘汰,並即將移除。

建議您搭配StoredCredential使用 AppEngineDataStoreFactory。如果您以舊方式儲存憑證,可以使用新增的輔助程式方法 migrateTo(AppEngineDataStoreFactory)migrateTo(DataStore) 進行遷移。

使用 DataStoreCredentialRefreshListener,並使用 GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener) 為憑證設定。

授權碼流程

使用授權碼流程,讓使用者授予應用程式存取其受保護資料的權限。此流程的通訊協定已在 授權碼授權規格中指定。

這個流程是使用 AuthorizationCodeFlow 實作。步驟如下:

或者,如果您未使用 AuthorizationCodeFlow,可以使用較低層級的類別:

JS 授權碼流程

這個程式庫提供 servlet 輔助程式類別,可大幅簡化基本用途的授權程式碼流程。您只需提供 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 上的授權碼流程幾乎與 servlet 授權碼流程相同,唯一的差異是我們可以利用 Google App Engine 的 Users Java API。使用者必須登入,才能啟用 Users Java API;如要將使用者重新導向至登入頁面 (如果他們尚未登入),請參閱「安全性和驗證」(位於 web.xml 中)。

與 servlet 情況的主要差異在於,您提供 AbstractAppEngineAuthorizationCodeServletAbstractAppEngineAuthorizationCodeCallbackServlet (來自 google-oauth-client-appengine) 的具體子類別。這些類別會擴充抽象 servlet 類別,並使用 Users Java API 為您實作 getUserId 方法。AppEngineDataStoreFactory (來自 Java 專用 Google HTTP 用戶端程式庫) 是使用 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 中,網址片段中找到的存取權權杖。

網頁應用程式使用範例:

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 承載者規格,當伺服器收到使用已過期存取權杖存取受保護資源的呼叫時,通常會傳回 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 參數。這會以秒為單位指定已授予存取權杖的效期,通常為一小時。不過,存取權杖可能不會在該期限結束時過期,且伺服器可能會繼續允許存取。因此,我們通常建議等待 401 Unauthorized 狀態碼,而不是根據經過的時間假設權杖已過期。或者,您也可以在存取權杖即將到期前,嘗試重新整理存取權杖。如果無法使用權杖伺服器,請繼續使用存取權杖,直到收到 401 為止。這是 憑證中預設使用的策略。

另一個做法是在每次要求前取得新的存取權存證,但這需要每次向權杖伺服器提出額外的 HTTP 要求,因此在速度和網路用量方面可能不是理想的做法。最好將存取權杖儲存在安全的永久儲存空間,盡量減少應用程式對新存取權杖的要求。(但對於已安裝的應用程式,安全儲存空間是一個難解的問題)。

請注意,存取權存證可能會因其他原因而失效,而非僅限於到期,例如使用者明確撤銷存證,因此請確保錯誤處理程式碼的健全性。一旦偵測到權杖已失效 (例如已過期或遭到撤銷),就必須從儲存空間中移除存取權杖。例如在 Android 上,您必須呼叫 AccountManager.invalidateAuthToken