Blazor WebAssembly项目访问Identity Server 4
Blazor WebAssembly项目访问Identity Server 4
Identity Server系列目录
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登录页面,并成功返回。
重新映射用户角色
参考微软官网的例子,把角色数组拆分为单个角色。
////// 自定义用户工厂 /// 在 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 /// public class CustomUserFactory : AccountClaimsPrincipalFactory { public CustomUserFactory(IAccessTokenProviderAccessor accessor) : base(accessor) { } public override async ValueTask 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();
给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。
https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-6.0#configure-the-httpclient-handler
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() .ConfigureHandler( authorizedUrls: new[] { "https://localhost:5601" }, scopes: new[] { "scope1" })); builder.Services.AddScoped(sp => sp.GetRequiredService () .CreateClient("MyWebApi"));
FetchData.razor页面改为访问MyWebApi项目获取数据
protected override async Task OnInitializedAsync() { //forecasts = await Http.GetFromJsonAsync("sample-data/weather.json"); try { forecasts = await Http.GetFromJsonAsync("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(); builder.Services.AddScoped(sp => sp.GetRequiredService () .CreateClient("MyWebApi"));
DEMO代码地址:https://gitee.com/woodsun/blzid4