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

NetCore中IdentityServer4重启后令牌丢失的持久化配置求助

IdentityServer4 令牌持久化配置方案

服务器重启后内存令牌丢失的问题,本质是因为IdentityServer4默认使用内存存储保存持久化授权(如刷新令牌、授权码),重启后内存数据清空导致用户需要重新认证。以下提供两种磁盘级别的持久化方案:

方案一:文件系统存储(自定义实现,适合测试/轻量场景)

1. 实现IPersistedGrantStore接口

这个接口是IdentityServer4管理持久化授权的核心契约,我们基于JSON文件实现读写逻辑:

public class FilePersistedGrant
{
    public string Key { get; set; }
    public string Type { get; set; }
    public string SubjectId { get; set; }
    public string ClientId { get; set; }
    public DateTime CreationTime { get; set; }
    public DateTime? Expiration { get; set; }
    public string Data { get; set; }
}

public class FilePersistedGrantStore : IPersistedGrantStore
{
    private readonly string _filePath;
    private readonly object _lock = new object();

    public FilePersistedGrantStore(string filePath)
    {
        _filePath = filePath;
        if (!File.Exists(filePath))
        {
            File.WriteAllText(filePath, JsonSerializer.Serialize(new List<FilePersistedGrant>()));
        }
    }

    public async Task<IEnumerable<PersistedGrant>> GetAllAsync(PersistedGrantFilter filter)
    {
        lock (_lock)
        {
            var grants = JsonSerializer.Deserialize<List<FilePersistedGrant>>(File.ReadAllText(_filePath));
            var query = grants.AsQueryable();

            if (!string.IsNullOrEmpty(filter.ClientId))
                query = query.Where(x => x.ClientId == filter.ClientId);
            if (!string.IsNullOrEmpty(filter.SubjectId))
                query = query.Where(x => x.SubjectId == filter.SubjectId);
            if (!string.IsNullOrEmpty(filter.Type))
                query = query.Where(x => x.Type == filter.Type);

            return query.Select(MapToPersistedGrant).ToList();
        }
    }

    public async Task<PersistedGrant> GetAsync(string key)
    {
        lock (_lock)
        {
            var grants = JsonSerializer.Deserialize<List<FilePersistedGrant>>(File.ReadAllText(_filePath));
            var grant = grants.FirstOrDefault(x => x.Key == key);
            return grant != null ? MapToPersistedGrant(grant) : null;
        }
    }

    public async Task RemoveAllAsync(PersistedGrantFilter filter)
    {
        lock (_lock)
        {
            var grants = JsonSerializer.Deserialize<List<FilePersistedGrant>>(File.ReadAllText(_filePath));
            grants = grants.Where(x => 
                (string.IsNullOrEmpty(filter.ClientId) || x.ClientId != filter.ClientId) &&
                (string.IsNullOrEmpty(filter.SubjectId) || x.SubjectId != filter.SubjectId) &&
                (string.IsNullOrEmpty(filter.Type) || x.Type != filter.Type)).ToList();

            File.WriteAllText(_filePath, JsonSerializer.Serialize(grants));
        }
    }

    public async Task RemoveAsync(string key)
    {
        lock (_lock)
        {
            var grants = JsonSerializer.Deserialize<List<FilePersistedGrant>>(File.ReadAllText(_filePath));
            grants.RemoveAll(x => x.Key == key);
            File.WriteAllText(_filePath, JsonSerializer.Serialize(grants));
        }
    }

    public async Task StoreAsync(PersistedGrant grant)
    {
        lock (_lock)
        {
            var grants = JsonSerializer.Deserialize<List<FilePersistedGrant>>(File.ReadAllText(_filePath));
            var existing = grants.FirstOrDefault(x => x.Key == grant.Key);
            if (existing != null)
                grants.Remove(existing);

            grants.Add(MapFromPersistedGrant(grant));
            File.WriteAllText(_filePath, JsonSerializer.Serialize(grants));
        }
    }

