التعامل مع عمليات تسجيل الدخول المتكرّرة

هذا هو الجزء الثالث من سلسلة الجولات الإرشادية حول إضافات Classroom.

في هذا الشرح، ستتعامل مع الزيارات المتكررة إلى الإضافة من خلال استرداد بيانات اعتماد المستخدم التي تم منحها سابقًا تلقائيًا. بعد ذلك، يمكنك توجيه المستخدمين إلى صفحات يمكنهم من خلالها إصدار طلبات إلى واجهة برمجة التطبيقات على الفور. وهذا السلوك مطلوب لإضافات Classroom.

خلال هذا الشرح، ستكمل ما يلي:

  • تنفيذ سعة تخزين ثابتة لبيانات اعتماد المستخدمين
  • استرداد مَعلمة طلب البحث الخاصة بالإضافة login_hint وتقييمها هذا هو رقم تعريف فريد على Google للمستخدم الذي سجّل الدخول.

بعد الانتهاء، يمكنك منح المستخدمين إذنًا كاملاً في تطبيقك على الويب وإجراء طلبات إلى واجهات Google API.

فهم مَعلمات طلب البحث في إطار iframe

تحمّل Classroom معرّف الموارد المنتظم (URI) لإعداد المرفقات الخاص بالإضافة عند فتحها. يُلحق تطبيق Classroom عدة مَعلمات طلب بحث GET بمعرّف الموارد المنتظم (URI)، وهي تحتوي على معلومات سياقية مفيدة. إذا كان معرّف URI الخاص بميزة "اكتشاف المرفقات" هو https://example.com/addon مثلاً، ينشئ Classroom إطار iframe مع ضبط عنوان URL المصدر على https://example.com/addon?courseId=XXX&itemId=YYY&itemType=courseWork&addOnToken=ZZZ، حيث XXX وYYY وZZZ هي معرّفات السلسلة. راجِع دليل إطارات iframe للحصول على وصف تفصيلي لهذه الحالة.

هناك خمس مَعلمات طلب بحث محتملة لعنوان URL الخاص بمستند الاستكشاف:

  • courseId: معرّف دورة Classroom التدريبية الحالية.
  • itemId: رقم تعريف عنصر البث الذي يعدّله المستخدم أو ينشئه.
  • itemType: نوع عنصر البث الذي ينشئه المستخدم أو يعدّله، وهو أحد القيم التالية: courseWork أو courseWorkMaterial أو announcement
  • addOnToken: رمز مميّز يُستخدَم للسماح بتنفيذ إجراءات معيّنة في إضافة Classroom.
  • login_hint: معرّف Google للمستخدم الحالي.

يتناول هذا الدليل login_hint. يتم توجيه المستخدمين استنادًا إلى ما إذا كانت مَعلمة طلب البحث هذه متوفّرة أم لا، إما إلى مسار منح الإذن في حال عدم توفّرها، أو إلى صفحة استكشاف الوظائف الإضافية في حال توفّرها.

الوصول إلى مَعلمات طلب البحث

يتم تمرير مَعلمات طلب البحث إلى تطبيق الويب في سلسلة معرّف الموارد المنتظم (URI). خزِّن هذه القيم في جلستك، إذ يتم استخدامها في عملية منح الإذن ولتخزين معلومات عن المستخدم واستردادها. لا يتم تمرير مَعلمات طلب البحث هذه إلا عند فتح الإضافة لأول مرة.

Python

انتقِل إلى تعريفات مسارات Flask (routes.py إذا كنت تتّبع المثال الذي قدّمناه). في أعلى مسار الصفحة المقصودة للإضافة (/classroom-addon في المثال الذي قدّمناه)، استرجِع مَعلمة طلب البحث login_hint وخزِّنها:

# If the login_hint query parameter is available, we'll store it in the session.
if flask.request.args.get("login_hint"):
    flask.session["login_hint"] = flask.request.args.get("login_hint")

تأكَّد من تخزين login_hint (في حال توفّره) في الجلسة. هذا هو المكان المناسب لتخزين هذه القيم، فهي مؤقتة وتتلقّى قيمًا جديدة عند فتح الإضافة.

