將 OAuth 2.0 與 Java 適用的 Google API 用戶端程式庫搭配使用

總覽

目的:本文件說明如何透過 GoogleCredential 公用程式類別,以 Google 服務執行 OAuth 2.0 授權。如要進一步瞭解我們提供的一般 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 控制台設定專案以進行驗證和帳單用途,無論用戶端已安裝的應用程式、行動應用程式、網路伺服器或在瀏覽器中執行的用戶端都一樣。

如需正確設定憑證的操作說明,請參閱 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 身分

這個替代憑證是以 Google App Engine App Identity Java API 為基礎。 與用戶端應用程式要求存取使用者資料的憑證不同,App Identity API 提供存取用戶端應用程式自有資料的憑證。

使用 AppIdentityCredential (來自 google-api-client-appengine)。這個憑證更簡單了,因為 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 實作項目,也可以使用程式庫提供的下列其中一個實作項目:

App Engine 使用者AppEngineCredentialStore 已淘汰,並將在近期內移除。建議您使用 AppEngineDataStoreFactory 搭配 StoredCredential。如果您以舊版方式儲存憑證,可以使用新增的輔助程式方法 migrationTo(AppEngineDataStoreFactory)migrationTo(DataStore) 進行遷移作業。

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

授權碼流程

只要使用授權碼流程,使用者就能授予應用程式 Google API 上受保護資料的存取權。此流程的通訊協定可在授權碼授權中指定。

系統會使用 GoogleAuthorizedCodeFlow 實作這個流程。步驟如下:

如果您沒有使用 GoogleAuthorizationCodeFlow,也可以使用較低層級的類別:

Google API 控制台中設定專案時,您可以根據使用的流程選取不同的憑證。詳情請參閱設定 OAuth 2.0OAuth 2.0 情境。每個流程的程式碼片段如下。

網路伺服器應用程式

請參閱使用 OAuth 2.0 處理網路伺服器應用程式中的流程說明。

這個程式庫提供 bq 輔助程式類別,可大幅簡化基本用途的授權程式碼流程。您只需提供 AbstractAuthorizationCodeListenerAbstractAuthorizationCodeCallbackListener 的具體子類別 (來自 google-oauth-client-內建),並將其新增至 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 上的授權碼流程幾乎與 src 授權程式碼流程相同,差別在於我們可以使用 Google App Engine 和 Users Java API。使用者必須登入,才能啟用 Users Java API;想瞭解如何將使用者重新導向至尚未登入的頁面,請參閱安全性和驗證 (web.xml 中)。

與 Webhook 案例的主要差異在於,您可以提供 AbstractAppEngineAuthorizationCodeListenerAbstractAppEngineAuthorizationCodeCallbackListener 的具體子類別 (來自 google-oauth-client-appengine)。這些類別會擴充抽象 Webhook 類別,並使用 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 中,處理網址片段中的存取權杖。

網頁應用程式使用範例:

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

@Beta 版

Android 要使用的程式庫:

如果您要為 Android 進行開發,且您要使用的 Google API 包含在 Google Play 服務程式庫中,請使用該程式庫取得最佳效能和體驗。如果您要與 Android 搭配使用的 Google API 不屬於 Google Play 服務程式庫的一部分,您可以使用 Java 適用的 Google API 用戶端程式庫 (支援 Android 4.0 (Ice Cream Sandwich) 以上版本)。Google API Java Java 用戶端程式庫提供對 @Beta 版的支援。

背景說明:

從 Eclair (SDK 2.1) 開始,使用者帳戶是透過 Android 裝置使用「帳戶管理員」,SDK 會使用 AccountManager 集中管理所有 Android 應用程式授權。您可以指定應用程式需要的 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;
  }
}