zoukankan      html  css  js  c++  java
  • Blazor Server获取Token访问外部Web Api

    Blazor Server获取Token访问外部Web Api

    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)

    一个企业内部可能包含好几个不同业务的子系统,所有子系统共用一个Identity Server 4认证中心,用户在一个子系统登录之后,可以获取token访问其他子系统受保护的Web Api。关于Blazor Server项目如何获取token,微软官网有介绍:ASP.NET Core Blazor Server 其他安全方案 | Microsoft Docs

    新建Web Api项目

    项目名称MyWebApi,用模板创建的WeatherForecastController足以。

     

    Program.cs增加认证和授权的配置,Web Api项目采用Bearer认证。

    //NuGet安装Microsoft.AspNetCore.Authentication.JwtBearer
    //NuGet安装IdentityServer4.AccessTokenValidation
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme, options =>
        {
            //指定IdentityServer的地址
            options.Authority = "https://localhost:5001"; ;
    
            options.ApiName = "https://localhost:5001/resources";
        });
    
    //添加认证和授权
    app.UseAuthentication();
    app.UseAuthorization();

    WeatherForecastController.cs控制器增加访问权限

        [ApiController]
        [Route("[controller]")]
        [Authorize]
        public class WeatherForecastController : ControllerBase

    增加打印HttpContext获取token和用户声明claims调试信息。

    [HttpGet(Name = "GetWeatherForecast")]
            public async Task<IEnumerable<WeatherForecast>> Get()
            {
                var claims = User.Claims.Select(x => $"{x.Type}={x.Value}").ToList();
    
                var accessToken = await HttpContext.GetTokenAsync("access_token");
                var refreshToken = await HttpContext.GetTokenAsync("refresh_token");
    
                string msg = $"从HttpContext获取accessToken={accessToken}{Environment.NewLine}, refreshToken={refreshToken}{Environment.NewLine}, 用户声明={string.Join(", ", claims)}";
                _logger.LogInformation(msg);
    
                return Enumerable.Range(1, 5).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = Random.Shared.Next(-20, 55),
                    Summary = Summaries[Random.Shared.Next(Summaries.Length)]
                })
                .ToArray();
            }

    Blazor Server项目获取token

    BlzOidc项目参考官网代码,通过_Host.cshtml网页的HttpContext,获取token

    首先定义token提供者

    public class TokenProvider
        {
            public string? AccessToken { get; set; }
            public string? RefreshToken { get; set; }
        }

    Program.cs注册Token提供者

            //注册Token提供者

            builder.Services.AddScoped<TokenProvider>();

    Pages/_Host.cshtml 文件中,通过HttpContext获取Token,作为参数传递到App.razor组件。

    @page "/"
    @namespace BlzOidc.Pages
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
    @using Microsoft.AspNetCore.Authentication
    @using BlzOidc.Data
    @{
        Layout = "_Layout";
    }
    
    @{
        var tokens = new TokenProvider
                {
                    AccessToken = await HttpContext.GetTokenAsync("access_token"),
                    RefreshToken = await HttpContext.GetTokenAsync("refresh_token")
                };
    }
    
    <component type="typeof(App)" render-mode="ServerPrerendered" param-InitialToken="tokens" />

    App.razor组件保存Token,这些都可以抄跟官网代码,但是我不明白,为什么在_Host.cshtml中获取到Token,还要传到App.razor去保存呢?直接在_Host.cshtml保存不行吗?

    @using BlzOidc.Data
    @inject TokenProvider TokenProvider
    
    @code {
        [Parameter]
        public TokenProvider? InitialToken { get; set; }
    
        protected override Task OnInitializedAsync()
        {
            TokenProvider.AccessToken = InitialToken?.AccessToken;
            TokenProvider.RefreshToken = InitialToken?.RefreshToken;
    
            Console.WriteLine($"初始化获取AccessToken={TokenProvider.AccessToken}, RefreshToken={TokenProvider.RefreshToken}");
    
            return base.OnInitializedAsync();
        }
    }

    官网代码是每次在HttpClient手动填充token然后访问外部Web Api

    public class WeatherForecastService
    {
        private readonly HttpClient http;
        private readonly TokenProvider tokenProvider;
    
        public WeatherForecastService(IHttpClientFactory clientFactory, 
            TokenProvider tokenProvider)
        {
            http = clientFactory.CreateClient();
            this.tokenProvider = tokenProvider;
        }
    
        public async Task<WeatherForecast[]> GetForecastAsync()
        {
            var token = tokenProvider.AccessToken;
            var request = new HttpRequestMessage(HttpMethod.Get, 
                "https://localhost:5003/WeatherForecast");
            request.Headers.Add("Authorization", $"Bearer {token}");
            var response = await http.SendAsync(request);
            response.EnsureSuccessStatusCode();
    
            return await response.Content.ReadAsAsync<WeatherForecast[]>();
        }
    }

    我更喜欢写一个类型化的HttpClient,然后注册服务。

    using System.Net.Http.Headers;
    
    namespace BlzOidc.Data
    {
        public class WeatherForecastApiClient
        {
            private readonly HttpClient _httpClient;
            private readonly TokenProvider _tokenProvider;
    
            public WeatherForecastApiClient(HttpClient httpClient, TokenProvider tokenProvider)
            {
                _httpClient = httpClient;
                _tokenProvider = tokenProvider;
            }
    
            public async Task<WeatherForecast[]?> GetForecastAsync()
            {
                var token = _tokenProvider.AccessToken;
                _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
    
                var result = await _httpClient.GetFromJsonAsync<WeatherForecast[]>("/WeatherForecast");
    
                return result;
            }
        }
    }

    Program.cs注册服务

            //注册WeatherForecastApiClient

            builder.Services.AddHttpClient<WeatherForecastApiClient>(client => client.BaseAddress = new Uri("https://localhost:5601"));

    然后在网页上注入类型化的WeatherForecastApiClientMyWebApi获取天气数据。

    @page "/fetchdataapi"
    @attribute [Authorize]
    
    <PageTitle>Weather forecast</PageTitle>
    
    @using BlzOidc.Data
    @inject WeatherForecastApiClient ForecastService
    
    <h1>Weather forecast</h1>
    
    <p>This component demonstrates fetching data from a service.</p>
    
    @if (forecasts == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        <table class="table">
            <thead>
                <tr>
                    <th>Date</th>
                    <th>Temp. (C)</th>
                    <th>Temp. (F)</th>
                    <th>Summary</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var forecast in forecasts)
                {
                    <tr>
                        <td>@forecast.Date.ToShortDateString()</td>
                        <td>@forecast.TemperatureC</td>
                        <td>@forecast.TemperatureF</td>
                        <td>@forecast.Summary</td>
                    </tr>
                }
            </tbody>
        </table>
    }
    
    @code {
        private WeatherForecast[]? forecasts;
    
        protected override async Task OnInitializedAsync()
        {
            forecasts = await ForecastService.GetForecastAsync();
        }
    }

    同时运行AspNetId4Web认证服务器,MyWebApi项目,BlzOidc项目,在BlzOidc项目未登录状态下直接访问Fetch Data Api菜单,浏览器自动跳转到AspNetId4Web登录页面,输入种子用户的手机号13512345001获取验证码,看AspNetId4Web控制台输出获取验证码,填写验证码登录,浏览器自动跳转回到BlzOidc项目的Fetch Data Api页面,获取到了天气数据。

     

    问题

    MyWebApi认证参数ApiName究竟应该填写什么值?默认情况下,它是Identity Server 4服务器的一个固定路由:

            options.ApiName = "https://localhost:5001/resources";

    我也可以修改config.cs

            public static IEnumerable<ApiResource> ApiResources =>
                new ApiResource[]
                {
                    new ApiResource("api1", "api1")
                    {
                        Scopes = { "scope1" },
    
                        //认证服务器返回的附加身份属性
                        UserClaims =
                        {
                            //增加aud="api1"
                            JwtClaimTypes.Audience,
                        },
                    }
                };

    AddIdentityServer增加配置AddInMemoryApiResources

                var builder = services.AddIdentityServer(options =>
                {
                    options.Events.RaiseErrorEvents = true;
                    options.Events.RaiseInformationEvents = true;
                    options.Events.RaiseFailureEvents = true;
                    options.Events.RaiseSuccessEvents = true;
    
                    // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
                    options.EmitStaticAudienceClaim = true;
                })
                    .AddInMemoryIdentityResources(Config.IdentityResources)
                    .AddInMemoryApiScopes(Config.ApiScopes)
                    .AddInMemoryClients(Config.Clients)
                    .AddExtensionGrantValidator<PhoneCodeGrantValidator>()
                    .AddInMemoryApiResources(Config.ApiResources)
                    .AddAspNetIdentity<ApplicationUser>();

    Web Api的认证参数就可以采用api1”了

    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme, options =>
        {
            //指定IdentityServer的地址
            options.Authority = "https://localhost:5001"; ;
    
            //默认aud="https://localhost:5001/resources"
            //options.ApiName = "https://localhost:5001/resources";
            //Bearer was not authenticated. Failure message: IDX10214: Audience validation failed. Audiences: 'System.String'. Did not match: validationParameters.ValidAudience: 'System.String' or validationParameters.ValidAudiences: 'System.String'.
            //Identity Server 4 config.cs的ApiResources增加JwtClaimTypes.Audience,AddInMemoryApiResources(Config.ApiResources),可以增加aud="api1"
            options.ApiName = "api1";
        });

    此时可以查看Identity Server 4返回的token,它确实有2aud

    [20:54:59 Debug] IdentityServer4.Validation.TokenValidator

    Token validation success

    {"ClientId": null, "ClientName": null, "ValidateLifetime": true, "AccessTokenType": "Jwt", "ExpectedScope": "openid", "TokenHandle": null, "JwtId": "4F3D0DE1EE8E8DEFD3EC27E602F0790C", "Claims": {"nbf": 1638708899, "exp": 1638712499, "iss": "https://localhost:5001", "aud": ["api1", "https://localhost:5001/resources"], "client_id": "BlazorServerOidc", "sub": "d2f64bb2-789a-4546-9107-547fcb9cdfce", "auth_time": 1638708898, "idp": "local", "name": "Alice Smith", "role": ["Admin", "Guest"], "email": "AliceSmith@email.com", "phone_number": "13512345001", "nation": "汉族", "jti": "4F3D0DE1EE8E8DEFD3EC27E602F0790C", "sid": "FDB59080B24468B76300AE9554354D67", "iat": 1638708899, "scope": ["openid", "profile", "scope1"], "amr": "pwd"}, "$type": "TokenValidationLog"}

    MyWebApi项目打印出来的token也有2aud

    info: MyWebApi.Controllers.WeatherForecastController[0]

          HttpContext获取accessToken=eyJh...WlA

          , refreshToken=

          , 用户声明=nbf=1638708899, exp=1638712499, iss=https://localhost:5001, aud=api1, aud=https://localhost:5001/resources, client_id=BlazorServerOidc, sub=d2f64bb2-789a-4546-9107-547fcb9cdfce, auth_time=1638708898, idp=local, name=Alice Smith, role=Admin, role=Guest, email=AliceSmith@email.com, phone_number=13512345001, nation=汉族, jti=4F3D0DE1EE8E8DEFD3EC27E602F0790C, sid=FDB59080B24468B76300AE9554354D67, iat=1638708899, scope=openid, scope=profile, scope=scope1, amr=pwd

    我也不是很理解,但是感觉用api1好一点。

    DEMO代码地址:https://gitee.com/woodsun/blzid4

  • 相关阅读:
    17.1.2.1 Advantages and Disadvantages of Statement-Based and Row-Based Replication
    17.1.2 Replication Formats
    Setting the Master Configuration on the Slave
    17.1.1.9 Introducing Additional Slaves to an Existing Replication Environment
    17.1.1.8 Setting Up Replication with Existing Data
    17.1.1.7 Setting Up Replication with New Master and Slaves
    17.1.1.6 Creating a Data Snapshot Using Raw Data Files
    列出display的值,并说明它们的作用
    CSS设置DIV居中
    CSS选择符有哪些?哪些属性可以继承?优先级算法如何计算?
  • 原文地址:https://www.cnblogs.com/sunnytrudeau/p/15647345.html
Copyright © 2011-2022 走看看