blazorwasm+duende.identityserver+efcore认证授权企业级实战(代码片段)

JimCarter JimCarter     2023-01-06     206

关键词:

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;"
  

这里我们配置了两个数据库字符串DemoMainDemoIds,一个是业务数据库,一个是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上下文,存储那些受保护的ApiScopeIdentityResourceClientAddOperationalStore用来配置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,并设置了RedirectUrisPostLogoutRedirectUris,用来登录成功之后和退出之后进行跳转。
这里的localhost:7000是我们接下来要创建的Blazor WebAssembly应用。因为Blazor WebAssembly属于客户端应用,所有的代码都保留在用户电脑里,而客户是不可信的,所以我们这里不需要设置ClientSecretAccessToken,因为没法保证它们的安全。


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.csConfigure方法,调用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();
    );

主要工作是以下几点:

  1. 因为我们需要跨域,所以在服务和请求管道里配置了Cors。
  2. 配置了基于Bearer的token验证,拿到token之后会到localhost:5000上进行验证,也就是我们上一步创建的IdentityServer应用。
  3. 然后配置另一个基于策略的授权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);
    builder查看详情  

Blazor WASM - IIS 上的独立部署

】BlazorWASM-IIS上的独立部署【英文标题】:BlazorWASM-StandaloneDeploymentonIIS【发布时间】:2021-02-0904:13:20【问题描述】:我正在尝试按照here的说明在IIS上部署BlazorWASM应用。这只是开箱即用的示例BlazorWASM应用我已将应用发布到文件夹... 查看详情

Blazor WASM 在线/活跃用户

】BlazorWASM在线/活跃用户【英文标题】:BlazorWASMonline/activeusers【发布时间】:2021-11-1617:34:45【问题描述】:我正在使用BlazorWASM+Asp.NETAPI开发出租车管理应用程序,我想知道我的哪些驱动程序在线/可用,因此我需要了解连接状态... 查看详情

.NET 4.7 MVC 中的 Blazor WASM

】.NET4.7MVC中的BlazorWASM【英文标题】:BlazorWASMin.NET4.7MVC【发布时间】:2021-11-0906:17:25【问题描述】:是否可以将BlazorWASM组件放入.NET4.7上的旧MVC应用程序中?可以将BlazorWASM组件导出为类似于Angular中的自定义元素吗?目前,如果... 查看详情

Blazor WASM - 流畅的验证

】BlazorWASM-流畅的验证【英文标题】:BlazorWASM-FluentValidation【发布时间】:2021-09-1719:49:24【问题描述】:我在自定义验证器中创建了以下规则:publicclassAddInvoiceParameterCommandValidator:AbstractValidator<AddInvoiceParameterCommand>publicAddInvoi... 查看详情

Blazor Wasm 身份验证

