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
相关产品推荐
相关产品推荐

