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
相关产品推荐
相关产品推荐

