Pengendali Callback Otorisasi Build

Dokumen ini menjelaskan cara menerapkan pengendali callback otorisasi OAuth 2.0 menggunakan servlet Java melalui aplikasi web contoh yang akan menampilkan tugas pengguna menggunakan Google Tasks API. Aplikasi contoh akan terlebih dahulu meminta otorisasi untuk mengakses Google Tasks pengguna, kemudian menampilkan tugas pengguna dalam daftar tugas default.

Audiens

Dokumen ini disesuaikan untuk orang-orang yang mengenal arsitektur aplikasi web Java dan J2EE. Anda sebaiknya memiliki pengetahuan tentang alur otorisasi OAuth 2.0.

Daftar Isi

Agar contoh fungsi berfungsi sepenuhnya, beberapa langkah diperlukan:

Mendeklarasikan pemetaan servlet di file web.xml

Kita akan menggunakan 2 servlet dalam aplikasi:

  • PrintTasksTitlesServlet (dipetakan ke /): Titik entri aplikasi yang akan menangani autentikasi pengguna, dan akan menampilkan tugas pengguna
  • OAuthCodeCallbackHandlerServlet (dipetakan ke /oauth2callback): Callback OAuth 2.0 yang menangani respons dari endpoint otorisasi OAuth

Berikut adalah file web.xml yang memetakan 2 servlet ini ke URL di aplikasi kita:

<?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>
File /WEB-INF/web.xml

Mengautentikasi pengguna di sistem dan meminta otorisasi untuk mengakses tugasnya.

Pengguna memasuki aplikasi melalui URL root '/' yang dipetakan ke servlet PrintTaskListsTitlesServlet. Dalam servlet tersebut, tugas-tugas berikut akan dijalankan:

  • Memeriksa apakah pengguna telah diotentikasi pada sistem
  • Jika pengguna tidak diautentikasi, dia akan dialihkan ke halaman autentikasi
  • Jika pengguna diautentikasi, kami akan memeriksa apakah ada token refresh yang sudah ada di penyimpanan data kami - yang ditangani oleh OAuthTokenDao di bawah. Jika tidak ada token refresh yang tersedia untuk pengguna, ini berarti pengguna belum memberikan otorisasi pada aplikasi untuk mengakses tugasnya. Dalam hal ini, pengguna dialihkan ke endpoint Otorisasi OAuth 2.0 Google.
Berikut cara untuk menerapkannya:

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;
  }
}
File PrintTasksTitlesServlet.java

Catatan: Implementasi di atas menggunakan beberapa library App Engine, yang digunakan sebagai penyederhanaan. Jika Anda mengembangkan aplikasi untuk platform lain, jangan ragu untuk menerapkan kembali antarmuka UserService yang menangani autentikasi pengguna.

Aplikasi menggunakan DAO untuk mempertahankan dan mengakses token otorisasi pengguna. Di bawah ini adalah antarmuka - OAuthTokenDao - dan implementasi tiruan (dalam memori) - OAuthTokenDaoMemoryImpl - yang digunakan dalam contoh ini:

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);
}
File 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);
  }
}
File OAuthTokenDaoMemoryImpl.java

Selain itu, kredensial OAuth 2.0 untuk aplikasi disimpan dalam file properti. Atau, Anda dapat menjadikannya sebagai konstanta di suatu tempat dalam salah satu class java, meskipun berikut adalah class OAuthProperties dan file oauth.properties yang digunakan dalam contoh:

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 {
  }
}
File OAuthProperties.java

Berikut adalah file oauth.properties yang berisi kredensial OAuth 2.0 aplikasi Anda. Anda perlu mengubah sendiri nilai di bawah.

# 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
file oauth.properties

Client ID dan Rahasia klien OAuth 2.0 mengidentifikasi aplikasi Anda dan memungkinkan Tasks API menerapkan filter dan aturan kuota yang ditetapkan untuk aplikasi Anda. Client ID dan rahasia dapat ditemukan di Konsol API Google. Setelah berada di konsol, Anda harus:

  • Buat atau pilih project.
  • Aktifkan Tasks API dengan mengalihkan status Tasks API ke AKTIF di daftar layanan.
  • Di bagian Akses API, buat Client ID OAuth 2.0 jika belum dibuat.
  • Pastikan URL pengendali callback kode OAuth 2.0 project sudah terdaftar/diizinkan di Redirect URI. Misalnya, dalam contoh project ini, Anda harus mendaftarkan https://www.example.com/oauth2callback jika aplikasi web disalurkan dari domain https://www.example.com.

