Build handler של קריאה חוזרת להרשאות

מסמך זה מסביר כיצד ליישם handler של קריאה חוזרת (callback) להרשאות OAuth 2.0 באמצעות שרתי Java מסוג Java, דרך אפליקציית אינטרנט לדוגמה שתציג את משימות המשתמש באמצעות Google Tasks API. האפליקציה לדוגמה תבקש תחילה הרשאה לגשת למשימות Google של המשתמש, ולאחר מכן תציג את משימות המשתמש ברשימת המשימות המוגדרת כברירת מחדל.

קהל

מסמך זה מותאם לאנשים שמכירים את ארכיטקטורת אפליקציות האינטרנט Java ו-J2EE. מומלץ להכיר את תהליך ההרשאה של OAuth 2.0.

תוכן עניינים

יש צורך בכמה שלבים כדי לקבל דוגמית עבודה מלאה:

הצהרה על מיפויים של servlet בקובץ web.xml

נשתמש בשני משרתים באפליקציה שלנו:

  • PrintTasksTitlesServlet (ממופה אל /): נקודת הכניסה של האפליקציה שתטפל באימות המשתמשים ותציג את המשימות של המשתמש
  • OAuthCodeCallbackHandlerServlet (ממופה אל /oauth2callback): הקריאה החוזרת של OAuth 2.0 שמטפלת בתגובה מנקודת הקצה של הרשאת OAuth

בהמשך מופיע הקובץ web.xml שממפה את שני ה-servlets האלה לכתובות URL באפליקציה שלנו:

<?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>
קובץ /WEB-INF/web.xml

אימות המשתמשים במערכת וקבלת הרשאה לגשת למשימות שלה

המשתמש נכנס לאפליקציה דרך כתובת ה-URL ברמה הבסיסית '/', שממופה אל ה-servlet PrintTaskListsTitlesServlet. בשרת ייעודי זה מתבצעות המשימות הבאות:

  • בודקת אם המשתמש מאומת במערכת
  • אם המשתמש לא מאומת, הוא מופנה לדף האימות
  • אם המשתמש מאומת, אנחנו בודקים אם כבר קיים אסימון רענון באחסון הנתונים שלנו, שמטופל על ידי OAuthTokenDao שבהמשך. אם אין אסימון רענון עבור המשתמש, פירוש הדבר הוא שהמשתמש עדיין לא העניק לאפליקציה הרשאה לגשת למשימות שלו. במקרה כזה, המשתמש יופנה אוטומטית לנקודת הקצה של הרשאת OAuth 2.0 של Google.
כך אפשר להטמיע את זה:

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;
  }
}
הקובץ PrintTasksTitlesServlet.java

הערה: היישום שלמעלה משתמש בספריות מסוימות של App Engine. ספריות אלה משמשות לצורך פישוט. אם אתם מפתחים לפלטפורמה אחרת, אתם יכולים להטמיע מחדש את ממשק UserService שמטפל באימות המשתמשים.

האפליקציה משתמשת ב-DAO כדי לשמור על הגישה לאסימוני ההרשאה של המשתמשים ולגשת אליהם. לפניכם הממשק - OAuthTokenDao - והטמעה מדומה (בזיכרון) - OAuthTokenDaoMemoryImpl - המשמשים בדוגמה זו:

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);
}
קובץ 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);
  }
}
קובץ OAuthTokenDaoMemoryImpl.java

כמו כן, פרטי הכניסה של האפליקציה OAuth 2.0 מאוחסנים בקובץ מאפיינים. לחלופין, ניתן להגדיר אותם לקבועים במקום כלשהו באחת ממחלקות Java, אם כי הנה המחלקה OAuthProperties והקובץ oauth.properties המשמש בדוגמה:

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 {
  }
}
קובץ OAuthProperties.java

בהמשך מופיע הקובץ oauth.properties המכיל את פרטי הכניסה של OAuth 2.0 עבור האפליקציה שלך. עליך לשנות את הערכים שבהמשך בעצמך.

# 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
קובץ oauth.properties

מזהה הלקוח וסוד הלקוח של OAuth 2.0 מזהים את האפליקציה ומאפשרים ל-Tasks API להחיל מסננים וכללי מכסה שמוגדרים לאפליקציה שלכם. מזהה הלקוח והסוד מופיעים במסוף Google APIs. אחרי שתיכנסו למסוף, תצטרכו:

  • יוצרים או בוחרים פרויקט.
  • כדי להפעיל את Tasks API, מעבירים את המתג של Tasks API למצב מופעל ברשימת השירותים.
  • בקטע גישה לממשק API, יוצרים מזהה לקוח ב-OAuth 2.0, אם עדיין לא נוצר מזהה כזה.
  • מוודאים שכתובת ה-URL של הגורם המטפל בקריאה חוזרת (callback) בקוד OAuth 2.0 של הפרויקט רשומה/נוספה לרשימת ההיתרים במזהי URI של הפניות לכתובות אחרות. לדוגמה, בפרויקט הדוגמה הזה צריך לרשום https://www.example.com/oauth2callback אם אפליקציית האינטרנט שלכם מוצגת מהדומיין https://www.example.com.

עליך להפנות מחדש URI במסוף ממשקי ה-API
עליך להפנות מחדש URI במסוף ממשקי ה-API

האזנה לקוד ההרשאה מנקודת הקצה של Google Authorization