】BlazorWasm身份验证【英文标题】:BlazorWasmAuthentication【发布时间】:2021-11-0515:24:37【问题描述】:我正在尝试构建一个blazorwasm应用程序并尝试使用Jwt令牌进行身份验证。我面临一个有趣的问题。当我登录时,我无法访问具有[Aut... 查看详情

Blazor WASM 没有达到断点

】BlazorWASM没有达到断点【英文标题】:BlazorWASMdoesn\'thitbreakpoint【发布时间】:2020-08-1115:00:27【问题描述】:我有一个带有版本5程序集的BlazorWASM项目,并尝试根据这篇文章激活调试:https://docs.microsoft.com/en-us/aspnet/core/blazor/debug?v... 查看详情

Blazor WASM 中的 MudSelect 问题

】BlazorWASM中的MudSelect问题【英文标题】:MudSelectIssueinBlazorWASM【发布时间】:2021-04-2211:53:17【问题描述】:我是BlazorWASM的新手,我正在开发一个使用MudBlazor的项目。但是,我对MudSelect有疑问。我想在MudSelect中显示部门名称。但... 查看详情

Blazor Wasm 身份登录/注销事件

】BlazorWasm身份登录/注销事件【英文标题】:BlazorWasmIdentityLogin/Logoutevent【发布时间】:2021-06-0815:06:54【问题描述】:在使用来自Wasm模板的标准身份的.Net5BlazorWASM应用程序中,我想在用户登录(或注销)时执行一些操作。我已经... 查看详情

Css 的 Blazor WASM 问题

】Css的BlazorWASM问题【英文标题】:BlazorWASMproblemswithcss【发布时间】:2022-01-1118:22:32【问题描述】:由于某种原因,我的BlazorWASM项目无法正确渲染视频文件。当我运行我的项目时,视频标签的CSS不适用。这是我的视频html标签:&l... 查看详情

Blazor WASM 路由发布请求到 index.html

】BlazorWASM路由发布请求到index.html【英文标题】:BlazorWASMRoutePostRequesttoindex.html【发布时间】:2021-12-0622:38:00【问题描述】:我有一个使用VisualStudio模板设置的BlazorWASM应用。我在Server项目的Startup.cs文件中的路由如下所示:app.UseBl... 查看详情

Blazor WASM - 如何在布局内更改正文类动态

】BlazorWASM-如何在布局内更改正文类动态【英文标题】:BlazorWASM-howtochangebodyclassdynamicinsideLayout【发布时间】:2020-11-1807:07:28【问题描述】:我有一个Blazor应用程序。在我的Index.html(wwwroot)中,我有以下正文:<bodyclass="vertical-layo... 查看详情

如何更改 blazor wasm 应用程序的基本 URL

】如何更改blazorwasm应用程序的基本URL【英文标题】:HowtochangethebaseURLofablazorwasmapp【发布时间】:2021-06-2523:06:33【问题描述】:为blazorwasm托管应用程序生成的模板具有从“/”开始的基本URL(即,用于本地开发的https://localhost:5001/... 查看详情

调试 Blazor WASM(Aspnet 托管)时无法使用热重载

】调试BlazorWASM(Aspnet托管)时无法使用热重载【英文标题】:UnabletouseHotReloadwhiledebuggingBlazorWASM(Aspnethosted)【发布时间】:2021-12-1722:10:53【问题描述】:如果我创建一个新的BlazorWASM应用程序,我可以通过在终端窗口中运行dotnetwat... 查看详情

如何绕过 blazor wasm 路由系统并调用服务器

】如何绕过blazorwasm路由系统并调用服务器【英文标题】:Howtobypassblazorwasmroutingsystemandmakecallstotheserver【发布时间】:2021-06-0214:38:20【问题描述】:我有一个blazorwasm托管应用程序,用户可以通过填写表格来创建帐户。创建帐户后... 查看详情

使用 IIS URL 重写动态设置 Blazor WASM 基本 href

】使用IISURL重写动态设置BlazorWASM基本href【英文标题】:SetBlazorWASMbasehrefdynamicallywithIISURLRewrite【发布时间】:2021-12-1705:21:10【问题描述】:我有一个使用标准模板创建的BlazorWASM应用程序。在开发过程中,我一直在根目录下测试... 查看详情

在 Azure Function 中接收 Blazor wasm 发送的身份验证令牌

】在AzureFunction中接收Blazorwasm发送的身份验证令牌【英文标题】:ReceiveauthenticationtokeninAzureFunctionsentbyBlazorwasm【发布时间】:2020-10-1502:56:17【问题描述】:我正在使用AzureFunctions作为API开发Blazorwasm。我已经可以在客户端应用程序... 查看详情

升级到 .net 6 时托管的 Blazor WASM 身份验证中断

】升级到.net6时托管的BlazorWASM身份验证中断【英文标题】:BlazorWASMHostedwithAuthenticationBreakswhenupgradingto.net6【发布时间】:2021-12-2718:40:01【问题描述】:我有一个使用用户身份验证托管的BlazorWASM,它正在运行.net5,我已升级到.net6... 查看详情