הכנסת המשתמש

זוהי ההדרכה המפורטת השנייה בנושא תוספים ל-Classroom של הדרכות מפורטות.

בהדרכה המפורטת הזו מוסיפים את 'כניסה באמצעות חשבון Google' לאפליקציית האינטרנט. זהו ההתנהגות הנדרשת לתוספים ל-Classroom. משתמשים בפרטי הכניסה של את תהליך ההרשאה הזה לכל הקריאות העתידיות ל-API.

במהלך ההדרכה המפורטת הזו, תצטרכו:

  • מגדירים את אפליקציית האינטרנט כדי לשמור על נתוני הסשנים ב-iframe.
  • הטמעת תהליך כניסה מסוג שרת-אל-שרת של Google OAuth 2.0.
  • לשלוח קריאה ל-OAuth 2.0 API.
  • יצירת מסלולים נוספים לתמיכה באישור, ביציאה ובבדיקות קריאות ל-API.

לאחר סיום התהליך, תהיה לך אפשרות לתת הרשאה מלאה למשתמשים באפליקציית האינטרנט שלך ולשלוח שיחות אל ממשקי API של Google.

הסבר על תהליך ההרשאה

ממשקי Google API משתמשים בפרוטוקול OAuth 2.0 לצורך אימות והרשאה. התיאור המלא של הטמעת OAuth של Google זמין מדריך Google Identity ל-OAuth.

פרטי הכניסה של האפליקציה מנוהלים ב-Google Cloud. אחרי שהם יש ליישם תהליך בן ארבעה שלבים לאימות ולאישור user:

  1. בקשת הרשאה. צריך לציין כתובת URL לקריאה חוזרת (callback) כחלק מהבקשה. בסיום, מקבלים כתובת URL להרשאה.
  2. מפנים את המשתמש לכתובת ה-URL של ההרשאה. הדף שמתקבל מספק מידע משתמש עם ההרשאות שנדרשות לאפליקציה, ומבקש ממנו לאפשר גישה. בסיום, המשתמש ינותב לכתובת ה-URL לקריאה חוזרת (callback).
  3. מקבלים קוד הרשאה במסלול הקריאה החוזרת (callback). מחליפים את קוד הרשאה לאסימון גישה ולאסימון רענון.
  4. ביצוע קריאות ל-Google API באמצעות האסימונים.

קבלת פרטי כניסה בפרוטוקול OAuth 2.0

מוודאים שיצרתם והורדתם פרטי כניסה של OAuth כפי שמתואר ב דף הסקירה הכללית. כדי להיכנס עם המשתמש, הפרויקט צריך להשתמש בפרטי הכניסה האלה.

הטמעת תהליך ההרשאה

מוסיפים לוגיקה ומסלולים לאפליקציית האינטרנט שלנו כדי לממש את התהליך המתואר, כולל תכונות אלה:

  • מתחילים את תהליך ההרשאה כשמגיעים לדף הנחיתה.
  • מבקשים הרשאה ומטפלים בתגובה של שרת ההרשאות.
  • מנקים את פרטי הכניסה המאוחסנים.
  • ביטול ההרשאות של האפליקציה.
  • בודקים קריאה ל-API.

הפעלת ההרשאה

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

Python

פותחים את הקובץ routes.py. קודם כל, מגדירים כמה קבועים תצורה בהתאם להמלצות האבטחה ב-iframe.

# The file that contains the OAuth 2.0 client_id and client_secret.
CLIENT_SECRETS_FILE = "client_secret.json"

# The OAuth 2.0 access scopes to request.
# These scopes must match the scopes in your Google Cloud project's OAuth Consent
# Screen: https://console.cloud.google.com/apis/credentials/consent
SCOPES = [
    "openid",
    "https://www.googleapis.com/auth/userinfo.profile",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/classroom.addons.teacher",
    "https://www.googleapis.com/auth/classroom.addons.student"
]

# Flask cookie configurations.
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="None",
)

עוברים אל מסלול הנחיתה של התוסף (בדוגמה הערך הוא /classroom-addon) ). צריך להוסיף לוגיקה לעיבוד דף כניסה אם הסשן לא כולל פרטי הכניסה מקש.

