Как эффективно установить маркер доступа в заголовке авторизации, полученный от IdentityServer4 при каждом вызове API

#identityserver4 #access-token #dotnet-httpclient #blazor-server-side #httpclientfactory

Вопрос:

В настоящее время у меня есть серверное приложение blazor, identityserver4 и API. В настоящее время поток кода работает просто отлично. Текущая проблема заключается в том, что класс клиента службы, используемый в приложении blazor, должен устанавливать маркер доступа в заголовках авторизации, полученных от IdentityServer4, для каждого вызова API. Это утомительно/повторять. Каков наилучший способ использовать HttpClientFactory и реализовать его, чтобы мне не приходилось беспокоиться о настройке токена на предъявителя для каждого вызова API?

Следуя некоторым документам, в настоящее время у меня есть классы ниже. Оба добавлены как службы с областью действия. 1.Поставщик токенов 2.Менеджер токенов

 public class TokenManager
    {
        private readonly TokenProvider _tokenProvider;
        private readonly IHttpClientFactory _httpClientFactory;
       
        public TokenManager(TokenProvider tokenProvider,
            IHttpClientFactory httpClientFactory)
        {
            _tokenProvider = tokenProvider ??
                throw new ArgumentNullException(nameof(tokenProvider));
            _httpClientFactory = httpClientFactory ??
                throw new ArgumentNullException(nameof(httpClientFactory));         

        }


        public async Task<string> RetrieveAccessTokenFromIdentityProvider()
        {
            if ((_tokenProvider.ExpiresAt.AddSeconds(-60)).ToUniversalTime()
                    > DateTime.UtcNow)
            {               
                return _tokenProvider.AccessToken;
            }
            var idpClient = _httpClientFactory.CreateClient();
            var discoveryReponse = await idpClient
                .GetDiscoveryDocumentAsync("https://localhost:44359/");
            var refreshResponse = await idpClient.RequestRefreshTokenAsync(
               new RefreshTokenRequest
               {
                   Address = discoveryReponse.TokenEndpoint,
                   ClientId = "someclient",
                   ClientSecret = "supersecret",
                   RefreshToken = _tokenProvider.RefreshToken
               });

            _tokenProvider.AccessToken = refreshResponse.AccessToken;
            _tokenProvider.RefreshToken = refreshResponse.RefreshToken;
            _tokenProvider.ExpiresAt = DateTime.UtcNow.AddSeconds(refreshResponse.ExpiresIn);
            return _tokenProvider.AccessToken;
        }
    }
 
 public class TokenProvider
    {
        public TokenProvider()
        {

        }
        public string XsrfToken { get; set; }
        public string AccessToken { get; set; }
        public string RefreshToken { get; set; }
        public DateTimeOffset ExpiresAt { get; set; }
    }

 

Приложение.бритва:

 @inject  TokenProvider TokenProvider

@code{
    [Parameter]
    public InitialApplicationState InitialState { get; set; } 

    protected override Task OnInitializedAsync()
    {
        TokenProvider.XsrfToken = InitialState.XsrfToken;
        TokenProvider.AccessToken = InitialState.AccessToken;
        TokenProvider.RefreshToken = InitialState.RefreshToken;
        TokenProvider.ExpiresAt = InitialState.ExpiresAt;
        return base.OnInitializedAsync();
    }

}

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <h1>Sorry, unauthorized access request</h1>
                    <p>Try logging in...</p>
                </NotAuthorized>               
            </AuthorizeRouteView>
            
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

 

_Host.cshtml:

 <body>
    @{
        var initialTokenState = new InitialApplicationState
        {
            XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken,
            AccessToken = await HttpContext.GetTokenAsync("access_token"),
            RefreshToken = await HttpContext.GetTokenAsync("refresh_token")

        };

        var expiresAt = await HttpContext.GetTokenAsync("expires_at");
        if (DateTimeOffset.TryParse(expiresAt,
            CultureInfo.InvariantCulture,
            DateTimeStyles.None,
            out var expiration))
        {
            initialTokenState.ExpiresAt = expiration;
        }
        else
        {
            initialTokenState.ExpiresAt = DateTimeOffset.UtcNow;
        }

}

    <app>
        <component type="typeof(App)" render-mode="ServerPrerendered"
                   param-InitialState="initialTokenState" />
    </app>

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.server.js"></script>
</body>
 

Startup.cs

 public void ConfigureServices(IServiceCollection services)
        {
         services.AddHttpClient<IReportService, ReportService>(client =>
            {
                client.BaseAddress = new Uri("https://localhost:44340/");
            });
            services.AddSingleton<WeatherForecastService>();            
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme,
                options =>
                {
                    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.Authority = "https://localhost:44359/"; 
                    options.ClientId = "someclient";
                    options.ClientSecret = "supersecret";
                    options.CallbackPath = "/signin-oidc";  
                    options.ResponseType = "code"; 
                    options.ResponseMode = "form_post";
                    options.UsePkce = true; 
                    options.Scope.Add("openid");
                    options.Scope.Add("profile");
                    options.Scope.Add("email");
                    options.Scope.Add("phone");
                    options.Scope.Add("someclient-api");
                    options.Scope.Add("offline_access");
                    options.SaveTokens = true;
                    options.GetClaimsFromUserInfoEndpoint = true;
                    options.TokenValidationParameters.NameClaimType = "given_name";

                });

            
            services.AddScoped<TokenProvider>();
            services.AddScoped<TokenManager>();
}
 

ServiceClient.cs The line of code that sets the Bearer token concerns me. How to avoid writing this line for every call to API?

 public async Task<IEnumerable<Report>> GetReportList(DefaultDates dates)
        {
            *httpClient.SetBearerToken(await tokenManager.RetrieveAccessTokenFromIdentityProvider());*
            var response = await httpClient.PostAsJsonAsync($"api/Reports/GetReport", dates);
            if (response.IsSuccessStatusCode)            
            {
                return await response.ReadContentAs<List<Report>>();                
            }
            return null;

        }