OAuth 2.0 và Thư viện ứng dụng OAuth của Google cho Java

Tổng quan

Mục đích: Tài liệu này mô tả các chức năng OAuth 2.0 chung được cung cấp bởi Thư viện ứng dụng OAuth của Google dành cho Java. Bạn có thể sử dụng các hàm này để xác thực và cấp phép cho bất kỳ dịch vụ Internet nào.

Để xem hướng dẫn về cách sử dụng GoogleCredential để thực hiện uỷ quyền OAuth 2.0 với Các dịch vụ của Google, hãy xem Sử dụng OAuth 2.0 với Thư viện ứng dụng API của Google cho Java.

Tóm tắt: OAuth 2.0 là thông số kỹ thuật tiêu chuẩn để cho phép người dùng cuối cấp phép cho ứng dụng một cách an toàn để truy cập các tài nguyên được bảo vệ phía máy chủ. Ngoài ra, Mã thông báo người dùng OAuth 2.0 thông số kỹ thuật giải thích cách truy cập các tài nguyên được bảo vệ đó bằng cách sử dụng mã thông báo được cấp trong quá trình uỷ quyền cho người dùng cuối.

Để biết thông tin chi tiết, hãy xem tài liệu Javadoc cho các gói sau:

Đăng ký ứng dụng

Trước khi sử dụng Thư viện ứng dụng OAuth của Google cho Java, có thể bạn cần phải đăng ký ứng dụng của bạn với máy chủ uỷ quyền để nhận được mã ứng dụng khách và mật khẩu ứng dụng khách. (Để biết thông tin chung về quy trình này, hãy xem Khách hàng Quy cách đăng ký.)

Kho lưu trữ thông tin xác thực và thông tin xác thực

Thông tin xác thực là một lớp trình trợ giúp OAuth 2.0 an toàn cho luồng để truy cập các tài nguyên được bảo vệ bằng cách sử dụng mã truy cập. Khi sử dụng mã làm mới, Credential cũng làm mới quyền truy cập khi mã truy cập hết hạn bằng mã làm mới. Ví dụ: nếu bạn đã có mã truy cập, bạn có thể thực hiện yêu cầu theo cách sau:

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

Hầu hết các ứng dụng cần duy trì mã truy cập của thông tin xác thực và mã làm mới để tránh bị chuyển hướng đến lệnh uỷ quyền trong tương lai trong trình duyệt. Chiến lược phát hành đĩa đơn CredentialStore Triển khai trong thư viện này không được dùng nữa và sẽ bị loại bỏ trong tương lai bản phát hành. Phương án thay thế là sử dụng DataStoreFactoryDataStore giao diện với StoredCredential (Thông tin xác thực được lưu trữ), được cung cấp bởi Thư viện ứng dụng HTTP của Google dành cho Java.

Bạn có thể sử dụng một trong các cách triển khai sau đây do thư viện cung cấp:

  • JdoDataStoreFactory duy trì thông tin đăng nhập bằng JDO.
  • AppEngineDataStoreFactory duy trì thông tin đăng nhập bằng cách sử dụng API Google App Engine Data Store.
  • MemoryDataStoreFactory "bền vững" thông tin đăng nhập trong bộ nhớ, vốn chỉ hữu ích trong thời gian ngắn bộ nhớ trong suốt vòng đời của quá trình.
  • FileDataStoreFactory vẫn giữ nguyên thông tin đăng nhập trong một tệp.

Người dùng Google App Engine:

AppEngineCredentialStore không được dùng nữa và sẽ bị loại bỏ.

Bạn nên sử dụng AppEngineDataStoreFactory bằng StoredCredential. Nếu có thông tin đăng nhập được lưu trữ theo cách cũ, bạn có thể sử dụng các phương thức trợ giúp đã thêm migrateTo(AppEngineDataStoreFactory) hoặc migrateTo(DataStore) để di chuyển.

Sử dụng DataStoreCredentialRefreshListener và đặt nó cho thông tin đăng nhập bằng cách sử dụng GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener).

Quy trình sử dụng mã uỷ quyền

Sử dụng quy trình mã uỷ quyền để cho phép người dùng cuối cấp ứng dụng của bạn quyền truy cập vào dữ liệu được bảo vệ của họ. Giao thức cho luồng này được chỉ định trong Thông số kỹ thuật của tính năng Cấp mã uỷ quyền.

Quy trình này được triển khai bằng AuthorizationCodeFlow. Các bước thực hiện:

  • Người dùng cuối đăng nhập vào ứng dụng của bạn. Bạn cần phải liên kết người dùng đó với mã nhận dạng người dùng dành riêng cho ứng dụng của bạn.
  • Gọi điện AuthorizationCodeFlow.loadCredential(String), dựa trên user ID, để kiểm tra xem liệu thông tin xác thực của người dùng đã được biết hay chưa. Nếu có, bạn đã hoàn tất.
  • Nếu không, hãy gọi AuthorizationCodeFlow.newAuthorizationUrl() và chuyển hướng trình duyệt của người dùng cuối đến trang uỷ quyền để họ có thể cấp quyền ứng dụng của bạn có quyền truy cập vào dữ liệu được bảo vệ của họ.
  • Sau đó, trình duyệt web sẽ chuyển hướng đến URL chuyển hướng có chứa một "mã" truy vấn mà sau đó có thể được dùng để yêu cầu mã truy cập bằng AuthorizationCodeFlow.newTokenRequest(String).
  • Sử dụng uỷ quyềnCodeFlow.createAndStoreCredential(TokenResponse, Chuỗi) để lưu trữ và lấy thông tin xác thực để truy cập các tài nguyên được bảo vệ.

