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

如何通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 03:24:31