将 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 控制台

无论您的客户端是安装式应用、移动应用、Web 服务器,还是在浏览器中运行的客户端,您都需要在 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 实现;也可以使用该库提供的以下实现之一:

AppEngine 用户AppEngineCredentialStore 已弃用,很快就会被移除。我们建议您将 AppEngineDataStoreFactoryStoredCredential 结合使用。 如果您以旧方式存储凭据,则可以使用添加的辅助方法 migrateTo(AppEngineDataStoreFactory)migrateTo(DataStore) 进行迁移。

您可以使用 DataStoreCredentialRefreshListener,并使用 GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener) 为凭据设置该事件。

授权代码流程

使用授权代码流程,让最终用户能够授权您的应用访问其在 Google API 上受保护的数据。授权代码授权中指定了此流程的协议。

此流程是使用 GoogleAuthorizationCodeFlow 实现的。具体步骤包括:

或者,如果您未使用 GoogleAuthorizationCodeFlow,可以使用较低级别的类:

Google API 控制台中设置项目时,您可以根据所使用的流程选择不同的凭据。如需了解详情,请参阅设置 OAuth 2.0OAuth 2.0 场景。以下是各个流程的代码段。

Web 服务器应用

如需了解此流程的协议,请参阅针对网络服务器应用使用 OAuth 2.0

此库提供了 WebView 帮助程序类,可显著简化基本用例的授权代码流程。您只需提供 AbstractAuthorizationCodeServletAbstractAuthorizationCodeCallbackServlet(来自 google-oauth-client-servlet)的具体子类,并将其添加到您的 web.xml 文件中。请注意,您仍然需要处理 Web 应用的用户登录并提取用户 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 上的授权代码流程与 WebView 授权代码流程几乎完全相同,不同之处在于我们可以利用 Google App Engine 的 Users Java API。用户需要登录才能启用用户 Java API;如需了解如何在用户尚未登录的情况下将其重定向到登录页面,请参阅安全和身份验证(在 web.xml 中)。

与 WebView 的主要区别在于,您提供 AbstractAppEngineAuthorizationCodeServletAbstractAppEngineAuthorizationCodeCallbackServlet(来自 google-oauth-client-appengine)的具体子类。它们扩展了抽象 WebView 类,并使用用户 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 处的网址片段中找到的访问令牌。

Web 应用的用法示例:

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)(或更高版本),如此处所述。Java 版 Google API 客户端库对 Android 的支持为 @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;
  }
}