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

.NET 6 多站点iframe嵌入场景下标签页级会话隔离实现方案

可行方案(基于.NET 6原生能力,无第三方依赖)

核心利用浏览器原生的sessionStorage实现标签页级上下文隔离,这个存储天然和单个标签页+同源绑定,同URL开多个标签页时存储完全独立,关闭标签页自动清空,刚好匹配标签页维度会话隔离的需求,不需要依赖已被移除的无Cookie会话方案。

整体实现逻辑

放弃把认证Claims存在全局共享Cookie的做法,改用「标签页唯一上下文ID+服务端缓存+前端自动透传」的模式:

  • 首次加载iframe时,后端接收父页面POST过来的AppCode,完成SSO校验后生成当前App专属的Claims,同时生成全局唯一的TabContextId(用Guid即可),将Claims、AppCode、用户ID存在服务端分布式缓存(内存缓存/Redis都可以,配置和原会话一致的滑动过期时间),缓存Key就是TabContextId
  • 首次返回的入口页HTML内联一段JS,把TabContextId直接写入当前iframe的sessionStorage,这个ID只在当前标签页/iframe上下文内可见,其他标签页无法访问
  • 前端全局拦截所有同域请求(Ajax/Fetch/表单提交),自动从sessionStorage取出TabContextId放到自定义请求头X-Tab-Context-Id中,业务代码不需要手动传参
  • 后端认证中间件不再从全局Cookie读Claims,改为从请求头取TabContextId,查缓存拿到对应上下文后构造ClaimsPrincipal赋值给HttpContext.User,后续业务逻辑完全不需要改动

具体代码改造点

1. 后端改造

(1)调整Claim存储逻辑

把原来SetUtenteCorrente方法里写Cookie认证的逻辑,改成写缓存:

// 提前注入IDistributedCache,用内存缓存或者Redis都可以
private readonly IDistributedCache _cache;
public async Task<string> SetUtenteCorrente(HttpContext context, string appCode)
{
    var utente = GetUserIdFromRequestHeader(context);
    var utenteSPA = _UserDataServiceProvider.GetUtente(utente);
    
    // 构造当前App专属的Claims,不要和其他App的Claim混存
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, utenteSPA.Nominativo),
        new Claim(TipoClaim.matricola.ToString(), utenteSPA.Matricola),
        new Claim(TipoClaim.ufficio.ToString(), utenteSPA.Ufficio),
        new Claim(ClaimTypes.Role, utenteSPA.Ruolo),
        new Claim(TipoClaim.linguaPredefinita.ToString(), utenteSPA.LinguaDefault.ToString()),
        new Claim(TipoClaim.linguaCorrente.ToString(), utenteSPA.LinguaCorrente.ToString()),
        new Claim(TipoClaim.profiloPredefinito.ToString(), utenteSPA.ProfiloDefault),
        new Claim(TipoClaim.profiloCorrente.ToString(), utenteSPA.ProfiloCorrente),
        new Claim(TipoClaim.applicazioneAttiva.ToString(), appCode),
    };

    // 生成标签页唯一上下文ID
    var tabContextId = Guid.NewGuid().ToString("N");
    // 序列化上下文存入缓存,配置30分钟滑动过期
    var cacheData = JsonSerializer.Serialize(new { UserId = utente, AppCode = appCode, Claims = claims });
    var cacheOptions = new DistributedCacheEntryOptions
    {
        SlidingExpiration = TimeSpan.FromMinutes(30)
    };
    await _cache.SetStringAsync($"TabContext_{tabContextId}", cacheData, cacheOptions);

    return tabContextId;
}

(2)入口页Action处理首次POST请求

[HttpPost]
public async Task<IActionResult> Index(string AppCode)
{
    // 校验AppCode合法性
    if (string.IsNullOrEmpty(AppCode)) return Redirect("/error/400");
    // 生成当前标签页上下文ID
    var tabContextId = await userManager.SetUtenteCorrente(HttpContext, AppCode);
    // 把上下文ID传给视图,渲染内联脚本
    ViewBag.TabContextId = tabContextId;
    return View();
}

