客户端登录做得再漂亮,服务端没有一套可靠的令牌校验机制,整个系统的大门依然是敞开的。Firebase虽然以客户端SDK闻名,但它的Admin SDK在Node.js服务端同样能力完整:验证ID令牌、管理用户账号、下发自定义权限声明、撤销刷新令牌,这些都可以用几十行代码搞定。这篇文章围绕实际项目中的典型需求,讲清楚如何在Node.js环境下正确接入和使用Firebase Admin认证能力。

一、初始化Admin SDK:服务账号配置是关键
Admin SDK与客户端SDK最大的区别在于权限模型。客户端SDK代表的是某一个终端用户,而Admin SDK代表的是你的整个Firebase项目,拥有近乎上帝视角的权限,所以它的初始化凭据是一份服务账号(Service Account)JSON文件,而不是普通的API Key。
获取方式是在Firebase控制台的项目设置里找到“服务账号”标签页,点击“生成新的私钥”,浏览器会下载一份JSON文件。这份文件包含client_email、private_key等字段,务必妥善保管,绝对不能提交到Git仓库。初始化代码如下:
const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
// 如果需要用到Realtime Database或Storage,可以在这里补充 databaseURL 等配置
});
// 验证初始化是否成功
console.log('Admin SDK 已初始化,项目ID:', serviceAccount.project_id);生产环境更推荐用环境变量GOOGLE_APPLICATION_CREDENTIALS指向服务账号文件路径,代码里直接调用admin.credential.applicationDefault(),这样密钥文件就不会出现在代码目录中。在Google Cloud环境中运行时(比如Cloud Run、Cloud Functions),甚至不需要任何文件,运行时会自动注入默认凭据,这是最安全的做法。
初始化只能执行一次,重复调用initializeApp会抛出app/duplicate-app错误。如果你的项目里多个模块都需要用到admin实例,建议在一个单独模块中完成初始化,其他地方统一从这个模块引入,避免重复初始化的问题。
二、在Express中间件中验证ID令牌
客户端登录成功后,Firebase客户端SDK会拿到一个ID令牌(JWT格式,有效期一小时)。服务端要做的就是把请求头中的这个令牌验证掉。典型的做法是写一个Express中间件:
const express = require('express');
const app = express();
// 认证中间件
async function authenticate(req, res, next) {
const authHeader = req.headers.authorization || '';
// ID令牌通过 Bearer 方式传递
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
if (!token) {
return res.status(401).json({ error: '缺少认证令牌' });
}
try {
const decodedToken = await admin.auth().verifyIdToken(token);
// 验证通过,把用户信息挂到 req 上供后续处理使用
req.user = decodedToken;
next();
} catch (error) {
// 令牌无效、过期或被撤销都会走到这里
return res.status(401).json({ error: '令牌验证失败', detail: error.code });
}
}
// 保护需要登录的路由
app.get('/api/profile', authenticate, (req, res) => {
res.json({
uid: req.user.uid,
email: req.user.email,
signInProvider: req.user.firebase.sign_in_provider,
});
});
app.listen(3000, () => console.log('服务运行在3000端口'));verifyIdToken内部做了完整的工作:校验JWT签名、检查过期时间、验证受众(audience)确实是当前项目。解码后的对象包含uid、email、email_verified以及firebase.claims中的自定义声明,这些信息足以支撑绝大多数业务接口的权限判断。
有几个常见的报错需要认识一下。auth/id-token-expired表示令牌已过期,客户端应该在收到401后强制刷新令牌重试;auth/argument-error通常是客户端传的不是ID令牌,而是把刷新令牌或Web API Key传上来了,这是一个非常高频的踩坑点。另外,如果服务器时钟与标准时间偏差过大,验证也会失败,部署在虚拟机里时要留意NTP同步。
三、自定义声明与基于角色的权限控制
Firebase Auth本身没有传统的“角色”字段,但它提供了自定义声明(Custom Claims)机制,可以把任意JSON片段塞进用户的ID令牌里,服务端验证令牌后直接读取,不需要再查一次数据库,这是它相比传统方案非常优雅的地方。
// 给某个用户设置 admin 角色
async function setAdminRole(uid) {
await admin.auth().setCustomUserClaims(uid, { role: 'admin', level: 2 });
console.log('自定义声明已设置');
}
// 验证时直接读取声明
async function requireAdmin(req, res, next) {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: '需要管理员权限' });
}
next();
}使用自定义声明有几个注意点。第一,声明会直接嵌入ID令牌,令牌在一小时内不会刷新,所以修改声明后客户端必须重新登录或者强制刷新令牌(客户端调用getIdToken(true))才能生效。第二,声明的大小有限制,最多1000字节,所以不要把用户资料往里塞,只放权限相关的少量字段。第三,设置声明的操作应该放在服务端受保护的接口里执行,绝不能暴露给普通用户。
读取当前声明的接口是admin.auth().getUser(uid),返回结果中的customClaims字段就是全部自定义声明。搭建权限体系时,可以设计一个管理后台接口专门负责角色变更,变更后通知客户端刷新令牌,这样整个授权链路就闭环了。
四、用户管理与令牌撤销
Admin SDK的auth()模块还提供了完整的用户管理能力,包括创建、更新、禁用、删除用户,以及按条件批量查询:
async function manageUsers() {
// 创建用户
const user = await admin.auth().createUser({
email: 'newuser@ipipp.com',
password: 'aVeryStrongPassword123',
displayName: '测试用户',
});
// 禁用账号(封禁场景)
await admin.auth().updateUser(user.uid, { disabled: true });
// 分页列出全部用户
const listResult = await admin.auth().listUsers(100);
listResult.users.forEach(u => console.log(u.uid, u.email));
}当怀疑某个账号的令牌泄露,或者用户主动修改密码需要踢掉所有已登录设备时,可以用revokeRefreshTokens撤销该用户的所有刷新令牌:
async function kickOut(uid) {
// 撤销刷新令牌,已签发的ID令牌仍需配合 checkRevoked 使用
await admin.auth().revokeRefreshTokens(uid);
// 验证时开启撤销检查,revoked 的令牌会直接报错
try {
const decoded = await admin.auth().verifyIdToken(token, true);
} catch (e) {
console.log('令牌已被撤销:', e.code); // auth/id-token-revoked
}
}注意revokeRefreshTokens只处理刷新令牌,已签发的ID令牌在过期前仍然有效,所以验证时必须把checkRevoked参数设为true才能真正拦截。出于性能考虑,开启撤销检查会增加一次后端校验,一般只在敏感接口上启用即可。
五、常见坑与排查思路
最后总结几条实践经验。权限报错permission denied多半是服务账号JSON与项目不匹配,或者用错了项目的密钥文件;验证一直失败的另一个常见原因是本地开发时用了模拟器环境但服务端连的是生产项目,两边环境必须一致。如果需要搭配Firebase模拟器做本地测试,可以设置FIREBASE_AUTH_EMULATOR_HOST环境变量指向模拟器地址,Admin SDK会自动切换到模拟器。
另外要明确职责边界:客户端SDK负责登录、获取令牌、自动刷新;Admin SDK只部署在可信的服务端,负责验证和管理。千万不要把服务账号JSON打包进前端或移动端应用,那等于把整个项目的钥匙交出去。把这条边界守住,再结合前面讲的中间件、自定义声明和令牌撤销机制,一套安全可靠的Firebase服务端认证体系就基本成型了。