URI Pengalihan di Konsol API
URI Pengalihan di Konsol API

Memproses kode Otorisasi dari endpoint Otorisasi Google

Apabila pengguna belum memberikan otorisasi kepada aplikasi untuk mengakses tugasnya sehingga telah dialihkan ke endpoint Otorisasi OAuth 2.0 Google, dialog otorisasi dari Google akan ditampilkan kepada pengguna agar memberi aplikasi Anda akses ke tugasnya:

Dialog otorisasi Google
Dialog otorisasi Google

Setelah memberikan atau menolak akses, pengguna akan dialihkan kembali ke pengendali callback kode OAuth 2.0 yang telah ditetapkan sebagai pengalihan/callback saat menyusun URL otorisasi Google:

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

Handler callback kode OAuth 2.0 - OAuthCodeCallbackHandlerServlet - menangani pengalihan dari endpoint Google OAuth 2.0. Ada 2 kasus yang harus ditangani:

  • Pengguna telah memberikan akses: menguraikan permintaan untuk mendapatkan kode OAuth 2.0 dari parameter URL
  • Pengguna telah menolak akses: menampilkan pesan kepada pengguna

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;
  }
}
File OAuthCodeCallbackHandlerServlet.java

Tukarkan kode otorisasi dengan token refresh dan akses

Kemudian, OAuthCodeCallbackHandlerServlet menukar kode Auth 2.0 untuk token refresh dan akses, mempertahankannya di datastore, dan mengalihkan pengguna kembali ke URL PrintTaskListsTitlesServlet:

Kode yang ditambahkan ke file di bawah ditandai dengan sintaksis, kode yang sudah ada akan berwarna abu-abu.

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 yang menjadi tujuan pengalihan pengguna setelah menangani callback. Pertimbangkan * simpan ini dalam cookie sebelum mengalihkan pengguna ke URL otorisasi Google jika Anda memiliki beberapa kemungkinan URL untuk mengalihkan pengguna. */ public static final String REDIRECT_URL = "/"; /** Implementasi DAO Token OAuth. Pertimbangkan untuk memasukkannya, bukan menggunakan * inisialisasi statis. Kami juga menggunakan implementasi memori sederhana * sebagai tiruan. Ubah implementasinya untuk menggunakan sistem database Anda. *
  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 Kode yang didapatkan kembali dari layanan otorisasi * @param currentUrl URL callback * @param oauthProperties Objek yang berisi konfigurasi OAuth * @return Objek yang berisi token akses dan refresh * @throws IOException */ public AccessTokenResponse exchangeCodeForAccessAndRefreshTokens(String code, String currentUrl) menampilkan IOOAuth Exception(),
File OAuthCodeCallbackHandlerServlet.java

Catatan: Implementasi di atas menggunakan beberapa library App Engine, yang digunakan sebagai penyederhanaan. Jika Anda mengembangkan aplikasi untuk platform lain, jangan ragu untuk menerapkan kembali antarmuka UserService yang menangani autentikasi pengguna.

Membaca tugas pengguna dan menampilkannya

Pengguna telah memberi aplikasi akses ke tugasnya. Aplikasi memiliki token refresh yang disimpan di datastore yang dapat diakses melalui OAuthTokenDao. Servlet PrintTaskListsTitlesServlet kini dapat menggunakan token tersebut untuk mengakses tugas pengguna dan menampilkannya:

Kode yang ditambahkan ke file di bawah ditandai dengan sintaksis, kode yang sudah ada akan berwarna abu-abu.

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;
    }
// Mencetak judul daftar tugas pengguna di response resp.setContentType("text/plain"); resp.getWriter().append("Task Lists title for user " + user.getEmail() + ":\n\n"); printTasksTitles(accessToken, 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;
  }
/** * Menggunakan daftar tugas Google di Tasks default untuk diambil pengguna Tasks API. * * @param accessTokenResponse Objek AccessTokenResponse OAuth 2.0 * berisi token akses dan token refresh. * @param menghasilkan output stream writer tempat menulis judul daftar tugas * @return Daftar judul tugas pengguna dalam daftar tugas default.
File PrintTasksTitlesServlet.java

Pengguna akan ditampilkan dengan tugasnya:

Tugas pengguna
Tugas pengguna

Contoh aplikasi

Kode untuk aplikasi contoh ini dapat didownload di sini. Jangan ragu untuk mencobanya.