Sử dụng OAuth 2.0 với Thư viện ứng dụng API của Google cho Java

Tổng quan

Mục đích: Tài liệu này giải thích cách sử dụng lớp tiện ích GoogleCredential để uỷ quyền OAuth 2.0 với các dịch vụ của Google. Để biết thông tin về các hàm OAuth 2.0 chung mà chúng tôi cung cấp, hãy xem OAuth 2.0 và Thư viện ứng dụng Google OAuth cho Java.

Tóm tắt: Để truy cập vào dữ liệu được bảo vệ được lưu trữ trên các dịch vụ của Google, hãy dùng OAuth 2.0 để uỷ quyền. Các API của Google hỗ trợ quy trình OAuth 2.0 cho nhiều loại ứng dụng khách. Trong tất cả các quy trình này, ứng dụng đều yêu cầu một mã truy cập chỉ liên kết với ứng dụng của bạn và chủ sở hữu của dữ liệu được bảo vệ đang được truy cập. Mã truy cập cũng được liên kết với một phạm vi giới hạn dùng để xác định loại dữ liệu mà ứng dụng của bạn có quyền truy cập (ví dụ: "Quản lý công việc của bạn"). Mục tiêu quan trọng của OAuth 2.0 là cung cấp quyền truy cập an toàn và thuận tiện vào dữ liệu được bảo vệ, đồng thời giảm thiểu tác động tiềm ẩn nếu mã truy cập bị đánh cắp.

Các gói OAuth 2.0 trong Thư viện ứng dụng API của Google cho Java được xây dựng dựa trên Thư viện ứng dụng Google OAuth 2.0 cho Java đa năng.

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

Bảng điều khiển API của Google

Trước khi có thể truy cập API Google, bạn cần thiết lập một dự án trên Google API Console cho mục đích xác thực và thanh toán, cho dù ứng dụng của bạn là ứng dụng đã cài đặt, ứng dụng dành cho thiết bị di động, máy chủ web hay ứng dụng chạy trong trình duyệt.

Để biết hướng dẫn về cách thiết lập đúng thông tin xác thực, hãy xem bài viết Trợ giúp về Bảng điều khiển API.

Bằng chứng xác thực

GoogleCredential

GoogleCredential là một lớp trợ giúp an toàn cho luồng cho OAuth 2.0 để truy cập vào các tài nguyên được bảo vệ bằng mã truy cập. Ví dụ: nếu đã có mã truy cập, bạn có thể gửi yêu cầu theo cách sau:

GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
Plus plus = new Plus.builder(new NetHttpTransport(),
                             GsonFactory.getDefaultInstance(),
                             credential)
    .setApplicationName("Google-PlusSample/1.0")
    .build();

Danh tính của Google App Engine

Thông tin đăng nhập thay thế này dựa trên API Java của Google App Engine App Identity. Không giống như thông tin đăng nhập mà ứng dụng khách yêu cầu quyền truy cập vào dữ liệu của người dùng cuối, App Identity API cung cấp quyền truy cập vào dữ liệu của chính ứng dụng đó.

Sử dụng AppIdentityCredential (từ google-api-client-appengine). Thông tin đăng nhập này đơn giản hơn nhiều vì Google App Engine xử lý tất cả các chi tiết. Bạn chỉ chỉ định phạm vi OAuth 2.0 mình cần.

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

Lưu trữ dữ liệu

Mã truy cập thường có ngày hết hạn là 1 giờ. Sau ngày đó, bạn sẽ gặp lỗi nếu cố sử dụng mã này. GoogleCredential sẽ tự động "làm mới" mã thông báo, đơn giản là nhận mã truy cập mới. Bạn có thể thực hiện việc này bằng một mã làm mới tồn tại trong thời gian dài. Mã này thường được nhận cùng với mã truy cập nếu bạn sử dụng tham số access_type=offline trong quy trình mã uỷ quyền (hãy xem GoogleAuthorizationCodeFlow.Builder.setAccessType(String)).

Hầu hết ứng dụng sẽ cần duy trì mã truy cập của thông tin xác thực và/hoặc mã làm mới. Để duy trì quyền truy cập và/hoặc mã làm mới của thông tin xác thực, bạn có thể cung cấp phương thức triển khai DataStoreFactory của riêng mình bằng StoredCredential; hoặc 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:

  • AppEngineDataStoreFactory: giữ thông tin đăng nhập 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: giữ thông tin xác thực trong một tệp.

Người dùng AppEngine: AppEngineCredentialStore không được dùng nữa và sẽ sớm bị xoá. 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 kiểu cũ, bạn có thể sử dụng các phương thức trợ giúp mà chúng tôi đã thêm là migrationTo(AppEngineDataStoreFactory) hoặc migrationTo(DataStore) để thực hiện việc di chuyển.

Bạn có thể sử dụng DataStoreCredentialRefreshListener và đặt cho thông tin xác thực bằng cách sử dụ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ọ trên các API của Google. Giao thức cho quy trình này được chỉ định trong bài viết Cấp mã uỷ quyền.

Quy trình này được triển khai bằng GoogleAuthorizationCodeFlow. 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 sẽ 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 cuối đã được xác định hay chưa. Nếu vậy thì chúng ta đã 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 để 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 đó, máy chủ uỷ quyền của Google sẽ chuyển hướng trình duyệt trở lại URL chuyển hướng do ứng dụng của bạn chỉ định, cùng với tham số truy vấn code. Sử dụng tham số code để yêu cầu mã truy cập bằ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 GoogleAuthorizationCodeFlow, bạn có thể sử dụng các lớp cấp thấp hơn:

