如何使用jose验证Firebase Session Cookie(JWT)?
环境设置
- 我正在遵循Firebase Auth管理Cookie的官方文档。
- 我使用Next.js中间件,无法用Firebase函数验证Cookie,因此尝试用jose库来验证。
- Firebase文档指出:
确保会话Cookie是由与令牌kid声明对应的私钥签名的。从Google的公钥地址获取公钥,并使用JWT库验证签名。
已尝试的方案
// middleware.js import { NextResponse } from 'next/server' import * as jose from 'jose' export async function middleware(request) { // 检查所有Cookie const allCookies = request.cookies.getAll() console.log(allCookies) // 获取sessionCookie(如果存在) const sessionCookie = request.cookies.get("sessionCookie") console.log("sessionCookie", sessionCookie) // 使用从指定地址获取的硬编码公钥进行验证测试 const alg = 'RS256' const spki = `-----BEGIN CERTIFICATE-----\n MIIDHDCCAgSgAwIBAgIEIhgI5zANBgkqhkiG9w0BAQsFADAzMQ8wDQYDVQQDEwZH\n aXRraXQxEzARBgNVBAoTCkdvb2dsZSBJbmMxCzAJBgNVBAYTAlVTMB4XDTE5MDEx\n NDIzMjgzMVoXDTIwMDEwOTIzMjgzMVowMzEPMA0GA1UEAxMGR2l0a2l0MRMwEQYD\n VQQKEwpHb29nbGUgSW5jMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQAD\n ggEPADCCAQoCggEBAMLh7U8PNsOebb1HVLDa81W/CZK8DrdYl9Vlwj/0/GF7kfLP\n zF4qyShw1zAZddlzt7f6lDy2ZvmQ1nqW0IRy8xgYTeAb6aCLY+rm4DEnwJOCnAVo\n m1xpgcOVExsXXpleWsP0MugM5xa91Y79CYkVTevPZThgjqfGjan3GiXLQwIJTLG8\n xEXXSzmMDeW6dP6CBVPsbbTYSat6CX2nBm2YoJ4v2dij3DQBzOD/d5WLaYL4yE9p\n TjmreitFvs1r0AZF53Cq5ju7M10indVyk1zwfK+tuk7VnCZz/mBHEPxT9+Q5EWFi\n lVVkZ+2Qau0qRIvar/1QukVpyEsVHPZH+aZ9ilMCAwEAAaM4MDYwDAYDVR0TAQH/\n BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCB4AwDQYJ\n KoZIhvcNAQELBQADggEBAH4ca9fhlBd9XmaX9E0rZ8QCWT8AnxcVLY+S+nHJg2IT\n dj7ZytNJWttKEssOdslrQyamxt2SdQSfpd2UpA+fCO2sRSJOBEA3squGrj1yh1Pt\n lajf2TM1MwpdO7ZqBbfE3hLH7srBbVXHxKsUemepxviC+dpF0u4o3EjqHpAvkruO\n 4RtC4grbFBNRIbBTu7oJHpKnBJWxK7w11a5Sabs6reAuFh68QaVAQzrscYP/M4i9\n SPwArWKoCJgrxxVof1N1cal4UpziEH9OaWa7RfyZTYAeEJ/atDo5s8AACPUH542f\n JEwmwAJKbJKLRoFYJJXCPeKp1Q7EXQHJNNdS2ABjw+w=\n -----END CERTIFICATE-----` const publicKey = await jose.importSPKI(spki, alg) const { payload, protectedHeader } = await jose.jwtVerify(sessionCookie.value, publicKey) console.log(protectedHeader) console.log(payload) }
执行代码后出现错误:
error "spki" must be SPKI formatted string
问题
- 如何解决这个问题,实现验证功能?
- 为什么指定公钥地址提供的不是一个,而是五个不同的公钥?
- 每个公钥都有唯一的键值对,例如:
"skIBNg": "-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIB...vb2dsZSBJbmMxCzAJBgNVBAYTA.f+9Vg=\n-----END CERTIFICATE-----\n"
其中的键"skIBNg"有什么作用?我目前并未使用它。
解决方案与解释
1. 解决SPKI格式错误并实现验证
你遇到的错误是因为传入的是X.509证书,而importSPKI方法需要的是SPKI格式的公钥字符串。解决方式如下:
- 直接用
jose的importX509方法导入证书,无需转换格式:// 替换原importSPKI代码 const publicKey = await jose.importX509(spki, alg) - 更规范的动态验证流程:
- 解析Session Cookie的JWT头部,提取
kid字段 - 从公钥接口获取对应
kid的证书 - 导入证书后验证JWT
示例代码:
export async function middleware(request) { const sessionCookie = request.cookies.get("sessionCookie") if (!sessionCookie) return NextResponse.redirect(new URL('/login', request.url)) // 解析JWT头部获取kid const [headerEncoded] = sessionCookie.value.split('.') const header = JSON.parse(Buffer.from(headerEncoded, 'base64url').toString()) const kid = header.kid // 获取公钥列表并匹配对应证书 const response = await fetch('https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys') const publicKeys = await response.json() const certificate = publicKeys[kid] if (!certificate) return NextResponse.redirect(new URL('/login', request.url)) // 验证JWT const alg = 'RS256' const publicKey = await jose.importX509(certificate, alg) try { await jose.jwtVerify(sessionCookie.value, publicKey) return NextResponse.next() } catch (err) { return NextResponse.redirect(new URL('/login', request.url)) } } - 解析Session Cookie的JWT头部,提取
2. 为什么存在多个公钥?
这是Google的密钥轮换机制:
- 为提升安全性,服务商定期更换签名私钥,对应公钥同步更新
- 保留多个公钥是为了兼容不同时间段签发的JWT,避免旧令牌突然失效
- 通常包含当前生效、即将生效、刚过期不久的公钥,保证系统平滑过渡
3. 键"skIBNg"的作用
这个键就是JWT头部的kid(Key ID)字段:
- 每个公钥对应唯一
kid,用来标识签名该JWT的私钥对应的公钥 - 验证时必须用这个
kid从公钥列表中匹配对应证书,确保用正确的公钥验证签名 - 硬编码证书时不需要,但动态获取公钥是核心匹配依据
内容的提问来源于stack exchange,提问作者Ben
相关产品推荐
相关产品推荐

