Blazor WebAssembly项目访问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项目提供了丰富的认证和授权支持,参考微软官网两篇文章,编写一个Blazor WebAssembly项目访问之前已经建好的Identity Server 4服务器。
https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-6.0&tabs=visual-studio
https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/webassembly/hosted-with-identity-server?view=aspnetcore-6.0&tabs=visual-studio
创建Blazor WebAssembly项目
新建Blazor WebAssembly项目WebAsmOidc,身份验证类型=个人账户,无托管主机。框架自动引用认证相关的NuGet类库,自动生成认证相关的文件,改一下就能用。
appsettings.Development.json改为访问已有的Identity Server 4服务器
"Local": { "Authority": "https://localhost:5001/", "ClientId": "WebAssemblyOidc", "DefaultScopes": [ "scope1" ], "PostLogoutRedirectUri": "/", "ResponseType": "code" }
launchSettings.json改一下项目的端口
"applicationUrl": "https://localhost:5801;http://localhost:5800",
AspNetId4Web项目增加Blazor WebAssembly项目的客户端配置,因为WebAssembly代码在浏览器里边可以看到,没有必要用秘钥了
// 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", }, },
同时运行AspNetId4Web项目、WebAsmOidc项目,在WebAsmOidc项目登录,可以跳转到Identity Server 4登录页面,并成功返回。
重新映射用户角色
参考微软官网的例子,把角色数组拆分为单个角色。
/// <summary> /// 自定义用户工厂 /// 在 Client 应用中,创建自定义用户工厂。 Identity 服务器在一个 role 声明中发送多个角色作为 JSON 数组。 单个角色在该声明中作为单个字符串值进行发送。 /// 工厂为每个用户的角色创建单个 role 声明。 /// https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/webassembly/hosted-with-identity-server?view=aspnetcore-6.0&tabs=visual-studio#name-and-role-claim-with-api-authorization /// </summary> public class CustomUserFactory : AccountClaimsPrincipalFactory<RemoteUserAccount> { public CustomUserFactory(IAccessTokenProviderAccessor accessor) : base(accessor) { } public override async ValueTask<ClaimsPrincipal> CreateUserAsync( RemoteUserAccount account, RemoteAuthenticationUserOptions options) { var user = await base.CreateUserAsync(account, options); if (user.Identity.IsAuthenticated) { var identity = (ClaimsIdentity)user.Identity; var roleClaims = identity.FindAll(identity.RoleClaimType).ToArray(); if (roleClaims.Any()) { foreach (var existingClaim in roleClaims) { identity.RemoveClaim(existingClaim); } var rolesElem = account.AdditionalProperties[identity.RoleClaimType]; if (rolesElem is JsonElement roles) { if (roles.ValueKind == JsonValueKind.Array) { foreach (var role in roles.EnumerateArray()) { identity.AddClaim(new Claim(options.RoleClaim, role.GetString())); } } else { identity.AddClaim(new Claim(options.RoleClaim, roles.GetString())); } } } } return user; }
Program.cs注册工厂,注意角色的名称也要转换
builder.Services.AddOidcAuthentication(options => { // Configure your authentication provider options here. // For more information, see https://aka.ms/blazor-standalone-auth builder.Configuration.Bind("Local", options.ProviderOptions); //这里是个ClaimType的转换,Identity Server的ClaimType和Blazor中间件使用的名称有区别,需要统一。 options.UserOptions.NameClaim = "name"; options.UserOptions.RoleClaim = "role"; }) .AddAccountClaimsPrincipalFactory<CustomUserFactory>();
给FetchData.razor页面增加认证要求
@using Microsoft.AspNetCore.Authorization @attribute [Authorize(Roles = "Admin")]
再次运行2个项目,测试alice有Admin权限,可以访问FetchData.razor页面,bob不行。
获取Access Token访问资源Web Api
参考微软官网定义,在Program.cs访问资源服务器的HttpClient参数,框架会自动获取Access Token到HttpClient的Header。
AuthorizationMessageHandler 是一个 DelegatingHandler,用于将访问令牌附加到传出 HttpResponseMessage 实例。 令牌是使用由框架注册的 IAccessTokenProvider 服务获取的。
可以使用 ConfigureHandler 方法将 AuthorizationMessageHandler 配置为授权的 URL、作用域和返回 URL。 ConfigureHandler 配置此处理程序,以使用访问令牌授权出站 HTTP 请求。 仅当至少有一个授权 URL 是请求 URI (HttpRequestMessage.RequestUri) 的基 URI 时,才附加访问令牌。
builder.Services.AddHttpClient("MyWebApi", client => client.BaseAddress = new Uri("https://localhost:5601")) .AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>() .ConfigureHandler( authorizedUrls: new[] { "https://localhost:5601" }, scopes: new[] { "scope1" })); builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>() .CreateClient("MyWebApi"));
FetchData.razor页面改为访问MyWebApi项目获取数据
protected override async Task OnInitializedAsync() { //forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json"); try { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); } catch (AccessTokenNotAvailableException exception) { exception.Redirect(); } }
资源Web Api配置跨域共享
同时运行AspNetId4Web项目、MyWebAPi项目、WebAsmOidc项目,用管理员alice登录,访问FetchData.razor页面,提示跨域访问错误。
blazor.webassembly.js:1 info: System.Net.Http.HttpClient.MyWebApi.ClientHandler[100]
Sending HTTP request GET https://localhost:5601/WeatherForecast
fetchdata:1
Access to fetch at 'https://localhost:5601/WeatherForecast' from origin 'https://localhost:5801' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
:5601/WeatherForecast:1
参考微软官网给MyWebApi项目增加跨域共享配置
https://docs.microsoft.com/zh-cn/aspnet/core/blazor/call-web-api?view=aspnetcore-6.0&pivots=webassembly#call-web-api-example
app.UseCors(policy => policy.WithOrigins("https://localhost:5801") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials());
Fiddler抓包看一下,WebAsmOidc项目访问了2次MyWebAPi项目。
第一次是OPTIONS方法,获取MyWebAPi项目支持的功能。
OPTIONS https://localhost:5601/WeatherForecast HTTP/1.1 Host: localhost:5601 Connection: keep-alive Accept: */* Access-Control-Request-Method: GET Access-Control-Request-Headers: authorization Origin: https://localhost:5801 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.39 Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site Sec-Fetch-Dest: empty Referer: https://localhost:5801/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 HTTP/1.1 204 No Content Date: Wed, 16 Mar 2022 12:13:16 GMT Server: Kestrel Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: authorization Access-Control-Allow-Methods: GET Access-Control-Allow-Origin: https://localhost:5801
第二次才是查询数据。
GET https://localhost:5601/WeatherForecast HTTP/1.1 Host: localhost:5601 Connection: keep-alive sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="99", "Microsoft Edge";v="99" authorization: Bearer eyJ……ihg sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.39 sec-ch-ua-platform: "Windows" Accept: */* Origin: https://localhost:5801 Sec-Fetch-Site: same-site Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: https://localhost:5801/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Wed, 16 Mar 2022 12:13:17 GMT Server: Kestrel Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: https://localhost:5801 Transfer-Encoding: chunked 1ee [{"date":"2022-03-17T20:13:18.0963201+08:00","temperatureC":13,"temperatureF":55,"summary":"Cool"},{"date":"2022-03-18T20:13:18.0966368+08:00","temperatureC":24,"temperatureF":75,"summary":"Balmy"},{"date":"2022-03-19T20:13:18.0966403+08:00","temperatureC":-17,"temperatureF":2,"summary":"Mild"},{"date":"2022-03-20T20:13:18.0966405+08:00","temperatureC":15,"temperatureF":58,"summary":"Chilly"},{"date":"2022-03-21T20:13:18.0966406+08:00","temperatureC":10,"temperatureF":49,"summary":"Mild"}] 0
问题
Blazor WebAssembly项目访问跨域的资源Web Api配置比较麻烦,这是由浏览器安全机制规定的,简单的Blazor WebAssembly项目最好还是配合托管主机一起使用,网页客户端只访问配套的托管主机服务端,对于第三方资源Web Api也通过托管主机中转,托管主机起到类似网关的作用。托管主机是后台服务器,不受浏览器跨域访问的约束。这样网页客户端的HttpClient配置比较简单,资源Web Api也不用配置跨域共享,当然这个会牺牲性能,有利有弊。
访问托管主机的简单配置:
builder.Services.AddHttpClient("MyWebApi", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>(); builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>() .CreateClient("MyWebApi"));
DEMO代码地址:https://gitee.com/woodsun/blzid4