Khi thiết lập dự án trong Bảng điều khiển API của Google, bạn có thể chọn trong số các thông tin xác thực khác nhau, tuỳ thuộc vào quy trình bạn đang sử dụng. Để biết thêm thông tin chi tiết, hãy xem bài viết Thiết lập OAuth 2.0Các trường hợp OAuth 2.0. Dưới đây là đoạn mã cho từng luồng.

Ứng dụng máy chủ web

Giao thức cho quy trình này được giải thích trong bài viết Sử dụng OAuth 2.0 cho ứng dụng máy chủ web.

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

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

Ứng dụng 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 đối tượng 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ừ google-http-client-appengine) là một lựa chọn phù hợp để duy trì thông tin xác thực bằng cách sử dụng Google App Engine Data Store API.

Ví dụ được lấy (sửa đổi một chút) từ 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();
  }
}

Để xem thêm mẫu, hãy xem phần storage-serviceaccount-appengine-sample.

Tài khoản dịch vụ

GoogleCredential cũng hỗ trợ tài khoản dịch vụ. Không giống như thông tin đăng nhập mà ứng dụng khách yêu cầu quyền truy cập vào dữ liệu của người dùng cuối, Tài khoản dịch vụ cung cấp quyền truy cập vào dữ liệu riêng của ứng dụng đó. Ứng dụng khách của bạn ký yêu cầu mã truy cập bằng cách sử dụng khoá riêng tư được tải xuống từ Google API Console.

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

Để xem thêm mẫu, hãy xem mục storage-serviceaccount-cmdline-sample.

Mạo danh

Bạn cũng có thể sử dụng quy trình tài khoản dịch vụ để mạo danh người dùng trong miền mà bạn sở hữu. Quy trình này rất giống với quy trình của tài khoản dịch vụ ở trên, nhưng bạn có thể gọi thêm GoogleCredential.Builder.setServiceAccountUser(String).

Các ứng dụng đã cài đặt

Đây là quy trình mã uỷ quyền dòng lệnh được mô tả trong phần Sử dụng OAuth 2.0 cho ứng dụng đã cài đặt.

Đoạn mã mẫu từ 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");
}

Ứng dụng phía máy khách

Để sử dụng luồng ứng dụng dựa trên trình duyệt như mô tả trong bài viết Sử dụng OAuth 2.0 cho ứng dụng phía máy khách, thông thường, bạn sẽ làm theo các bước sau:

  1. Chuyển hướng người dùng cuối trong trình duyệt đến trang uỷ quyền bằng cách sử dụng GoogleBrowserClientRequestUrl để cấp cho ứng dụng trình duyệt của bạn quyền truy cập vào dữ liệu được bảo vệ của người dùng cuối.
  2. Sử dụng Thư viện ứng dụng API Google cho JavaScript để xử lý mã truy cập có trong mảnh URL tại URI chuyển hướng đã đăng ký tại Bảng điều khiển API của Google.

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

Bạn nên sử dụng thư viện nào với Android:

Nếu bạn đang phát triển cho Android và API Google mà bạn muốn sử dụng đã có trong thư viện Dịch vụ Google Play, hãy sử dụng thư viện đó để có được hiệu suất và trải nghiệm tốt nhất. Nếu API Google mà bạn muốn sử dụng với Android không nằm trong thư viện Dịch vụ Google Play, thì bạn có thể sử dụng Thư viện ứng dụng Google API cho Java, hỗ trợ Android 4.0 (Ice Kem Sandwich) (trở lên) và được mô tả ở đây. Hỗ trợ cho Android trong Thư viện ứng dụng API của Google cho Java là @Beta.

Thông tin cơ bản:

Kể từ Eclair (SDK 2.1), tài khoản người dùng được quản lý trên thiết bị Android bằng Trình quản lý tài khoản. Mọi quy trình uỷ quyền cho ứng dụng Android đều được SDK quản lý tập trung bằng AccountManager. Bạn chỉ định phạm vi OAuth 2.0 mà ứng dụng cần và phạm vi này sẽ trả về một mã truy cập để sử dụng.

Phạm vi OAuth 2.0 được chỉ định thông qua tham số authTokenType dưới dạng oauth2: cùng với phạm vi. Ví dụ:

oauth2:https://www.googleapis.com/auth/tasks

Thao tác này chỉ định quyền đọc/ghi đối với API Google Tasks. Nếu bạn cần nhiều phạm vi OAuth 2.0, hãy sử dụng một danh sách được phân tách bằng dấu cách.

Một số API có các tham số authTokenType đặc biệt cũng hoạt động được. Ví dụ: "Quản lý công việc của bạn" là biệt hiệu của ví dụ authtokenType hiển thị ở trên.

Bạn cũng phải chỉ định khoá API trong Google API Console. Nếu không, mã thông báo mà AccountManager cung cấp cho bạn chỉ cung cấp cho bạn hạn mức ẩn danh, thường là rất thấp. Ngược lại, bằng cách chỉ định khoá API, bạn sẽ nhận được hạn mức miễn phí cao hơn và có thể tuỳ ý thiết lập thông tin thanh toán cho mức sử dụng trên.

Đoạn mã mẫu được lấy từ 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;
  }
}