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

  • 相关阅读:
    MYSQL学习中
    正则相关记录
    JS前台相关
    .net 时间格式
    SQL问题整理
    IIS 错误
    小型文件系统(littlefs)
    三极管NPN和PNP开关电路
    事件EVENT与waitforsingleobject的使用
    UpdateData(TRUE)与UpdateData(FALSE)的使用
  • 原文地址:https://www.cnblogs.com/sunnytrudeau/p/15647345.html
Copyright © 2011-2022 走看看