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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM