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

 

posted on 2022-03-16 20:22  SunnyTrudeau  阅读(308)  评论(0编辑  收藏  举报