Trình xử lý gọi lại uỷ quyền bản dựng

Tài liệu này giải thích cách triển khai trình xử lý gọi lại uỷ quyền OAuth 2.0 bằng các dịch vụ Java thông qua một ứng dụng web mẫu sẽ hiển thị công việc của người dùng bằng API Google Tasks. Trước tiên, ứng dụng mẫu sẽ yêu cầu cấp quyền truy cập vào Google Tasks của người dùng, sau đó sẽ hiển thị các công việc của người dùng trong danh sách công việc mặc định.

Đối tượng người xem

Tài liệu này được thiết kế cho những người quen với kiến trúc ứng dụng web Java và J2EE. Bạn nên áp dụng một số kiến thức về quy trình uỷ quyền OAuth 2.0.

Nội dung

Để có một số bước mẫu hoạt động đầy đủ như vậy, bạn cần phải:

Khai báo ánh xạ servlet trong tệp web.xml

Chúng ta sẽ sử dụng 2 servlet trong ứng dụng này:

  • PrintTasksTitlesServlet (được liên kết tới /): Điểm truy cập của ứng dụng sẽ xử lý quy trình xác thực người dùng và sẽ hiển thị những việc cần làm của người dùng
  • OAuthCodeCallbackHandlerServlet (được ánh xạ tới /OAuth2callback): Lệnh gọi lại OAuth 2.0 xử lý phản hồi từ điểm cuối uỷ quyền OAuth

Dưới đây là tệp web.xml giúp ánh xạ 2 servlet này với các URL trong ứng dụng của chúng tôi:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

 <servlet>
   <servlet-name>PrintTasksTitles</servlet-name>
   <servlet-class>com.google.oauthsample.PrintTasksTitlesServlet</servlet-class>
 </servlet>

 <servlet-mapping>
   <servlet-name>PrintTasksTitles</servlet-name>
   <url-pattern>/</url-pattern>
 </servlet-mapping>

 <servlet>
   <servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name>
   <servlet-class>com.google.oauthsample.OAuthCodeCallbackHandlerServlet</servlet-class>
 </servlet>

 <servlet-mapping>
   <servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name>
   <url-pattern>/oauth2callback</url-pattern>
 </servlet-mapping>

</web-app>
Tệp /WEB-INF/web.xml

Xác thực người dùng trên hệ thống của bạn và yêu cầu uỷ quyền để truy cập vào các nhiệm vụ của hệ thống

Người dùng nhập ứng dụng thông qua URL "/" gốc được ánh xạ tới dịch vụ PrintTaskListsTitlesServlet. Trong servlet đó, các tác vụ sau sẽ được thực hiện:

  • Kiểm tra xem người dùng có được xác thực trên hệ thống hay không
  • Nếu người dùng chưa được xác thực, họ sẽ được chuyển hướng đến trang xác thực
  • Nếu người dùng được xác thực, chúng tôi sẽ kiểm tra xem đã có mã làm mới trong bộ nhớ dữ liệu của mình hay chưa. Mã này do OAuthTokenDao xử lý bên dưới. Nếu không có mã làm mới nào trong cửa hàng cho người dùng, thì tức là người dùng chưa cấp quyền cho ứng dụng truy cập vào tác vụ của mình. Trong trường hợp đó, người dùng sẽ được chuyển hướng đến điểm cuối Uỷ quyền OAuth 2.0 của Google.
Dưới đây là cách triển khai việc này:

package com.google.oauthsample;

import ...

/**
 * Simple sample Servlet which will display the tasks in the default task list of the user.
 */
@SuppressWarnings("serial")
public class PrintTasksTitlesServlet extends HttpServlet {

  /**
   * The OAuth Token DAO implementation, used to persist the OAuth refresh token.
   * Consider injecting it instead of using a static initialization. Also we are
   * using a simple memory implementation as a mock. Change the implementation to
   * using your database system.
   */
  public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();

  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // Getting the current user
    // This is using App Engine's User Service but you should replace this to
    // your own user/login implementation
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();

    // If the user is not logged-in it is redirected to the login service, then back to this page
    if (user == null) {
      resp.sendRedirect(userService.createLoginURL(getFullRequestUrl(req)));
      return;
    }