# It's possible that we might return to this route later, in which case the
# parameters will not be passed in. Instead, use the values cached in the
# session.
login_hint = flask.session.get("login_hint")

# If there's still no login_hint query parameter, this must be their first
# time signing in, so send the user to the sign in page.
if login_hint is None:
    return start_auth_flow()

Java

انتقِل إلى مسار الصفحة المقصودة للإضافة في فئة وحدة التحكّم (/addon-discovery في AuthController.java في المثال المقدَّم). في بداية هذا المسار، استرجِع مَعلمة طلب البحث login_hint وخزِّنها.

/** Retrieve the login_hint query parameter from the request URL if present. */
String login_hint = request.getParameter("login_hint");

تأكَّد من تخزين login_hint (في حال توفّره) في الجلسة. هذا هو المكان المناسب لتخزين هذه القيم، فهي مؤقتة وتتلقّى قيمًا جديدة عند فتح الإضافة.

/** If login_hint wasn't sent, use the values in the session. */
if (login_hint == null) {
    login_hint = (String) session.getAttribute("login_hint");
}

/** If the there is still no login_hint, route the user to the authorization
 *  page. */
if (login_hint == null) {
    return startAuthFlow(model);
}

/** If the login_hint query parameter is provided, add it to the session. */
else if (login_hint != null) {
    session.setAttribute("login_hint", login_hint);
}

إضافة مَعلمات طلب البحث إلى مسار التفويض

يجب أيضًا تمرير المَعلمة login_hint إلى خوادم المصادقة التابعة لـ Google. يسهّل ذلك عملية المصادقة، فإذا كان تطبيقك يعرف المستخدم الذي يحاول المصادقة، يستخدم الخادم التلميح لتبسيط عملية تسجيل الدخول من خلال ملء حقل البريد الإلكتروني مسبقًا في نموذج تسجيل الدخول.

Python

انتقِل إلى مسار التفويض في ملف خادم Flask (/authorize في المثال الذي قدّمناه). أضِف الوسيطة login_hint إلى الاستدعاء إلى flow.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",
    # The user will automatically be selected if we have the login_hint.
    login_hint=flask.session.get("login_hint"),

Java

انتقِل إلى طريقة authorize() في فئة AuthService.java. أضِف login_hint كمعلَمة إلى الطريقة، وأضِف login_hint والوسيطة إلى أداة إنشاء عنوان URL الخاص بالتفويض.

String authUrl = flow
    .newAuthorizationUrl()
    .setState(state)
    .set("login_hint", login_hint)
    .setRedirectUri(REDIRECT_URI)
    .build();

إضافة مساحة تخزين ثابتة لبيانات اعتماد المستخدم

إذا تلقّيت login_hint كمعلَمة طلب بحث عند تحميل الإضافة، يشير ذلك إلى أنّ المستخدم قد أكمل عملية منح الإذن لتطبيقنا. عليك استرداد بيانات الاعتماد السابقة بدلاً من إجبارهم على تسجيل الدخول مرة أخرى.

تذكَّر أنّك تلقّيت رمزًا مميّزًا لإعادة التحميل عند اكتمال عملية منح الإذن. احفظ هذا الرمز المميز، إذ يمكن إعادة استخدامه للحصول على رمز مميز للوصول، وهو رمز قصير الأمد وضروري لاستخدام Google APIs. لقد حفظت بيانات الاعتماد هذه في الجلسة سابقًا، ولكن عليك تخزينها للتعامل مع الزيارات المتكررة.

تحديد مخطط المستخدم وإعداد قاعدة البيانات

إعداد مخطط قاعدة بيانات User

Python

تحديد مخطط المستخدم

يتضمّن User السمات التالية:

  • id: معرّف Google الخاص بالمستخدم يجب أن تتطابق هذه القيمة مع القيم المقدَّمة في مَعلمة طلب البحث login_hint.
  • display_name: الاسم الأول واسم العائلة للمستخدم، مثل "أحمد علي".
  • email: عنوان البريد الإلكتروني للمستخدم.
  • portrait_url: عنوان URL لصورة الملف الشخصي للمستخدم.
  • refresh_token: الرمز المميز لإعادة التحميل الذي تم الحصول عليه سابقًا

