.NET服务端存储登录验证QRSecret的可行方案咨询
可行实现方案及细节
针对你的BankID动态二维码登录需求,以下是几个经过验证的可行方案,覆盖不同场景需求:
方案一:缓存(MemoryCache/Redis)+ 唯一请求ID
这是最通用的方案,适合单实例或分布式部署场景,自动处理过期清理,且能保证每个用户的QRSecret独立。
实现步骤
初始化登录请求:
- 用户进入登录页时,服务端生成唯一请求ID(如Guid)和QRSecret。
- 将
请求ID -> (QRSecret, QRStart)键值对存入缓存,设置过期时间30秒。 - 返回给客户端:请求ID、QRStart、初始TimeElapsed=0(不返回QRSecret)。
客户端轮询更新二维码:
- 客户端每秒计算当前TimeElapsed(基于QRStart和当前时间),携带请求ID发起AJAX请求。
- 服务端通过请求ID从缓存取出QRSecret,结合TimeElapsed生成新的二维码参数返回给客户端。
登录验证与清理:
- 用户扫码完成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是最简单的选择,无需额外依赖,天然和用户会话绑定。
实现步骤
- 用户进入登录页时,服务端生成QRSecret并存入Session:
HttpContext.Session.SetString("QRSecret", qrSecret); // 设置Session过期时间为30秒(若全局Session超时更长,可单独配置) HttpContext.Session.SetCookieOptions(new CookieOptions { Expires = DateTime.UtcNow.AddSeconds(30) }); - 客户端轮询时,服务端直接从Session取出QRSecret生成二维码参数。
- 登录验证完成后,清除Session中的QRSecret:
HttpContext.Session.Remove("QRSecret");
注意事项
- 分布式部署时,需将Session存储改为Redis等分布式存储,否则多实例间Session不共享。
方案三:SignalR实时推送
适合追求低请求量、高实时性的场景,替代客户端轮询,由服务端主动推送二维码更新。
实现步骤
- 用户进入登录页时,建立SignalR连接,服务端为每个连接生成QRSecret和连接ID,存入缓存(30秒过期)。
- 服务端启动定时任务,每秒计算TimeElapsed,生成二维码参数后推送给对应客户端。
- 用户扫码完成验证后,服务端通过连接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
相关产品推荐
相关产品推荐

