用户登录后 payload 返回 undefined,如何获取 Unique ID?
支付小周
结论
登录成功后,应从 Google Sign-In 返回的 ID Token payload 中读取 sub:
const userId = conv.user.profile.payload.sub;
sub 是该 Google 账号在当前 OAuth 客户端下稳定的唯一标识,可以用来关联 Firestore 用户记录。
当前代码返回 undefined 有三个原因:
- 代码读取了整个
payload,但没有继续读取其中的sub。 clientId是空字符串,无法正确验证和解析 Google Sign-In 的 ID Token。- 同一个 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 userIntent 用于发起登录。Get SigninIntent 必须关联 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-googleDialogflow 集成方式。其他版本的 Actions SDK 或迁移后的平台可能使用不同的登录回调结构,应以项目实际使用的 SDK 版本为准。
备注:内容仅供参考。