ينفّذ هذا المثال عملية التخزين باستخدام SQLite، وهو متوافق أصلاً مع Python. يستخدم هذا التطبيق الوحدة flask_sqlalchemy لتسهيل إدارة قاعدة البيانات.

إعداد قاعدة البيانات

أولاً، حدِّد موقعًا جغرافيًا للملف الخاص بقاعدة البيانات. انتقِل إلى ملف إعداد الخادم (config.py في المثال الذي قدّمناه) وأضِف ما يلي.

import os

# Point to a database file in the project root.
DATABASE_FILE_NAME = os.path.join(
    os.path.abspath(os.path.dirname(__file__)), 'data.sqlite')

class Config(object):
    SQLALCHEMY_DATABASE_URI = f"sqlite:///{DATABASE_FILE_NAME}"
    SQLALCHEMY_TRACK_MODIFICATIONS = False

يشير ذلك إلى أنّ Flask سيستخدم الملف data.sqlite في الدليل نفسه الذي يتضمّن الملف main.py.

بعد ذلك، انتقِل إلى دليل الوحدة وأنشئ ملف models.py جديدًا. هذا هو webapp/models.py إذا كنت تتّبع المثال المقدَّم. أضِف ما يلي إلى الملف الجديد لتحديد جدول User، مع استبدال webapp باسم الوحدة إذا كان مختلفًا.

from webapp import db

# Database model to represent a user.
class User(db.Model):
    # The user's identifying information:
    id = db.Column(db.String(120), primary_key=True)
    display_name = db.Column(db.String(80))
    email = db.Column(db.String(120), unique=True)
    portrait_url = db.Column(db.Text())

    # The user's refresh token, which will be used to obtain an access token.
    # Note that refresh tokens will become invalid if:
    # - The refresh token has not been used for six months.
    # - The user revokes your app's access permissions.
    # - The user changes passwords.
    # - The user belongs to a Google Cloud organization
    #   that has session control policies in effect.
    refresh_token = db.Column(db.Text())

أخيرًا، في ملف __init__.py الخاص بالوحدة، أضِف ما يلي لاستيراد النماذج الجديدة وإنشاء قاعدة البيانات.

from webapp import models
from os import path
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy(app)

# Initialize the database file if not created.
if not path.exists(config.DATABASE_FILE_NAME):
    db.create_all()

Java

تحديد مخطط المستخدم

يتضمّن User السمات التالية:

  • id: معرّف Google الخاص بالمستخدم يجب أن تتطابق هذه القيمة مع القيمة المقدَّمة في مَعلمة طلب البحث login_hint.
  • email: عنوان البريد الإلكتروني للمستخدم.

أنشئ ملف schema.sql في دليل resources الخاص بالوحدة. يقرأ Spring هذا الملف وينشئ مخططًا لقاعدة البيانات وفقًا لذلك. حدِّد الجدول باستخدام اسم جدول وusers وأعمدة لتمثيل سمات User وid وemail.

CREATE TABLE IF NOT EXISTS users (
    id VARCHAR(255) PRIMARY KEY, -- user's unique Google ID
    email VARCHAR(255), -- user's email address
);

أنشئ فئة Java لتحديد نموذج User لقاعدة البيانات. هذا هو User.java في المثال المقدَّم.

أضِف التعليق التوضيحي @Entity للإشارة إلى أنّ هذا العنصر POJO يمكن حفظه في قاعدة البيانات. أضِف التعليق التوضيحي @Table مع اسم الجدول المقابل الذي ضبطته في schema.sql.

يُرجى العِلم أنّ مثال الرمز البرمجي يتضمّن أدوات إنشاء وأدوات ضبط للسمتَين. يتم استخدام الدالة الإنشائية ودوال الضبط في AuthController.java لإنشاء مستخدم أو تعديله في قاعدة البيانات. يمكنك أيضًا تضمين دوال جلب البيانات ودالة toString حسب ما تراه مناسبًا، ولكن في هذا الشرح التفصيلي، لن يتم استخدام هذه الدوال وسيتم حذفها من مثال الرمز البرمجي في هذه الصفحة للاختصار.