    private FilePersistedGrant MapFromPersistedGrant(PersistedGrant grant)
    {
        return new FilePersistedGrant
        {
            Key = grant.Key,
            Type = grant.Type,
            SubjectId = grant.SubjectId,
            ClientId = grant.ClientId,
            CreationTime = grant.CreationTime,
            Expiration = grant.Expiration,
            Data = grant.Data
        };
    }

    private PersistedGrant MapToPersistedGrant(FilePersistedGrant grant)
    {
        return new PersistedGrant
        {
            Key = grant.Key,
            Type = grant.Type,
            SubjectId = grant.SubjectId,
            ClientId = grant.ClientId,
            CreationTime = grant.CreationTime,
            Expiration = grant.Expiration,
            Data = grant.Data
        };
    }
}

2. 注册自定义存储到IdentityServer

在.NET 6+的Program.cs中替换默认内存存储:

var builder = WebApplication.CreateBuilder(args);

// 配置IdentityServer核心服务
builder.Services.AddIdentityServer()
    .AddInMemoryClients(Config.Clients) // 替换为你的客户端配置
    .AddInMemoryIdentityResources(Config.IdentityResources)
    .AddInMemoryApiScopes(Config.ApiScopes)
    .AddTestUsers(Config.TestUsers) // 生产环境替换为真实用户存储
    .AddPersistedGrantStore<FilePersistedGrantStore>()
    .AddDeveloperSigningCredential(); // 生产环境需替换为正式证书

// 注册文件存储实例,指定令牌存储文件路径
builder.Services.AddSingleton<IPersistedGrantStore>(provider => 
    new FilePersistedGrantStore(Path.Combine(Directory.GetCurrentDirectory(), "persisted_grants.json")));

var app = builder.Build();

app.UseIdentityServer();
app.Run();

方案二:SQLite文件数据库存储(适合生产环境)

SQLite是轻量的文件型数据库,比纯文件存储更可靠,支持并发和自动清理过期令牌。

1. 安装依赖包

执行NuGet命令安装:

Install-Package IdentityServer4.EntityFramework
Install-Package Microsoft.EntityFrameworkCore.Sqlite

2. 配置数据库连接与IdentityServer

在Program.cs中配置:

var builder = WebApplication.CreateBuilder(args);

// 配置SQLite数据库连接(文件路径为项目根目录的identityserver.db)
builder.Services.AddDbContext<PersistedGrantDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("IdentityServerDb")));

// 配置IdentityServer使用EntityFramework存储持久化授权
builder.Services.AddIdentityServer()
    .AddInMemoryClients(Config.Clients)
    .AddInMemoryIdentityResources(Config.IdentityResources)
    .AddInMemoryApiScopes(Config.ApiScopes)
    .AddTestUsers(Config.TestUsers)
    .AddOperationalStore(options =>
    {
        options.ConfigureDbContext = b => b.UseSqlite(builder.Configuration.GetConnectionString("IdentityServerDb"));
        options.EnableTokenCleanup = true; // 开启自动清理过期令牌
        options.TokenCleanupInterval = 3600; // 每小时执行一次清理
    })
    .AddDeveloperSigningCredential();

var app = builder.Build();

// 初始化数据库(自动创建表结构)
using (var scope = app.Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>();
    context.Database.Migrate();
}

app.UseIdentityServer();
app.Run();

3. 添加连接字符串到appsettings.json

{
  "ConnectionStrings": {
    "IdentityServerDb": "Data Source=identityserver.db"
  }
}

注意事项

  • 生产环境禁止使用AddDeveloperSigningCredential,需使用正式的签名证书(如从文件、密钥存储加载)。
  • 文件存储方案需自行实现过期令牌清理逻辑,避免存储文件膨胀;SQLite方案通过EnableTokenCleanup自动处理。
  • 若需要更高性能和扩展性,可替换SQLite为SQL Server、MySQL等数据库,只需修改数据库驱动和连接字符串即可。

内容的提问来源于stack exchange,提问作者MANUEL SOLER PUERTO

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 05:25:38