适用于 Java 的 OAuth 2.0 和 Google OAuth 客户端库

概览

用途:本文档介绍了适用于 Java 的 Google OAuth 客户端库提供的通用 OAuth 2.0 函数。您可以使用这些函数对任何互联网服务进行身份验证和授权。

如需了解如何使用 GoogleCredential 对 Google 服务进行 OAuth 2.0 授权,请参阅将 OAuth 2.0 与适用于 Java 的 Google API 客户端库搭配使用

摘要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 已废弃,即将移除。

我们建议您将 AppEngineDataStoreFactoryStoredCredential 结合使用。如果您以旧方式存储凭据,则可以使用添加的辅助方法 migrateTo(AppEngineDataStoreFactory)migrateTo(DataStore) 进行迁移。

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

授权代码流程

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

此流程使用 AuthorizationCodeFlow 实现。具体步骤包括:

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

REST 授权代码流程

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

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

Web 应用的用法示例:

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