1. 程式人生 > 其它 >Blazor WebAssembly專案訪問Identity Server 4

Blazor WebAssembly專案訪問Identity Server 4

Blazor WebAssembly專案訪問Identity Server 4

 

Identity Server系列目錄

  1. Blazor Server訪問Identity Server 4單點登入 - SunnyTrudeau - 部落格園 (cnblogs.com)
  2. Blazor Server訪問Identity Server 4單點登入2-整合Asp.Net角色 - SunnyTrudeau - 部落格園 (cnblogs.com)
  3. Blazor Server訪問Identity Server 4-手機驗證碼登入 - SunnyTrudeau - 部落格園 (cnblogs.com)
  4. Blazor MAUI客戶端訪問Identity Server登入 - SunnyTrudeau - 部落格園 (cnblogs.com)
  5. Identity Server 4專案整合Blazor元件 - SunnyTrudeau - 部落格園 (cnblogs.com)
  6. Identity Server 4退出登入自動跳轉返回 - SunnyTrudeau - 部落格園 (cnblogs.com)
  7. Identity Server通過ProfileService返回使用者角色 - SunnyTrudeau - 部落格園 (cnblogs.com)
  8. Identity Server 4返回自定義使用者Claim - SunnyTrudeau - 部落格園 (cnblogs.com)
  9. Blazor Server獲取Token訪問外部Web Api - SunnyTrudeau - 部落格園 (cnblogs.com)
  10. 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個專案,測試aliceAdmin許可權,可以訪問FetchData.razor頁面,bob不行。

 

獲取Access Token訪問資源Web Api

參考微軟官網定義,在Program.cs訪問資源伺服器的HttpClient引數,框架會自動獲取Access TokenHttpClientHeader

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、作用域和返回 URLConfigureHandler 配置此處理程式,以使用訪問令牌授權出站 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專案訪問了2MyWebAPi專案。

第一次是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