Ngoài ra, nếu bạn không sử dụng AuthorizationCodeFlow, bạn có thể dùng các lớp cấp thấp hơn:

Quy trình mã uỷ quyền servlet

Thư viện này cung cấp các lớp trình trợ giúp servlet để đơn giản hoá đáng kể quy trình mã uỷ quyền cho các trường hợp sử dụng cơ bản. Bạn chỉ cung cấp các lớp con cụ thể trong số AbstractAuthorizationCodeServletAbstractAuthorizationCodeCallbackServlet (từ google-oauth-client-servlet) và thêm chúng vào tệp web.xml của bạn. Xin lưu ý rằng bạn vẫn cần quan tâm đến vấn đề người dùng đăng nhập cho ứng dụng web của bạn và trích xuất mã nhận dạng người dùng.

Mã mẫu:

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

Quy trình mã uỷ quyền Google App Engine

Quy trình mã uỷ quyền trên App Engine gần giống với servlet mã uỷ quyền của Google, ngoại trừ việc chúng tôi có thể tận dụng API Java của người dùng. Người dùng cần đăng nhập để bật API Java của người dùng; với thông tin về cách chuyển hướng người dùng đến trang đăng nhập nếu họ chưa truy cập đã đăng nhập, xem Bảo mật và xác thực (trong web.xml).

Sự khác biệt chính so với trường hợp servlet là bạn cung cấp dữ liệu cụ thể lớp con của AbstractAppEngineAuthorizationCodeServletAbstractAppEngineAuthorizationCodeCallbackServlet (từ google-oauth-client-appengine). Chúng mở rộng các lớp servlet trừu tượng và triển khai phương thức getUserId cho bạn bằng API Java của người dùng. AppEngineDataStoreFactory (từ Thư viện ứng dụng HTTP của Google dành cho Java là một lựa chọn thích hợp để duy trì thông tin đăng nhập bằng cách sử dụng API Google App Engine Data Store).

Mã mẫu:

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

Quy trình mã uỷ quyền dòng lệnh

Mã ví dụ đơn giản được lấy từ 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);
  ...
}

Luồng ứng dụng dựa trên trình duyệt

Đây là các bước điển hình của quy trình ứng dụng dựa trên trình duyệt được chỉ định trong Thông số kỹ thuật ngầm định về việc cấp:

  • Sử dụng BrowserClientRequestUrl, chuyển hướng trình duyệt của người dùng cuối tới trang uỷ quyền nơi người dùng cuối có thể cấp cho ứng dụng của bạn quyền truy cập vào dữ liệu được bảo vệ của họ.
  • Sử dụng ứng dụng JavaScript để xử lý mã thông báo truy cập có trong URL tại URI chuyển hướng đã đăng ký với máy chủ uỷ quyền.

Ví dụ về cách sử dụng một ứng dụng 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);
}

Phát hiện mã truy cập đã hết hạn

Theo thông số kỹ thuật về trình mang OAuth 2.0, khi máy chủ được gọi để truy cập vào một tài nguyên được bảo vệ có quyền truy cập đã hết hạn mã thông báo, máy chủ thường phản hồi bằng một mã trạng thái HTTP 401 Unauthorized chẳng hạn như:

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

Tuy nhiên, có vẻ như có rất nhiều tính linh hoạt trong thông số kỹ thuật. Cho hãy xem tài liệu của trình cung cấp OAuth 2.0.

Một cách khác là kiểm tra tham số expires_in trong phản hồi mã truy cập. Thao tác này chỉ định thời gian tồn tại tính bằng giây của mã truy cập đã cấp, tức là thường là một giờ. Tuy nhiên, mã truy cập có thể không thực sự hết hạn vào thời điểm cuối trong khoảng thời gian đó và máy chủ có thể tiếp tục cho phép truy cập. Đó là lý do chúng tôi thường khuyên bạn đợi một mã trạng thái 401 Unauthorized, thay vì giả sử mã thông báo đã hết hạn dựa trên thời gian đã trôi qua. Ngoài ra, bạn có thể hãy thử làm mới mã truy cập ngay trước khi hết hạn và nếu máy chủ mã thông báo không có sẵn, hãy tiếp tục sử dụng mã truy cập cho đến khi bạn nhận được 401. Chiến dịch này là chiến lược được dùng theo mặc định trong Thông tin xác thực.

Một cách khác là lấy mã truy cập mới trước mỗi lần yêu cầu, nhưng điều đó mỗi lần gửi yêu cầu HTTP bổ sung tới máy chủ mã thông báo, nên đây có thể là lựa chọn kém chất lượng về tốc độ và mức sử dụng mạng. Tốt nhất là bạn nên lưu trữ mã truy cập trong bộ nhớ an toàn, liên tục để giảm thiểu yêu cầu truy cập mới của ứng dụng mã thông báo. (Nhưng đối với các ứng dụng đã cài đặt, lưu trữ an toàn là một vấn đề khó khăn.)

Xin lưu ý rằng mã thông báo truy cập có thể trở nên không hợp lệ vì những lý do khác ngoài thời gian hết hạn, Ví dụ: nếu người dùng thu hồi mã thông báo một cách rõ ràng, hãy đảm bảo mã xử lý lỗi rất mạnh mẽ. Khi bạn đã phát hiện thấy một mã thông báo không còn hợp lệ, ví dụ: nếu quyền truy cập đã hết hạn hoặc bị thu hồi, bạn phải xoá quyền truy cập khỏi bộ nhớ của bạn. Ví dụ: trên Android, bạn phải gọi AccountManager.invalidateAuthToken.