/** An entity class that provides a model to store user information. */
@Entity
@Table(name = "users")
public class User {
    /** The user's unique Google ID. The @Id annotation specifies that this
     *   is the primary key. */
    @Id
    @Column
    private String id;

    /** The user's email address. */
    @Column
    private String email;

    /** Required User class no args constructor. */
    public User() {
    }

    /** The User class constructor that creates a User object with the
    *   specified parameters.
    *   @param id the user's unique Google ID
    *   @param email the user's email address
    */
    public User(String id, String email) {
        this.id = id;
        this.email = email;
    }

    public void setId(String id) { this.id = id; }

    public void setEmail(String email) { this.email = email; }
}

أنشئ واجهة باسم UserRepository.java للتعامل مع عمليات CRUD في قاعدة البيانات. توسّع هذه الواجهة نطاق الواجهة CrudRepository.

/** Provides CRUD operations for the User class by extending the
 *   CrudRepository interface. */
@Repository
public interface UserRepository extends CrudRepository<User, String> {
}

تسهّل فئة وحدة التحكّم التواصل بين العميل والمستودع. لذلك، عدِّل الدالة الإنشائية لفئة وحدة التحكّم لإدخال فئة UserRepository.

/** Declare UserRepository to be used in the Controller class constructor. */
private final UserRepository userRepository;

/**
*   ...
*   @param userRepository the class that interacts with User objects stored in
*   persistent storage.
*/
public AuthController(AuthService authService, UserRepository userRepository) {
    this.authService = authService;
    this.userRepository = userRepository;
}

إعداد قاعدة البيانات

لتخزين المعلومات المتعلّقة بالمستخدم، استخدِم قاعدة بيانات H2 المتوافقة بشكل أساسي مع Spring Boot. يتم استخدام قاعدة البيانات هذه أيضًا في الجولات الإرشادية اللاحقة لتخزين معلومات أخرى ذات صلة بـ Classroom. يتطلّب إعداد قاعدة بيانات H2 إضافة الإعداد التالي إلى application.properties.

# Enable configuration for persistent storage using an H2 database
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:file:./h2/userdb
spring.datasource.username=<USERNAME>
spring.datasource.password=<PASSWORD>
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false

ينشئ إعداد spring.datasource.url مجلدًا باسم h2، ويتم تخزين الملف userdb بداخله. أضِف مسار قاعدة بيانات H2 إلى .gitignore. عليك تعديل spring.datasource.username وspring.datasource.password قبل تشغيل التطبيق لضبط قاعدة البيانات باستخدام اسم مستخدم وكلمة مرور من اختيارك. لتعديل اسم المستخدم وكلمة المرور لقاعدة البيانات بعد تشغيل التطبيق، احذف الدليل h2 الذي تم إنشاؤه، وعدِّل الإعدادات، وأعِد تشغيل التطبيق.

يضمن ضبط إعداد spring.jpa.hibernate.ddl-auto على update الاحتفاظ بالبيانات المخزّنة في قاعدة البيانات عند إعادة تشغيل التطبيق. لمحو قاعدة البيانات في كل مرة تتم فيها إعادة تشغيل التطبيق، اضبط هذا الإعداد على create.

اضبط إعدادات spring.jpa.open-in-view على false. يكون هذا الإعداد مفعَّلاً تلقائيًا، وقد يؤدي إلى حدوث مشاكل في الأداء يصعُب تشخيصها في بيئة الإنتاج.

كما هو موضّح سابقًا، يجب أن تتمكّن من استرداد بيانات اعتماد المستخدم المتكرّر. ويتم ذلك من خلال ميزة التوافق مع مخزن بيانات الاعتماد المضمّن التي يوفّرها GoogleAuthorizationCodeFlow.

