基于Node.js自定义认证的Google Actions Builder账号关联问题
Google Actions Builder 自定义OAuth账号关联问题解答
问题描述
我正在创建Google Actions Builder,在账号关联环节有疑问,希望基于自有数据库数据完成账号关联认证。具体需求是:账号关联时传入邮箱,仅当该邮箱处于活跃状态时才允许完成关联,因此编写了自定义Node.js函数。已将关联类型设为OAuth,授权和令牌URL指向自定义函数,但有两个疑问:
- 账号关联时如何传递邮箱ID;
- 验证邮箱后如何在Google Actions Builder中完成账号关联。
附上我的Node.js代码,希望在/auth回调的if(result)中编写允许关联的逻辑,else中返回提示:
const express = require('express'); const port = 5003; const app = express(); app.get('/', (req, res) =>{ res.send(`Welcome to Test App Nodejs`); }) app.get('/auth', (req, res) =>{ var email = req.query.email; userModel.findAll({ where: { emailId: email, status:1 } }).then((result) =>{ if(result){ // Allow to account link } else{ // to return Thanks for trying to account linking } }).catch((err)=>{ res.send(err); }) }); app.listen(port, (req, res)=>{ console.log(`Test App runing with ${port}`) })
疑问解答与代码修正
1. 账号关联时传递邮箱ID的方式
由于你使用的是OAuth 2.0授权码流程,Google Actions会引导用户跳转到你配置的授权页面。你需要在这个页面中添加邮箱输入框,让用户主动输入邮箱,同时保留Google传递的OAuth标准参数(client_id、redirect_uri、state等),提交表单时将邮箱与这些参数一起传给后端接口。
2. 验证邮箱后完成账号关联的逻辑
OAuth 2.0流程要求你的服务完成两个核心接口:
- 授权接口:验证邮箱有效性,通过则生成授权码并跳转回Google的重定向URL;
- 令牌接口:接收Google传递的授权码,交换生成访问令牌(access token),供后续Actions请求识别用户。
以下是修正后的完整代码:
const express = require('express'); const crypto = require('crypto'); const port = 5003; const app = express(); app.use(express.urlencoded({ extended: true })); // 生产环境建议用数据库存储授权码,关联用户信息 const authCodes = new Map(); app.get('/', (req, res) => { res.send(`Welcome to Test App Nodejs`); }) // 授权页面:展示邮箱输入框,保留OAuth参数 app.get('/authorize', (req, res) => { const { client_id, redirect_uri, response_type, state } = req.query; res.send(` <form method="POST" action="/auth"> <input type="email" name="email" placeholder="输入你的邮箱" required> <input type="hidden" name="client_id" value="${client_id}"> <input type="hidden" name="redirect_uri" value="${redirect_uri}"> <input type="hidden" name="response_type" value="${response_type}"> <input type="hidden" name="state" value="${state}"> <button type="submit">提交</button> </form> `); }); // 邮箱验证与授权码生成 app.post('/auth', (req, res) => { const { email, client_id, redirect_uri, state } = req.body; userModel.findAll({ where: { emailId: email, status: 1 } }).then((result) => { // findAll返回数组,需判断长度确认是否存在有效用户 if (result.length > 0) { // 生成随机授权码(生产环境建议用更安全的算法) const authCode = crypto.randomBytes(16).toString('hex'); // 存储授权码与用户关联信息 authCodes.set(authCode, { email }); // 跳转回Google的重定向URL,携带授权码和state参数 const redirectUrl = new URL(redirect_uri); redirectUrl.searchParams.set('code', authCode); redirectUrl.searchParams.set('state', state); res.redirect(redirectUrl.toString()); } else { // 返回验证失败提示 res.send('抱歉,该邮箱未激活或不存在,无法完成账号关联'); } }).catch((err) => { res.send(`服务器错误:${err.message}`); }); }); // 令牌交换接口:Google用授权码换access token app.post('/token', (req, res) => { const { code, client_id, grant_type } = req.body; const userInfo = authCodes.get(code); if (userInfo && grant_type === 'authorization_code') { // 生成访问令牌(生产环境建议用JWT) const accessToken = crypto.randomBytes(32).toString('hex'); // 销毁已使用的授权码 authCodes.delete(code); // 返回符合OAuth标准的响应 res.json({ access_token: accessToken, token_type: 'Bearer', expires_in: 3600 // 令牌有效期1小时 }); } else { res.status(400).json({ error: 'invalid_grant' }); } }); app.listen(port, () => { console.log(`Test App running on port ${port}`); });
关键注意事项
- 授权页面必须保留Google传递的
state参数,用于防止CSRF攻击; findAll返回的是数组,需通过result.length > 0判断是否存在有效用户;- 令牌接口需严格遵循OAuth 2.0响应格式,否则Google会拒绝完成账号关联;
- 生产环境中,授权码和访问令牌的存储与生成需采用更安全的方案(如数据库存储、JWT令牌)。
内容的提问来源于stack exchange,提问作者Ramesh S
相关产品推荐
相关产品推荐

