ربط الحساب عبر تسجيل الدخول بحساب Google (Dialogflow)

إنّ تسجيل الدخول باستخدام حساب Google في "مساعد Google" يوفّر تجربة المستخدم أبسط وأسهل. للمستخدمين والمطوّرين على حدّ سواء لربط الحسابات أو إنشائها. الإجراء الخاص بك طلب الوصول إلى الملف الشخصي للمستخدم في حساب Google أثناء محادثة، بما في ذلك اسم المستخدم وعنوان بريده الإلكتروني وصورة ملفه الشخصي

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

لربط الحساب باستخدام ميزة "تسجيل الدخول باستخدام حساب Google"، يجب أن تطلب من المستخدم منح موافقته للوصول إلى ملفه الشخصي في Google. يمكنك بعد ذلك استخدام المعلومات الموجودة في ملفه الشخصي مثل عنوان بريده الإلكتروني، للتعرف على المستخدم في نظامك.

تنفيذ عملية ربط حساب "تسجيل الدخول بحساب Google"

يُرجى اتّباع الخطوات الواردة في الأقسام التالية لإضافة ربط حساب "تسجيل الدخول بحساب Google" إلى الحركة.

ضبط المشروع

لضبط مشروعك على استخدام ربط حساب "تسجيل الدخول بحساب Google"، اتّبِع الخطوات التالية:

  1. افتح "وحدة تحكّم المهام" واختَر مشروعًا.
  2. انقر على علامة التبويب تطوير واختَر ربط الحساب.
  3. فعِّل مفتاح التبديل بجانب ربط الحساب.
  4. في القسم إنشاء الحساب، اختَر نعم.
  5. في نوع الربط، اختَر تسجيل الدخول بحساب Google.

  6. افتح معلومات العميل ودوِّن قيمة رقم تعريف العميل الصادرة عن Google لإجراءاتك.

  7. انقر على حفظ.

بدء مسار المصادقة

استخدِم هدف المساعدة الخاص بتسجيل الدخول إلى الحساب. لبدء عملية المصادقة

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

للوصول إلى معلومات الملف الشخصي للمستخدم، عليك أولاً التحقّق من الرمز المميّز وفك ترميزه. من خلال تنفيذ ما يلي:

  1. استخدِم مكتبة فك ترميز JWT بلغتك لفك ترميز واستخدام مفاتيح Google العامة (المتوفرة في JWK أو PEM) للتحقق من توقيع الرمز المميّز.
  2. تأكَّد من أنّ جهة إصدار الرمز المميّز (الحقل iss في الرمز المميّز الذي تم فك ترميزه) هي https://accounts.google.com. وأن الجمهور (الحقل aud في الرمز المميز تم فك ترميزه) هي قيمة معرّف العميل الصادر عن Google لإجراءاتك، الذي يتم تعيينه لمشروعك في وحدة تحكّم "المهام مع مساعد Google".

في ما يلي مثال على رمز مميّز تم فك ترميزه:

{
  "sub": 1234567890,        // The unique ID of the user's Google Account
  "iss": "https://accounts.google.com",        // The token's issuer
  "aud": "123-abc.apps.googleusercontent.com", // Client ID assigned to your Actions project
  "iat": 233366400,         // Unix timestamp of the token's creation time
  "exp": 233370000,         // Unix timestamp of the token's expiration time
  "name": "Jan Jansen",
  "given_name": "Jan",
  "family_name": "Jansen",
  "email": "jan@gmail.com", // If present, the user's email address
  "locale": "en_US"
}

إذا كنت تستخدم مكتبة برامج "المهام مع مساعد Google" لنظام Node.js أو مكتبة برامج Java، فهو يتولى التحقق من صحة الرمز وفك ترميزه نيابةً عنك، ويتيح لك الوصول إلى لمحتوى الملف الشخصي، كما هو موضح في مقتطفات الرمز التالية. تجدر الإشارة إلى أنّ ملف JSON أدناه يصف طلب ردّ تلقائي على الويب لحزمة تطوير البرامج (SDK) وحزمة SDK الخاصة بالإجراءات على التوالي.

تستخدم المقتطفات التالية منصة Dialogflow لتسجيل الدخول:

