IdentityServer 配置項持久化
對於 IdentityServer4 認證和授權框架,是支持數據庫持久化操作的,也就是在 IdentityServer4 服務器上需要配置的一些數據存儲到數據庫中永久存儲.
在 IdentityServer4 服務器上,配置的數據有:客戶端、API 作用域等,之前我們都是存儲在內存中進行操作,這裏,我們將這些數據存儲到數據庫中。IdentityServer4 支持的持久存儲體有 Redis、SQLServer、
Oracle、MySql 等。
注意:這裏永久存儲的數據不包括用戶信息,用戶信息需要使用ASP.NET Core Identity 認證框架來實現。
創建 IdentityServer 項目
打開 VS IDE 開發工具,新建一個 ASP.NET Core 6 空白項目,名稱爲:Dotnet.WebApi.Ids4.AuthService。
添加依賴包
添加 IdentityServer 配置項持久化所需的依賴包:
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3">
然後在其中添加如下程序包:
(1). IdentityServer4 最新穩定程序包,主程序包。
(2). IdentityServer4.EntityFramework:用於 IdentityServer4 的 EF Core 程序包,安裝最新穩定版本。
(3). Microsoft.EntityFrameworkCore.SqlServer:使用 EF Core 工具操作微軟的 SQL Server 數據庫程序包,安裝最新穩定版本。
(4). Microsoft.EntityFrameworkCore.Tools:使用 Nuget 包管理器控制檯進行配置遷移操作的程序包,安裝最新穩定版本。
添加
下載 Quickstart UI:https://github.com/IdentityServer/IdentityServer4.Quickstart.UI,
然後把 Quickstart、Views、wwwroot 三個文件夾複製到 Dotnet.WebApi.Ids4.AuthService 項目根目錄下。
數據庫遷移
ConfigurationDbContext
使用 ConfigurationDbContext 生成遷移客戶端、資源數據的代碼,命令如下:
Add-Migration init -Context ConfigurationDbContext -OutputDir Data/Migrations/Ids4/ConfigurationDb
使用如下命令生成 ConfigurationDbContext 相關表結構:
Update-Database -Context ConfigurationDbContext
PersistedGrantDbContext
使用 PersistedGrantDbContext 生成遷移同意授權的臨時數據、Token 代碼,命令如下:
Add-Migration init -Context PersistedGrantDbContext -OutputDir Data/Migrations/Ids4/PersistedGrantDb
使用如下命令生成 PersistedGrantDbContext 相關的表結構:
Update-Database -Context PersistedGrantDbContext
生成初始化數據
在 Program 類中找到如下代碼,
//同步數據
SyncData.InitializeDatabase(app);
SyncData的代碼如下:
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Mappers;
using Microsoft.EntityFrameworkCore;
namespace Dotnet.WebApi.Ids4.AuthService
{
public class SyncData
{
public static void InitializeDatabase(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices.CreateScope())
{
serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
context.Database.Migrate();
if (!context.Clients.Any())
{
foreach (var client in IdentityConfig.GetClients())
{
context.Clients.Add(client.ToEntity());
}
context.SaveChanges();
}
if (!context.IdentityResources.Any())
{
foreach (var resource in IdentityConfig.GetIdentityResources())
{
context.IdentityResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
if (!context.ApiScopes.Any())
{
foreach (var api in IdentityConfig.GetApiScopes())
{
context.ApiScopes.Add(api.ToEntity());
}
context.SaveChanges();
}
}
}
}
}
嚴重 BUG
如果使用 .net7, 在調用方法context.Clients.Add(client.ToEntity());
,會出現一個BUG:
System.TypeInitializationException:“The type initializer for 'IdentityServer4.EntityFramework.Mappers.ClientMappers' threw an exception.”
The type initializer for 'IdentityServer4.EntityFramework.Mappers.ClientMappers' threw an exception.
InnerException {"GenericArguments[0], 'System.Char', on 'T MaxFloat[T](System.Collections.Generic.IEnumerable`1[T])'
violates the constraint of type 'T'."} System.Exception {System.ArgumentException}
而開源項目 IdentityServer4 已經停止維護,
請將 Dotnet.WebApi.Ids4.AuthService 項目改爲 .net6, 修改方法爲編輯項目文件
裏面的
改爲
集成代碼
集成 IdentityServer4 代碼:
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Ids4Connection": "Server=localhost;Database=Ids4_Db;Uid=sa;Pwd=123456;Encrypt=True;TrustServerCertificate=True;"
}
}
Program.cs
using Microsoft.EntityFrameworkCore;
using System.Reflection;
namespace Dotnet.WebApi.Ids4.AuthService
{
public class Program
{
public static void Main(string[] args)
{
Console.Title = "IdentityServer4服務器";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
//獲取數據庫連接字符串,從appsettings.json中讀取。
var connetionString = builder.Configuration.GetConnectionString("Ids4Connection");
//獲取遷移使用的程序集,這裏是在Program類中實現遷移操作的。
var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name;
//註冊IdentityServer,並使用EFCore存儲客戶端和API作用域。
var ids4Builder = builder.Services.AddIdentityServer()
//配置存儲客戶端、資源等到數據庫中。
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = dbBuilder =>
dbBuilder.UseSqlServer(connetionString, t_builder =>
t_builder.MigrationsAssembly(migrationsAssembly));
})
//配置用戶授權的同意授權的數據、Token等存儲到數據庫中。
.AddOperationalStore(options =>
{
options.ConfigureDbContext = dbBuilder =>
dbBuilder.UseSqlServer(connetionString, t_builder =>
t_builder.MigrationsAssembly(migrationsAssembly));
})
//使用臨時的用戶,後續使用ASP.NET Identity認證存儲用戶。
.AddTestUsers(IdentityConfig.GetTestUsers());
//RSA證書加密,使用開發環境下的臨時證書,後續使用固定證書。
ids4Builder.AddDeveloperSigningCredential();
var app = builder.Build();
//同步數據
SyncData.InitializeDatabase(app);
//發佈後的端口號
app.Urls.Add("https://*:6001");
//啓用靜態文件。
app.UseStaticFiles();
//路由
app.UseRouting();
//啓用IdentityServer4。
app.UseIdentityServer();
//身份驗證
app.UseAuthentication();
//授權
app.UseAuthorization();
//終結點
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
app.Run();
}
}
}
IdentityConfig.cs
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System.Security.Claims;
namespace Dotnet.WebApi.Ids4.AuthService
{
public class IdentityConfig
{
/// <summary>
/// 配置IdentityResource。
/// </summary>
/// <returns></returns>
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource> {
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
/// <summary>
/// 配置可訪問的API範圍。
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiScope> GetApiScopes()
{
return new List<ApiScope>
{
new ApiScope("OAAPI","OA辦公平臺API。"),
new ApiScope("ERPAPI","ERP平臺API。")
};
}
/// <summary>
/// 配置可從IDS4認證中心獲取授權碼和令牌的客戶端。
/// </summary>
/// <returns></returns>
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
//客戶端ID。
ClientId="MvcApp",
//客戶端名稱。
ClientName="MvcApplication",
//客戶端密鑰。
ClientSecrets =
{
new Secret("MvcAppOA00000001".Sha256())
},
//Code表示授權碼認證模式。
AllowedGrantTypes=GrantTypes.Code,
//是否支持授權操作頁面,true表示顯示授權界面,否則不顯示。
RequireConsent=true,
//認證成功之後重定向的客戶端地址,默認就是signin-oidc。
RedirectUris={ "https://localhost:6003/signin-oidc"},
//登出時重定向的地址,默認是siginout-oidc。
PostLogoutRedirectUris={"https://localhost:6003/signout-callback-oidc"},
//是否允許返回刷新Token。
AllowOfflineAccess=true,
//指定客戶端獲取的AccessToken能訪問到的API作用域。
AllowedScopes={
//API作用域。
"OAAPI",
//OpenId身份信息權限。
IdentityServerConstants.StandardScopes.OpenId,
//Profile身份信息權限。
IdentityServerConstants.StandardScopes.Profile
}
}
};
}
/// <summary>
/// 配置賬戶,用於登錄。
/// </summary>
/// <returns></returns>
public static List<TestUser> GetTestUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId="00001",
Username="kevin",
Password="123456",
Claims =
{
new Claim(JwtClaimTypes.Name, "kevin"),
new Claim(JwtClaimTypes.GivenName, "kevin"),
new Claim(JwtClaimTypes.FamilyName, "Li"),
new Claim(JwtClaimTypes.Email, "[email protected]"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean)
}
}
};
}
}
}
創建資源Api項目
創建資源Api項目,打開 VS,新建 AspNetCore WebApi 項目,名爲: Dotnet.WebApi.Ids4.Api
添加依賴包:
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.3" />
添加Api
添加 Controllers/WeatherForecastController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Dotnet.WebApi.Ids4.Api.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[Authorize("OAScope")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = new DateTime(2060, 9, 23).AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}
添加認證方案
修改 Program.cs 類爲如下代碼:
using Microsoft.IdentityModel.Tokens;
namespace Dotnet.WebApi.Ids4.Api
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
//註冊認證組件並配置Bearer
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
//認證服務器地址
options.Authority = "https://localhost:6001";
//在驗證token時,不驗證Audience
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
//配置策略授權
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("OAScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "OAAPI");
});
});
var app = builder.Build();
//設置端口號
app.Urls.Add("https://*:6002");
app.UseHttpsRedirection();
//認證中間件
app.UseAuthentication();
//授權中間件
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
客戶端項目
創建資源Api項目,打開 VS,新建 AspNetCore MVC 項目,名爲: Dotnet.WebApi.Ids4.MvcApp。
添加依賴包
添加依賴包
<PackageReference Include="IdentityModel" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
Program.cs
修改 Program.cs 代碼爲如下代碼:
using Microsoft.AspNetCore.Authentication.Cookies;
using System.IdentityModel.Tokens.Jwt;
namespace Dotnet.WebApi.Ids4.MvcApp
{
public class Program
{
public static void Main(string[] args)
{
Console.Title = "MVC客戶端";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
//去除映射,保留Jwt原有的Claim名稱
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
builder.Services.AddAuthentication(options => {
//使用Cookies
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
//使用OpenID Connect
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
//客戶端ID
options.ClientId = "MvcApp";
//客戶端密鑰
options.ClientSecret = "MvcAppOA00000001";
//IdentityServer4服務器地址
options.Authority = "https://localhost:6001";
//響應授權碼
options.ResponseType = "code";
//允許Token保存的Cookies中
options.SaveTokens = true;
//權限範圍
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
//設置允許獲取刷新Token
options.Scope.Add("offline_access");
//設置訪問的API範圍
options.Scope.Add("OAAPI");
//獲取用戶的Claims信息
options.GetClaimsFromUserInfoEndpoint = true;
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
//發佈後的端口號
app.Urls.Add("https://*:6003");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
//Cookie策略
app.UseCookiePolicy();
//身份驗證
app.UseAuthentication();
//授權。
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
}
}
}
調用Api
在 Controllers/HomeController.cs 中添加如下代碼:
using Dotnet.WebApi.Ids4.MvcApp.Models;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Diagnostics;
namespace Dotnet.WebApi.Ids4.MvcApp.Controllers
{
[Authorize]
public class HomeController : Controller
{
......
/// <summary>
/// 獲取API資源。
/// </summary>
/// <returns></returns>
public async Task<IActionResult> ApiData()
{
//獲取accessToken
var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
//請求API資源
var httpClient = new HttpClient();
//將獲取到的AccessToken以Bearer的方案設置在請求頭中
httpClient.SetBearerToken(accessToken);
//向API資源服務器請求受保護的API
var data = await httpClient.GetAsync("https://localhost:6002/api/WeatherForecast");
if (data.IsSuccessStatusCode)
{
var r = await data.Content.ReadAsStringAsync();
ViewBag.ApiData = r;
}
else
{
ViewBag.ApiData = $"獲取API數據失敗。狀態碼:{data.StatusCode}";
}
return View();
}
}
}