IdentityServer4: 配置项持久化
IdentityServer4 配置项持久化
对于 IdentityServer4 认证和授权框架,是支持数据库持久化操作的,也就是在 IdentityServer4 服务器上需要配置的一些数据存储到数据库中永久存储.
在 IdentityServer4 服务器上,配置的数据有:客户端、API 作用域等,之前我们都是存储在内存中进行操作,这里,我们将这些数据存储到数据库中。IdentityServer4 支持的持久存储体有 Redis、SQLServer、
Oracle、MySql 等。
注意:这里永久存储的数据不包括用户信息,用户信息需要使用ASP.NET Core Identity 认证框架来实现。
创建 IdentityServer4 项目
打开 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
下载 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
查看迁移结果
迁移成功后,数据库表会新增相关的数据表,如下图所示:
其中,除了 PersistedGrants
和 DeviceCodes
这两张表来自 ``PersistedGrantDbContext外,其它表都来自
ConfigurationDbContext`
生成初始化数据
在 Program 类中添加如下代码,初始化 Client 数据:
//同步数据
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, 修改方法为编辑项目文件里面的: <TargetFramework>net7.0</TargetFramework>
改为: <TargetFramework>net6.0</TargetFramework>
集成 IdentityServer4
集成 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。")
};
}
/// <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, "kevin@dotnet.com"),
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();
}
}
}
添加视图: Views/Home/ApiData.cshtml
<p>
@ViewBag.ApiData
</p>