PAYATHON 2026

用户登录后 payload 返回 undefined,如何获取 Unique ID?

支付小周

结论

登录成功后,应从 Google Sign-In 返回的 ID Token payload 中读取 sub:

const userId = conv.user.profile.payload.sub;

sub 是该 Google 账号在当前 OAuth 客户端下稳定的唯一标识,可以用来关联 Firestore 用户记录。

当前代码返回 undefined 有三个原因:

  1. 代码读取了整个 payload,但没有继续读取其中的 sub。
  2. clientId 是空字符串,无法正确验证和解析 Google Sign-In 的 ID Token。
  3. 同一个 webhook 混用了 actions-on-google 的 app.intent() 和 dialogflow-fulfillment 的 agent.handleRequest()。实际请求只传给了 agent.handleRequest(),所以注册在 app.intent() 上的登录处理函数不会执行。

另外,conv.user.access.token 是 OAuth access token,不是用户 ID,不应将它作为 Firestore 文档 ID。

正确的处理方式

可以让 actions-on-google 的 app 直接处理整个 webhook,不再同时使用 WebhookClient。

'use strict';

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {
  dialogflow,
  SignIn
} = require('actions-on-google');

admin.initializeApp();

const db = admin.firestore();

const app = dialogflow({
  clientId: 'YOUR_WEB_OAUTH_CLIENT_ID.apps.googleusercontent.com'
});

app.intent('Sign in user', conv => {
  conv.ask(new SignIn('To personalize your experience'));
});

app.intent('Get Signin', async (conv, params, signin) => {
  if (!signin || signin.status !== 'OK') {
    conv.ask('登录没有完成,请重试。');
    return;
  }

  const profile = conv.user.profile;
  const payload = profile && profile.payload;
  const userId = payload && payload.sub;

  if (!userId) {
    console.error('Google Sign-In payload does not contain sub');
    conv.ask('已经完成登录,但无法取得账号标识,请稍后重试。');
    return;
  }

  const userRef = db.collection('users').doc(userId);

  await userRef.set({
    displayName: payload.name || null,
    email: payload.email || null,
    updatedAt: admin.firestore.FieldValue.serverTimestamp()
  }, {
    merge: true
  });

  conv.ask(`欢迎回来,${payload.name || '用户'}。请问需要查询什么?`);
});

app.intent('GetOrderDetails', async conv => {
  const payload =
    conv.user.profile &&
    conv.user.profile.payload;

  const userId = payload && payload.sub;

  if (!userId) {
    conv.ask(new SignIn('To retrieve your order details'));
    return;
  }

  const snapshot = await db.collection('users').doc(userId).get();

  if (!snapshot.exists) {
    conv.ask('没有找到您的订单信息。');
    return;
  }

  const data = snapshot.data();
  conv.ask(`您的订单状态是 ${data.orderStatus || '暂时未知'}。`);
});

exports.dialogflowFirebaseFulfillment =
  functions.https.onRequest(app);

Dialogflow 中的配置

请确认以下配置均已完成:

  • Sign in user Intent 用于发起登录。
  • Get Signin Intent 必须关联 Google Assistant 的 actions_intent_SIGN_IN 事件。
  • clientId 必须填写项目中正确的 Web OAuth 2.0 Client ID,不能留空。
  • 上述 Intent 必须启用 Fulfillment webhook。
  • Intent 名称必须与代码完全一致,包括大小写和空格。

登录完成且 signin.status 为 OK 时,通常可以取得类似下面的 payload:

{
  iss: 'https://accounts.google.com',
  aud: 'YOUR_CLIENT_ID',
  sub: 'ACCOUNT_UNIQUE_IDENTIFIER',
  email: 'user@example.com',
  name: 'Example User'
}

应使用的唯一标识是:

payload.sub

payload.email 和 payload.name 不适合作为主键,因为邮箱和显示名称可能发生变化。

为什么变量 payload 不能跨 Intent 使用

原代码中的变量:

var payload;

只在当前这一次 Cloud Function 请求中存在。登录回调结束后,用户下次调用订单 Intent 通常会产生新的 webhook 请求,此时该变量会重新初始化为 undefined。

因此,不能依赖全局变量或请求内变量保存登录身份。每次请求都应从经过验证的用户 profile 中读取 payload.sub,也可以通过服务端会话和数据库映射恢复身份。

Firestore 使用建议

可以直接将 sub 用作文档 ID:

const userRef = db.collection('users').doc(userId);

订单、付款和配送信息可以存入对应的用户文档:

await userRef.set({
  orderStatus: 'Processing',
  paymentStatus: 'Paid',
  deliveryDetails: 'Expected tomorrow'
}, {
  merge: true
});

查询时,也应根据当前登录用户的 sub 获取文档:

const userId = conv.user.profile.payload.sub;
const snapshot = await db.collection('users').doc(userId).get();

不要继续使用固定的文档 ID:

db.collection('users').doc('test');

否则,所有登录用户最终都会读取同一条记录。

注意事项

  • 不要将 access.token 当作用户 ID。Access token 有有效期,可能刷新,而且属于敏感凭据。
  • 不要在生产日志中记录完整的 ID Token、access token 或整个请求体。
  • sub 的唯一性与 OAuth issuer/client 配置有关。如果系统接入了多个身份提供方,可以同时保存身份提供方,例如使用 google:${sub} 建立内部用户映射。
  • 如果 conv.user.profile 仍然不存在,请先检查 OAuth Client ID、actions_intent_SIGN_IN 事件,以及 webhook 是否确实由 app 处理。
  • 以上代码适用于问题中使用的 actions-on-google Dialogflow 集成方式。其他版本的 Actions SDK 或迁移后的平台可能使用不同的登录回调结构,应以项目实际使用的 SDK 版本为准。

备注:内容仅供参考。