(3)改造认证中间件

public async Task Invoke(HttpContext context)
{
    var requestPath = context.Request.Path.Value;
    // 跳过错误页和入口页POST请求
    if (requestPath.Contains("rror") || (requestPath == "/" && context.Request.Method == "POST"))
    {
        await _next(context);
        return;
    }

    try
    {
        // 从请求头取上下文ID
        if (!context.Request.Headers.TryGetValue("X-Tab-Context-Id", out var tabId) || string.IsNullOrEmpty(tabId))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("Invalid context");
            return;
        }

        // 查缓存拿上下文
        var cacheData = await _cache.GetStringAsync($"TabContext_{tabId}");
        if (string.IsNullOrEmpty(cacheData))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("Context expired");
            return;
        }

        var tabContext = JsonSerializer.Deserialize<TabContext>(cacheData);
        // 构造当前请求的ClaimsPrincipal
        var identity = new ClaimsIdentity(tabContext.Claims, CookieAuthenticationDefaults.AuthenticationScheme);
        context.User = new ClaimsPrincipal(identity);

        // 滑动续期
        await _cache.RefreshAsync($"TabContext_{tabId}");
    }
    catch (Exception ex)
    {
        _logger.Information("Authentication middleware error: {msg}", 
            ex.InnerException?.Message ?? ex.Message);
        context.Response.StatusCode = 502;
        context.Response.Redirect($"{percorsoErrore}502");
        return;
    }

    await _next(context);
}

// 上下文实体类
public class TabContext
{
    public string UserId { get; set; }
    public string AppCode { get; set; }
    public List<Claim> Claims { get; set; }
}

2. 前端改造

(1)入口页视图内联脚本写入sessionStorage

在入口页的head标签最前面加这段脚本,确保后续脚本执行前上下文已经写入:

<script>
// 直接把后端生成的上下文ID写入当前标签页sessionStorage
window.sessionStorage.setItem('tabContextId', '@ViewBag.TabContextId');
</script>

(2)全局拦截所有请求自动透传上下文ID

根据你用的请求库加全局拦截,不需要改单个业务接口:

// 拦截jQuery Ajax请求
if (window.jQuery) {
    $.ajaxSetup({
        beforeSend: function(xhr) {
            const tabId = window.sessionStorage.getItem('tabContextId');
            if (tabId) xhr.setRequestHeader('X-Tab-Context-Id', tabId);
        }
    });
}

// 拦截fetch请求
const originalFetch = window.fetch;
window.fetch = function(url, options = {}) {
    const tabId = window.sessionStorage.getItem('tabContextId');
    options.headers = options.headers || {};
    if (tabId) options.headers['X-Tab-Context-Id'] = tabId;
    // 拦截401响应,会话过期时跳错误页
    return originalFetch.call(this, url, options).then(res => {
        if (res.status === 401) window.location.href = '/error/401';
        return res;
    });
}

// 拦截原生表单提交
document.addEventListener('submit', function(e) {
    const form = e.target;
    const tabId = window.sessionStorage.getItem('tabContextId');
    if (tabId) {
        const input = document.createElement('input');
        input.type = 'hidden';
        input.name = 'X-Tab-Context-Id';
        input.value = tabId;
        form.appendChild(input);
    }
});

边界情况处理

  • 页面刷新:同标签页刷新时sessionStorage数据不会丢失,加载完成后拦截器会自动带上上下文ID,不需要重新走POST AppCode的流程
  • 标签页关闭:sessionStorage自动清空,服务端缓存到期后自动清理,无残留
  • 跨iframe安全:如果你的iframe和父页面不同源,父页面无法访问iframe内的sessionStorage,不会有上下文泄露风险
  • 同标签页跳转:同域跳转时sessionStorage数据保留,上下文自动续期,跨域跳转后sessionStorage自动隔离,不会串数据

内容的提问来源于stack exchange,提问作者gt.guybrush

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 21:39:15