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

Azure SignalR无服务器模式下,认证Function App协商失败的解决方法

问题:在禁用未经身份验证请求的Azure Function App中使用SignalR无服务器模式协商连接

我需要在禁用了未经身份验证请求的Azure Function App中,使用Azure SignalR无服务器模式完成连接协商。关闭该安全设置后函数能正常运行,但出于安全要求,当前必须通过身份提供商做身份验证。本地开发时无需身份验证,所以没有问题。

推测问题根源是Azure SignalR服务返回的通信未携带正确身份验证信息,无法通过Function App的身份验证拦截。排除Function App的访问授权问题——我的应用能正常调用其他40个函数。

已尝试的方法

  • 为Function App和SignalR服务双向添加Azure身份访问角色:能让Function App访问SignalR实例,但这不是初始连接的问题,用访问密钥也能正常访问SignalR。
  • 在Kudu控制台排查日志:只能找到成功的函数调用记录,被身份验证拦截的401请求日志无法找到,也无法通过Kudu访问相关存储位置(已搜索IIS程序文件等路径)。

使用的代码示例(基于Microsoft.Azure.Webjobs.extensions.SignalRService)

示例1:继承ServerlessHub的实现

public class SignalRTestHub : ServerlessHub
{
    [FunctionName("negotiate")]
    public SignalRConnectionInfo Negotiate([HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req)
    {
        return Negotiate(req.Headers["x-ms-signalr-user-id"], GetClaims(req.Headers["Authorization"]));
    }

    [FunctionName(nameof(OnConnected))]
    public async Task OnConnected([SignalRTrigger]InvocationContext invocationContext, ILogger logger)
    {
        await Clients.All.SendAsync(NewConnectionTarget, new NewConnection(invocationContext.ConnectionId));
        logger.LogInformation($"{invocationContext.ConnectionId} has connected");
    }

    [FunctionName(nameof(Broadcast))]
    public async Task Broadcast([SignalRTrigger]InvocationContext invocationContext, string message, ILogger logger)
    {
        await Clients.All.SendAsync(NewMessageTarget, new NewMessage(invocationContext, message));
        logger.LogInformation($"{invocationContext.ConnectionId} broadcast {message}");
    }

    [FunctionName(nameof(OnDisconnected))]
    public void OnDisconnected([SignalRTrigger]InvocationContext invocationContext)
    {
    }
}

示例2:直接返回SignalRConnectionInfo

[FunctionName("negotiate")]
public static SignalRConnectionInfo Negotiate(
    [HttpTrigger(AuthorizationLevel.Anonymous, Route = "negotiate")] HttpRequest req,
    [SignalRConnectionInfo(HubName = AzureConstants.SignalRHub , UserId = "{headers.x-ms-client-principal-name}")] SignalRConnectionInfo connectionInfo,
    ILogger log
)
{
    return connectionInfo;
}

注:当允许未经身份验证请求时,这两个示例都能正常工作,但禁用该设置后连接失败。


解决方案(已解决)

之前的错误认知:Angular的signalr包默认不会像HTTP Client那样自动携带身份认证令牌,而是会剥离令牌。需要手动为SignalR连接添加身份验证信息,借助MSAL返回的令牌,通过请求头传递Bearer令牌并配置跨域头来解决。

前端(Angular)实现示例

export type MsalTokenClass = {
    cachedAt: number;
    clientId: string;
    credentialType: string;
    environment: string;
    expiresOn: number;
    extendedExpiresOn: number;
    homeAccountId: string;
    realm: string;
    secret: string;
    target: string;
    tokenType: string;
}

isMyType(o: any): o is MsalTokenClass {
    return "cachedAt" in o && "clientId" in o && "credentialType" in o && 
           "environment" in o && "expiresOn" in o && "extendedExpiresOn" in o && 
           "homeAccountId" in o && "realm" in o && "secret" in o && 
           "target" in o && "tokenType" in o;
}

getAndSetApiToken() {
    let cache: any = this.authService.instance.getTokenCache();
    let getStorage = cache.storage.browserStorage.windowStorage;

    for (const key in getStorage) {
        let current = getStorage[key];
        let parsed: any;

        try {
            parsed = JSON.parse(current);

            if (this.isMyType(parsed)) {
                console.log("Current parsed = ", parsed);
                console.log("Current parsed target = ", parsed.target);
                // 此处需与msalinterceptorConfigFactory中protectedResourceMap配置的scope一致
                // 示例格式:https://yourtenant.com/myapi/api.ReadWrite
                if (parsed.target === environment.resourceIds.apiScope + api.ReadWrite) {
                    console.log("Current Bearer = ", parsed.secret);
                    this.currentBearerToken = parsed.secret;
                }
            } else {
                console.log("not the right type");
            }
        } catch {
            console.log("Not Json type");
            continue;
        }
    }
}

public startConnection = () => {
    this.getAndSetApiToken();
    this.hubConnection = new signalR.HubConnectionBuilder()
        .withUrl(this.apiAddress, {
            headers: {
                'Authorization': 'Bearer ' + this.currentBearerToken,
                'Access-Control-Allow-Headers': 'X-Auth-Token'
            },
        })
        .configureLogging(LogLevel.Trace)
        .build();

    this.hubConnection
        .start()
        .then(() => { })
        .catch(err => console.log('Error while starting connection: ' + err))
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 16:59:55