Node.js
const {dialogflow, SignIn} = require('actions-on-google');
const app = dialogflow({
  // REPLACE THE PLACEHOLDER WITH THE CLIENT_ID OF YOUR ACTIONS PROJECT
  clientId: CLIENT_ID,
});

// Intent that starts the account linking flow.
app.intent('Start Signin', (conv) => {
  conv.ask(new SignIn('To get your account details'));
});
// Create a Dialogflow intent with the `actions_intent_SIGN_IN` event.
app.intent('Get Signin', (conv, params, signin) => {
  if (signin.status === 'OK') {
    const payload = conv.user.profile.payload;
    conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`);
  } else {
    conv.ask(`I won't be able to save your data, but what do you want to do next?`);
  }
});
Java
private String clientId = "<your_client_id>";

@ForIntent("Start Signin")
public ActionResponse text(ActionRequest request) {
  ResponseBuilder rb = getResponseBuilder(request);
  return rb.add(new SignIn().setContext("To get your account details")).build();
}
@ForIntent("actions.intent.SIGN_IN")
public ActionResponse getSignInStatus(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  if (request.isSignInGranted()) {
    GoogleIdToken.Payload profile = getUserProfile(request.getUser().getIdToken());
    responseBuilder.add(
        "I got your account details, "
            + profile.get("given_name")
            + ". What do you want to do next?");
  } else {
    responseBuilder.add("I won't be able to save your data, but what do you want to do next?");
  }
  return responseBuilder.build();
}

private GoogleIdToken.Payload getUserProfile(String idToken) {
  GoogleIdToken.Payload profile = null;
  try {
    profile = decodeIdToken(idToken);
  } catch (Exception e) {
    LOGGER.error("error decoding idtoken");
    LOGGER.error(e.toString());
  }
  return profile;
}

private GoogleIdToken.Payload decodeIdToken(String idTokenString)
    throws GeneralSecurityException, IOException {
  HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
  JacksonFactory jsonFactory = JacksonFactory.getDefaultInstance();
  GoogleIdTokenVerifier verifier =
      new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
          // Specify the CLIENT_ID of the app that accesses the backend:
          .setAudience(Collections.singletonList(clientId))
          .build();
  GoogleIdToken idToken = verifier.verify(idTokenString);
  return idToken.getPayload();
}
ملف JSON لـ Dialogflow
{
  "responseId": "",
  "queryResult": {
    "queryText": "",
    "action": "",
    "parameters": {},
    "allRequiredParamsPresent": true,
    "fulfillmentText": "",
    "fulfillmentMessages": [],
    "outputContexts": [],
    "intent": {
      "name": "Get Signin",
      "displayName": "Get Signin"
    },
    "intentDetectionConfidence": 1,
    "diagnosticInfo": {},
    "languageCode": ""
  },
  "originalDetectIntentRequest": {
    "source": "google",
    "version": "2",
    "payload": {
      "isInSandbox": true,
      "surface": {
        "capabilities": [
          {
            "name": "actions.capability.SCREEN_OUTPUT"
          },
          {
            "name": "actions.capability.AUDIO_OUTPUT"
          },
          {
            "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
          },
          {
            "name": "actions.capability.WEB_BROWSER"
          }
        ]
      },
      "inputs": [
        {
          "rawInputs": [],
          "intent": "",
          "arguments": [
            {
              "name": "SIGN_IN",
              "extension": {
                "@type": "type.googleapis.com/google.actions.v2.SignInValue",
                "status": "OK"
              }
            }
          ]
        }
      ],
      "user": {
        "idToken": "peJaCGci..."
      },
      "conversation": {},
      "availableSurfaces": [
        {
          "capabilities": [
            {
              "name": "actions.capability.SCREEN_OUTPUT"
            },
            {
              "name": "actions.capability.AUDIO_OUTPUT"
            },
            {
              "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
            },
            {
              "name": "actions.capability.WEB_BROWSER"
            }
          ]
        }
      ]
    }
  },
  "session": ""
}

تستخدم المقتطفات التالية حزمة تطوير البرامج (SDK) الخاصة بـ "المهام" لتسجيل الدخول:

Node.js
const {actionssdk, SignIn} = require('actions-on-google');
const app = actionssdk({
  // REPLACE THE PLACEHOLDER WITH THE CLIENT_ID OF YOUR ACTIONS PROJECT
  clientId: CLIENT_ID,
});

