使用 Firebase Auth Webhook 为 Hasura GraphQL Engine 实现自定义认证:Node.js 实战指南
【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine
本指南以仓库中的 nodejs-firebase 认证 Webhook 样板 为主体,讲解如何用 Node.js 编写一个转发到 Firebase Auth 并返回会话变量的认证 Webhook,并把它接入 Hasura GraphQL Engine 的 Webhook 认证模式。读完本文,你将掌握该样板的源码结构、三种部署方式(Heroku / Now / Glitch)、FIREBASE_CONFIG环境变量的配置,以及 Webhook 请求/响应协议(HASURA_GRAPHQL_AUTH_HOOK、X-Hasura-*会话变量)的完整细节,可以直接落地一套基于 Firebase ID Token 的 Hasura 认证方案。
背景:为什么需要 Auth Webhook
Hasura GraphQL Engine 本身不做身份认证,而是把"谁在请求、拥有什么角色"的判断交给外部认证服务。除了 JWT 模式外,最灵活的方式就是Webhook 认证模式:把 GraphQL Engine 配置为在收到请求时调用一个自定义 HTTP 端点,该端点读取客户端请求头、验证用户身份,并返回一组以X-Hasura-*开头的会话变量(session variables),Hasura 再依据这些变量执行角色与权限(Permission)规则。
Firebase 生态中,客户端通过 Firebase SDK 登录后拿到id_token,服务端可用 Firebase Admin SDK 校验其有效性。于是,一个典型的组合是:Node.js + Express 的 Webhook 服务负责校验id_token,Hasura 负责基于校验结果执行细粒度权限控制。
本样板的完整文件位于仓库的 community/boilerplates/auth-webhooks/nodejs-firebase 目录,同目录下还提供了 nodejs-express、lambda-cognito、firebase-cloud-functions 等其他语言的样板,可供对照。
项目结构:源码级剖析
该样板是一个标准的 Express 应用,目录结构如下:
community/boilerplates/auth-webhooks/nodejs-firebase/ ├── Procfile # Heroku 启动声明:web: node server.js ├── package.json # 依赖声明(express、firebase-admin) ├── server.js # Express 入口,挂载 /firebase 路由 ├── firebase/ │ ├── config.js # 读取 FIREBASE_CONFIG 环境变量 │ └── firebaseHandler.js # 核心认证逻辑 └── assets/ └── deploy-glitch.png # Glitch 一键部署按钮图片入口 server.js
server.js 非常精简:
// init project var express = require('express'); var app = express(); var port = process.env.PORT || 3000; app.get('/', (req, res) => { res.send('Webhooks are running'); }); // Firebase handler var firebaseRouter = require('./firebase/firebaseHandler'); app.use('/firebase', firebaseRouter); // listen for requests :) var listener = app.listen(port, function () { console.log('Your app is listening on port ' + port); });要点:
- 端口来自
process.env.PORT,默认3000(云平台通常会自动注入PORT); - 根路径
/返回Webhooks are running,用于健康检查; - 认证逻辑全部挂在
/firebase前缀下,最终对外暴露的 Webhook 地址为http://<host>:<port>/firebase/webhook。
核心认证逻辑 firebaseHandler.js
firebase/firebaseHandler.js 是整个样板的灵魂,其处理流程可以拆成四步:
第一步:初始化 Firebase Admin SDK。服务启动时读取FIREBASE_CONFIG环境变量(见 firebase/config.js),并以其内容初始化 Admin SDK:
var admin = require('firebase-admin'); var serviceAccount = require('./config.js'); var error = null; if (serviceAccount) { try { admin.initializeApp({ credential: admin.credential.cert(JSON.parse(serviceAccount)) }); } catch (e) { error = e; } }注意:config.js只是把process.env.FIREBASE_CONFIG原样导出,所以部署时必须把 Firebase 服务账号 JSON 的完整内容作为该环境变量的值。若未配置,后续请求会返回500 Firebase not configured;若配置了但无法解析(如 JSON 格式错误),会返回500 Invalid firebase configuration。
第二步:读取并解析 Authorization 头。Webhook 路由为GET /firebase/webhook。无Authorization头时直接返回匿名角色:
var authHeaders = request.get('Authorization'); if (!authHeaders) { response.json({'x-hasura-role': 'anonymous'}); return; }第三步:提取 Bearer Token。通过正则从Authorization: Bearer <id_token>中抽出 token:
const extractToken = (bearerToken) => { const regex = /^(Bearer) (.*)$/g; const match = regex.exec(bearerToken); if (match && match[2]) { return match[2]; } return null; }第四步:校验并返回会话变量。调用admin.auth().verifyIdToken(idToken)校验 token 真实性,成功后返回X-Hasura-User-Id(取 Firebase 用户的uid)和角色user;校验失败则同样回退到匿名角色:
admin.auth().verifyIdToken(idToken) .then((decodedToken) => { var hasuraVariables = { 'X-Hasura-User-Id': decodedToken.uid, 'X-Hasura-Role': 'user' }; response.json(hasuraVariables); }) .catch((e) => { console.log(e); response.json({'x-hasura-role': 'anonymous'}); });从源码可以推断该样板的安全策略:Firebase 校验失败的请求不会收到 401,而是被降级为匿名角色。因此若要严格拒绝未授权请求,可自行改为返回 401(见下文"响应规范")。
依赖与进程管理
package.json 声明依赖express ^4.22.2与firebase-admin ^14.2.0,Node 版本要求24.x.x;Procfile 只有一行web: node server.js,供 Heroku 等平台识别启动命令。本地验证只需:
npm install npm start # 另开终端 curl -H "Authorization: Bearer <id_token>" http://localhost:3000/firebase/webhook部署到云端:三种方式
原文档给出了三种快速部署路径,均以"把FIREBASE_CONFIG注入环境变量"为核心。
方式一:Heroku(推荐)
将仓库中该样板目录复制到独立目录并初始化 git:
cp -r <仓库路径>/community/boilerplates/auth-webhooks/nodejs-firebase <some-dir> cd <some-dir> git init && git add . && git commit -m "init auth webhook"创建 Heroku 应用并推送部署:
heroku apps:create git push heroku master部署完成后,进入Manage App > Settings,为应用设置如下环境变量:
- FIREBASE_CONFIG:把 Firebase 服务账号 JSON 的完整内容作为该字段的值。示例:
{ "type": "service_account", "project_id": "testapp-2222", "private_key_id": "f02aca08952f702de43ed577b428f405efe2d377", "private_key": "-----BEGIN PRIVATE KEY-----\n<your-private-key>\n-----END PRIVATE KEY-----\n", "client_email": "firebase-adminsdk-t4sik@testapp-24a60.iam.gserviceaccount.com", "client_id": "113608616484852272199", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-t4sik%40testapp-22222.iam.gserviceaccount.com" }该 JSON 可在 Firebase 控制台
Project settings > Service accounts > Generate new private key中下载;private_key内含换行转义符\n,注入环境变量时需保留原样。
方式二:Now(Zeit)
使用 Now 平台部署时,直接在now命令中以-e参数注入环境变量:
npm install -g now now -e \ FIREBASE_CONFIG='{ "type": "service_account", "project_id": "testapp-2222", "private_key_id": "f02aca08952f702de43ed577b428f405efe2d377", "private_key": "-----BEGIN PRIVATE KEY-----\n<your-private-key>\n-----END PRIVATE KEY-----\n", "client_email": "firebase-adminsdk-t4sik@testapp-24a60.iam.gserviceaccount.com", "client_id": "113608616484852272199", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-t4sik%40testapp-22222.iam.gserviceaccount.com" }'方式三:Glitch
仓库提供了 Glitch 一键导入所需的部署按钮图片 assets/deploy-glitch.png(原 README 中的按钮会跳转到 Glitch 的 GitHub 导入页面)。进入 Glitch 编辑器后,在.env文件中添加同样的环境变量:
FIREBASE_CONFIG='{ "type": "service_account", "project_id": "testapp-2222", "private_key_id": "f02aca08952f702de43ed577b428f405efe2d377", "private_key": "-----BEGIN PRIVATE KEY-----\n<your-private-key>\n-----END PRIVATE KEY-----\n", "client_email": "firebase-adminsdk-t4sik@testapp-24a60.iam.gserviceaccount.com", "client_id": "113608616484852272199", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-t4sik%40testapp-22222.iam.gserviceaccount.com" }'接入 Hasura GraphQL Engine
配置 Webhook 认证模式
部署好 Webhook 后,把它的 URL 配置到运行 GraphQL Engine 的容器环境变量中。Hasura 的 Webhook 认证模式由以下两个环境变量(或等价的启动参数)控制,官方规范详见 docs/docs/auth/authentication/webhook.mdx:
| 环境变量 | 对应 Flag | 说明 |
|---|---|---|
HASURA_GRAPHQL_AUTH_HOOK | --auth-hook | Webhook 端点地址,例如https://<your-webhook>/firebase/webhook |
HASURA_GRAPHQL_AUTH_HOOK_MODE | --auth-hook-mode | 请求方式,GET(默认)或POST |
以 Docker 为例,在docker run(或docker-compose)中追加:
-e HASURA_GRAPHQL_AUTH_HOOK=https://<your-webhook>/firebase/webhook \ -e HASURA_GRAPHQL_AUTH_HOOK_MODE=GET需要注意:
- 若请求头中包含
X-Hasura-Admin-Secret(管理员密钥),则跳过 Webhook 校验、直接授予管理员权限; - 必须保证 Hasura 容器能通过网络访问到 Webhook 地址,否则认证会失败;
- Webhook 模式生效的前提是先为 GraphQL 端点启用管理员密钥(即
HASURA_GRAPHQL_ADMIN_SECRET),否则端点默认可匿名访问,认证形同虚设。
Webhook 请求规范
配置为GET时,Hasura 会把客户端请求的绝大部分请求头原样转发给 Webhook(唯一例外是Content-Length、Content-Type、User-Agent、Host、Origin、Accept、Cache-Control等约 14 个传输层/浏览器层头不会转发)。因此本样板能够通过Authorization头拿到客户端的 Firebaseid_token。
配置为POST时,Hasura 会在转发全部客户端请求头之外,把请求体封装为 JSON 一并发送,形如:
{ "headers": { "header-key1": "header-value1" }, "request": { "variables": { "a": 1 }, "operationName": "UserQuery", "query": "query UserQuery($a: Int) { users(where: { id: { _eq: $a } }) { id } }" } }Webhook 响应规范(会话变量协议)
Hasura 只接受两种状态码,其余一律按500 Internal Server Error处理:
| 响应 | 含义 |
|---|---|
200 OK | 认证通过(或授权为匿名角色),响应体携带X-Hasura-*会话变量 |
401 Unauthorized | 拒绝该 GraphQL 请求 |
200响应体至少要包含X-Hasura-Role以告知 Hasura 使用哪个角色;其余自定义变量(如X-Hasura-User-Id)都会进入权限规则上下文。所有值都必须是字符串,Hasura 收到后会自动转换类型。标准示例:
HTTP/1.1 200 OK Content-Type: application/json { "X-Hasura-User-Id": "25", "X-Hasura-Role": "user", "X-Hasura-Is-Owner": "true", "X-Hasura-Custom": "custom value" }本样板成功时返回的是X-Hasura-User-Id(Firebaseuid)+X-Hasura-Role: user,与上述规范完全一致。
若要使用匿名(public/unauthorized)角色,则返回200且X-Hasura-Role设为匿名角色名:
HTTP/1.1 200 OK Content-Type: application/json { "X-Hasura-Role": "anonymous", }本样板在"无 Authorization 头"和"token 校验失败"两种场景下正是返回这个结果。
客户端如何请求
客户端使用 Firebase SDK 登录获得id_token后,向 GraphQL Engine 发起请求时携带如下请求头:
{ "Authorization": "Bearer <id_token>" }GraphQL Engine 收到后会把Authorization头转发给 Webhook,Webhook 校验通过并回传X-Hasura-User-Id、X-Hasura-Role,Hasura 即依据该用户角色执行表级/行级权限(例如{"user_id": {"_eq": "X-Hasura-User-Id"}}这类行选择规则)。
WebSocket 连接续期(实时查询)
对于实时查询(live queries)与流式订阅,认证后的 WebSocket 连接默认没有超时。若需要定期重新认证,Webhook 可在200响应中额外返回以下任一字段:
Cache-Control:相对过期时间(秒),如"Cache-Control": "max-age=600";Expires:绝对过期时间,格式为"%a, %d %b %Y %T GMT",如"Expires": "Mon, 30 Mar 2020 13:25:18 GMT"。
到达过期时间后,Hasura 会重新请求 Webhook 并建立新的 WebSocket 连接。
生产环境注意事项
结合源码与官方 Webhook 规范,落地到生产时有几点建议:
- Webhook 必须走 HTTPS:
id_token属于敏感凭证,明文传输有被截获风险; - 明确失败策略:当前样板对校验失败返回匿名角色,若业务要求"无效 token 直接拒绝",可改为返回
401 Unauthorized(注意 Hasura 只接受200与401); - 保护服务账号密钥:
FIREBASE_CONFIG包含private_key,切勿提交进代码仓库或暴露在客户端; - 最小化会话变量:只返回权限规则真正需要的
X-Hasura-*变量,避免把多余的用户信息透传给 GraphQL 引擎; - 结合权限规则使用:Webhook 只负责"认证"(你是谁),数据隔离仍由 Hasura 的 Permission 规则负责,二者需配套设计。
延伸阅读
- 样板总览与贡献规范:community/boilerplates/auth-webhooks/README.md
- 官方 Webhook 认证协议完整规范:docs/docs/auth/authentication/webhook.mdx
- 同目录其他语言样板:nodejs-express、lambda-cognito、firebase-cloud-functions
- 架构说明文档:architecture/live-queries.md(了解 WebSocket 认证续期机制的应用场景)
【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考