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

.NET服务端存储登录验证QRSecret的可行方案咨询

可行实现方案及细节

针对你的BankID动态二维码登录需求,以下是几个经过验证的可行方案,覆盖不同场景需求:

方案一:缓存(MemoryCache/Redis)+ 唯一请求ID

这是最通用的方案,适合单实例或分布式部署场景,自动处理过期清理,且能保证每个用户的QRSecret独立。

实现步骤

  1. 初始化登录请求:

    • 用户进入登录页时,服务端生成唯一请求ID(如Guid)和QRSecret。
    • 将请求ID -> (QRSecret, QRStart)键值对存入缓存,设置过期时间30秒。
    • 返回给客户端:请求ID、QRStart、初始TimeElapsed=0(不返回QRSecret)。
  2. 客户端轮询更新二维码:

    • 客户端每秒计算当前TimeElapsed(基于QRStart和当前时间),携带请求ID发起AJAX请求。
    • 服务端通过请求ID从缓存取出QRSecret,结合TimeElapsed生成新的二维码参数返回给客户端。
  3. 登录验证与清理:

    • 用户扫码完成BankID验证后,服务端用请求ID取出QRSecret完成校验,校验通过后删除缓存条目。

代码示例

.NET 服务端初始化代码

private readonly IMemoryCache _cache;

// 构造函数注入IMemoryCache
public LoginController(IMemoryCache cache)
{
    _cache = cache;
}

[HttpGet]
public IActionResult InitializeQRLogin()
{
    var requestId = Guid.NewGuid().ToString();
    var qrSecret = GenerateUniqueSecret(); // 自定义生成唯一敏感值的方法
    var qrStart = DateTime.UtcNow;

    // 存入缓存,30秒后自动过期
    _cache.Set(requestId, new { QRSecret = qrSecret, QRStart = qrStart }, TimeSpan.FromSeconds(30));

    return Json(new {
        RequestId = requestId,
        QRStart = qrStart.ToString("o"), // 标准化UTC时间格式
        InitialTimeElapsed = 0
    });
}

客户端轮询代码

let requestId = "";
let qrStart = new Date();

// 初始化获取请求ID和QRStart
$.get("/Login/InitializeQRLogin", function(data) {
    requestId = data.RequestId;
    qrStart = new Date(data.QRStart);
    startQRRefresh();
});

function startQRRefresh() {
    setInterval(() => {
        const timeElapsed = Math.floor((new Date() - qrStart) / 1000);
        if (timeElapsed >= 30) {
            alert("二维码已过期,请刷新页面");
            return;
        }

        $.post("/Login/GetQRParameters", {
            requestId: requestId,
            timeElapsed: timeElapsed
        }, function(response) {
            if (response.error) {
                alert(response.error);
                return;
            }
            // 更新页面二维码
            updateQRCode(response.qrParams);
        });
    }, 1000);
}

服务端生成二维码参数代码

[HttpPost]
public IActionResult GetQRParameters(string requestId, int timeElapsed)
{
    if (!_cache.TryGetValue(requestId, out dynamic loginData))
    {
        return Json(new { error = "二维码已过期" });
    }

    // 验证TimeElapsed合理性,防止客户端篡改
    var actualElapsed = (int)(DateTime.UtcNow - loginData.QRStart).TotalSeconds;
    if (Math.Abs(timeElapsed - actualElapsed) > 2)
    {
        return Json(new { error = "无效的时间参数" });
    }

    // 生成二维码参数(不包含QRSecret)
    var qrParams = GenerateQRParams(timeElapsed, loginData.QRStart, loginData.QRSecret);
    return Json(new { qrParams = qrParams });
}

方案二:Session存储

如果是单实例小型项目,Session是最简单的选择,无需额外依赖,天然和用户会话绑定。

实现步骤

  1. 用户进入登录页时,服务端生成QRSecret并存入Session:
    HttpContext.Session.SetString("QRSecret", qrSecret);
    // 设置Session过期时间为30秒(若全局Session超时更长,可单独配置)
    HttpContext.Session.SetCookieOptions(new CookieOptions { Expires = DateTime.UtcNow.AddSeconds(30) });
    
  2. 客户端轮询时,服务端直接从Session取出QRSecret生成二维码参数。
  3. 登录验证完成后,清除Session中的QRSecret:
    HttpContext.Session.Remove("QRSecret");
    

注意事项

  • 分布式部署时,需将Session存储改为Redis等分布式存储,否则多实例间Session不共享。

方案三:SignalR实时推送

适合追求低请求量、高实时性的场景,替代客户端轮询,由服务端主动推送二维码更新。

实现步骤

  1. 用户进入登录页时,建立SignalR连接,服务端为每个连接生成QRSecret和连接ID,存入缓存(30秒过期)。
  2. 服务端启动定时任务,每秒计算TimeElapsed,生成二维码参数后推送给对应客户端。
  3. 用户扫码完成验证后,服务端通过连接ID推送登录成功消息给客户端。

核心代码示例

SignalR Hub代码

public class QRLoginHub : Hub
{
    private readonly IMemoryCache _cache;

    public QRLoginHub(IMemoryCache cache)
    {
        _cache = cache;
    }

    public async Task StartQRLogin()
    {
        var connectionId = Context.ConnectionId;
        var qrSecret = GenerateUniqueSecret();
        var qrStart = DateTime.UtcNow;

        _cache.Set(connectionId, new { QRSecret = qrSecret, QRStart = qrStart }, TimeSpan.FromSeconds(30));

        // 每秒推送二维码更新
        var timer = new Timer(async _ =>
        {
            if (_cache.TryGetValue(connectionId, out dynamic data))
            {
                var timeElapsed = (int)(DateTime.UtcNow - data.QRStart).TotalSeconds;
                if (timeElapsed >= 30)
                {
                    await Clients.Client(connectionId).SendAsync("QRExpired");
                    timer.Dispose();
                    return;
                }
                var qrParams = GenerateQRParams(timeElapsed, data.QRStart, data.QRSecret);
                await Clients.Client(connectionId).SendAsync("UpdateQR", qrParams);
            }
            else
            {
                timer.Dispose();
            }
        }, null, 0, 1000);
    }
}

客户端SignalR连接代码

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/qrLoginHub")
    .build();

connection.on("UpdateQR", function(qrParams) {
    updateQRCode(qrParams);
});

connection.on("QRExpired", function() {
    alert("二维码已过期,请刷新页面");
});

connection.start().then(() => {
    connection.invoke("StartQRLogin");
}).catch(err => console.error(err));

方案选择建议

  • 单实例小型项目:优先选Session方案,实现成本最低。
  • 分布式部署或需要扩展性:选缓存+唯一请求ID方案,适配性最强。
  • 追求用户体验(减少请求量):选SignalR方案,实时性更好。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 12:53:22