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 OAuth của Google cho Java cung cấp. Bạn có thể sử dụng các hàm này để xác thực và uỷ quyền cho bất kỳ dịch vụ Internet nào.

Để biết hướng dẫn về cách sử dụng GoogleCredential để uỷ quyền OAuth 2.0 bằng các dịch vụ của Google, hãy xem phần Sử dụng OAuth 2.0 với Thư viện ứng dụng API Google cho Java.

Tóm tắt: OAuth 2.0 là một thông số kỹ thuật tiêu chuẩn cho phép người dùng cuối uỷ quyền một cách 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, quy cách về mã thông báo mang tải OAuth 2.0 sẽ giải thích cách truy cập vào các tài nguyên được bảo vệ đó bằng mã truy cập được cấp trong quá trình uỷ quyền của 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 khách OAuth của Google cho Java, có thể bạn cần đăng ký ứng dụng của mình với một máy chủ uỷ quyền để nhận mã ứng dụng và mật khẩu ứng dụng. (Để biết thông tin chung về quy trình này, hãy xem Quy cách của tính năng Đăng ký ứng dụng.)

Thông tin xác thực và vùng lưu trữ 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 với 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 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 cần duy trì mã truy cập của thông tin đăng nhập và mã làm mới để tránh việc sau này chuyển hướng đến trang uỷ quyền trong trình duyệt. 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. Một phương án thay thế là sử dụng giao diện DataStoreFactoryDataStoreStoredCredential do Thư viện ứng dụng HTTP của Google dành cho Java cung cấp.

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

  • JdoDataStoreFactory giữ lại thông tin đăng nhập bằng cách sử dụng JDO.
  • AppEngineDataStoreFactory lưu trữ thông tin xác thực bằng API Google App Engine Data Store.
  • MemoryDataStoreFactory "duy trì" thông tin xác thực trong bộ nhớ, chỉ hữu ích như một bộ nhớ ngắn hạn trong suốt vòng đời của quy trình.
  • FileDataStoreFactory lưu trữ 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ị loại bỏ.

Bạn nên sử dụng AppEngineDataStoreFactory với StoredCredential. Nếu đã lưu trữ thông tin xác thực 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 thông tin xác thực đó 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 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 luồng này được chỉ định trong quy cách cấp mã uỷ quyền.

Quy trình này được triển khai bằng cách sử dụ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 duy nhất cho ứng dụng của mình.
  • 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 đăng nhập 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à hướng trình duyệt của người dùng cuối đến một 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 có tham số truy vấn "mã" mà sau đó có thể được dùng để yêu cầu mã truy cập bằng AuthorizationCodeFlow.newTokenRequest(String).
  • Sử dụng AuthorizationCodeFlow.createAndStoreCredential(TokenResponse, String) để lưu trữ và lấy thông tin xác thực để truy cập vào các tài nguyên được bảo vệ.

Ngoài ra, nếu hiện 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ể luồng 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 con đó vào tệp web.xml. Xin lưu ý rằng bạn vẫn cần xử lý 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 sử dụng mã uỷ quyền của 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 Người dùng Java của Google App Engine. Người dùng cần đăng nhập để bật API Java của 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 của người dùng. AppEngineDataStoreFactory (từ Thư viện ứng dụng HTTP của Google cho Java là một lựa chọn phù hợp để lưu trữ thông tin xác thực bằng API Kho dữ liệu Google App Engine.

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 sử dụng 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

Sau đây là các bước thông thường của luồng ứng dụng dựa trên trình duyệt được chỉ định trong quy cách Cấp quyền ngầm ẩn:

  • Sử dụng BrowserClientRequestUrl, 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ọ.
  • Dùng ứng dụng JavaScript để xử lý mã truy cập có trong phân đoạn 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 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 quy cách của thông số truy cập 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, chẳng hạn như sau:

   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ư quy cách này 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 phương pháp thay thế là kiểm tra tham số expires_in trong phản hồi mã thông báo truy cập. Mã này chỉ định thời gian tồn tại tính bằng giây của mã truy cập được cấp (thường là một giờ). Tuy nhiên, mã thông báo 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 mã đó hết hạn. Nếu máy chủ mã 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 cách khác là lấy mã truy cập mới trước mỗi yêu cầu. Tuy nhiên, mỗi lần yêu cầu, bạn sẽ phải gửi thêm yêu cầu HTTP tới máy chủ mã thông báo. Vì vậy, bạn có thể sẽ không dùng được tốc độ và mức sử dụng mạng. Tốt nhất là lưu trữ mã thông báo truy cập trong bộ nhớ cố định, an toàn để giảm thiểu các yêu cầu của ứng dụng về mã thông báo truy cập mới. (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ể không hợp lệ vì các lý do khác ngoài việc hết hạn, chẳng hạn như nếu người dùng đã thu hồi mã thông báo một cách rõ ràng, vì vậy, hãy đảm bảo mã xử lý lỗi của bạn là mạnh mẽ. Sau khi phát hiện một mã thông báo không còn hợp lệ, chẳng hạn như nếu mã thông báo đó đã hết hạn hoặc bị thu hồi, bạn phải xoá mã thông báo truy cập khỏi bộ nhớ. Ví dụ: trên Android, bạn phải gọi AccountManager.invalidateAuthToken.