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

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 để thực hiện ủy 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 OAuth của Google dành cho Java.

Tóm tắt: Để truy cập vào dữ liệu được bảo vệ lưu trữ trên các dịch vụ của Google, hãy dùng OAuth 2.0 để uỷ quyền. API của Google hỗ trợ luồng 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 khách yêu cầu một mã thông báo truy cập chỉ liên kết với ứng dụng khách và chủ sở hữu 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 xác định loại dữ liệu mà ứng dụng khách của bạn có quyền truy cập (ví dụ: "Quản lý việc cần làm"). 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 Java cho API của Google được xây dựng dựa trên Thư viện ứng dụng Google OAuth 2.0 cho Java.

Để 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 của Google, bạn cần thiết lập một dự án trên Bảng điều khiển API của Google 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 khách chạy trong trình duyệt.

Để biết hướng dẫn về cách thiết lập thông tin đăng nhập của bạn đúng cách, hãy xem phần Trợ giúp về Bảng điều khiển API.

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

Thông tin đăng nhập Google

GoogleCredential là một lớp trình trợ giúp an toàn luồng cho OAuth 2.0 để truy cập 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ể 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();

Thông tin nhận dạng trong Google App Engine

Thông tin đăng nhập thay thế này dựa trên API Java của Công cụ nhận dạng ứng dụng của Google App Engine. 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 người dùng cuối, API ứng dụng nhận dạng ứng dụng cung cấp quyền truy cập vào dữ liệu riêng của ứng dụng khách.

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

Mã mẫu đượ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();
}

Kho dữ liệu

Mã thông báo truy cập thường có ngày hết hạn là 1 giờ, sau đó bạn sẽ gặp lỗi nếu cố sử dụng. GoogleCredential sẽ tự động xử lý "Refreshing" mã thông báo, chỉ đơn giản có nghĩa là nhận mã thông báo truy cập mới. Việc này được thực hiện thông qua mã thông báo làm mới dài hạn. Mã này thường được nhận cùng với mã truy cập nếu bạn sử dụng thông số access_type=offline trong quy trình mã uỷ quyền (xem GoogleAuthorizationCodeFlow.Builder.setAccessType(String)).

Hầu hết các ứng dụng đều cần lưu giữ mã truy cập và/hoặc mã thông báo xác thực của thông tin đăng nhập. Để lưu giữ mã truy cập và/hoặc mã thông báo làm mới, bạn có thể triển khai DataStoreFactory của riêng mình với 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ữ nguyên thông tin xác thực bằng cách sử dụng API Google App Engine Data Store.
  • MemoryDataStoreFactory: "persist" thông tin xác thực trong bộ nhớ, chỉ hữu ích dưới dạng một bộ nhớ ngắn hạn trong suốt quá trình.
  • FileDataStoreFactory: giữ nguyên 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ị xóa. Bạn nên 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 migrationTo(AppEngineDataStoreFactory) hoặc migrationTo(DataStore) để di chuyển.

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

Luồng mã uỷ quyền

Sử dụng luồng 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ệ trên API Google. Giao thức của quy trình này được chỉ định trong phần 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 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.
  • 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 biết hay chưa. Nếu vậy, chúng ta sẽ hoàn tất.
  • Nếu không, hãy gọi AuthorizationCodeFlow.newauthorizedUrl() và chuyển hướng trình duyệt của người dùng cuối đến một 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ệ.
  • 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 thông số code để yêu cầu mã truy cập bằng cách sử dụng AuthorizationCodeFlow.newTokenRequest(String).
  • Sử dụng DelegateCodeFlow.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 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 Google API Console, bạn sẽ chọn một trong nhiều thông tin xác thực tuỳ thuộc vào quy trình mà bạn đang sử dụng. Để biết thêm thông tin chi tiết, hãy xem phần Thiết lập OAuth 2.0Trường hợp OAuth 2.0. Dưới đây là đoạn mã cho từng quy trình.

Ứng dụng máy chủ web

Giao thức của 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 GVP để đơ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 AbstractractCodeServletAbstractractCodeCallbackServlet(từ google-oauth-client-GVP) rồi thêm các tệp đó vào tệp web.xml. 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 của bạn 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 của GVP, ngoại trừ việc chúng tôi có thể tận dụng API Java người dùng của Google App Engine. Người dùng cần phải đăng nhập để bật 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 Bảo mật và xác thực (trong web.xml).

Điểm khác biệt chính với trường hợp GVP là bạn cung cấp các lớp con cụ thể của AbstractAppEngineDelegateCodeServletAbstractAppEngineDelegateCodeCallbackServlet (từ google-oauth-client-appengine). Các lớp này mở rộng các lớp GVP 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 người dùng. AppEngineDataStoreFactory (từ google-http-client-appengine) là một tùy chọn tốt để duy trì thông tin đăng nhập bằng cách sử dụng API Cửa hàng dữ liệu của Google App Engine.

Ví dụ về ảnh được chụp (được 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 nội dung 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 của ứng dụng khách. Ứng dụng khách của bạn ký yêu cầu về một mã truy cập bằng cách sử dụng một khóa riêng tư được tải xuống từ Bảng điều khiển API của Google.

Mã mẫu đượ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 nội dung 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. Điều này rất giống với quy trình tài khoản dịch vụ ở trên, nhưng bạn cũng sẽ gọi GoogleCredential.Builder.setServiceAccountUser(String).

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

Đây là luồng mã ủy quyền dòng lệnh được mô tả trong 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 quy trình ứng dụng dựa trên trình duyệt được 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 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 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. Hãy dùng Thư viện ứng dụng API của 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

@Thử nghiệm

Hãy sử dụng thư viện nào với Android:

Nếu bạn đang phát triển ứng dụng 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 đó để đạt được hiệu suất và trải nghiệm tốt nhất. Nếu API Google bạn muốn sử dụng với Android không thuộc thư viện Dịch vụ Google Play, bạn có thể sử dụng Thư viện ứng dụng Java cho API của Google. Thư viện này hỗ trợ Android 4.0 (Ice Cream Sandwich) (trở lên) và được mô tả ở đây. Hỗ trợ dành cho Android trong Thư viện ứng dụng API của Google dành 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 một thiết bị Android bằng cách sử dụng Trình quản lý tài khoản. Tất cả việc uỷ quyền ứng dụng Android đều do SDK quản lý tập trung bằng cách sử dụng AccountManager. Bạn chỉ định phạm vi OAuth 2.0 mà ứng dụng của mình cần và sẽ trả về mã truy cập để sử dụng.

Phạm vi OAuth 2.0 được xác đị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 vào API Google Tasks. Nếu bạn cần nhiều phạm vi OAuth 2.0, hãy dùng danh sách được phân tách bằng dấu cách.

Một số API có các thông số authTokenType đặc biệt cũng hoạt động như vậy. Ví dụ: "Manage your tasks" là bí danh của ví dụ authtokenType hiển thị ở trên.

Bạn cũng phải chỉ định khoá API từ Google API Console. Nếu không, mã thông báo mà Trình quản lý tài khoản 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 cao hơ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;
  }
}