Net 8 Blazor Web App项目访问Identity Server 4
Identity Server系列目录
- Blazor Server访问Identity Server 4单点登录 - SunnyTrudeau - 博客园 (cnblogs.com)
- Blazor Server访问Identity Server 4单点登录2-集成Asp.Net角色 - SunnyTrudeau - 博客园 (cnblogs.com)
- Blazor Server访问Identity Server 4-手机验证码登录 - SunnyTrudeau - 博客园 (cnblogs.com)
- Blazor MAUI客户端访问Identity Server登录 - SunnyTrudeau - 博客园 (cnblogs.com)
- 在Identity Server 4项目集成Blazor组件 - SunnyTrudeau - 博客园 (cnblogs.com)
- Identity Server 4退出登录自动跳转返回 - SunnyTrudeau - 博客园 (cnblogs.com)
- Identity Server通过ProfileService返回用户角色 - SunnyTrudeau - 博客园 (cnblogs.com)
- Identity Server 4返回自定义用户Claim - SunnyTrudeau - 博客园 (cnblogs.com)
- Blazor Server获取Token访问外部Web Api - SunnyTrudeau - 博客园 (cnblogs.com)
- Blazor Server通过RefreshToken更新AccessToken - SunnyTrudeau - 博客园 (cnblogs.com)
- Blazor WebAssembly项目访问Identity Server 4 - SunnyTrudeau - 博客园 (cnblogs.com)
.Net 8发布了新的Blazor WebApp项目模板,支持服务端和客户端混合呈现。初步使用了一下,感觉是把Blazor Server和Blazor WebAssembly合二为一了。使用Blazor WebApp项目模板有一个好处,可以先用服务端razor页面调用service的方法快速实现功能,验证需求,如果后期在线客户端数量多给服务端造成压力过大,可以再把部分razor页面改为在Web Assembly客户端运行,通过HttpClient访问服务端Web Api。
Blazor Server和Blazor WebAssembly项目访问Identity Server 4的方法是不一样的。为了研究Blazor WebApp项目的认证方法,新建了一个带有身份验证的Blazor WebApp项目,选择Auto混合呈现模式。该项目的服务端内置了一套认证方案代码,非常值得学习,有些代码和文件可以复制后改一下用于oidc方案。经过研究和测试,确定Blazor WebApp的方法跟Blazor Server是一致的。
下文基于已有的Identity Server4解决方案https://gitee.com/woodsun/blzid4增加项目,修改代码。
Identity Server 4服务端增加项目配置
AspNetId4Web项目增加Blazor WebApp项目的客户端配置,同时对比一下Blazor Server和Blazor WebAssembly项目的配置。
D:\Software\gitee\blzid4\BlzId4Web\AspNetId4Web\Config.cs
// Blazor WebAssembly客户端 new Client { ClientId = "WebAssemblyOidc", ClientName = "WebAssemblyOidc", RequireClientSecret = false, AllowedGrantTypes = GrantTypes.Code, AllowedScopes ={ "openid", "profile", "scope1", }, //网页客户端运行时的URL AllowedCorsOrigins = { "https://localhost:5801", }, //登录成功之后将要跳转的网页客户端的URL RedirectUris = { "https://localhost:5801/authentication/login-callback", }, //退出登录之后将要跳转的网页客户端的URL PostLogoutRedirectUris = { "https://localhost:5801", }, }, // Blazor Server客户端 new Client() { ClientId = "BlazorServerOidc", ClientName = "BlazorServerOidc", ClientSecrets = new []{ new Secret("BlazorServerOidc.Secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, AllowedCorsOrigins = { "https://localhost:5501" }, RedirectUris = { "https://localhost:5501/signin-oidc" }, PostLogoutRedirectUris = { "https://localhost:5501/signout-callback-oidc" }, //效果等同客户端项目配置options.GetClaimsFromUserInfoEndpoint = true //AlwaysIncludeUserClaimsInIdToken = true, //AllowedScopes = { "openid", "profile", "scope1", "role", } //通过ProfileService返回用户角色 AllowedScopes = { "openid", "profile", "scope1", }, //如果要获取refresh_tokens ,必须把AllowOfflineAccess设置为true AllowOfflineAccess = true, //AccessToken有效期,默认1小时,改为1分钟做试验 AccessTokenLifetime = 60, }, // Blazor WebApp客户端 new Client() { ClientId = "BlazorWebAppOidc", ClientName = "BlazorWebAppOidc", ClientSecrets = new []{ new Secret("BlazorWebAppOidc.Secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, AllowedCorsOrigins = { "https://localhost:5581" }, RedirectUris = { "https://localhost:5581/signin-oidc" }, PostLogoutRedirectUris = { "https://localhost:5581/signout-callback-oidc" }, //效果等同客户端项目配置options.GetClaimsFromUserInfoEndpoint = true //AlwaysIncludeUserClaimsInIdToken = true, //AllowedScopes = { "openid", "profile", "scope1", "role", } //通过ProfileService返回用户角色 AllowedScopes = { "openid", "profile", "scope1", }, //如果要获取refresh_tokens ,必须把AllowOfflineAccess设置为true AllowOfflineAccess = true, //AccessToken有效期,默认1小时,改为1分钟做试验 AccessTokenLifetime = 60, },
创建Blazor WebApp项目
在解决方案添加Blazor WebApp项目WebAppOidc,身份验证类型=无,呈现模式=Auto(Server and WebAssemby),interactive location=per page/component(注意不要选global,否则很多文件位置会有变化)。
修改WebAppOidc.Client客户端项目
WebAppOidc.Client客户端项目NuGet安装id4认证相关的库。
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc.Client\WebAppOidc.Client.csproj
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.0" />
增加用户信息类
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc.Client\UserInfo.cs
namespace WebAppOidc.Client; // Add properties to this class and update the server and client AuthenticationStateProviders // to expose more information about the authenticated user to the client. public class UserInfo { public required string UserId { get; set; } public required string Email { get; set; } public required string Name { get; set; } public required string PhoneNumber { get; set; } public required string Nation { get; set; } public required string Roles { get; set; } }
增加认证状态管理者PersistentAuthenticationStateProvider,这个类是从带有身份验证的Blazor WebApp客户端项目同名文件改过来的。
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc.Client\PersistentAuthenticationStateProvider.cs
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using System.Security.Claims; namespace WebAppOidc.Client; // This is a client-side AuthenticationStateProvider that determines the user's authentication state by // looking for data persisted in the page when it was rendered on the server. This authentication state will // be fixed for the lifetime of the WebAssembly application. So, if the user needs to log in or out, a full // page reload is required. // // This only provides a user name and email for display purposes. It does not actually include any tokens // that authenticate to the server when making subsequent requests. That works separately using a // cookie that will be included on HttpClient requests to the server. internal class PersistentAuthenticationStateProvider : AuthenticationStateProvider { private static readonly Task<AuthenticationState> defaultUnauthenticatedTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); private readonly Task<AuthenticationState> authenticationStateTask = defaultUnauthenticatedTask; public PersistentAuthenticationStateProvider(PersistentComponentState state) { if (!state.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null) { return; } Claim[] claims = [ new Claim(ClaimTypes.NameIdentifier, userInfo.UserId), //new Claim(ClaimTypes.Name, userInfo.Email), new Claim(ClaimTypes.Email, userInfo.Email), new Claim(ClaimTypes.Name, userInfo.Name), new Claim(ClaimTypes.MobilePhone, userInfo.PhoneNumber), new Claim("nation", userInfo.Nation), new Claim(ClaimTypes.Role, userInfo.Roles) ]; authenticationStateTask = Task.FromResult( new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(PersistentAuthenticationStateProvider))))); } public override Task<AuthenticationState> GetAuthenticationStateAsync() => authenticationStateTask; }
增加登录跳转razor组件,从带有身份验证的Balzor WebApp客户端项目复制过来。
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc.Client\RedirectToLogin.razor
@inject NavigationManager NavigationManager @code { protected override void OnInitialized() { NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true); } }
Program添加oidc认证的代码,从带有身份验证的Balzor WebApp客户端项目复制过来。
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc.Client\Program.cs
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; namespace WebAppOidc.Client; internal class Program { static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); //参考带有身份验证的Blazor WebApp客户端项目 builder.Services.AddAuthorizationCore(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>(); await builder.Build().RunAsync(); } }
WebAppOidc服务端项目
WebAppOidc服务端项目NuGet安装id4认证相关的库。
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\WebAppOidc.csproj
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.0" />
<PackageReference Include="IdentityModel" Version="5.2.0" />
添加oidc认证的代码,有些代码可以从之前的Blazor Server项目复制过来,有的从带有身份验证的Balzor WebApp服务端项目复制过来。
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\Program.cs
using IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Components.Authorization; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using WebAppOidc.Client.Pages; using WebAppOidc.Components; namespace WebAppOidc; public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); //参考带有身份验证的Blazor WebApp服务端项目 builder.Services.AddCascadingAuthenticationState(); builder.Services.AddScoped<AuthenticationStateProvider, PersistingRevalidatingAuthenticationStateProvider>(); builder.Services.AddAuthorization(); //参考Blazor Server项目 //添加认证相关的服务 ConfigureAuthServices(builder.Services); builder.Services.AddControllers(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { 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.UseStaticFiles(); app.UseAntiforgery(); //参考Blazor Server项目 //添加认证与授权中间件 app.UseAuthentication(); app.UseAuthorization(); app.MapRazorComponents<App>() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(Client._Imports).Assembly); app.MapControllers(); app.Run(); } //添加认证相关的服务 private static void ConfigureAuthServices(IServiceCollection services) { services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); //清除微软定义的clamis JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //默认采用cookie认证方案,添加oidc认证方案 services.AddAuthentication(options => { options.DefaultScheme = "cookies"; options.DefaultChallengeScheme = "oidc"; }) //配置cookie认证 .AddCookie("cookies", options => { // Cookie settings options.Cookie.HttpOnly = true; //options.ExpireTimeSpan = TimeSpan.FromMinutes(2); //OptionsValidationException: Cookie.Expiration is ignored, use ExpireTimeSpan instead. //options.Cookie.Expiration = TimeSpan.FromMinutes(3); //options.LoginPath = "/Account/Login"; //options.AccessDeniedPath = "/Account/AccessDenied"; //options.SlidingExpiration = true; }) .AddOpenIdConnect("oidc", options => { //id4服务的地址 options.Authority = "https://localhost:5001"; //id4配置的ClientId以及ClientSecrets options.ClientId = "BlazorWebAppOidc"; options.ClientSecret = "BlazorWebAppOidc.Secret"; //认证模式 options.ResponseType = "code"; //保存token到本地 options.SaveTokens = true; //很重要,指定从Identity Server的UserInfo地址来取Claim //效果等同id4配置AlwaysIncludeUserClaimsInIdToken = true options.GetClaimsFromUserInfoEndpoint = true; //指定要取哪些资料(除Profile之外,Profile是默认包含的) options.Scope.Add("scope1"); //获取RefreshToken options.Scope.Add("offline_access"); //通过ProfileService返回用户角色 //options.Scope.Add("role"); //映射自定义用户声明 options.ClaimActions.MapJsonKey(JwtClaimTypes.PhoneNumber, JwtClaimTypes.PhoneNumber); options.ClaimActions.MapJsonKey("nation", "nation"); //这里是个ClaimType的转换,Identity Server的ClaimType和Blazor中间件使用的名称有区别,需要统一。 //User.Identity.Name=JwtClaimTypes.Name options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; options.Events.OnUserInformationReceived = (context) => { //id4返回的角色是字符串数组或者字符串,blazor server的角色是字符串,需要转换,不然无法获取到角色 ClaimsIdentity claimsId = context.Principal.Identity as ClaimsIdentity; var roleElement = context.User.RootElement.GetProperty(JwtClaimTypes.Role); if (roleElement.ValueKind == System.Text.Json.JsonValueKind.Array) { var roles = roleElement.EnumerateArray().Select(e => e.ToString()); claimsId.AddClaims(roles.Select(r => new Claim(JwtClaimTypes.Role, r))); } else { claimsId.AddClaim(new Claim(JwtClaimTypes.Role, roleElement.ToString())); } return Task.CompletedTask; }; //设置登录状态有效期,模拟token失效后需要频繁登录 //options.MaxAge = TimeSpan.FromMinutes(3); }); } }
增加认证状态管理者PersistingRevalidatingAuthenticationStateProvider,这个类是从带有身份验证的Blazor WebApp服务端项目同名文件改过来的。
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\PersistingServerAuthenticationStateProvider.cs
using WebAppOidc.Client; using IdentityModel; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using System.Diagnostics; namespace WebAppOidc; // This is a server-side AuthenticationStateProvider that uses PersistentComponentState to flow the // authentication state to the client which is then fixed for the lifetime of the WebAssembly application. internal sealed class PersistingRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider, IDisposable { private readonly PersistentComponentState state; private readonly IdentityOptions options; private readonly PersistingComponentStateSubscription subscription; private Task<AuthenticationState>? authenticationStateTask; public PersistingRevalidatingAuthenticationStateProvider( ILoggerFactory loggerFactory, PersistentComponentState persistentComponentState, IOptions<IdentityOptions> optionsAccessor) : base(loggerFactory) { state = persistentComponentState; options = optionsAccessor.Value; AuthenticationStateChanged += OnAuthenticationStateChanged; subscription = state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); } protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); protected override async Task<bool> ValidateAuthenticationStateAsync( AuthenticationState authenticationState, CancellationToken cancellationToken) { //项目模板代码判断用户是否存在,SecurityStamp是否改变 return (authenticationState.User.Identity?.IsAuthenticated == true); } private void OnAuthenticationStateChanged(Task<AuthenticationState> task) { authenticationStateTask = task; } private async Task OnPersistingAsync() { if (authenticationStateTask is null) { throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}()."); } var authenticationState = await authenticationStateTask; var principal = authenticationState.User; if (principal.Identity?.IsAuthenticated == true) { //Asp.Net Core定义的ClaimType跟id4定义的不同,要改用id4的 var userId = principal.FindFirst(options.ClaimsIdentity.UserIdClaimType)?.Value; //var email = principal.FindFirst(options.ClaimsIdentity.EmailClaimType)?.Value; var email = principal.FindFirst(JwtClaimTypes.Email)?.Value; var name = principal.FindFirst(JwtClaimTypes.Name)?.Value; var phoneNumber = principal.FindFirst(JwtClaimTypes.PhoneNumber)?.Value; var nation = principal.FindFirst("nation")?.Value; var roles = principal.FindAll(JwtClaimTypes.Role)?.Select(x => x.Value); if (userId != null && email != null) { state.PersistAsJson(nameof(UserInfo), new UserInfo { UserId = userId, Email = email, Name = name, PhoneNumber = phoneNumber, Nation = nation, Roles = string.Join(",", roles) }); ; } } } protected override void Dispose(bool disposing) { subscription.Dispose(); AuthenticationStateChanged -= OnAuthenticationStateChanged; base.Dispose(disposing); } }
修改Routes.razor路由组件,需要登录时自动跳转到登录路由。
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\Components\Routes.razor
<Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)"> <NotAuthorized> <RedirectToLogin /> </NotAuthorized> </AuthorizeRouteView> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> </Found> </Router>
在Home页面增加显示当前登录用户信息。
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\Components\Pages\Home.razor
@page "/" @rendermode InteractiveServer @using Microsoft.AspNetCore.Authentication @using Microsoft.AspNetCore.Authorization @inject IHttpContextAccessor httpContextAccessor <PageTitle>Home</PageTitle> <h1>Hello, world!</h1> Welcome to your new app. <AuthorizeView> <Authorized> <p>您已经登录</p> <div class="card"> <div class="card-header"> <h2>context.User.Claims</h2> </div> <div class="card-body"> <dl> <dt>context.User.Identity.Name</dt> <dd>@context.User.Identity?.Name</dd> @foreach (var claim in context.User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> </div> </div> @if (AuthResult is not null) { <p>AuthResult.Principal.Identity.Name: <strong>@AuthResult.Principal?.Identity?.Name</strong></p> <div class="card"> <div class="card-header"> <h2>AuthenticateResult.Principal</h2> </div> <div class="card-body"> <dl> @foreach (var claim in AuthResult.Principal?.Claims!) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> </div> </div> <div class="card"> <div class="card-header"> <h2>AuthenticateResult.Properties.Items</h2> </div> <div class="card-body"> <dl> @foreach (var prop in AuthResult.Properties?.Items!) { <dt>@prop.Key</dt> <dd>@prop.Value</dd> } </dl> </div> </div> } <form action="Account/Logout" method="post"> <AntiforgeryToken /> <button type="submit" class="btn btn-link">退出登录(Post方法调用1次Action,推荐)</button> </form> <a class="nav-link" href="Account/Logout">退出登录(Get方法会调用2次Action,不推荐)</a> </Authorized> <NotAuthorized> <p>您还没有登录,请先登录</p> <form action="Account/Login" method="post"> <AntiforgeryToken /> <button type="submit" class="btn btn-link">登录(Post方法调用1次Action,推荐)</button> </form> <a class="nav-link" href="Account/Login">登录(Get方法会调用2次Action,不推荐)</a> </NotAuthorized> </AuthorizeView> @code { private AuthenticateResult AuthResult; //依赖注入的参数为null [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; //设置为InteractiveServer才触发 protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { Console.WriteLine($"依赖注入的参数HttpContext={HttpContext}"); AuthResult = await httpContextAccessor.HttpContext.AuthenticateAsync(); StateHasChanged(); } } }
增加AccountController账号控制器,从之前的Blazor Server项目复制过来,给Action增加了Get和Post方法属性,同时支持Get和Post方式访问。
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\AccountController.cs
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace WebAppOidc.Controllers; [ApiController] [Route("[controller]/[action]")] public class AccountController : Controller { private readonly ILogger _logger; public AccountController(ILogger<AccountController> logger) { _logger = logger; } /// <summary> /// 跳转到Identity Server 4统一登录 /// </summary> /// <param name="returnUrl">登录成功后,返回之前的网页路由</param> /// <returns></returns> [HttpGet] [HttpPost] public IActionResult Login(string returnUrl = "") { if (string.IsNullOrEmpty(returnUrl)) returnUrl = "./"; var properties = new AuthenticationProperties { //记住登录状态 IsPersistent = true, RedirectUri = returnUrl }; _logger.LogInformation($"id4跳转登录, returnUrl={returnUrl}"); //跳转到Identity Server 4统一登录 return Challenge(properties, "oidc"); } /// <summary> /// 退出登录 /// </summary> /// <returns></returns> [HttpGet] [HttpPost] public async Task<IActionResult> Logout() { var userName = HttpContext.User.Identity?.Name; _logger.LogInformation($"{userName}退出登录。"); //删除登录状态cookies await HttpContext.SignOutAsync("cookies"); var properties = new AuthenticationProperties { RedirectUri = "./" }; //跳转到Identity Server 4统一退出登录 return SignOut(properties, "oidc"); } }
修改https侦听端口号5581,跟id4的配置一致
D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\Properties\launchSettings.json
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5581",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
测试
同时运行AspNetId4Web项目、WebAppOidc项目。
在WebAppOidc项目登录,可以跳转到Identity Server 4登录页面,输入种子用户alice的手机号13512345001,在id4项目查看验证码,再填入登录页面验证码,登录成功,返回WebAppOidc主页,可以看到登录用户的信息。可以点击退出登录。
问题
采用Get方法从razor页面通过超级链接跳转到控制器Action路由的时候,会调用2次Action,我不知道为什么会这样?以前Blazor Server项目这样做是没问题的,只会调用1次。我看带有身份验证的Blazor WebApp项目是采用post方法执行控制器路由的,所以跟着改了,也算解决了问题,但是原因没想明白。
另外,Blazor Web App页面的依赖注入跟Blazor Server行为不一样了,Blazor Web App页面可以客户端和服务端动态呈现,页面初始化的时候,依赖注入的对象是空,需要在页面初始化之后,比如OnAfterRenderAsync,在这里可以依赖注入对象。
DEMO代码地址:https://gitee.com/woodsun/blzid4
标签:Web,WebAppOidc,App,Server,using,Blazor,options,Identity From: https://www.cnblogs.com/sunnytrudeau/p/18016657