Identity Server With ASP.NET Core Identity
到目前為止 Identity Server 基礎部份都學習過了,但是還缺少會員的添加與管理方式,之前我們使用 TestUsers 寫死兩個會員資料, 但在實務中還是要有動態新增會員的方式,這時候就可以加入 ASP.NET Core Identity 功能,不過要處理這部份需要對 ASP.NET Core Identity 有相當程度的了解, 本系列一開始有討論過了有需要的可以回去看看。
要在 IdentityServer 使用 ASP.NET Core Identity 可以使用預設的範本 duende-is-aspid 來建立專案
先建立一個新專案來看看跟我們目前的專案有什麼區別
dotnet new duende-is-aspid -n IdentityServerAspNetIdentity
- 安裝 Package
Duende.IdentityServer.AspNetIdentity,Microsoft.AspNetCore.Identity.EntityFrameworkCore,Microsoft.AspNetCore.Identity.UI - wwwroot 與 Pages 資料夾都是
isui這個範本提供的會幫你在專案添加一些預設頁面 - Models 資料夾新建立一個自定義的 IdentityUser 叫做 ApplicationUser
- Data 資料夾新建立一個自定義的 IdentityDbContext 叫做 ApplicationDbContext
- SeedData 內部會使用 UserManager 提供的方法來新增預設會員數據
- HostingExtensions 注入並使用 ApplicationDbContext
- 額外呼叫 AddAspNetIdentity 方法設定 Identity 相關功能並且呼叫
AddResourceOwnerValidator與AddProfileService
了解後回到原本的 IdentityServer 專案我們來進行實做
首先添加需要用到的 Package
dotnet add package Duende.IdentityServer.AspNetIdentity
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
建立 DbContext
namespace IdentityServer.Data;
public class AppIdentityDbContext : IdentityDbContext<IdentityUser>
{
public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options) : base(options)
{
}
}
將 AppIdentityDbContext 註冊到系統內,並且將 AddTestUsers 替換成 AddAspNetIdentity
const string connectionIdentity = @"User ID=postgres;Password=myPassw0rd;Host=localhost;Port=5432;Database=IdentityServerIdentity;Pooling=false;";
builder.Services.AddDbContext<AppIdentityDbContext>(options =>
options.UseNpgsql(connectionIdentity));
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<AppIdentityDbContext>();
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));
})
.AddAspNetIdentity<IdentityUser>()
.AddLicenseSummary();
因為要使用 Identity UI 我們需要在專案添加 /Views/Shared/_LoginPartial.cshtml 用來顯示右上角
根據登入狀態顯示 Login 或者 Log out,
cd Pages\Shared\
dotnet new page -np -n _LoginPartial
@if (User.Identity.IsAuthenticated)
{
<form asp-controller="Account" asp-action="Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })" method="post" id="logoutForm" class="navbar-right">
<ul class="nav navbar-nav navbar-right">
<li class="navbar-text">Hello @User.Identity.Name!</li>
<li>
<button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button>
</li>
</ul>
</form>
}
else
{
<ul class="nav navbar-nav navbar-right">
<li><a asp-controller="Account" asp-action="Login">Login</a></li>
</ul>
}
最後建立遷移腳本並且更新資料庫
dotnet ef migrations add InitialIdentityDbMigration -c AppIdentityDbContext -o Migrations/IdentityDb
dotnet ef database update -c AppIdentityDbContext
public static void SeedIdentityData(WebApplication app)
{
using (var scope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
var alice = userMgr.FindByNameAsync("alice").Result;
if (alice == null)
{
alice = new IdentityUser
{
UserName = "alice",
Email = "AliceSmith@example.com",
EmailConfirmed = true,
};
var result = userMgr.CreateAsync(alice, "Pass123$").Result;
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
result = userMgr.AddClaimsAsync(alice, new Claim[]{
new Claim(JwtClaimTypes.Name, "Alice Smith"),
new Claim(JwtClaimTypes.GivenName, "Alice"),
new Claim(JwtClaimTypes.FamilyName, "Smith"),
new Claim(JwtClaimTypes.WebSite, "http://alice.example.com"),
}).Result;
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
Log.Debug("alice created");
}
else
{
Log.Debug("alice already exists");
}
var bob = userMgr.FindByNameAsync("bob").Result;
if (bob == null)
{
bob = new IdentityUser
{
UserName = "bob",
Email = "BobSmith@example.com",
EmailConfirmed = true
};
var result = userMgr.CreateAsync(bob, "Pass123$").Result;
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
result = userMgr.AddClaimsAsync(bob, new Claim[]{
new Claim(JwtClaimTypes.Name, "Bob Smith"),
new Claim(JwtClaimTypes.GivenName, "Bob"),
new Claim(JwtClaimTypes.FamilyName, "Smith"),
new Claim(JwtClaimTypes.WebSite, "http://bob.example.com"),
new Claim("location", "somewhere")
}).Result;
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
Log.Debug("bob created");
}
else
{
Log.Debug("bob already exists");
}
}
}
ConfigurePipeline 內部呼叫此方法
SeedIdentityData(app);
要注意之前新增的 UI 是寫死依賴 TestUserStore,所以要將這些依賴替換成實際的 UserManager 與 SignInManager,
才是真正讀取資料庫進行登入,這部分可以參考一開始建立的 IdentityServerAspNetIdentity 模板專案,裡面已經修改完畢。
完成後運行 Identity Server 與 WebClient 專案進行測試
在瀏覽器打開 https://localhost:5002 會直接透過 OIDC 轉跳到 https://localhost:5001 要求登入,
這時輸入剛剛新增的帳號(alice, Pass123$)能夠正常登入並且跳回到 5002 並顯示目前 User 身上擁有的 Claim。
Summary
今天成功將 Identity Server 與 ASP.NET Core Identity 兩個技術結合在一起, 至此我們的功能已經可以做到類似 Google 的 OIDC 伺服器的提供的功能了, Identity Server 還有許多設定可以調整,可以按照自己的需求來做設定