في الفئة AuthService.java، حدِّد مسارًا للملف الذي يتم فيه تخزين فئة بيانات الاعتماد. في هذا المثال، يتم إنشاء الملف في الدليل /credentialStore. أضِف مسار وحدة تخزين بيانات الاعتماد إلى .gitignore. يتم إنشاء هذا الدليل بعد أن يبدأ المستخدم مسار التفويض.

private static final File dataDirectory = new File("credentialStore");

بعد ذلك، أنشِئ طريقة في ملف AuthService.java تنشئ كائن FileDataStoreFactory وتعرضه. هذا هو مخزن البيانات الذي يخزّن بيانات الاعتماد.

/** Creates and returns FileDataStoreFactory object to store credentials.
 *   @return FileDataStoreFactory dataStore used to save and obtain users ids
 *   mapped to Credentials.
 *   @throws IOException if creating the dataStore is unsuccessful.
 */
public FileDataStoreFactory getCredentialDataStore() throws IOException {
    FileDataStoreFactory dataStore = new FileDataStoreFactory(dataDirectory);
    return dataStore;
}

عدِّل طريقة getFlow() في AuthService.java لتضمين setDataStoreFactory في طريقة GoogleAuthorizationCodeFlow Builder() واستدعِ getCredentialDataStore() لضبط مخزن البيانات.

GoogleAuthorizationCodeFlow authorizationCodeFlow =
    new GoogleAuthorizationCodeFlow.Builder(
        HTTP_TRANSPORT,
        JSON_FACTORY,
        getClientSecrets(),
        getScopes())
    .setAccessType("offline")
    .setDataStoreFactory(getCredentialDataStore())
    .build();

بعد ذلك، عدِّل getAndSaveCredentials(String authorizationCode). في السابق، كانت هذه الطريقة تحصل على بيانات الاعتماد بدون تخزينها في أي مكان. عدِّل الطريقة لتخزين بيانات الاعتماد في مستودع البيانات المفهرس حسب معرّف المستخدم.

يمكن الحصول على رقم تعريف المستخدم من العنصر TokenResponse باستخدام id_token، ولكن يجب التحقّق منه أولاً. بخلاف ذلك، قد تتمكّن تطبيقات العميل من انتحال هوية المستخدمين عن طريق إرسال أرقام تعريف معدَّلة للمستخدمين إلى الخادم. ننصحك باستخدام مكتبات Google API Client للتحقّق من صحة id_token. يمكنك الاطّلاع على [صفحة "هوية Google" حول التحقّق من رمز تعريف Google] للحصول على مزيد من المعلومات.

// Obtaining the id_token will help determine which user signed in to the application.
String idTokenString = tokenResponse.get("id_token").toString();

// Validate the id_token using the GoogleIdTokenVerifier object.
GoogleIdTokenVerifier googleIdTokenVerifier = new GoogleIdTokenVerifier.Builder(
        HTTP_TRANSPORT,
        JSON_FACTORY)
    .setAudience(Collections.singletonList(
        googleClientSecrets.getWeb().getClientId()))
    .build();

GoogleIdToken idToken = googleIdTokenVerifier.verify(idTokenString);

if (idToken == null) {
    throw new Exception("Invalid ID token.");
}

بعد إثبات ملكية id_token، احصل على userId لتخزينه مع بيانات الاعتماد التي تم الحصول عليها.

// Obtain the user id from the id_token.
Payload payload = idToken.getPayload();
String userId = payload.getSubject();

عدِّل المكالمة إلى flow.createAndStoreCredential لتضمين userId.

// Save the user id and credentials to the configured FileDataStoreFactory.
Credential credential = flow.createAndStoreCredential(tokenResponse, userId);

أضِف طريقة إلى الفئة AuthService.java تعرض بيانات الاعتماد الخاصة بمستخدم معيّن إذا كانت متوفّرة في مخزن البيانات.

