Blazor WebAssembly專案訪問Identity Server 4
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