我通过 SignalR 从客户端(角度 9)和服务器(asp.net 核心 3.1)创建实时连接,并通过 JWT 令牌授权集线器,例如以下代码:
private createConnection() {
this.hubConnection = new HubConnectionBuilder().withUrl(`${this.appConfig.hubEndpoint}/Hubs`,
{ accessTokenFactory: () => jwtToken })
.withAutomaticReconnect()
.build();
}
private startConnection(): void {
this.hubConnection
.start()
.then(() => {
this.connectionIsEstablished = true;
this.connectionEstablished.emit(true);
})
.catch(err => {
console.log('Error while establishing connection, retrying...');
});
}
在令牌过期之前,这工作正常。根据我的研究,在收到带有刷新令牌的新令牌后,应停止以前的连接,并使用新令牌创建新连接。 现在我想知道我应该怎么做?我必须不断检查令牌吗?还是应该通过将每个请求发送到服务器来解决此问题?
我想出的解决方案是通过扩展 signalR 客户端使用的signalR.DefaultHttpClient
来拦截它的身份验证调用。如果有 401,那么我刷新令牌(通过我的authService
),然后重试调用:
打字稿:
const getAuthHeaders = () => {
return {
Authorization: `Bearer ${authService.getToken()?.accessToken}`,
};
};
class CustomHttpClient extends signalR.DefaultHttpClient {
constructor() {
super(console); // the base class wants a signalR.ILogger
}
public async send(
request: signalR.HttpRequest
): Promise<signalR.HttpResponse> {
const authHeaders = getAuthHeaders();
request.headers = { ...request.headers, ...authHeaders };
try {
const response = await super.send(request);
return response;
} catch (er) {
if (er instanceof signalR.HttpError) {
const error = er as signalR.HttpError;
if (error.statusCode == 401) {
//token expired - trying a refresh via refresh token
await authService.refresh();
const authHeaders = getAuthHeaders();
request.headers = { ...request.headers, ...authHeaders };
}
} else {
throw er;
}
}
//re try the request
return super.send(request);
}
}
const connection = new signalR.HubConnectionBuilder()
.withUrl("/MyHub", {
// use the custom client
httpClient: new CustomHttpClient(),
})
.configureLogging(signalR.LogLevel.Information)
.build();
在此处查看.withUrl(..)
选项:https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration?view=aspnetcore-6.0&tabs=dotnet
当令牌过期时,服务器将断开连接,您将在服务器端出现错误。我相信是您将获得的405错误代码Method now allowed
。
因此,您需要的是捕获此令牌过期错误并删除连接,以便您可以使用新令牌启动新连接。
根据微软文档,令牌的更新必须在accessToken函数中完成。
"提供的访问令牌函数在 SignalR 发出的每个 HTTP 请求之前调用。如果需要续订令牌以保持连接处于活动状态,请从此函数中执行此操作并返回更新的令牌。令牌可能需要续订,以便在连接期间不会过期。
如果是上面的链接,则需要在"this.loginToken"中完成。
我遇到了同样的问题,我以这种方式解决了。我正在使用 Angular 14 和 NET Core 6。
首先,我在用于续订访问令牌的服务中创建Subject<string>
,在续订访问令牌时发出新的访问令牌(或在注销时发出空字符串)。
tokenRefreshed$: Subject<string> = new Subject();
public PersistTokens(accessToken?: string, refreshToken?: string) {
this.tokenRefreshed$.next(accessToken??'');
SetJWTAccessToken(accessToken??'');
SetJWTRefreshToken(refreshToken??'');
}
之后,在我启动信令集线器连接的服务中,我订阅了前面描述的主题...然后我在连接请求中使用新的访问令牌。
constructor(private appService: ApplicationService, private jwtTokenService:JwtTokenService) {
// first hub connection on application start or page refresh (I want to allow anonymous siglr connection too)
this.startConnection(GetJWTAccessToken()??'');
// if user logged in restart the hub connection to handle the new token
jwtTokenService.tokenRefreshed$.subscribe(
token => {
// if already connected stop existing connection
if(this.hubConnection && this.hubConnection.state === signalR.HubConnectionState.Connected) {
this.hubConnection.stop();
}
this.startConnection(token);
}
);
}
async startConnection(accesToken:string) {
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(`${this.appService.appConfiguration.apiEndpointUrl}/hubs/notifyHub`, {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
accessTokenFactory: () => {
return accesToken;
}
})
.build();
this.hubConnection
.start()
.then(() => console.debug('Notify hub connected'))
// .then(() => this.getConnectionId())
.catch(err => console.log('Error while starting Notify hub connection: ' + err));
this.hubConnection.on('NotifyOrderCreated', (data) => {
this.notifyOrderCreated$.emit(data);
});
// Other messages subscriptions ...
}
该函数GetJWTAccessToken()
简单地读取保存在浏览器存储中的访问令牌。
没有"好"的方法可以做到这一点,5年后他们想出了:
public class HttpConnectionDispatcherOptions
{
bool EnableAuthenticationExpiration { get; set; }
}
令牌过期时,它将强制重新连接,但它是资源密集型解决方案。
有关更多信息,请参阅 https://github.com/dotnet/aspnetcore/issues/5297...
对我有用的,这是一个快速而肮脏的修复,是在关闭事件时重新加载页面:
this.hubConnection.onclose(() =>{
window.location.reload()
})
解释
我正在使用 RxJS 包装连接过程,因此对于我的情况,更好的解决方法是抛出错误而不是重新加载页面并使用retryWhen
运算符捕获它。但是由于这是一个硬错误(需要等待 1 小时才能使令牌过期,并且本地模拟器不关心令牌......),我只是更喜欢使用这个临时解决方案。