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

Azure AD多租户多数据库场景:如何按租户配置分布式令牌缓存

多租户场景下动态切换DistributedSqlServerCache的实现方案

针对你遇到的无法根据租户动态切换令牌缓存连接字符串的问题,核心解决思路是自定义一个IDistributedCache的实现,封装原生SqlServerCache,并在运行时根据当前请求的租户信息动态创建/复用对应租户的缓存实例。

步骤1:实现租户信息获取服务

首先需要一个服务从当前请求上下文提取租户ID,并获取对应租户的缓存数据库连接字符串:

public interface ITenantProvider
{
    string GetCurrentTenantId();
    string GetTenantConnectionString();
}

public class HttpContextTenantProvider : ITenantProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IConfiguration _configuration;

    public HttpContextTenantProvider(IHttpContextAccessor httpContextAccessor, IConfiguration configuration)
    {
        _httpContextAccessor = httpContextAccessor;
        _configuration = configuration;
    }

    public string GetCurrentTenantId()
    {
        // 从Azure AD用户Claims中提取租户ID(对应claim类型为http://schemas.microsoft.com/identity/claims/tenantid)
        return _httpContextAccessor.HttpContext?.User.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
    }

    public string GetTenantConnectionString()
    {
        var tenantId = GetCurrentTenantId();
        // 从配置中读取对应租户的缓存连接字符串(配置键格式示例:Cache_{TenantId})
        return _configuration.GetConnectionString($"Cache_{tenantId}");
    }
}

步骤2:实现租户感知的分布式缓存

自定义IDistributedCache实现,内部根据租户信息复用SqlServerCache实例:

using Microsoft.Extensions.Caching.SqlServer;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;

public class TenantDistributedCache : IDistributedCache
{
    private readonly ITenantProvider _tenantProvider;
    private readonly IServiceProvider _serviceProvider;
    // 缓存已创建的租户缓存实例,避免重复初始化
    private readonly ConcurrentDictionary<string, IDistributedCache> _tenantCachePool = new();

    public TenantDistributedCache(ITenantProvider tenantProvider, IServiceProvider serviceProvider)
    {
        _tenantProvider = tenantProvider;
        _serviceProvider = serviceProvider;
    }

    private IDistributedCache GetCurrentTenantCache()
    {
        var tenantId = _tenantProvider.GetCurrentTenantId() 
            ?? throw new InvalidOperationException("当前请求无法识别租户ID");
        
        var connectionString = _tenantProvider.GetTenantConnectionString() 
            ?? throw new InvalidOperationException($"未配置租户{tenantId}的缓存数据库连接字符串");

        // 用租户ID+连接字符串哈希作为缓存键,确保连接字符串变更时重新创建实例
        var cacheKey = $"{tenantId}_{connectionString.GetHashCode()}";
        return _tenantCachePool.GetOrAdd(cacheKey, _ =>
        {
            var cacheOptions = new SqlServerCacheOptions
            {
                ConnectionString = connectionString,
                TableName = "DistributedCache",
                SchemaName = "cache"
            };
            // 用服务提供者初始化原生SqlServerCache实例
            return ActivatorUtilities.CreateInstance<SqlServerCache>(_serviceProvider, Options.Create(cacheOptions));
        });
    }

    // 所有缓存操作委托给当前租户的缓存实例
    public byte[] Get(string key) => GetCurrentTenantCache().Get(key);
    public Task<byte[]> GetAsync(string key, CancellationToken token = default) 
        => GetCurrentTenantCache().GetAsync(key, token);
    public void Set(string key, byte[] value, DistributedCacheEntryOptions options) 
        => GetCurrentTenantCache().Set(key, value, options);
    public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) 
        => GetCurrentTenantCache().SetAsync(key, value, options, token);
    public void Refresh(string key) => GetCurrentTenantCache().Refresh(key);
    public Task RefreshAsync(string key, CancellationToken token = default) 
        => GetCurrentTenantCache().RefreshAsync(key, token);
    public void Remove(string key) => GetCurrentTenantCache().Remove(key);
    public Task RemoveAsync(string key, CancellationToken token = default) 
        => GetCurrentTenantCache().RemoveAsync(key, token);
}

步骤3:注册服务到DI容器

在Program.cs(或Startup.cs)中替换默认的分布式缓存实现:

var builder = WebApplication.CreateBuilder(args);

// 注册HttpContext访问器,用于获取当前请求上下文
builder.Services.AddHttpContextAccessor();

// 注册租户信息提供者
builder.Services.AddScoped<ITenantProvider, HttpContextTenantProvider>();

// 替换默认IDistributedCache为自定义的租户感知缓存
builder.Services.AddScoped<IDistributedCache, TenantDistributedCache>();

// 注意:无需再调用AddDistributedSqlServerCache,我们将动态创建实例

关键配置与注意事项

  1. 租户连接字符串配置:在appsettings.json中按租户ID配置缓存数据库连接字符串:
"ConnectionStrings": {
  "Cache_tenant-guid-1": "Server=your-sql-server;Database=Tenant1_Cache;Trusted_Connection=True;Encrypt=True;",
  "Cache_tenant-guid-2": "Server=your-sql-server;Database=Tenant2_Cache;Trusted_Connection=True;Encrypt=True;"
}
  1. 缓存表初始化:确保每个租户的缓存数据库都存在cache.DistributedCache表,可以手动执行以下SQL脚本初始化:
CREATE SCHEMA cache;

CREATE TABLE cache.DistributedCache (
    Id NVARCHAR(449) NOT NULL PRIMARY KEY,
    Value VARBINARY(MAX) NOT NULL,
    ExpiresAtTime DATETIMEOFFSET NOT NULL,
    SlidingExpirationInSeconds BIGINT NULL,
    AbsoluteExpiration DATETIMEOFFSET NULL
);
  1. Azure AD令牌缓存集成:Microsoft.Identity.Web的令牌缓存默认依赖IDistributedCache,替换后会自动使用租户隔离的缓存,无需额外修改令牌缓存配置。

内容的提问来源于stack exchange,提问作者S. ten Brinke

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 07:04:57