@app.route("/classroom-addon")
def classroom_addon():
    if "credentials" not in flask.session:
        return flask.render_template("authorization.html")

    return flask.render_template(
        "addon-discovery.html",
        message="You've reached the addon discovery page.")

Java

הקוד להדרכה המפורטת הזו מופיע במודול step_02_sign_in.

פותחים את הקובץ application.properties ומוסיפים הגדרות אישיות לסשן פועלת בהתאם להמלצות האבטחה של iframe.

# iFrame security recommendations call for cookies to have the HttpOnly and
# secure attribute set
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

# Ensures that the session is maintained across the iframe and sign-in pop-up.
server.servlet.session.cookie.same-site=none

יצירה של סיווג שירות (AuthService.java במודול step_02_sign_in) כדי לטפל בלוגיקה מאחורי נקודות הקצה בקובץ הבקר ולהגדיר ה-URI להפניה אוטומטית, מיקום קובץ סודות הלקוח והיקפים שהתוסף שלכם נדרש. ה-URI להפניה מחדש משמש לניתוב מחדש של המשתמשים שלך ל-URI ספציפי אחרי שהם יאשרו את האפליקציה. בקטע 'הגדרת פרויקט' README.md בקוד המקור כדי לקבל מידע על המיקום קובץ client_secret.json.

@Service
public class AuthService {
    private static final String REDIRECT_URI = "https://localhost:5000/callback";
    private static final String CLIENT_SECRET_FILE = "client_secret.json";
    private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
    private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();

    private static final String[] REQUIRED_SCOPES = {
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/userinfo.email",
        "https://www.googleapis.com/auth/classroom.addons.teacher",
        "https://www.googleapis.com/auth/classroom.addons.student"
    };

    /** Creates and returns a Collection object with all requested scopes.
    *   @return Collection of scopes requested by the application.
    */
    public static Collection<String> getScopes() {
        return new ArrayList<>(Arrays.asList(REQUIRED_SCOPES));
    }
}