אם המשתמש עדיין לא אישר לאפליקציה לגשת למשימות שלו, ולכן הוא ופנה מחדש לנקודת הקצה של הרשאת OAuth 2.0 של Google, מוצגת למשתמש תיבת דו-שיח לאישור מ-Google שבה הוא מתבקש להעניק לאפליקציה גישה למשימות שלה:

תיבת הדו-שיח לאישור של Google
תיבת הדו-שיח לאישור של Google

לאחר מתן גישה או דחייה, המשתמש יופנה מחדש אל ה-handler של קריאה חוזרת (callback) בקוד OAuth 2.0 שצוין כהפניה אוטומטית/קריאה חוזרת במהלך בניית כתובת ה-URL להרשאה של Google:

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

הגורם המטפל בקריאה חוזרת (callback) בקוד OAuth 2.0 – OAuthCodeCallbackHandlerServlet - מטפל בהפניה אוטומטית מנקודת הקצה של Google OAuth 2.0. צריך לטפל בשני מקרים:

  • המשתמש העניק גישה: מנתח את הבקשה לקבל קוד OAuth 2.0 מהפרמטרים של כתובת האתר
  • המשתמש דחה את הגישה: מציג הודעה למשתמש

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;
  }
}
קובץ OAuthCodeCallbackHandlerServlet.java

החלפת קוד ההרשאה ברענון ובאסימון גישה

לאחר מכן, הפונקציה OAuthCodeCallbackHandlerServlet מחליפה את קוד Auth 2.0 לאסימוני רענון ואסימוני גישה, שומרת אותו במאגר הנתונים ומפנה את המשתמש בחזרה לכתובת האתר של PrintTaskListsTitlesServlet:

הקוד שנוסף לקובץ הבא מודגש בתחביר, והקוד שכבר קיים מופיע באפור.

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 שאליה צריך להפנות את המשתמש אחרי הטיפול בקריאה החוזרת. כדאי לשמור אותה בקובץ cookie לפני שמופנים לכתובת ה-URL של * ההרשאה של Google, אם יש כמה כתובות URL אפשריות להפנות אנשים אליהן. */ Public static String REDIRECT_URL = "/"; /** הטמעת DAO של אסימון OAuth. כדאי לשקול להזריק אותו במקום להשתמש באתחול סטטי. בנוסף, אנחנו משתמשים בהטמעת זיכרון פשוטה * כדוגמה. שינוי היישום לשימוש במערכת מסד הנתונים שלך. */ Public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();
  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;
    }
// בונים כתובת אתר של בקשה נכנסת String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // חילופי הקוד של אסימוני OAuth (AccessTokenResponse AccessTokenResponse = exchange(AccessAndUpdateService)}
  /**
   * 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: הקוד קיבל חזרה משירות ההרשאה * @param currentUrl The URL of the callback * @param oauthProperties The האובייקט contains OAuth configuration * @return The האובייקט contains to access and Rebrorto * @throws IO זכות חריגת */ Public AccessTokenResponseCodeForAccessAndcksTokens(String code, String currentjson); s
קובץ OAuthCodeCallbackHandlerServlet.java

הערה: היישום שלמעלה משתמש בספריות מסוימות של App Engine. ספריות אלה משמשות לצורך פישוט. אם אתם מפתחים לפלטפורמה אחרת, אתם יכולים להטמיע מחדש את ממשק UserService שמטפל באימות המשתמשים.

קריאת המשימות של המשתמש והצגתן

המשתמש העניק לאפליקציה גישה למשימות שלה. לאפליקציה יש אסימון רענון שנשמר במאגר הנתונים שאליו ניתן לגשת באמצעות OAuthTokenDao. שרת ה-servlet PrintTaskListsTitlesServlet יכול להשתמש עכשיו באסימונים האלה כדי לגשת למשימות המשתמש ולהציג אותן:

הקוד שנוסף לקובץ הבא מודגש בתחביר, והקוד שכבר קיים מופיע באפור.

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;
    }
// הדפסת הכותרות של רשימות המשימות של המשתמש בתגובה resp.setContentType("text/plain"); resp.getWriter().append("רשימות משימות של כותרות למשתמש " + user.getEmail() + ":\n\n"); printTasksTitles(accessTokenResponse, resp.getWriter(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;
  }
/** * משתמש ברשימת המשימות של Google Tasks בתוך ה-API.
  }

  /**
   * 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;
  }
/** נעזר במשימות ברירת המחדל של Google Tasks. * * @param accessTokenResponse האובייקט OAuth 2.0 AccessTokenResponse * מכיל את אסימון הגישה ואסימון רענון. * @param פלט כותב זרם הפלט לאן צריך לנסח את רשימת המשימות. * @return רשימה של שמות המשימות של המשתמש ברשימת המשימות המוגדרת כברירת מחדל. * @throws IOניים */ public voidprintTasksTitles(AccessTokenResponse accessTokenResponse, Writeroutput) סורק את IO לחריגה { // אתחול שירות Tasks HttpTransportTransport = new NetHttpTransport(); JsonFactory jsonManufacturer = new JacksonFactory jsonManufacturer = new JacksonManufacturer(); OAuthProperties oauthProperties = new OAuthTasksAccess} .
הקובץ PrintTasksTitlesServlet.java

המשתמש יוצג עם המשימות שלו:

המשימות של המשתמש
המשימות של המשתמש

אפליקציה לדוגמה

ניתן להוריד את הקוד של האפליקציה לדוגמה כאן. אתם מוזמנים לבדוק את זה.