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 hàm OAuth 2.0 chung do Thư viện ứng dụng Google OAuth cung cấp cho Java. Bạn có thể sử dụng các hàm này để xác thực và uỷ quyền cho mọi dịch vụ Internet.

Để biết hướng dẫn về cách sử dụng GoogleCredential để uỷ quyền OAuth 2.0 với các dịch vụ của Google, hãy xem bài viết Sử dụng OAuth 2.0 bằng 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 uỷ quyền an toàn cho ứng dụng khách truy cập vào các tài nguyên phía máy chủ được bảo vệ. Ngoài ra, thông số kỹ thuật mã thông báo truy cập OAuth 2.0 giải thích cách truy cập vào các tài nguyên được bảo vệ đó bằng cách sử dụng mã truy cập đã cấp trong quy 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, bạn có thể cần đăng ký ứng dụng với máy chủ uỷ quyền để nhận 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, vui lòng xem Thông số kỹ thuật của gói đăng ký ứng dụng.)

Kho 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 vào các tài nguyên được bảo vệ bằng mã truy cập. Khi sử dụng mã làm mới, Credential cũng làm mới mã truy cập này khi mã truy cập hết hạn bằng mã làm mới. Ví dụ: nếu đã có mã truy cập, bạn có thể gửi 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 ứng dụng đều cần duy trì mã truy cập của thông tin xác thực và làm mới mã thông báo để tránh việc chuyển hướng đến trang uỷ quyền trong trình duyệt trong tương lai. Phương thức triển khai CredentialStore trong thư viện này không còn được dùng nữa và sẽ bị xoá trong các bản phát hành sau này. Cách khác là sử dụng giao diện DataStoreFactoryDataStore với StoredCredential do Thư viện ứng dụng Google HTTP cho Java cung cấp.

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

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

Người dùng Google App Engine:

AppEngineCredentialStore không được dùng nữa và sẽ bị xoá.

Bạn nên sử dụng AppEngineDataStoreFactory với 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 Di chuyểnTo(AppEngineDataStoreFactory) hoặc migrationTo(DataStore) để di chuyển.

Sử dụng DataStoreCredentialRefreshListener và đặt làm thông tin xác thực bằng GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener).

Quy trình 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 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ọ. Giao thức cho quy trình này được chỉ định trong Thông số kỹ thuật về việc 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 liên kết người dùng đó với một mã nhận dạng người dùng dành riêng cho ứng dụng của bạn.
  • Hãy gọi AuthorizationCodeFlow.loadCredential(String), dựa trên mã nhận dạng người dùng để kiểm tra xem thông tin xác thực của người dùng đã được xác định hay chưa. Nếu vậy, bạn đã hoàn tất.
  • Nếu không, hãy gọi AuthorizationCodeFlow.newAuthorizationUrl() rồi 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 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ọ.
  • Sau đó, trình duyệt web sẽ chuyển hướng đến URL chuyển hướng bằng một tham số truy vấn "mã". Sau đó, tham số truy vấn này có thể dùng để yêu cầu mã truy cập bằng cách sử dụng AuthorizationCodeFlow.newTokenRequest(String).
  • Sử dụng AuthorizeCodeFlow.createAndStoreCredential(TokenResponse, String) để lưu trữ và lấy thông tin xác thực cho việc truy cập vào các tài nguyên được bảo vệ.

Ngoài ra, nếu không sử dụng AuthorizationCodeFlow, bạn có thể sử 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ỉ cần cung cấp các lớp con cụ thể của AbstractAuthorizationCodeServletAbstractAuthorizationCodeCallbackServlet (từ google-oauth-client-servlet) rồi thêm các lớp đó 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 việc đăng nhập của người dùng cho ứng dụng web 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 quy trình mã uỷ quyền servlet, ngoại trừ việc chúng ta có thể tận dụng API Java cho người dùng của Google App Engine. Người dùng cần đăng nhập để bật API Java cho người dùng; để biết thông tin về cách chuyển hướng người dùng đến trang đăng nhập nếu họ chưa đăng nhập, hãy xem phần Bảo mật và xác thực (trong web.xml).

Điểm khác biệt chính so với trường hợp servlet là bạn cung cấp các lớp con cụ thể của AbstractAppEngineAuthorizationCodeServletAbstractAppEngineAuthorizationCodeCallbackServlet (từ google-oauth-client-appengine). Các lớp này 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 cách sử dụng API Java cho người dùng. AppEngineDataStoreFactory (từ Thư viện ứng dụng Google HTTP cho Java là một lựa chọn phù hợp để lưu giữ thông tin xác thực bằng cách sử dụng Google App Engine Data Store API.

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

Dưới đâ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 về Cấp quyền ngầm ẩn:

  • Bằng cách sử dụng BrowserClientRequestUrl, hãy chuyển hướng trình duyệt của người dùng cuối đến 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ã truy cập tìm thấy trong mảnh URL tại URI chuyển hướng đã được đăng ký với máy chủ uỷ quyền.

Cách sử dụng mẫu cho 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 của trình truyền 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ệ bằng mã truy cập đã hết hạn, máy chủ thường phản hồi bằng mã trạng thái HTTP 401 Unauthorized như sau:

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

Tuy nhiên, thông số kỹ thuật có vẻ rất linh hoạt. Để biết thông tin chi tiết, hãy xem tài liệu của 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 của mã truy cập. Giá trị 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, 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 cuối khoảng thời gian đó và máy chủ có thể tiếp tục cho phép truy cập. Đó là lý do bạn nên đợi mã trạng thái 401 Unauthorized thay vì giả định 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ể thử làm mới mã truy cập ngay trước khi hết hạn. Nếu máy chủ mã thông báo không hoạt động, hãy tiếp tục sử dụng mã truy cập đó cho đến khi bạn nhận được 401. Đây là chiến lược được sử dụng theo mặc định trong phần Thông tin xác thực.

Một phương án khác là lấy mã truy cập mới trước mỗi yêu cầu. Tuy nhiên, cách này luôn đòi hỏi một yêu cầu HTTP bổ sung đến máy chủ mã thông báo. Vì vậy, đây có thể là một lựa chọn không tốt 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 các yêu cầu của ứng dụng về mã truy cập mới. (Nhưng đối với các ứng dụng đã cài đặt, việc lưu trữ an toàn là một vấn đề khó khăn.)

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