פותחים את קובץ הבקר (AuthController.java בstep_02_sign_in ולהוסיף לוגיקה למסלול הנחיתה כדי לעבד את דף הכניסה, אם לא מכיל את המפתח credentials.

@GetMapping(value = {"/start-auth-flow"})
public String startAuthFlow(Model model) {
    try {
        return "authorization";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

@GetMapping(value = {"/addon-discovery"})
public String addon_discovery(HttpSession session, Model model) {
    try {
        if (session == null || session.getAttribute("credentials") == null) {
            return startAuthFlow(model);
        }
        return "addon-discovery";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

דף ההרשאה צריך להכיל קישור או לחצן שבאמצעותם המשתמש יכול "לחתום אינץ'". לחיצה על האפשרות הזו תפנה את המשתמש למסלול של authorize.

בקשת הרשאה

כדי לבקש הרשאה, יש ליצור ולהפנות את המשתמש לאימות כתובת URL. כתובת ה-URL הזו כוללת כמה פרטים, כמו היקף ההרשאות נדרש, נתיב היעד עבור ההרשאה אחרי, ואת מזהה לקוח. תוכלו לראות את כתובות ה-URL האלה בדוגמה לכתובת URL להרשאה.

Python

צריך להוסיף את הייבוא הבא לקובץ routes.py.

import google_auth_oauthlib.flow

יצירת מסלול חדש /authorize. יוצרים מופע של google_auth_oauthlib.flow.Flow; מומלץ מאוד להשתמש from_client_secrets_file כדי לעשות זאת.

@app.route("/authorize")
def authorize():
    # Create flow instance to manage the OAuth 2.0 Authorization Grant Flow
    # steps.
    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES)

מגדירים את redirect_uri של flow; זהו המסלול שאליו מתכוונים משתמשים כדי לחזור אחרי שמאשרים את האפליקציה. זה /callback לפי לדוגמה.

# The URI created here must exactly match one of the authorized redirect
# URIs for the OAuth 2.0 client, which you configured in the API Console. If
# this value doesn't match an authorized URI, you will get a
# "redirect_uri_mismatch" error.
flow.redirect_uri = flask.url_for("callback", _external=True)

משתמשים באובייקט הזרימה כדי ליצור את authorization_url ואת state. חנות state בסשן; הוא משמש לאימות האותנטיות של את תגובת השרת מאוחר יותר. לבסוף, מפנים את המשתמש authorization_url

authorization_url, state = flow.authorization_url(
    # Enable offline access so that you can refresh an access token without
    # re-prompting the user for permission. Recommended for web server apps.
    access_type="offline",
    # Enable incremental authorization. Recommended as a best practice.
    include_granted_scopes="true")

# Store the state so the callback can verify the auth server response.
flask.session["state"] = state

# Redirect the user to the OAuth authorization URL.
return flask.redirect(authorization_url)

Java

צריך להוסיף את השיטות הבאות לקובץ AuthService.java כדי ליצור את באובייקט זרימה ואז משתמשים בו כדי לאחזר את כתובת ה-URL של ההרשאה:

  • שיטת getClientSecrets() קוראת את הקובץ של סוד הלקוח ואת המבנה שלו אובייקט GoogleClientSecrets.
  • השיטה getFlow() יוצרת מופע של GoogleAuthorizationCodeFlow.
  • ה-method authorize() משתמשת באובייקט GoogleAuthorizationCodeFlow, state, וה-URI של ההפניה האוטומטית כדי לאחזר את כתובת ה-URL של ההרשאה. הפרמטר state משמש לאימות האותנטיות של התשובה משרת ההרשאות. לאחר מכן השיטה מחזירה מפה עם כתובת ה-URL להרשאה והפרמטר state.
/** Reads the client secret file downloaded from Google Cloud.
 *   @return GoogleClientSecrets read in from client secret file. */
public GoogleClientSecrets getClientSecrets() throws Exception {
    try {
        InputStream in = SignInApplication.class.getClassLoader()
            .getResourceAsStream(CLIENT_SECRET_FILE);
        if (in == null) {
            throw new FileNotFoundException("Client secret file not found: "
                +   CLIENT_SECRET_FILE);
        }
        GoogleClientSecrets clientSecrets = GoogleClientSecrets
            .load(JSON_FACTORY, new InputStreamReader(in));
        return clientSecrets;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns authorization code flow.
*   @return GoogleAuthorizationCodeFlow object used to retrieve an access
*   token and refresh token for the application.
*   @throws Exception if reading client secrets or building code flow object
*   is unsuccessful.
*/
public GoogleAuthorizationCodeFlow getFlow() throws Exception {
    try {
        GoogleAuthorizationCodeFlow authorizationCodeFlow =
            new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT,
                JSON_FACTORY,
                getClientSecrets(),
                getScopes())
                .setAccessType("offline")
                .build();
        return authorizationCodeFlow;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns a map with the authorization URL, which allows the
*   user to give the app permission to their account, and the state parameter,
*   which is used to prevent cross site request forgery.
*   @return map with authorization URL and state parameter.
*   @throws Exception if building the authorization URL is unsuccessful.
*/
public HashMap authorize() throws Exception {
    HashMap<String, String> authDataMap = new HashMap<>();
    try {
        String state = new BigInteger(130, new SecureRandom()).toString(32);
        authDataMap.put("state", state);

        GoogleAuthorizationCodeFlow flow = getFlow();
        String authUrl = flow
            .newAuthorizationUrl()
            .setState(state)
            .setRedirectUri(REDIRECT_URI)
            .build();
        String url = authUrl;
        authDataMap.put("url", url);

        return authDataMap;
    } catch (Exception e) {
        throw e;
    }
}

להשתמש בהחדרת constructor כדי ליצור מופע של סוג השירות מחלקה של נאמני מידע.

/** Declare AuthService to be used in the Controller class constructor. */
private final AuthService authService;

/** AuthController constructor. Uses constructor injection to instantiate
*   the AuthService and UserRepository classes.
*   @param authService the service class that handles the implementation logic
*   of requests.
*/
public AuthController(AuthService authService) {
    this.authService = authService;
}

מוסיפים את נקודת הקצה (endpoint) /authorize למחלקה של הבקרה. נקודת הקצה (endpoint) הזו קוראת שיטת AuthService authorize() כדי לאחזר את הפרמטר state ואת כתובת ה-URL של ההרשאה. לאחר מכן, נקודת הקצה שומרת את state בסשן ומפנה את המשתמשים לכתובת ה-URL של ההרשאה.

/** Redirects the sign-in pop-up to the authorization URL.
*   @param response the current response to pass information to.
*   @param session the current session.
*   @throws Exception if redirection to the authorization URL is unsuccessful.
*/
@GetMapping(value = {"/authorize"})
public void authorize(HttpServletResponse response, HttpSession session)
    throws Exception {
    try {
        HashMap authDataMap = authService.authorize();
        String authUrl = authDataMap.get("url").toString();
        String state = authDataMap.get("state").toString();
        session.setAttribute("state", state);
        response.sendRedirect(authUrl);
    } catch (Exception e) {
        throw e;
    }
}

טיפול בתגובת השרת

אחרי קבלת האישור, המשתמש חוזר למסלול redirect_uri מ לשלב הקודם. בדוגמה הקודמת, המסלול הזה הוא /callback.

מקבלים code בתגובה כשהמשתמש חוזר בדף ההרשאה. לאחר מכן מחליפים את הקוד באסימוני גישה ורענון:

Python

מוסיפים את פעולות הייבוא הבאות לקובץ שרת Flask.

import google.oauth2.credentials
import googleapiclient.discovery

מוסיפים את הנתיב לשרת. בונים מופע נוסף של google_auth_oauthlib.flow.Flow, אבל הפעם עושים שימוש חוזר במצב שנשמר לשלב הקודם.

@app.route("/callback")
def callback():
    state = flask.session["state"]

    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES, state=state)
    flow.redirect_uri = flask.url_for("callback", _external=True)

בשלב הבא צריך לבקש אסימוני גישה ורענון. למרבה המזל, האובייקט flow מכיל את ה-method fetch_token כדי לעשות זאת. השיטה מצפה או הארגומנטים code או authorization_response. משתמשים ב authorization_response, כי זו כתובת ה-URL המלאה של הבקשה.

authorization_response = flask.request.url
flow.fetch_token(authorization_response=authorization_response)

עכשיו יש לך פרטי כניסה מלאים! שמרו אותם בסשן ניתן לאחזר אותם בשיטות אחרות או במסלולים אחרים, ואז להפנות אותם לתוסף דף נחיתה.

credentials = flow.credentials
flask.session["credentials"] = {
    "token": credentials.token,
    "refresh_token": credentials.refresh_token,
    "token_uri": credentials.token_uri,
    "client_id": credentials.client_id,
    "client_secret": credentials.client_secret,
    "scopes": credentials.scopes
}

# Close the pop-up by rendering an HTML page with a script that redirects
# the owner and closes itself. This can be done with a bit of JavaScript:
# <script>
#     window.opener.location.href = "{{ url_for('classroom_addon') }}";
#     window.close();
# </script>
return flask.render_template("close-me.html")

Java

מוסיפים שיטה לסיווג השירות שמחזירה את האובייקט Credentials באמצעות העברת קוד ההרשאה שאוחזר מההפניה האוטומטית שבוצעה על ידי את כתובת ה-URL של ההרשאה. אובייקט Credentials הזה ישמש לאחזור מאוחר יותר את אסימון הגישה ואת אסימון הרענון.

/** Returns the required credentials to access Google APIs.
*   @param authorizationCode the authorization code provided by the
*   authorization URL that's used to obtain credentials.
*   @return the credentials that were retrieved from the authorization flow.
*   @throws Exception if retrieving credentials is unsuccessful.
*/
public Credential getAndSaveCredentials(String authorizationCode) throws Exception {
    try {
        GoogleAuthorizationCodeFlow flow = getFlow();
        GoogleClientSecrets googleClientSecrets = getClientSecrets();
        TokenResponse tokenResponse = flow.newTokenRequest(authorizationCode)
            .setClientAuthentication(new ClientParametersAuthentication(
                googleClientSecrets.getWeb().getClientId(),
                googleClientSecrets.getWeb().getClientSecret()))
            .setRedirectUri(REDIRECT_URI)
            .execute();
        Credential credential = flow.createAndStoreCredential(tokenResponse, null);
        return credential;
    } catch (Exception e) {
        throw e;
    }
}

מוסיפים נקודת קצה (endpoint) לבקרה על ה-URI להפניה אוטומטית. מאחזרים את קוד ההרשאה והפרמטר state מהבקשה. השוואה פרמטר state למאפיין state שמאוחסן בסשן. אם הן תואם, ואז ממשיכים בתהליך ההרשאה. אם הם לא תואמים, מחזירה שגיאה.

לאחר מכן, קוראים ל-method AuthService getAndSaveCredentials ומעבירים את את קוד ההרשאה כפרמטר. אחרי אחזור של Credentials נאחסן אותו בסשן. לאחר מכן, סוגרים את תיבת הדו-שיח ומפנה מחדש את אל דף הנחיתה של התוסף.

/** Handles the redirect URL to grant the application access to the user's
*   account.
*   @param request the current request used to obtain the authorization code
*   and state parameter from.
*   @param session the current session.
*   @param response the current response to pass information to.
*   @param model the Model interface to pass error information that's
*   displayed on the error page.
*   @return the close-pop-up template if authorization is successful, or the
*   onError method to handle and display the error message.
*/
@GetMapping(value = {"/callback"})
public String callback(HttpServletRequest request, HttpSession session,
    HttpServletResponse response, Model model) {
    try {
        String authCode = request.getParameter("code");
        String requestState = request.getParameter("state");
        String sessionState = session.getAttribute("state").toString();
        if (!requestState.equals(sessionState)) {
            response.setStatus(401);
            return onError("Invalid state parameter.", model);
        }
        Credential credentials = authService.getAndSaveCredentials(authCode);
        session.setAttribute("credentials", credentials);
        return "close-pop-up";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

בדיקת קריאה ל-API

כשהתהליך הושלם, אפשר עכשיו לשלוח קריאות ל-Google APIs.

לדוגמה, אפשר לבקש את פרטי הפרופיל של המשתמש. אפשר לבקש פרטי המשתמש מ-OAuth 2.0 API.

Python

לקרוא את התיעוד עבור API לגילוי OAuth 2.0 אפשר להשתמש בו כדי לקבל אובייקט UserInfo מאוכלס.

# Retrieve the credentials from the session data and construct a
# Credentials instance.
credentials = google.oauth2.credentials.Credentials(
    **flask.session["credentials"])

# Construct the OAuth 2.0 v2 discovery API library.
user_info_service = googleapiclient.discovery.build(
    serviceName="oauth2", version="v2", credentials=credentials)

# Request and store the username in the session.
# This allows it to be used in other methods or in an HTML template.
flask.session["username"] = (
    user_info_service.userinfo().get().execute().get("name"))

Java

יוצרים שיטה במחלקת השירות שבונות אובייקט UserInfo באמצעות את Credentials כפרמטר.

/** Obtains the Userinfo object by passing in the required credentials.
*   @param credentials retrieved from the authorization flow.
*   @return the Userinfo object for the currently signed-in user.
*   @throws IOException if creating UserInfo service or obtaining the
*   Userinfo object is unsuccessful.
*/
public Userinfo getUserInfo(Credential credentials) throws IOException {
    try {
        Oauth2 userInfoService = new Oauth2.Builder(
            new NetHttpTransport(),
            new GsonFactory(),
            credentials).build();
        Userinfo userinfo = userInfoService.userinfo().get().execute();
        return userinfo;
    } catch (Exception e) {
        throw e;
    }
}

מוסיפים את נקודת הקצה (endpoint) /test לבקר שמציג את כתובת האימייל של המשתמש.

/** Returns the test request page with the user's email.
*   @param session the current session.
*   @param model the Model interface to pass error information that's
*   displayed on the error page.
*   @return the test page that displays the current user's email or the
*   onError method to handle and display the error message.
*/
@GetMapping(value = {"/test"})
public String test(HttpSession session, Model model) {
    try {
        Credential credentials = (Credential) session.getAttribute("credentials");
        Userinfo userInfo = authService.getUserInfo(credentials);
        String userInfoEmail = userInfo.getEmail();
        if (userInfoEmail != null) {
            model.addAttribute("userEmail", userInfoEmail);
        } else {
            return onError("Could not get user email.", model);
        }
        return "test";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

ניקוי פרטי כניסה

אפשר "לנקות" את פרטי הכניסה של המשתמש על ידי הסרתם מהסשן הנוכחי. כך אפשר לבדוק את הניתוב בדף הנחיתה של התוסף.

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

Python

@app.route("/clear")
def clear_credentials():
    if "credentials" in flask.session:
        del flask.session["credentials"]
        del flask.session["username"]

    return flask.render_template("signed-out.html")

לחלופין, אפשר להשתמש ב-flask.session.clear(), אבל יכול להיות שהפעולה הזו בוצעה בלי כוונה אינו משפיע על ערכים אחרים שמאוחסנים במהלך הסשן.

Java

בבקר, מוסיפים נקודת קצה (endpoint) /clear.

/** Clears the credentials in the session and returns the sign-out
*   confirmation page.
*   @param session the current session.
*   @return the sign-out confirmation page.
*/
@GetMapping(value = {"/clear"})
public String clear(HttpSession session) {
    try {
        if (session != null && session.getAttribute("credentials") != null) {
            session.removeAttribute("credentials");
        }
        return "sign-out";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

ביטול ההרשאה של האפליקציה

משתמש יכול לבטל את ההרשאה של האפליקציה על ידי שליחת בקשת POST אל https://oauth2.googleapis.com/revoke. הבקשה צריכה להכיל את שם המשתמש אסימון גישה.

Python

import requests

@app.route("/revoke")
def revoke():
    if "credentials" not in flask.session:
        return flask.render_template("addon-discovery.html",
                            message="You need to authorize before " +
                            "attempting to revoke credentials.")

    credentials = google.oauth2.credentials.Credentials(
        **flask.session["credentials"])

    revoke = requests.post(
        "https://oauth2.googleapis.com/revoke",
        params={"token": credentials.token},
        headers={"content-type": "application/x-www-form-urlencoded"})

    if "credentials" in flask.session:
        del flask.session["credentials"]
        del flask.session["username"]

    status_code = getattr(revoke, "status_code")
    if status_code == 200:
        return flask.render_template("authorization.html")
    else:
        return flask.render_template(
            "index.html", message="An error occurred during revocation!")

Java

מוסיפים שיטה למחלקת השירות שמבצעת קריאה לנקודת הקצה לביטול.

/** Revokes the app's permissions to the user's account.
*   @param credentials retrieved from the authorization flow.
*   @return response entity returned from the HTTP call to obtain response
*   information.
*   @throws RestClientException if the POST request to the revoke endpoint is
*   unsuccessful.
*/
public ResponseEntity<String> revokeCredentials(Credential credentials) throws RestClientException {
    try {
        String accessToken = credentials.getAccessToken();
        String url = "https://oauth2.googleapis.com/revoke?token=" + accessToken;

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
        HttpEntity<Object> httpEntity = new HttpEntity<Object>(httpHeaders);
        ResponseEntity<String> responseEntity = new RestTemplate().exchange(
            url,
            HttpMethod.POST,
            httpEntity,
            String.class);
        return responseEntity;
    } catch (RestClientException e) {
        throw e;
    }
}

מוסיפים לבקר, נקודת קצה (endpoint) /revoke כדי למחוק את הסשן מפנה את המשתמש לדף ההרשאה אם הביטול היה הפעולה הצליחה.

/** Revokes the app's permissions and returns the authorization page.
*   @param session the current session.
*   @return the authorization page.
*   @throws Exception if revoking access is unsuccessful.
*/
@GetMapping(value = {"/revoke"})
public String revoke(HttpSession session) throws Exception {
    try {
        if (session != null && session.getAttribute("credentials") != null) {
            Credential credentials = (Credential) session.getAttribute("credentials");
            ResponseEntity responseEntity = authService.revokeCredentials(credentials);
            Integer httpStatusCode = responseEntity.getStatusCodeValue();

            if (httpStatusCode != 200) {
                return onError("There was an issue revoking access: " +
                    responseEntity.getStatusCode(), model);
            }
            session.removeAttribute("credentials");
        }
        return startAuthFlow(model);
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

בדיקת התוסף

נכנסים ל-Google Classroom. כאחד מהמשתמשים לבדיקות של המורים. נכנסים לכרטיסייה עבודות ולוחצים על ליצור מטלה חדשה. לוחצים על הלחצן תוספים שמתחת לאזור הטקסט, ובוחרים את התוסף הרצוי. ה-iframe נפתח והתוסף טוען את ה-URI להגדרת קובץ מצורף שציינת באפליקציה של GWM SDK הגדרה הדף הזה.

מעולה! אפשר להמשיך לשלב הבא: טיפול חוזר במספר ביקורים בתוסף שלכם.