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

使用独立Identity Server 4的ASP.NET Core应用切换至Azure SignalR Service时协商请求返回401的问题排查

解决Azure SignalR Service下的JWT认证及令牌过期重试问题

针对你遇到的两个核心问题,我整理了具体的解决方案,一步步帮你排查修复:


一、解决Azure SignalR协商请求401(签名密钥找不到)的问题

你当前的JWT配置硬编码了IssuerSigningKey和IssuerSigningKeyResolver,这是导致问题的关键。虽然自托管SignalR模式下运行正常,但Azure SignalR Service的请求转发逻辑,加上Identity Server 4的密钥管理机制,会让硬编码的密钥和令牌实际使用的签名密钥不匹配(比如密钥ID kid 不一致)。

正确的做法是让AddJwtBearer自动从Identity Server的JWKS(JSON Web Key Set)端点获取签名密钥,而不是手动指定。修改你的认证配置如下:

services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options => {
        var appSettings = Configuration.Get<AppSettingsModel>();
        options.Authority = appSettings.Authority;
        options.RefreshOnIssuerKeyNotFound = true;
        
        if (environment.IsDevelopment()) {
            options.RequireHttpsMetadata = false;
        }
        
        options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters {
            ValidateAudience = false
            // 移除手动设置的IssuerSigningKey和IssuerSigningKeyResolver
        };
        
        options.Events = new JwtBearerEvents {
            OnMessageReceived = context => {
                var accessToken = "";
                var headerToken = context.Request.Headers[HeaderNames.Authorization].ToString().Replace("Bearer ", "");
                if (!string.IsNullOrEmpty(headerToken)) {
                    accessToken = headerToken;
                }
                
                var queryStringToken = context.Request.Query["access_token"];
                if (!string.IsNullOrEmpty(queryStringToken)) {
                    accessToken = queryStringToken;
                }
                
                if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs")) {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

为什么这样改?

当你配置options.Authority后,AddJwtBearer会自动访问{Authority}/.well-known/openid-configuration/jwks端点,拉取Identity Server当前使用的所有签名密钥,确保和令牌的kid完全匹配,彻底解决"signature key was not found"的错误。


二、解决令牌过期休眠后协商失败的问题

移除自定义HttpClient后,你丢失了令牌过期时自动刷新并重试的逻辑。我们可以修复原来的自定义HttpClient,让它兼容Azure SignalR的协商流程,同时避免无限重试和并发刷新的问题:

export class CustomSignalRHttpClientService extends signalR.DefaultHttpClient {
    private token: string = "";
    private refreshInProgress = false;
    private refreshPromise: Promise<string> | null = null;

    constructor(private authService: AuthorizeService) {
        super(console);
        // 订阅令牌更新
        authService.accessToken$.subscribe(token => {
            this.token = token;
        });
    }

    public async send(request: signalR.HttpRequest): Promise<signalR.HttpResponse> {
        // 给请求添加当前令牌
        request.headers = { ...request.headers, Authorization: `Bearer ${this.token}` };
        
        try {
            return await super.send(request);
        } catch (er) {
            // 仅处理401且未重试过的请求
            if (er instanceof signalR.HttpError && er.statusCode === 401 && !request.headers['X-Retry-Attempt']) {
                let newToken: string;
                
                // 处理并发刷新请求,避免多次调用刷新接口
                if (!this.refreshInProgress) {
                    this.refreshInProgress = true;
                    try {
                        newToken = await this.authService.getAccessToken().toPromise();
                        this.token = newToken;
                    } finally {
                        this.refreshInProgress = false;
                        this.refreshPromise = null;
                    }
                } else {
                    // 等待正在进行的刷新请求完成
                    newToken = await this.refreshPromise!;
                }
                
                // 更新请求头,添加重试标记防止无限循环
                request.headers = { 
                    ...request.headers, 
                    Authorization: `Bearer ${newToken}`,
                    'X-Retry-Attempt': '1'
                };
                
                // 重试请求
                return await super.send(request);
            }
            
            // 非401错误或已重试,直接抛出
            throw er;
        }
    }
}

关键优化点:

  1. 添加了X-Retry-Attempt头,确保每个请求只重试一次,避免无限循环
  2. 处理了并发刷新场景,防止多个请求同时触发令牌刷新
  3. 简化了令牌更新的逻辑,确保请求始终使用最新的有效令牌

额外注意事项

  • 生产环境中务必将RequireHttpsMetadata设置为true,避免安全风险
  • 可以通过访问{Authority}/.well-known/openid-configuration/jwks端点,确认返回的密钥包含令牌中的kid值,验证密钥匹配性
  • 确保Identity Server 4的配置中开启了JWKS端点(默认是开启状态)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 20:22:29