You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# .NET WPF连接Google Cloud Firestore时自定义令牌偶发认证错误

问题背景与现象

我们的C# .NET WPF应用通过自定义令牌连接Google Cloud Firestore,令牌由同服务器的Node.js Firebase Function生成,令牌内嵌元数据用于Firestore Rules权限控制。该功能稳定运行18个月后,近一周出现偶发的Unauthenticated错误:

  • 读写操作时约40%概率触发错误,仅发生在FirestoreDb执行操作阶段(比如await batch.CommitAsync();),认证、令牌生成、FirestoreDb创建阶段无报错
  • 错误信息固定:
    Grpc.Core.RpcException: Status(StatusCode="Unauthenticated", Detail="Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential.") at Google.Cloud.Firestore.WriteBatch.<CommitAsync>d__15.MoveNext() in /_/apis/Google.Cloud.Firestore/Google.Cloud.Firestore/WriteBatch.cs:line 226
    
  • 用户立即重试连接,约90%概率恢复正常,剩余10%仍报错

相关代码

C# 客户端代码

using Firebase.Auth;
using Flurl.Http;
using Google.Cloud.Firestore;
using Google.Cloud.Firestore.V1;
using Grpc.Core;

dynamic response = await GetAuthenticationToken(requestToken);
FirestoreDb db = CreateFirestoreDbWithToken(response.token, FIREBASE_WEB_API, FIREBASE_PROJECT_ID);

private FirestoreDb CreateFirestoreDbWithToken(string token, string firebaseApiKey, string firebaseProjectId)
{
    FirebaseAuthProvider authProvider = new FirebaseAuthProvider(new FirebaseConfig(firebaseApiKey));
    firebaseAuth = authProvider.SignInWithCustomTokenAsync(token).Result;
    CallCredentials callCredentials = CallCredentials.FromInterceptor(async (context, metadata) =>
    {
        if (firebaseAuth.IsExpired())
            firebaseAuth = await firebaseAuth.GetFreshAuthAsync();
        if (string.IsNullOrEmpty(firebaseAuth.FirebaseToken))
            return;

        metadata.Clear();
        metadata.Add("authorization", $"Bearer {firebaseAuth.FirebaseToken}");
    });
    ChannelCredentials credentials = ChannelCredentials.Create(new SslCredentials(), callCredentials);
    FirestoreClientBuilder builder = new FirestoreClientBuilder { ChannelCredentials = credentials };
    return FirestoreDb.Create(firebaseProjectId, builder.Build());
}

public async Task<dynamic> GetAuthenticationToken(string requestToken)
{
    var functionPath = FIREBASE_FUNCTION_NAME + ".cloudfunctions.net/verifyClient";
    var response = await functionPath.WithHeader("Content-Type", "application/json; charset=utf-8")
    .PostJsonAsync
    (
        new
        {
            requestToken,
        }
    )
    .ReceiveJson();
    return response;
}

public async Task WriteData()
{
    // ...
    WriteBatch batch = db.StartBatch();
    batch.Set(dataRef, dataObject);
    await batch.CommitAsync();
    // ...
}

Node.js Firebase Function代码

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase)

exports.verifyClient = functions
    .https.onRequest((request, response) => {
    
    // ... 业务逻辑 ...
    
    return createAuthToken(requestEmail, groupID).then(authToken => {
        response.send({ result: "OK", token: authToken });
        return;
    })
});

function createAuthToken(requestEmail, groupID) {
    return admin.auth()
        .createCustomToken(requestEmail, { adminID: groupID })
        .then(customToken => {
            return customToken;
        })
        .catch(err => {
            console.error('createAuthToken Error: ' + err);
            return "";
        });
}

原因分析

  1. 令牌刷新竞态问题:C#代码中firebaseAuth是共享实例,无线程安全控制,并发场景下多个请求同时触发令牌刷新时,可能出现某请求拿到未完全刷新的无效令牌。
  2. Grpc元数据处理缺陷:metadata.Clear()会清空所有元数据(包括Grpc内部必要字段),可能导致Firestore服务端无法正确识别令牌格式。
  3. 服务端临时认证波动:近期Firestore认证服务可能存在偶发的令牌校验延迟或缓存不一致,导致有效令牌被误判为无效,重试可绕过。

解决方案

1. 修复线程安全的令牌刷新逻辑

为firebaseAuth添加线程锁,避免并发刷新冲突:

private readonly object _authLock = new object();
private FirebaseAuthLink firebaseAuth;

private async Task<FirebaseAuthLink> GetValidAuthLink(string token, string firebaseApiKey)
{
    lock (_authLock)
    {
        if (firebaseAuth != null && !firebaseAuth.IsExpired())
            return firebaseAuth;
    }
    
    FirebaseAuthProvider authProvider = new FirebaseAuthProvider(new FirebaseConfig(firebaseApiKey));
    var freshAuth = await authProvider.SignInWithCustomTokenAsync(token);
    
    lock (_authLock)
    {
        firebaseAuth = freshAuth;
    }
    return firebaseAuth;
}

修改拦截器逻辑,调用上述方法获取有效令牌:

CallCredentials callCredentials = CallCredentials.FromInterceptor(async (context, metadata) =>
{
    var validAuth = await GetValidAuthLink(token, firebaseApiKey);
    if (string.IsNullOrEmpty(validAuth.FirebaseToken))
        return;

    // 仅更新Authorization头,保留其他元数据
    metadata.Remove("authorization");
    metadata.Add("authorization", $"Bearer {validAuth.FirebaseToken}");
});

2. 添加操作重试机制

针对Unauthenticated错误自动重试,利用重试大概率成功的特性:

public async Task WriteDataWithRetry()
{
    int retryCount = 3;
    while (retryCount > 0)
    {
        try
        {
            WriteBatch batch = db.StartBatch();
            batch.Set(dataRef, dataObject);
            await batch.CommitAsync();
            return;
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated && retryCount > 0)
        {
            retryCount--;
            // 重试前强制清空旧令牌,触发重新获取
            lock (_authLock)
            {
                firebaseAuth = null;
            }
            await Task.Delay(100);
        }
    }
    throw new Exception("重试多次后仍无法完成Firestore操作");
}

3. 更新依赖库版本

确认Firebase.Auth和Google.Cloud.Firestore NuGet包为最新稳定版,修复依赖库中的已知认证bug。

内容的提问来源于stack exchange,提问作者Damian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 07:54:22