הכנסת המשתמש

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

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

במהלך ההדרכה המפורטת תשלימו את הפעולות הבאות:

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

בסיום התהליך, תוכלו לתת הרשאה מלאה למשתמשים באפליקציית האינטרנט ולבצע קריאות ל-Google APIs.

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

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

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

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

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

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

יישום תהליך ההרשאה

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

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

הפעלת ההרשאה

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

Python

פותחים את הקובץ routes.py. קודם כול, מגדירים כמה קבועים ואת ההגדרות של קובצי ה-cookie בהתאם להמלצות לאבטחת 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 בקובץ לדוגמה). יש להוסיף לוגיקה לעיבוד דף כניסה אם הסשן לא מכיל את המפתח 'Credentials'.

@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 ספציפי אחרי שהם מאשרים את האפליקציה. בקטע Project Set up (הגדרת פרויקט) ב-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 להרשאה.

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.
  • השיטה 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;
    }
}

משתמשים בהחדרה של בנאי כדי ליצור מכונה של מחלקת השירות במחלקה של בקרי הבקרה.

/** 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;
}

מוסיפים את נקודת הקצה /authorize למחלקה של הבקר. נקודת הקצה הזו קוראת לשיטה 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 מכיל גם את השיטה 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 ששמור בסשן. אם הם תואמים, תוכלו להמשיך בתהליך ההרשאה. אם הם לא תואמים, תצטרכו להחזיר שגיאה.

לאחר מכן, קוראים לשיטה 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!

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

Python

מומלץ לקרוא את התיעוד של OAuth 2.0 Discovery API. משתמשים בו כדי לקבל אובייקט 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;
    }
}

מוסיפים את נקודת הקצה /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 בתור אחד ממשתמשי הבחינה המורה. עוברים לכרטיסייה עבודות ויוצרים מטלה חדשה. לוחצים על הלחצן Add-ons מתחת לאזור הטקסט ובוחרים את התוסף. ה-iframe נפתח והתוסף טוען את ה-URI להגדרת קבצים מצורפים שציינת בדף App Configuration של GWM SDK.

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