/** Find credentials in the datastore based on a specific user id.
*   @param userId key to find in the file datastore.
*   @return Credential object to be returned if a matching key is found in the datastore. Null if
*   the key doesn't exist.
*   @throws Exception if building flow object or checking for userId key is unsuccessful. */
public Credential loadFromCredentialDataStore(String userId) throws Exception {
    try {
        GoogleAuthorizationCodeFlow flow = getFlow();
        Credential credential = flow.loadCredential(userId);
        return credential;
    } catch (Exception e) {
        e.printStackTrace();
        throw e;
    }
}

استرداد بيانات الاعتماد

حدِّد طريقة لجلب Users. يتم تزويدك بمعرّف id في مَعلمة طلب البحث login_hint، ويمكنك استخدام هذا المعرّف لاسترداد سجلّ مستخدم معيّن.

Python

def get_credentials_from_storage(id):
    """
    Retrieves credentials from the storage and returns them as a dictionary.
    """
    return User.query.get(id)

Java

في الفئة AuthController.java، حدِّد طريقة لاسترداد مستخدم من قاعدة البيانات استنادًا إلى رقم تعريف المستخدم.

/** Retrieves stored credentials based on the user id.
*   @param id the id of the current user
*   @return User the database entry corresponding to the current user or null
*   if the user doesn't exist in the database.
*/
public User getUser(String id) {
    if (id != null) {
        Optional<User> user = userRepository.findById(id);
        if (user.isPresent()) {
            return user.get();
        }
    }
    return null;
}

تخزين بيانات الاعتماد

هناك سيناريوهان عند تخزين بيانات الاعتماد. إذا كان id الخاص بالمستخدم مدرجًا في قاعدة البيانات، عدِّل السجلّ الحالي بأي قيم جديدة. بخلاف ذلك، أنشئ سجلّ User جديدًا وأضِفه إلى قاعدة البيانات.

Python

عليك أولاً تحديد طريقة مساعدة تنفّذ سلوك التخزين أو التعديل.

def save_user_credentials(credentials=None, user_info=None):
    """
    Updates or adds a User to the database. A new user is added only if both
    credentials and user_info are provided.

    Args:
        credentials: An optional Credentials object.
        user_info: An optional dict containing user info returned by the
            OAuth 2.0 API.
    """

    existing_user = get_credentials_from_storage(
        flask.session.get("login_hint"))

    if existing_user:
        if user_info:
            existing_user.id = user_info.get("id")
            existing_user.display_name = user_info.get("name")
            existing_user.email = user_info.get("email")
            existing_user.portrait_url = user_info.get("picture")

        if credentials and credentials.refresh_token is not None:
            existing_user.refresh_token = credentials.refresh_token

    elif credentials and user_info:
        new_user = User(id=user_info.get("id"),
                        display_name=user_info.get("name"),
                        email=user_info.get("email"),
                        portrait_url=user_info.get("picture"),
                        refresh_token=credentials.refresh_token)

        db.session.add(new_user)

    db.session.commit()

هناك حالتان يمكنك فيهما حفظ بيانات الاعتماد في قاعدة البيانات: عندما يعود المستخدم إلى تطبيقك في نهاية عملية التفويض وعند إصدار طلب إلى واجهة برمجة التطبيقات. وهي المكان الذي سبق أن حدّدنا فيه مفتاح الجلسة credentials.

اتّصِل بالرقم save_user_credentials في نهاية مسار callback. احتفِظ بالكائن user_info بدلاً من استخراج اسم المستخدم فقط.

# The flow is complete! We'll use the credentials to fetch the user's info.
user_info_service = googleapiclient.discovery.build(
    serviceName="oauth2", version="v2", credentials=credentials)

user_info = user_info_service.userinfo().get().execute()

flask.session["username"] = user_info.get("name")

save_user_credentials(credentials, user_info)

عليك أيضًا تعديل بيانات الاعتماد بعد إجراء طلبات البيانات من واجهة برمجة التطبيقات. في هذه الحالة، يمكنك تقديم بيانات الاعتماد المعدَّلة كمعلَمات للطريقة save_user_credentials.

# Save credentials in case access token was refreshed.
flask.session["credentials"] = credentials_to_dict(credentials)
save_user_credentials(credentials)

Java

