如何有效地在每次调用 API 时从 IdentityServer4 接收的 Authorization 标头中设置访问令牌

问题描述

我目前有一个 blazor 服务器应用程序、identityserver4 和一个 API。目前代码流工作得很好。当前的问题是 blazor 应用程序中使用的服务客户端类必须在从 IdentityServer4 接收到每个 API 调用的授权标头中设置访问令牌。这是乏味/重复的事情。使用 HttpClientFactory 并实现它的最佳方法是什么,这样我就不必担心为每个 API 调用设置承载令牌?

根据一些文档,目前我有以下课程。两者都作为范围服务添加。 1.TokenProvider 2.TokenManager

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; }
    }

App.razor:

@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 设置不记名令牌的代码行让我很担心。如何避免每次调用 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;

        }

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)