// Intent that starts the account linking flow.
app.intent('actions.intent.TEXT', (conv) => {
  conv.ask(new SignIn('To get your account details'));
});
// Create an Actions SDK intent with the `actions_intent_SIGN_IN` event.
app.intent('actions.intent.SIGN_IN', (conv, params, signin) => {
  if (signin.status === 'OK') {
    const payload = conv.user.profile.payload;
    conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`);
  } else {
    conv.ask(`I won't be able to save your data, but what do you want to do next?`);
  }
});
Java
private String clientId = "<your_client_id>";

@ForIntent("actions.intent.TEXT")
public ActionResponse text(ActionRequest request) {
  ResponseBuilder rb = getResponseBuilder(request);
  return rb.add(new SignIn().setContext("To get your account details")).build();
}
@ForIntent("actions.intent.SIGN_IN")
public ActionResponse getSignInStatus(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  if (request.isSignInGranted()) {
    GoogleIdToken.Payload profile = getUserProfile(request.getUser().getIdToken());
    responseBuilder.add(
        "I got your account details, "
            + profile.get("given_name")
            + ". What do you want to do next?");
  } else {
    responseBuilder.add("I won't be able to save your data, but what do you want to do next?");
  }
  return responseBuilder.build();
}

private GoogleIdToken.Payload getUserProfile(String idToken) {
  GoogleIdToken.Payload profile = null;
  try {
    profile = decodeIdToken(idToken);
  } catch (Exception e) {
    LOGGER.error("error decoding idtoken");
    LOGGER.error(e.toString());
  }
  return profile;
}

private GoogleIdToken.Payload decodeIdToken(String idTokenString)
    throws GeneralSecurityException, IOException {
  HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
  JacksonFactory jsonFactory = JacksonFactory.getDefaultInstance();
  GoogleIdTokenVerifier verifier =
      new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
          // Specify the CLIENT_ID of the app that accesses the backend:
          .setAudience(Collections.singletonList(this.clientId))
          .build();
  GoogleIdToken idToken = verifier.verify(idTokenString);
  return idToken.getPayload();
}
ملف JSON لحزمة تطوير البرامج (SDK) الخاصة بالإجراءات
{
  "user": {
    "idToken": "peJaCGci..."
  },
  "device": {},
  "surface": {
    "capabilities": [
      {
        "name": "actions.capability.SCREEN_OUTPUT"
      },
      {
        "name": "actions.capability.AUDIO_OUTPUT"
      },
      {
        "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
      },
      {
        "name": "actions.capability.WEB_BROWSER"
      }
    ]
  },
  "conversation": {},
  "inputs": [
    {
      "rawInputs": [],
      "intent": "actions.intent.SIGN_IN",
      "arguments": [
        {
          "name": "SIGN_IN",
          "extension": {
            "@type": "type.googleapis.com/google.actions.v2.SignInValue",
            "status": "OK"
          }
        }
      ]
    }
  ],
  "availableSurfaces": [
    {
      "capabilities": [
        {
          "name": "actions.capability.SCREEN_OUTPUT"
        },
        {
          "name": "actions.capability.AUDIO_OUTPUT"
        },
        {
          "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
        },
        {
          "name": "actions.capability.WEB_BROWSER"
        }
      ]
    }
  ]
}

التعامل مع طلبات الوصول إلى البيانات

لمعالجة طلب الوصول إلى البيانات، ما عليك سوى التأكد من تأكيد المستخدم من خلال رقم تعريف Google. الرمز المميز موجودًا بالفعل في قاعدة البيانات الخاصة بك. يعرض مقتطف الرمز التالي: مثال على كيفية التحقق مما إذا كان حساب المستخدم موجودًا بالفعل في قاعدة بيانات Firestore.

Node.js
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp();
const auth = admin.auth();
const db = admin.firestore();