    // Checking if we already have tokens for this user in store
    AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail());

    // If we don't have tokens for this user
    if (accessTokenResponse == null) {
      OAuthProperties oauthProperties = new OAuthProperties();
      // Redirect to the Google OAuth 2.0 authorization endpoint
      resp.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
          OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
              .getScopesAsString()).build());
      return;
    }
  }

  /**
   * Construct the request's URL without the parameter part.
   *
   * @param req the HttpRequest object
   * @return The constructed request's URL
   */
  public static String getFullRequestUrl(HttpServletRequest req) {
    String scheme = req.getScheme() + "://";
    String serverName = req.getServerName();
    String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort();
    String contextPath = req.getContextPath();
    String servletPath = req.getServletPath();
    String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo();
    String queryString = (req.getQueryString() == null) ? "" : "?" + req.getQueryString();
    return scheme + serverName + serverPort + contextPath + servletPath + pathInfo + queryString;
  }
}
Tệp PrintTasksTitlesServlet.java

Lưu ý: Cách triển khai ở trên sử dụng một số thư viện App Engine. Các thư viện này được dùng để đơn giản hoá. Nếu bạn đang phát triển cho một nền tảng khác, vui lòng triển khai lại giao diện UserService. Giao diện này xử lý việc xác thực người dùng.

Ứng dụng dùng DAO để duy trì và truy cập mã thông báo uỷ quyền của người dùng. Dưới đây là giao diện – OAuthTokenDao - và mô phỏng triển khai (trong bộ nhớ) – OAuthTokenDaoMemoryImpl - được sử dụng trong mẫu này:

package com.google.oauthsample;

import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse;

/**
 * Allows easy storage and access of authorization tokens.
 */
public interface OAuthTokenDao {

  /**
   * Stores the given AccessTokenResponse using the {@code username}, the OAuth
   * {@code clientID} and the tokens scopes as keys.
   *
   * @param tokens The AccessTokenResponse to store
   * @param userName The userName associated wit the token
   */
  public void saveKeys(AccessTokenResponse tokens, String userName);

  /**
   * Returns the AccessTokenResponse stored for the given username, clientId and
   * scopes. Returns {@code null} if there is no AccessTokenResponse for this
   * user and scopes.
   *
   * @param userName The username of which to get the stored AccessTokenResponse
   * @return The AccessTokenResponse of the given username
   */
  public AccessTokenResponse getKeys(String userName);
}
Tệp OAuthTokenDao.java
package com.google.oauthsample;

import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse;
...

/**
 * Quick and Dirty memory implementation of {@link OAuthTokenDao} based on
 * HashMaps.
 */
public class OAuthTokenDaoMemoryImpl implements OAuthTokenDao {

  /** Object where all the Tokens will be stored */
  private static Map tokenPersistance = new HashMap();

  public void saveKeys(AccessTokenResponse tokens, String userName) {
    tokenPersistance.put(userName, tokens);
  }

  public AccessTokenResponse getKeys(String userName) {
    return tokenPersistance.get(userName);
  }
}
Tệp OAuthTokenDaoMemoryImpl.java

Ngoài ra, thông tin xác thực OAuth 2.0 cho ứng dụng cũng được lưu trữ trong tệp thuộc tính. Ngoài ra, bạn có thể chỉ cần đặt chúng dưới dạng hằng số ở một nơi nào đó trong một trong các lớp Java của mình, mặc dù đây là lớp OAuthProperties và tệp oauth.properties đang được dùng trong mẫu:

package com.google.oauthsample;

import ...

/**
 * Object representation of an OAuth properties file.
 */
public class OAuthProperties {

  public static final String DEFAULT_OAUTH_PROPERTIES_FILE_NAME = "oauth.properties";

  /** The OAuth 2.0 Client ID */
  private String clientId;

  /** The OAuth 2.0 Client Secret */
  private String clientSecret;

  /** The Google APIs scopes to access */
  private String scopes;

  /**
   * Instantiates a new OauthProperties object reading its values from the
   * {@code OAUTH_PROPERTIES_FILE_NAME} properties file.
   *
   * @throws IOException IF there is an issue reading the {@code propertiesFile}
   * @throws OauthPropertiesFormatException If the given {@code propertiesFile}
   *           is not of the right format (does not contains the keys {@code
   *           clientId}, {@code clientSecret} and {@code scopes})
   */
  public OAuthProperties() throws IOException {
    this(OAuthProperties.class.getResourceAsStream(DEFAULT_OAUTH_PROPERTIES_FILE_NAME));
  }