عليك أولاً تحديد طريقة تخزِّن كائن User أو تعدّله في قاعدة بيانات H2.

/** Adds or updates a user in the database.
*   @param credential the credentials object to save or update in the database.
*   @param userinfo the userinfo object to save or update in the database.
*   @param session the current session.
*/
public void saveUser(Credential credential, Userinfo userinfo, HttpSession session) {
    User storedUser = null;
    if (session != null && session.getAttribute("login_hint") != null) {
        storedUser = getUser(session.getAttribute("login_hint").toString());
    }

    if (storedUser != null) {
        if (userinfo != null) {
            storedUser.setId(userinfo.getId());
            storedUser.setEmail(userinfo.getEmail());
        }
        userRepository.save(storedUser);
    } else if (credential != null && userinfo != null) {
        User newUser = new User(
            userinfo.getId(),
            userinfo.getEmail(),
        );
        userRepository.save(newUser);
    }
}

هناك حالتان يمكنك فيهما حفظ بيانات الاعتماد في قاعدة البيانات: عندما يعود المستخدم إلى تطبيقك في نهاية عملية التفويض وعند إصدار طلب إلى واجهة برمجة التطبيقات. وهي المكان الذي سبق أن حدّدنا فيه مفتاح الجلسة credentials.

اتّصِل بالرقم saveUser في نهاية المسار /callback. عليك الاحتفاظ بالكائن user_info بدلاً من استخراج البريد الإلكتروني للمستخدم فقط.

/** This is the end of the auth flow. We should save user info to the database. */
Userinfo userinfo = authService.getUserInfo(credentials);
saveUser(credentials, userinfo, session);

عليك أيضًا تعديل بيانات الاعتماد بعد إجراء طلبات البيانات من واجهة برمجة التطبيقات. في هذه الحالة، يمكنك تقديم بيانات الاعتماد المعدَّلة كمعلَمات لطريقة saveUser.

/** Save credentials in case access token was refreshed. */
saveUser(credentials, null, session);

بيانات الاعتماد المنتهية الصلاحية

يُرجى العِلم أنّ هناك بعض الأسباب التي قد تؤدي إلى إبطال رموز التحديث. وتشمل هذه الخدمات ما يلي:

  • لم يتم استخدام الرمز المميّز لإعادة التحميل لمدة ستة أشهر.
  • إذا ألغى المستخدم أذونات الوصول التي منحها لتطبيقك
  • يغيّر المستخدم كلمات المرور.
  • ينتمي المستخدم إلى مؤسسة Google Cloud التي تتضمّن سياسات سارية للتحكّم في الجلسة.

يمكنك الحصول على رموز مميّزة جديدة من خلال إعادة توجيه المستخدم إلى عملية التفويض إذا أصبحت بيانات اعتماده غير صالحة.

توجيه المستخدم تلقائيًا

عدِّل مسار الصفحة المقصودة للإضافة لرصد ما إذا كان المستخدم قد سبق له منح الإذن لتطبيقنا. إذا كان الأمر كذلك، أرسِلهم إلى صفحة الإضافات الرئيسية. وإلا، اطلب منهم تسجيل الدخول.

Python

تأكَّد من إنشاء ملف قاعدة البيانات عند تشغيل التطبيق. أدرِج ما يلي في أداة تهيئة الوحدة (مثل webapp/__init__.py في المثال الذي قدّمناه) أو في الطريقة الرئيسية التي تُشغّل الخادم.

# Initialize the database file if not created.
if not os.path.exists(DATABASE_FILE_NAME):
    db.create_all()

بعد ذلك، يجب أن تعالج طريقتك مَعلمة طلب البحث login_hint كما هو موضّح أعلاه. بعد ذلك، حمِّل بيانات اعتماد المتجر إذا كان هذا المستخدم قد زار المتجر من قبل. يمكنك معرفة ما إذا كان الزائر متكررًا إذا تلقّيت login_hint. استرداد أي بيانات اعتماد مخزّنة لهذا المستخدم وتحميلها في الجلسة

stored_credentials = get_credentials_from_storage(login_hint)

