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

 

posted on 2021-12-05 22:02  SunnyTrudeau  阅读(1303)  评论(0编辑  收藏  举报