Blazor WebAssembly+Duende.IdentityServer+EF Core认证授权企业级实战
Posted JimCarter
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Blazor WebAssembly+Duende.IdentityServer+EF Core认证授权企业级实战相关的知识,希望对你有一定的参考价值。
文章目录
1. 前言
本文将从0开始介绍如何搭建一个适用于Blazor WASM应用的且基于OpenID和OAuth2.0的认证授权服务。我们会从创建空白项目一步一步开始,让大家了解到整个搭建流程,没有直接使用微软给定的认证模板或者IdentityServer的UI模板。
- 前端使用的是Blazor WebAssembly最新版本(基于 .net 5.0),主要我认为相对于Blazor Server来说,这个才是Web的未来。而且可以充分开会客户机的性能。
- 认证服务使用的Duende.IdentityServer 5.0版本。至于为什么没有使用现在常用的IdentityServer4呢?主要因为Duende.IdentityServer是IdentityServer4的后续版本,本质是同一个产品,会长期更新,虽然对于商业使用是需要授权的。但是对于一般的测试开发免费。IdentityServer4到2022年5月份就停止支持了,后续怎么发展不得而知。
- 数据存储使用的是目前的最新版的EF Core 5.0
至于为什么起名为企业级实战?在开始写这篇博文之前,在互联网上经历一番搜索后发现,大多数文章介绍的都很笼统,要么是基于官方的template解读(比如Blazor+Asp.net core Identity),要么还是用的一些测试配置(比如用的IdentityServer的TestUser,InMemoryClients等)。并没有说如何基于我们已有应用进行改造,并没有说生产上如何实践。
基于以上种种,所以就有了这篇文章。本文可能没有覆盖到你所使用场景的方方面面,欢迎在评论区交流探讨。
2. 搭建IdentityServer服务
2.1 安装dotnet命令行工具
安装IdentityServer的dotnet工具dotnet new --install Duende.IdentityServer.Templates
,接下我们会用这个工具添加一些默认UI
2.2 创建ASP.NET Core Web MVC项目:
创建一个ASP.NET Core Web MVC项目,启动端口设置为:5000。
然后在项目的根目录下执行dotnet new isui --force
命令,添加所需要的控制器和视图,执行完之后可以在右侧的解决方案中看到新添加的controll和view:
此时可以移除原有Controller->HomeControll.cs了,因为在Quickstart里已经有了。
2.3 配置IdentityServer
因为我们这次不整IdentityServer的TestUser,InMemoryClients练习数据的那一套,这些配置我们都需要从数据库读取。ORM我们选择使用EF Core,所以需要安装以下NuGet包:
- Duende.IdentityServer.EntityFramework:提供认证授权服务和数据存储功能
- Microsoft.EntityFrameworkCore.Tools:用来更新数据库结构
- Microsoft.EntityFrameworkCore.SqlServer:这里使用SqlServer数据库
安装后如下:
2.3.1 配置数据库连接字符串
"Logging":
"LogLevel":
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
,
"AllowedHosts": "*",
"ConnectionStrings":
"DemoMain": "Server=.,1433;Database=Demo.Main;Uid=xxx;Pwd=xxxx;",
"DemoIds": "Server=.,1433;Database=Demo.IdentityServer;Uid=xx;Pwd=xxxx;"
这里我们配置了两个数据库字符串DemoMain
和DemoIds
,一个是业务数据库,一个是IdentityServer的数据库(存放Ids认证和授权用到的一些东西)。
然后创建一个DemoMainDbContext
数据库上下文,以后会通过这个上下文验证用户密码。
public class SysUser
public long Id get; set;
public string UserName get; set;
public string Password get; set;
public string Email get; set;
public class DemoMainDbContext : DbContext
public DemoMainDbContext(DbContextOptions<DbContextOptions<DemoMainDbContext>> options) : base(options)
this.Database.EnsureCreated();
public DbSet<SysUser> Users get; set;
protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<SysUser>().ToTable("SysUser");
2.3.2 注册服务与配置管道
主要是以下几点:
- 注册
DemoMainDbContext
的上下文 - 注册Cors:因为我们的解决方案将来会有多个应用,其他应用访问Ids的话会涉及到跨域问题。这里配置跨域的代码依自身需求灵活变通即可。
- 注册IdentityServer:
AddConfigurationStore
用来配置ConfigurationDbContext
上下文,存储那些受保护的ApiScope
、IdentityResource
、Client
。AddOperationalStore
用来配置PersistedGrantDbContext
上下文,存储请求的token。 - 配置Cookie管道:
- 配置Cors:
- 配置IdentityServer:
Startup.cs
完整代码参考如下:
public void ConfigureServices(IServiceCollection services)
var conStrMain = Configuration.GetConnectionString("DemoMain");
var conStrIds = Configuration.GetConnectionString("DemoIds");
//注册数据库上下文
services.AddDbContext<DemoMainDbContext>(opt => opt.UseSqlServer(conStrMain));
//注册跨域
services.AddCors(opt =>
opt.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
);
//注册IdentityServer
var migrationAsm = typeof(Startup).Assembly.GetName().Name;
services.AddIdentityServer(opt =>
opt.Authentication.CookieSameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
)
.AddConfigurationStore(opt =>
opt.ConfigureDbContext = b => b.UseSqlServer(conStrIds, sql => sql.MigrationsAssembly(migrationAsm));
)
.AddOperationalStore(opt =>
opt.ConfigureDbContext = b => b.UseSqlServer(conStrIds, sql => sql.MigrationsAssembly(migrationAsm));
);
services.AddControllersWithViews();
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
else
app.UseExceptionHandler("/Home/Error");
app.UseStaticFiles();
//配置Cookie策略
app.UseCookiePolicy(new CookiePolicyOptions
MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.Lax
);
app.UseRouting();
//Use跨域
app.UseCors();
//Use Ids
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
endpoints.MapControllerRoute(
name: "default",
pattern: "controller=Home/action=Index/id?");
);
到这里IdentityServer的配置基本已经完成,但是我们还是没法运行,因为没有测试数据也没有对应的IdentityServer数据库架构。
2.3.3 配置测试数据
新建一个IdsConfig.cs
类,添加以下方法:
//配置要保护的身份资源
public static List<IdentityResource> IdentityResources => new List<IdentityResource>()
new IdentityResources.OpenId(),//必须有
new IdentityResources.Profile()
;
//配置要保护的API资源
public static List<ApiScope> ApiScopes => new List<ApiScope>
new ApiScope
Name="Classified",
DisplayName="机密资源"
;
//配置请求端
public static List<Client> Clients => new List<Client>
new Client
ClientId="blazor_code2",
AllowedGrantTypes=GrantTypes.Code,
AllowedScopes=
"Classified",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
,
RedirectUris=
//如果请求过来的redirectUri在这个列表里,则会允许跳转。
"http://localhost:7000/authentication/login-callback",
,
PostLogoutRedirectUris=
//同上
"http://localhost:7000/authentication/logout-callback",
,
RequireClientSecret=false,//不需要设置secret
AllowedCorsOrigins= "http://localhost:7000"//不需要跨域的话,可以不用这么设置
,
;
以上的配置数据我们仅用来初始化数据库。
如上所看到的,我们添加了一个名为blazor_code2
的client,并设置了RedirectUris
和PostLogoutRedirectUris
,用来登录成功之后和退出之后进行跳转。
这里的localhost:7000
是我们接下来要创建的Blazor WebAssembly应用。因为Blazor WebAssembly属于客户端应用,所有的代码都保留在用户电脑里,而客户是不可信的,所以我们这里不需要设置ClientSecret
和AccessToken
,因为没法保证它们的安全。
2.3.4 数据初始化
接下来我们在IdsConfig.cs
里添加一个SeedData
方法,用来初始化上一步的配置数据:
public static void SeedData(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();
//设置Clients
context.Clients.RemoveRange(context.Clients.ToArray());//清空之前的,方便调试
IdsConfig.Clients.ForEach(c => context.Clients.Add(c.ToEntity()));
context.SaveChanges();
//设置IdentityResource
if (!context.IdentityResources.Any())
IdsConfig.IdentityResources.ForEach(r => context.IdentityResources.Add(r.ToEntity()));
context.SaveChanges();
//设置ApiScope
if (!context.ApiScopes.Any())
IdsConfig.ApiScopes.ForEach(a => context.ApiScopes.Add(a.ToEntity()));
context.SaveChanges();
//给数据库加一个测试用户
var mainContext = serviceScope.ServiceProvider.GetRequiredService<DemoMainDbContext>();
if(!mainContext.Users.Any())
mainContext.Users.Add(new SysUser
UserName = "admin",
Password = "1234",
Email = "aa@aa.com"
);
mainContext.SaveChanges();
然后在Stratup.cs
的Configure
方法,调用SeedData
:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
IdsConfig.SeedData(app);
//...
2.3.5 数据库迁移
打开PMC(Package Management Console),执行以下命令创建迁移记录:
Add-Migration InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb
Add-Migration InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb
当应用运行起来,就会执行迁移记录。
3. 创建WebApi应用
IdentityServer的创建告一段落了,接下来我们创建一个受IdentityServer保护的WebApi应用。
启动端口设置为:6001。
3.1 安装NuGet包
首先安装NuGet包:Microsoft.AspNetCore.Authentication.JwtBearer,用来验证请求资源时传进来的token。
3.2 配置Startup.cs
示例代码如下:
public void ConfigureServices(IServiceCollection services)
services.AddCors(opt =>
opt.AddDefaultPolicy(builder =>
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
);
);
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", opt =>
opt.Authority = "http://localhost:5000";//IdentityServer的地址
opt.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
ValidateAudience = false//验证token是否来自trusted issuer, 大多数情况下都不需要验证。
;
opt.RequireHttpsMetadata = false;//我们没有使用https,所以这里disable。
);
services.AddAuthorization(opt =>
opt.AddPolicy("Policy1", builder =>
//首先需要通过验证
builder.RequireAuthenticatedUser();
//其次需要有Classified的scope
builder.RequireClaim("scope", "Classified");
);
);
services.AddControllers();
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseCors();//添加跨域
app.UseAuthentication();//添加认证
app.UseAuthorization();
app.UseEndpoints(endpoints =>
endpoints.MapControllers();
);
主要工作是以下几点:
- 因为我们需要跨域,所以在服务和请求管道里配置了Cors。
- 配置了基于
Bearer
的token验证,拿到token之后会到localhost:5000
上进行验证,也就是我们上一步创建的IdentityServer应用。 - 然后配置另一个基于策略的授权
Policy1
,这个策略表示需要包含名为Classified
的scope
3.3 保护控制器
在控制器上应用[Authorize]
特性,保护控制器:
namespace Demo.ApiResource.Controllers
[ApiController]
[Route("[controller]")]
[Authorize("Policy1")]
public class WeatherForecastController : ControllerBase
//...
到此WebApi的配置结束。
4. 创建Blazor WebAssembly应用
在创建Blazor WASM应用之前,建议先看下这篇文章:Blazor WebAssembly身份认证与授权,对Blazor的认证组件有个大致了解。
然后我们创建一个用ASP.NET Core 作为Host的Blazor WebAssembly应用,认证类型里选中Individual Accounts
,然后勾上ASP.NET Core hosted:
启动端口设置为7000
4.1 Host端配置
创建好项目之后,Host端的这些东西我们暂时不用可以都删除,简简单单的让它只做为一个Host就够了:
然后修改下Startup.cs
把不要的代码也注释掉,清清爽爽:
public void ConfigureServices(IServiceCollection services)
//services.AddDbContext<ApplicationDbContext>(options =>
// options.UseSqlServer(
// Configuration.GetConnectionString("DefaultConnection")));
//services.AddDatabaseDeveloperPageExceptionFilter();
//services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
// .AddEntityFrameworkStores<ApplicationDbContext>();
//services.AddIdentityServer()
// .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
//services.AddAuthentication()
// .AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddRazorPages();
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
else
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
//app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
//app.UseIdentityServer();
//app.UseAuthentication();
//app.UseAuthorization();
app.UseEndpoints(endpoints =>
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
);
4.2 Client端配置
因为需要用到一些认证组件,所以需要首先在Client端安装NuGet包:Microsoft.AspNetCore.Components.WebAssembly.Authentication。
接下来配置Program.cs
:
public static async Task Main(string[] args)
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builderBlazor WASM+Duende.IdentityServer+EF Core认证授权企业级实战
Blazor WASM+Duende.IdentityServer+EF Core认证授权企业级实战