Identity Server EntityFramework Core
在上一篇的文章中 已經成功架設了 Identity Server 服務與 UI 但是目前資料都是保存在記憶體內,今天來看看該怎麼把會員資料透過 EFCore 保存到資料庫內。
首先先回顧一下當初 Identity Server 使用的設定,在這裡我們呼叫了三個 AddInMemory 方法將對應的資料添加到記憶體內。
builder.Services.AddIdentityServer()
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryApiResources(Config.ApiResources)
.AddInMemoryClients(Config.Clients)
.AddTestUsers(TestUsers.Users)
.AddLicenseSummary();
這邊在 IdentityServer 專案添加 Duende.IdentityServer.EntityFramework 函式庫與 EFCore PostgreSQL。
dotnet add package Duende.IdentityServer.EntityFramework
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Tools
接下來可以使用 Identity Server 預設提供的 DbContext,
依照功能性區分成兩個 DbContext 分別為 ConfigurationDbContext 與 PersistedGrantDbContext。
ConfigurationDbContext,這類型的資料通常是在後台經由管理員新增,變動頻率低讀取頻率高,包含以下 DBSet:
- Client
- ClientCorsOrigin
- IdentityResource
- ApiResource
- ApiScope
- IdentityProvider
PersistedGrantDbContext,這類型的資料會在系統運行中自動新增,變動頻率高,包含以下 DBSet:
- PersistedGrant
- DeviceFlowCodes
- Key
- ServerSideSession
接下來將回到 HostingExtensions.cs 將 AddInMemory 替換掉,這裡採用的是 Repository Pattern 將實際的存取邏輯分散到各個 Store 當中,
例如 Client 相關的操作有 ClientStore,IdentityResource 整合在 ResourceStore 當中,也就代表你如果不喜歡官方的存取方式,
可以透過依賴注入的方式直接替換掉官方實做。
不過在一般情況使用官方實做就很夠用了,這裡也不用一個一個 Store 註冊,可以直接使用 AddConfigurationStore 與 AddOperationalStore 方法,
一次將底層的 Store 註冊進來,上面也有提到 ConfigurationDbContext 的資料都是高頻率讀取的,
所以官方也加了 AddConfigurationStoreCache 背後採用 Decorator Pattern 在中間添加一層快取層。
下面是它們的原始碼,可以看到實際的運作原理。
public static IIdentityServerBuilder AddConfigurationStore<TContext>(
this IIdentityServerBuilder builder,
Action<ConfigurationStoreOptions>? storeOptionsAction = null)
where TContext : DbContext, IConfigurationDbContext
{
builder.Services.AddConfigurationDbContext<TContext>(storeOptionsAction);
builder.AddClientStore<ClientStore>();
builder.AddResourceStore<ResourceStore>();
builder.AddCorsPolicyService<CorsPolicyService>();
builder.AddIdentityProviderStore<IdentityProviderStore>();
return builder;
}
public static IIdentityServerBuilder AddConfigurationStoreCache(
this IIdentityServerBuilder builder)
{
builder.AddInMemoryCaching();
// add the caching decorators
builder.AddClientStoreCache<ClientStore>();
builder.AddResourceStoreCache<ResourceStore>();
builder.AddCorsPolicyCache<CorsPolicyService>();
builder.AddIdentityProviderStoreCache<IdentityProviderStore>();
return builder;
}
public static IIdentityServerBuilder AddOperationalStore<TContext>(
this IIdentityServerBuilder builder,
Action<OperationalStoreOptions>? storeOptionsAction = null)
where TContext : DbContext, IPersistedGrantDbContext
{
builder.Services.AddOperationalDbContext<TContext>(storeOptionsAction);
builder.AddSigningKeyStore<SigningKeyStore>();
builder.AddPersistedGrantStore<PersistedGrantStore>();
builder.AddDeviceFlowStore<DeviceFlowStore>();
builder.AddServerSideSessionStore<ServerSideSessionStore>();
builder.AddPushedAuthorizationRequestStore<PushedAuthorizationRequestStore>();
builder.Services.AddSingleton<IHostedService, TokenCleanupHost>();
return builder;
}
接下來將 AddInMemory 替換成 AddConfigurationStore 與 AddOperationalStore 方法,並且透過 MigrationsAssembly 將指定遷移腳本生成到自己的專案中。
要注意使用者資料並不包含在內,要將使用者資料持久化需要搭配 ASP.NET Core Identity。
var migrationsAssembly = typeof(Program).Assembly.GetName().Name;
const string connectionStringConfigure = @"User ID=postgres;Password=myPassw0rd;Host=localhost;Port=5432;Database=IdentityServerConfigure;Pooling=false;";
const string connectionStringOperational = @"User ID=postgres;Password=myPassw0rd;Host=localhost;Port=5432;Database=IdentityServerOperational;Pooling=false;";
builder.Services.AddIdentityServer()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseNpgsql(connectionStringConfigure,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseNpgsql(connectionStringOperational,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddTestUsers(TestUsers.Users)
.AddLicenseSummary();
接下來分別幫這兩個 DbContext 建立遷移腳本與更新資料庫
dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Migrations/ConfigurationDb
dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Migrations/PersistedGrantDb
dotnet ef database update -c ConfigurationDbContext
dotnet ef database update -c PersistedGrantDbContext
額外建立一個方法將初始化資料寫到資料庫內
private static void InitializeDatabase(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
context.Database.Migrate();
if (!context.Clients.Any())
{
foreach (var client in Config.Clients)
{
context.Clients.Add(client.ToEntity());
}
context.SaveChanges();
}
if (!context.IdentityResources.Any())
{
foreach (var resource in Config.IdentityResources)
{
context.IdentityResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
if (!context.ApiScopes.Any())
{
foreach (var resource in Config.ApiScopes)
{
context.ApiScopes.Add(resource.ToEntity());
}
context.SaveChanges();
}
if (!context.ApiResources.Any())
{
foreach (var resource in Config.ApiResources)
{
context.ApiResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
}
}
ConfigurePipeline 內部呼叫此方法
InitializeDatabase(app);
完成後測試 Identity Server 專案與 WebClient 專案可以正常登入就代表成功了。
Summary
今天成功將保存在記憶體的資料改成保存到資料庫內, 但想要在後台頁面建立與管理 Client 或 ApiScope 的話, Identity Server 並沒有提供,必須要搭配其他開源的 admin 專案, 才可以在頁面上管理這些資源。