# If we have stored credentials, store them in the session.
if stored_credentials:
    # Load the client secrets file contents.
    client_secrets_dict = json.load(
        open(CLIENT_SECRETS_FILE)).get("web")

    # Update the credentials in the session.
    if not flask.session.get("credentials"):
        flask.session["credentials"] = {}

    flask.session["credentials"] = {
        "token": stored_credentials.access_token,
        "refresh_token": stored_credentials.refresh_token,
        "token_uri": client_secrets_dict["token_uri"],
        "client_id": client_secrets_dict["client_id"],
        "client_secret": client_secrets_dict["client_secret"],
        "scopes": SCOPES
    }

    # Set the username in the session.
    flask.session["username"] = stored_credentials.display_name

وأخيرًا، وجِّه المستخدم إلى صفحة تسجيل الدخول إذا لم تتوفر لدينا بيانات الاعتماد الخاصة به. وفي حال توفُّرها، سنوجّه المستخدمين إلى صفحة الإضافة الرئيسية.

if "credentials" not in flask.session or \
    flask.session["credentials"]["refresh_token"] is None:
    return flask.render_template("authorization.html")

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

Java

انتقِل إلى مسار الصفحة المقصودة للإضافة (/addon-discovery في المثال المقدَّم). كما ذكرنا أعلاه، هذا هو المكان الذي تعاملت فيه مع مَعلمة طلب البحث login_hint.

تحقَّق أولاً مما إذا كانت هناك بيانات اعتماد في الجلسة. إذا لم يكن الأمر كذلك، وجِّه المستخدم إلى مسار المصادقة من خلال استدعاء الطريقة startAuthFlow.

/** Check if the credentials exist in the session. The session could have
 *   been cleared when the user clicked the Sign-Out button, and the expected
 *   behavior after sign-out would be to display the sign-in page when the
 *   iframe is opened again. */
if (session.getAttribute("credentials") == null) {
    return startAuthFlow(model);
}

بعد ذلك، حمِّل المستخدم من قاعدة بيانات H2 إذا كان زائرًا متكررًا. يُعدّ الزائر متكرّرًا إذا تلقّيت مَعلمة طلب البحث login_hint. إذا كان المستخدم متوفّرًا في قاعدة بيانات H2، حمِّل بيانات الاعتماد من مخزن بيانات الاعتماد الذي تم إعداده سابقًا، واضبط بيانات الاعتماد في الجلسة. إذا لم يتم الحصول على بيانات الاعتماد من متجر بيانات الاعتماد، عليك توجيه المستخدم خلال عملية المصادقة من خلال استدعاء startAuthFlow.

/** At this point, we know that credentials exist in the session, but we
 *   should update the session credentials with the credentials in persistent
 *   storage in case they were refreshed. If the credentials in persistent
 *   storage are null, we should navigate the user to the authorization flow
 *   to obtain persisted credentials. */

User storedUser = getUser(login_hint);

if (storedUser != null) {
    Credential credential = authService.loadFromCredentialDataStore(login_hint);
    if (credential != null) {
        session.setAttribute("credentials", credential);
    } else {
        return startAuthFlow(model);
    }
}

أخيرًا، وجِّه المستخدم إلى الصفحة المقصودة للإضافة.

/** Finally, if there are credentials in the session and in persistent
 *   storage, direct the user to the addon-discovery page. */
return "addon-discovery";

اختبار الإضافة

سجِّل الدخول إلى Google Classroom بصفتك أحد مستخدمي المعلّم التجريبيين. انتقِل إلى علامة التبويب الواجب الدراسي وأنشِئ مهمة جديدة. انقر على زر الإضافات أسفل مساحة النص، ثم اختَر الإضافة. يتم فتح إطار iframe ويتم تحميل معرّف الموارد المنتظم لإعداد المرفقات الذي حدّدته في صفحة إعدادات التطبيق في حزمة تطوير البرامج (SDK) في Google Workspace Marketplace.

تهانينا! أنت الآن جاهز للانتقال إلى الخطوة التالية: إنشاء المرفقات وتحديد دور المستخدم.