  /**
   * Instantiates a new OauthProperties object reading its values from the given
   * properties file.
   *
   * @param propertiesFile the InputStream to read an OAuth Properties file. The
   *          file should contain the keys {@code clientId}, {@code
   *          clientSecret} and {@code scopes}
   * @throws IOException IF there is an issue reading the {@code propertiesFile}
   * @throws OAuthPropertiesFormatException If the given {@code propertiesFile}
   *           is not of the right format (does not contains the keys {@code
   *           clientId}, {@code clientSecret} and {@code scopes})
   */
  public OAuthProperties(InputStream propertiesFile) throws IOException {
    Properties oauthProperties = new Properties();
    oauthProperties.load(propertiesFile);
    clientId = oauthProperties.getProperty("clientId");
    clientSecret = oauthProperties.getProperty("clientSecret");
    scopes = oauthProperties.getProperty("scopes");
    if ((clientId == null) || (clientSecret == null) || (scopes == null)) {
      throw new OAuthPropertiesFormatException();
    }
  }

  /**
   * @return the clientId
   */
  public String getClientId() {
    return clientId;
  }

  /**
   * @return the clientSecret
   */
  public String getClientSecret() {
    return clientSecret;
  }

  /**
   * @return the scopes
   */
  public String getScopesAsString() {
    return scopes;
  }

  /**
   * Thrown when the OAuth properties file was not at the right format, i.e not
   * having the right properties names.
   */
  @SuppressWarnings("serial")
  public class OAuthPropertiesFormatException extends RuntimeException {
  }
}
Tệp OAuthProperties.java

Dưới đây là tệp oauth.properties chứa thông tin xác thực OAuth 2.0 trong ứng dụng của bạn. Bạn cần tự thay đổi các giá trị bên dưới.

# Client ID and secret. They can be found in the APIs console.
clientId=1234567890.apps.googleusercontent.com
clientSecret=aBcDeFgHiJkLmNoPqRsTuVwXyZ
# API scopes. Space separated.
scopes=https://www.googleapis.com/auth/tasks
Tệp OAuth.properties

Mã ứng dụng khách OAuth 2.0 và Mật khẩu ứng dụng khách sẽ nhận dạng ứng dụng của bạn và cho phép API Tasks áp dụng bộ lọc và quy tắc hạn mức đã được xác định cho ứng dụng của bạn. Bạn có thể tìm thấy mã ứng dụng khách và khóa bí mật trong Bảng điều khiển API của Google. Khi đã vào bảng điều khiển, bạn sẽ phải:

  • Tạo hoặc chọn một dự án.
  • Bật API Tasks bằng cách chuyển đổi trạng thái API Tasks thành BẬT trong danh sách dịch vụ.
  • Trong phần Quyền truy cập API, hãy tạo một Mã ứng dụng khách OAuth 2.0 nếu chưa tạo.
  • Hãy đảm bảo rằng URL trình xử lý gọi lại mã OAuth 2.0 của dự án đã được đăng ký/đưa vào danh sách cho phép trong URI chuyển hướng. Ví dụ: trong dự án mẫu này, bạn sẽ phải đăng ký https://www.example.com/oauth2callback nếu ứng dụng web được phân phát từ miền https://www.example.com/oauth2callback.

URI chuyển hướng trong Bảng điều khiển API
URI chuyển hướng trong Bảng điều khiển API

Theo dõi mã uỷ quyền từ điểm cuối Google Uỷ quyền

Trong trường hợp người dùng chưa cho phép ứng dụng truy cập vào tác vụ của ứng dụng và do đó đã được chuyển hướng đến điểm cuối Uỷ quyền OAuth 2.0 của Google, người dùng sẽ hiển thị hộp thoại uỷ quyền từ Google yêu cầu người dùng cấp cho ứng dụng của bạn quyền truy cập vào các tác vụ của ứng dụng:

Hộp thoại uỷ quyền của Google
Hộp thoại uỷ quyền của Google

Sau khi cấp hoặc từ chối quyền truy cập, người dùng sẽ được chuyển hướng trở lại trình xử lý gọi lại mã OAuth 2.0 đã được chỉ định làm lệnh chuyển hướng/lệnh gọi lại khi tạo URL uỷ quyền cho Google:

new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
      OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
          .getScopesAsString()).build()