// Save the user in the Firestore DB after successful signin
app.intent('Get Sign In', async (conv, params, signin) => {
  if (signin.status !== 'OK') {
    return conv.close(`Let's try again next time.`);
  }
  const color = conv.data[Fields.COLOR];
  const {email} = conv.user;
  if (!conv.data.uid && email) {
    try {
      conv.data.uid = (await auth.getUserByEmail(email)).uid;
    } catch (e) {
      if (e.code !== 'auth/user-not-found') {
        throw e;
      }
      // If the user is not found, create a new Firebase auth user
      // using the email obtained from the Google Assistant
      conv.data.uid = (await auth.createUser({email})).uid;
    }
  }
  if (conv.data.uid) {
    conv.user.ref = db.collection('users').doc(conv.data.uid);
  }
  conv.close(`I saved ${color} as your favorite color for next time.`);
});

// Retrieve the user's favorite color if an account exists, ask if it doesn't.
app.intent('Default Welcome Intent', async (conv) => {
  const {payload} = conv.user.profile;
  const name = payload ? ` ${payload.given_name}` : '';
  conv.ask(`Hi${name}!`);
  // conv.user.ref contains the id of the record for the user in a Firestore DB
  if (conv.user.ref) {
    const doc = await conv.user.ref.get();
    if (doc.exists) {
      const color = doc.data()[Fields.COLOR];
      return conv.ask(`Your favorite color was ${color}. ` +
        'Tell me a color to update it.');
    }
  }
  conv.ask(`What's your favorite color?`);
});
Java
private class FirestoreManager {
  private final Firestore db;
  private final DocumentReference userDocRef;
  private final String uid;
  public FirestoreManager(String databaseUrl, String email)
      throws IOException, FirebaseAuthException {
    if (FirebaseApp.getApps().isEmpty()) {
      // Use the application default credentials (works on GCP based hosting).
      FirebaseOptions options =
          new FirebaseOptions.Builder()
              .setCredentials(GoogleCredentials.getApplicationDefault())
              .setDatabaseUrl(databaseUrl)
              .build();
      FirebaseApp.initializeApp(options);
    }
    this.db = FirestoreClient.getFirestore();
    UserRecord userRecord;
    try {
      userRecord = FirebaseAuth.getInstance().getUserByEmail(email);
    } catch (FirebaseAuthException e) {
      if (e.getErrorCode() == FIREBASE_USER_NOT_FOUND_ERROR) {
        UserRecord.CreateRequest createRequest = new UserRecord.CreateRequest().setEmail(email);
        userRecord = FirebaseAuth.getInstance().createUser(createRequest);
      } else {
        throw e;
      }
    }
    uid = userRecord.getUid();
    userDocRef = db.collection(FIRESTORE_USERS_PATH).document(uid);
  }

  public String readUserColor() throws ExecutionException, InterruptedException {
    ApiFuture<DocumentSnapshot> future = userDocRef.get();
    // future.get() blocks on response
    DocumentSnapshot document = future.get();
    if (document.exists()) {
      return document.get(COLOR_KEY).toString();
    } else {
      return "";
    }
  }
  public Timestamp writeUserColor(String color) throws ExecutionException, InterruptedException {
    Map<String, Object> docData = new HashMap<>();
    docData.put(COLOR_KEY, color);
    ApiFuture<WriteResult> future = userDocRef.set(docData);
    // future.get() blocks on response
    return future.get().getUpdateTime();
  }
}

@ForIntent("Get Sign In")
public ActionResponse getSignIn(ActionRequest request) {
  LOGGER.info("Get sign in intent start.");
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  if (request.isSignInGranted()) {
    String color = request.getConversationData().get(COLOR_KEY).toString();
    GoogleIdToken.Payload profile = getUserProfile(request.getUser().getIdToken());
    try {
      FirestoreManager firestoreManager =
          new FirestoreManager(DATABASE_URL, profile.getEmail());
      saveColor(firestoreManager, color);
    } catch (Exception e) {
      LOGGER.error(e.toString());
    }
    responseBuilder
        .add("I saved " + color + " as your favorite color for next time.")
        .endConversation();
  } else {
    responseBuilder.add("Let's try again next time");
  }
  LOGGER.info("Get sign in intent end.");
  return responseBuilder.build();
}

private void saveColor(FirestoreManager firestoreManager, String color) {
  try {
    Timestamp updateTime = firestoreManager.writeUserColor(color);
    LOGGER.info(String.format("Update time: %s", updateTime.toString()));
  } catch (Exception e) {
    LOGGER.error(e.toString());
  }
}