如何通过SignalR向服务端传递Token 解决大文件上传Token过期问题
实现方案
核心利用SignalR双向通信能力,由服务端在Token临期/过期时主动向当前上传操作对应的客户端请求最新鉴权信息,比前端定时推送的方案更精准、无冗余请求,不需要大幅改动现有上传逻辑即可解决大文件上传过程中Token过期的问题。
前端改造点
- 提前全局初始化SignalR连接,不要在AJAX的
beforeSend回调中重复绑定事件、重复启动连接,避免事件多次触发、连接泄漏问题。 - 注册供服务端调用的客户端方法,被调用时直接读取localStorage中存储的最新有效
serviceUrl和token返回。 - 上传请求发起前拿到SignalR连接的
connectionId,随请求一起发给服务端,让服务端可以精准定位当前上传对应的客户端,不需要全局广播消息。
改造后的前端代码参考:
// 全局初始化SignalR连接,不要放在beforeSend内部 const connection = new signalR.HubConnectionBuilder() .withUrl("/progressHub") .withAutomaticReconnect() .build(); // 全局只绑定一次进度回调,避免重复触发 connection.on("lastprogress", function (itemId, progresbar) { $(".progress-bar").each(function (index, element) { const fileid = $(this).data("fileid"); if (fileid === itemId) { $(this).width(progresbar + "%"); } const obj = CancelUploadFileList.find(el => el == object.itemId); if (obj) { $(".progress-bar").each(function (index, element) { const fileid = $(this).data("fileid"); if (fileid === object.itemId) { $(this).removeClass("progress-bar"); $(this).parent().removeClass("progress-custom"); $(this).html("<div style='font-weight:normal;'>Canceled</div>"); ajaxResponse.abort(); } }); } }); }); // 注册供服务端调用的取最新鉴权信息方法 connection.on("GetLatestAuthInfo", function () { // 直接返回localStorage中存储的最新值,依赖现有Token刷新逻辑保证这里的值有效 return { serviceUrl: getToLocalStorage("serviceUrl"), token: getToLocalStorage("token") }; }); // 提前启动连接 async function initSignalR() { try { await connection.start(); console.log("SignalR connected"); } catch (err) { console.log(err.toString()); setTimeout(initSignalR, 5000); } } initSignalR(); // 上传请求逻辑 var ajaxResponse = $.ajaxQueue({ type: "POST", url: "/Upload/Upload", enctype: 'multipart/form-data', contentType: false, processData: false, async: true, beforeSend: function(request) { request.setRequestHeader("serviceUrl", getToLocalStorage("serviceUrl")); request.setRequestHeader("token", getToLocalStorage("token")); // 把当前连接ID传给服务端 request.setRequestHeader("connectionId", connection.connectionId); }, headers: { 'serviceUrl': getToLocalStorage("serviceUrl"), 'token': getToLocalStorage("token"), 'connectionId': connection.connectionId }, data: formData, success: function (data) { if (data.success) { UploadControl(newItemId, object.overWrite); } else { new PNotify({ text: data.message == null ? object.itemName+" file is empty": data.message, type: "error", addClass: ".notifybg", delay: 5000 }); } }, error: function (data) { const uploadedFile = uploadFileCollectionListData.findIndex(el => el.itemId == newItemId); uploadFileCollectionListData.splice(uploadedFile, 1); } });
后端改造点
- 上传接口从请求头中拿到前端传的
connectionId,和初始Token、serviceUrl一起作为上下文参数传入分块处理逻辑。 - 分块处理循环中,每次调用需要鉴权的上传逻辑前,先判断当前Token是否临期(比如剩余有效期不足30秒),如果失效就通过
IHubContext调用对应客户端的GetLatestAuthInfo方法,拿到最新的Token和serviceUrl更新上下文后,再继续处理分块。 - 进度推送不要用
Clients.All,直接定向推给当前上传对应的connectionId即可,避免无关用户收到其他人的上传进度。 - 所有异步调用记得加
await,否则会出现取值为空、异常无法捕获的问题。
改造后的后端代码参考:
// 从请求头取上传上下文参数 var currentToken = Request.Headers["token"].ToString(); var currentServiceUrl = Request.Headers["serviceUrl"].ToString(); var clientConnectionId = Request.Headers["connectionId"].ToString(); // 分块处理逻辑 while (bytesToRead > 0) { int n = await stream.ReadAsync(buffer, 0, chunkSize); if (n == 0) break; if (n != buffer.Length) Array.Resize(ref buffer, n); // 自行实现Token有效期判断逻辑,剩余有效期不足30秒就触发刷新 if (IsTokenExpiringSoon(currentToken)) { // 加10秒超时避免连接断开导致循环卡死 using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var latestAuth = await _progressHub.Clients.Client(clientConnectionId) .InvokeAsync<AuthInfo>("GetLatestAuthInfo", cts.Token); // 更新当前上下文的鉴权信息 currentToken = latestAuth.Token; currentServiceUrl = latestAuth.ServiceUrl; } // 用最新Token处理当前分块上传 uploadCloudItem = UploadCloudItem(currentToken, new MemoryStream(buffer), u); bytesRead += n; bytesToRead -= n; var progress = (int)((float)bytesRead / (float)uploadRequest.itemSize * 100.0); // 定向推进度给当前上传用户 await _progressHub.Clients.Client(clientConnectionId) .SendAsync("lastprogress", u.itemId, progress); await Task.Delay(500); } // 定义鉴权信息接收类 public class AuthInfo { public string ServiceUrl { get; set; } public string Token { get; set; } }
注意事项
- 现有Token自动刷新逻辑必须保证localStorage中存储的永远是最新有效Token,否则SignalR拉取到的仍然是过期Token,无法解决问题。
- 调用客户端获取Token的逻辑要加异常捕获,如果客户端断开连接拿不到最新Token,要及时终止上传返回错误提示,不要陷入死循环。
- 不要在
beforeSend里重复绑定SignalR事件、重复启动连接,否则会出现一次进度更新触发多次回调、连接数泄漏的问题。
内容的提问来源于stack exchange,提问作者Mehmet Ceylan
相关产品推荐
相关产品推荐