Trình xử lý lệnh gọi lại mã OAuth 2.0 – OAuthCodeCallbackHandlerServlet – xử lý hoạt động chuyển hướng từ điểm cuối của Google OAuth 2.0. Có 2 trường hợp cần xử lý:

  • Người dùng đã cấp quyền truy cập: phân tích cú pháp yêu cầu để lấy mã OAuth 2.0 qua tham số URL
  • Người dùng đã từ chối quyền truy cập: hiển thị thông báo cho người dùng

package com.google.oauthsample;

import ...

/**
 * Servlet handling the OAuth callback from the authentication service. We are
 * retrieving the OAuth code, then exchanging it for a refresh and an access
 * token and saving it.
 */
@SuppressWarnings("serial")
public class OAuthCodeCallbackHandlerServlet extends HttpServlet {

  /** The name of the Oauth code URL parameter */
  public static final String CODE_URL_PARAM_NAME = "code";

  /** The name of the OAuth error URL parameter */
  public static final String ERROR_URL_PARAM_NAME = "error";

  /** The URL suffix of the servlet */
  public static final String URL_MAPPING = "/oauth2callback";

  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // Getting the "error" URL parameter
    String[] error = req.getParameterValues(ERROR_URL_PARAM_NAME);

    // Checking if there was an error such as the user denied access
    if (error != null && error.length > 0) {
      resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "There was an error: \""+error[0]+"\".");
      return;
    }
    // Getting the "code" URL parameter
    String[] code = req.getParameterValues(CODE_URL_PARAM_NAME);

    // Checking conditions on the "code" URL parameter
    if (code == null || code.length == 0) {
      resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "The \"code\" URL parameter is missing");
      return;
    }
  }

  /**
   * Construct the OAuth code callback handler URL.
   *
   * @param req the HttpRequest object
   * @return The constructed request's URL
   */
  public static String getOAuthCodeCallbackHandlerUrl(HttpServletRequest req) {
    String scheme = req.getScheme() + "://";
    String serverName = req.getServerName();
    String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort();
    String contextPath = req.getContextPath();
    String servletPath = URL_MAPPING;
    String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo();
    return scheme + serverName + serverPort + contextPath + servletPath + pathInfo;
  }
}
Tệp OAuthCodeCallbackHandlerServlet.java

Đổi mã uỷ quyền để lấy mã làm mới và mã truy cập

Sau đó, OAuthCodeCallbackHandlerServlet trao đổi mã Auth 2.0 để lấy mã thông báo làm mới và truy cập, lưu mã này trong kho dữ liệu và chuyển hướng người dùng quay lại URL OAuthCodeCallbackHandlerServlet:

Mã được thêm vào tệp bên dưới được đánh dấu cú pháp, mã hiện có có màu xám.

package com.google.oauthsample;

import ...

/**
 * Servlet handling the OAuth callback from the authentication service. We are
 * retrieving the OAuth code, then exchanging it for a refresh and an access
 * token and saving it.
 */
@SuppressWarnings("serial")
public class OAuthCodeCallbackHandlerServlet extends HttpServlet {

  /** The name of the Oauth code URL parameter */
  public static final String CODE_URL_PARAM_NAME = "code";

  /** The name of the OAuth error URL parameter */
  public static final String ERROR_URL_PARAM_NAME = "error";

  /** The URL suffix of the servlet */
  public static final String URL_MAPPING = "/oauth2callback";
/** URL cần chuyển hướng người dùng đến sau khi xử lý lệnh gọi lại. Hãy cân nhắc việc * lưu URL này vào cookie trước khi chuyển hướng người dùng đến URL uỷ quyền của Google * nếu bạn có nhiều URL để chuyển hướng người dùng đến. */ public static end String REDIRECT_URL = "/"; /** Triển khai DAO của mã thông báo OAuth. Hãy cân nhắc việc chèn dữ liệu này thay vì sử dụng * phương thức khởi chạy tĩnh. Ngoài ra, chúng tôi đang sử dụng một mô-đun triển khai bộ nhớ đơn giản * để mô phỏng. Thay đổi phương thức triển khai sang sử dụng hệ thống cơ sở dữ liệu. */ public static OAuthTokenDao dmca
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // Getting the "error" URL parameter
    String[] error = req.getParameterValues(ERROR_URL_PARAM_NAME);

    // Checking if there was an error such as the user denied access
    if (error != null && error.length > 0) {
      resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "There was an error: \""+error[0]+"\".");
      return;
    }

    // Getting the "code" URL parameter
    String[] code = req.getParameterValues(CODE_URL_PARAM_NAME);

    // Checking conditions on the "code" URL parameter
    if (code == null || code.length == 0) {
      resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "The \"code\" URL parameter is missing");
      return;
    }
  /**
   * Construct the OAuth code callback handler URL.
   *
   * @param req the HttpRequest object
   * @return The constructed request's URL
   */
  public static String getOAuthCodeCallbackHandlerUrl(HttpServletRequest req) {
    String scheme = req.getScheme() + "://";
    String serverName = req.getServerName();
    String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort();
    String contextPath = req.getContextPath();
    String servletPath = URL_MAPPING;
    String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo();
    return scheme + serverName + serverPort + contextPath + servletPath + pathInfo;
  }
* * @param code .
Tệp OAuthCodeCallbackHandlerServlet.java

Lưu ý: Cách triển khai ở trên sử dụng một số thư viện App Engine. Các thư viện này được dùng để đơn giản hoá. Nếu bạn đang phát triển cho một nền tảng khác, vui lòng triển khai lại giao diện UserService. Giao diện này xử lý việc xác thực người dùng.

Đọc và hiển thị công việc của người dùng

Người dùng đã cấp cho ứng dụng quyền truy cập vào các tác vụ của ứng dụng. Ứng dụng có một mã làm mới được lưu trong kho dữ liệu có thể truy cập được qua OAuthTokenDao. Giờ đây, dịch vụ PrintTaskListsTitlesServlet có thể sử dụng các mã thông báo này để truy cập vào các việc cần làm của người dùng và hiển thị chúng:

Mã được thêm vào tệp bên dưới được đánh dấu cú pháp, mã hiện có có màu xám.

package com.google.oauthsample;

import ...

/**
 * Simple sample Servlet which will display the tasks in the default task list of the user.
 */
@SuppressWarnings("serial")
public class PrintTasksTitlesServlet extends HttpServlet {

  /**
   * The OAuth Token DAO implementation, used to persist the OAuth refresh token.
   * Consider injecting it instead of using a static initialization. Also we are
   * using a simple memory implementation as a mock. Change the implementation to
   * using your database system.
   */
  public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();

  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // Getting the current user
    // This is using App Engine's User Service but you should replace this to
    // your own user/login implementation
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();

    // If the user is not logged-in it is redirected to the login service, then back to this page
    if (user == null) {
      resp.sendRedirect(userService.createLoginURL(getFullRequestUrl(req)));
      return;
    }

    // Checking if we already have tokens for this user in store
    AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail());

    // If we don't have tokens for this user
    if (accessTokenResponse == null) {
      OAuthProperties oauthProperties = new OAuthProperties();
      // Redirect to the Google OAuth 2.0 authorization endpoint
      resp.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
          OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
              .getScopesAsString()).build());
      return;
    }
// In tác vụ của người dùng liệt kê các tiêu đề trong phản hồi resp.setContentType("text/plain"); resp.getWriter().append("Danh sách nhiệm vụ cho người dùng " + user.getEmail() + ":\n\n"); printTasksTitles(accessTokenResponse, resp.getWriter();
  }

  /**
   * Construct the request's URL without the parameter part.
   *
   * @param req the HttpRequest object
   * @return The constructed request's URL
   */
  public static String getFullRequestUrl(HttpServletRequest req) {
    String scheme = req.getScheme() + "://";
    String serverName = req.getServerName();
    String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort();
    String contextPath = req.getContextPath();
    String servletPath = req.getServletPath();
    String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo();
    String queryString = (req.getQueryString() == null) ? "" : "?" + req.getQueryString();
    return scheme + serverName + serverPort + contextPath + servletPath + pathInfo + queryString;
  }
/** * Dùng API Google Tasks để truy xuất một danh sách các tác vụ trong danh sách * * @param accessTokenResponse Đối tượng AccessTokenResponse của OAuth 2.0 * chứa mã truy cập và một mã làm mới. * @param xuất ra trình ghi luồng đầu ra nơi để tiêu đề các công việc liệt kê các tiêu đề * @return Danh sách các công việc của người dùng trong danh sách công việc mặc định. * @throws IOException
Tệp PrintTasksTitlesServlet.java

Người dùng sẽ thấy các nhiệm vụ sau đây:

Việc cần làm của người dùng
Việc cần làm của người dùng

Ứng dụng mẫu

Bạn có thể tải mã cho ứng dụng mẫu này xuống tại đây. Bạn cứ tự nhiên khám phá nhé.