2.22Access Token 自动管理

在与第三方服务(如微信公众号、企业微信等)对接时,通常需要先获取 Access Token,并在后续请求中携带该 Access Token 才能正常调用接口。Access Token 具有有效期(通常为两小时),过期后需重新获取并更新。

在与第三方服务(如微信公众号、企业微信等)对接时,通常需要先获取 Access Token,并在后续请求中携带该 Access Token 才能正常调用接口。Access Token 具有有效期(通常为两小时),过期后需重新获取并更新。

为简化这一流程,框架内置了 Access Token 自动管理机制:当 Access Token 不存在或已过期时,自动获取新 Access Token,并按配置将其注入到请求的 HeaderQueryCookie 等位置,同时支持在请求因 Access Token 失效(如返回 401)时自动重试。

HttpAccessToken 模型#

HttpAccessToken 表示一个 Access Token 信息,包含以下构造函数、属性和方法:

  • 构造函数
    • new(value, expiresAt):传入 Access Token 和绝对过期时间(UTC 时间)。
    • new(jwtToken):传入 JWT Token 字符串。
  • 属性
    • ValueAccess Token 值(string 类型)。
    • ExpiresAtAccess Token 的绝对过期时间(DateTimeOffset 类型)。
    • SchemeHTTP 认证方案(string? 类型)。
    • RefreshToken:刷新令牌(string? 类型),内部基于 Items["refresh_token"] 提供便捷访问。
    • Items:共享数据字典(IDictionary<object, object?> 类型),用于存储与 Access Token 相关的自定义数据(如 refresh_token、用户标识等)。
  • 静态属性
    • None:表示没有可用的 Access TokenHttpAccessToken? 类型)。
  • 方法
    • IsExpired() 检查 Access Token 是否过期。
    • SetExpiresAt(expiresAt) 设置 Access Token 的绝对过期时间。

启用步骤#

1. 实现 IHttpAccessTokenProvider 接口#

该接口负责定义如何获取和刷新 Access Token。所有方法均接收 HttpAccessTokenContext 参数,可通过 context.Items 获取请求时传入的自定义数据(如用户名、密码)。示例:

cs
public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider{    /// <inheritdoc />    public async Task<HttpAccessToken?> GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken)    {        // 请求微信服务器获取 Access Token        var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login")            .WithoutTokenManagement(), cancellationToken);    // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement])        return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt));    }}

2. 为指定 HttpClient 客户端启用 Access Token 自动管理:#

cs
// 配置默认客户端services.AddHttpClient(string.Empty)    .ConfigureOptions((options, serviceProvider) =>    {        options.HttpAccessTokenProvider = ActivatorUtilities.CreateInstance<WeiXinHttpAccessTokenProvider>(serviceProvider);    });// 配置特定客户端services.AddHttpClient("weixin")    .ConfigureOptions((options, serviceProvider) =>    {        options.HttpAccessTokenProvider = ActivatorUtilities.CreateInstance<WeiXinHttpAccessTokenProvider>(serviceProvider);    });

完成上述配置后,该客户端发出的所有请求都将自动管理 Access Token

配置注入位置(HeaderQueryCookie 等)#

默认情况下,Access Token 会以 Authorization 请求头形式发送。开发者可通过设置 HttpAccessToken.Scheme 属性指定认证方案(如 Bearer):

cs
public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider{    /// <inheritdoc />    public async Task<HttpAccessToken?> GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken)    {        var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login")            .WithoutTokenManagement(), cancellationToken);   // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement])        return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt))        {            Scheme = "Bearer"   // 指定 Bearer 方案        };    }}

若需更精细地控制 Access Token 的携带方式(如放入 URL 参数或 Cookie),可实现 IHttpAccessTokenConfigurator 接口。推荐直接在 IHttpAccessTokenProvider 实现类上继承该接口,既减少类型定义,又便于集中管理:

cs
public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService)    : IHttpAccessTokenProvider, IHttpAccessTokenConfigurator{    /// <inheritdoc />    public async Task<HttpAccessToken?> GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken)    {        var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login")            .WithoutTokenManagement(), cancellationToken);   // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement])        return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt));    }    /// <inheritdoc />    public void Configure(HttpRequestBuilder httpRequestBuilder, HttpAccessToken httpAccessToken)    {        // 自定义 Token 注入方式        httpRequestBuilder.AddBearerAuthentication(httpAccessToken.Value);   // Bearer 认证        // httpRequestBuilder.WithQueryParameter("access_token", httpAccessToken.Value);   // URL 参数        // httpRequestBuilder.WithCookie("access_token", httpAccessToken.Value);   // Cookie        // 其他方式...    }}

当然,也支持独立实现 IHttpAccessTokenConfigurator

cs
public sealed class CustomHttpAccessTokenConfigurator : IHttpAccessTokenConfigurator{    /// <inheritdoc />    public void Configure(HttpRequestBuilder httpRequestBuilder, HttpAccessToken httpAccessToken)    {        // 将 Access Token 放入自定义请求头(建议添加 replace: true)        httpRequestBuilder.WithHeader("X-Custom-Token", httpAccessToken.Value, replace: true);    }}

然后在服务容器中注册该实现:

cs
services.TryAddSingleton<IHttpAccessTokenConfigurator, CustomHttpAccessTokenConfigurator>();

自定义 Access Token 刷新触发条件#

默认情况下,当接收到 HTTP 401 Unauthorized 响应时,框架会强制刷新 Access Token 并重试请求。若您的 API 通过其他状态码(如 403)或响应内容内容标识 Access Token 失效,可重写 IHttpAccessTokenProvider 接口的 ShouldRefreshAsync 方法:

cs
public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider{    /// <inheritdoc />    public async Task<HttpAccessToken?> GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken)    {        var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login")            .WithoutTokenManagement(), cancellationToken);   // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement])        return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt));    }    /// <inheritdoc />    public async Task<bool> ShouldRefreshAsync(HttpAccessTokenContext context, HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken)    {        // 示例 1:状态码为 401 或 403 时刷新        // return httpResponseMessage.StatusCode == HttpStatusCode.Unauthorized        //        || httpResponseMessage.StatusCode == HttpStatusCode.Forbidden;        // 示例 2:解析响应内容 JSON 中的错误码        var content = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken);        return content?.Contains("\"errorCode\":\"TOKEN_EXPIRED\"") == true;    }}

方法返回 true 时将强制刷新 Access Token 并重试请求。注意,重试仅执行一次,避免无限循环。

传递自定义数据到 IHttpAccessTokenProvider#

如果需要在获取 Access Token 时传入动态参数(如用户名、密码等),可使用 HttpRequestBuilder.WithAccessTokenData 方法。这些数据会被自动复制到 HttpAccessTokenContext.Items 中,供 GetAsync 等方法使用。

cs
var result = await httpRemoteService.SendAsync(    HttpRequestBuilder.Get("https://api.furion.net/data")        .SetHttpClientName("myapi") // 可选        .WithAccessTokenData("username", "admin")        .WithAccessTokenData("password", "123456"));

手动设置 Access TokenSetAsync#

除了让框架自动调用 GetAsync 获取 Access Token,还支持在登录成功后手动设置 Access Token,然后重写 RefreshAsync 实现基于 RefreshToken 的刷新逻辑。

通过注入 IHttpAccessTokenManager 接口,调用 SetAsync 方法将 Token 存入框架缓存:

cs
// 登录成功后,手动设置 Access Tokenvar token = new HttpAccessToken(accessToken, expiresAt) { RefreshToken = refreshToken };await httpAccessTokenManager.SetAsync("myapi", token);

此后框架将从缓存中读取 Access Token,过期时自动调用您重写的 RefreshAsync 进行刷新。此时 GetAsync 可以返回 null 或抛出异常(不会被调用)。

一个典型的手动刷新实现如下:

cs
public sealed class ManualTokenProvider : IHttpAccessTokenProvider, IHttpAccessTokenConfigurator{    public Task<HttpAccessToken?> GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken)        => Task.FromResult(HttpAccessToken.None);    public async Task<HttpAccessToken?> RefreshAsync(HttpAccessTokenContext context, HttpAccessToken? currentToken, CancellationToken cancellationToken)    {        // 使用当前 Token 中的 RefreshToken 去获取新 Token        var refreshToken = currentToken?.RefreshToken;        var response = await httpRemoteService.SendAsync(            HttpRequestBuilder.Post("https://auth.furion.net/refresh")                .WithHeader("X-Refresh-Token", refreshToken, replace: true)                .WithoutTokenManagement(), cancellationToken);   // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement])        return new HttpAccessToken(response.Headers.GetValues("X-Access-Token").First(),            DateTimeOffset.UtcNow.AddHours(1)) { RefreshToken = response.Headers.GetValues("X-Refresh-Token").First() };    }    public void Configure(HttpRequestBuilder httpRequestBuilder, HttpAccessToken httpAccessToken)    {        // 将 Access Token 放入自定义请求头(建议添加 replace: true)        httpRequestBuilder.WithHeader("Authorization", $"Bearer {httpAccessToken.Value}", replace: true);    }}

内置 FurionAccessTokenProvider(Furion 框架专用)#

如果您的服务端使用 Furion 框架的 JWT 令牌机制,可直接使用内置的 FurionAccessTokenProvider。该提供器自动处理 access-tokenx-access-token 响应头,实现无感滚动刷新。

1. 注册提供器

cs
services.AddHttpClient("furion_api")    .ConfigureOptions((options, serviceProvider) =>    {        options.HttpAccessTokenProvider = ActivatorUtilities.CreateInstance<FurionAccessTokenProvider>(serviceProvider);    });

如果希望手动控制实例化,也可以显式传入依赖:

cs
services.AddHttpClient("furion_api")    .ConfigureOptions((options, serviceProvider) =>    {        options.HttpAccessTokenProvider = new FurionAccessTokenProvider(serviceProvider.GetRequiredService<IHttpAccessTokenManager>());    });

2. 登录成功后手动设置初始 Access Token

cs
var token = new HttpAccessToken(initialAccessToken, expiresAt) { RefreshToken = initialRefreshToken };await httpAccessTokenManager.SetAsync("furion_api", token);

之后每次请求时,FurionAccessTokenProvider 会自动携带 Authorization: Bearer {token},并在 Access Token 过期时附加 X-Authorization: Bearer {refresh_token};服务端返回的新 Access Token 会通过 access-tokenx-access-token 响应头自动更新缓存,无需任何额外代码。

注意:FurionAccessTokenProvider 不会根据 HTTP 401 触发刷新,因为它的刷新逻辑完全由响应头驱动。首次使用前务必调用 SetAsync 设置初始 Access Token

内置 WeChatAccessTokenProvider(微信开放平台专用)#

若您的项目需要调用微信公众号/小程序等微信服务端接口,可直接使用内置的 WeChatAccessTokenProvider。该提供器自动管理 access_token 的获取、缓存和刷新,并支持根据微信错误码自动重试。

1. 注册提供器

微信 access_token 的获取需要 appIdappSecret,使用 ActivatorUtilities.CreateInstance 传入:

cs
services.AddHttpClient("wechat_api")    .ConfigureOptions((options, serviceProvider) =>    {        options.HttpAccessTokenProvider = ActivatorUtilities.CreateInstance<WeChatAccessTokenProvider>(            serviceProvider, "YourAppId", "YourAppSecret");    });

2. 自动管理流程 https://developers.weixin.qq.com/miniprogram/dev/server/API/mp-access-token/api_getaccesstoken.html

  • 首次请求:提供器自动调用微信 /cgi-bin/token 接口获取 access_token,并缓存到内存(默认提前 5 秒过期,避免因网络延迟导致使用失效 access_token)。
  • 自动注入access_token 会作为查询参数 ?access_token=xxx 附加到请求 URL
  • 过期刷新:当收到微信错误码 40001(无效凭证)、40014(无效 access_token)、42001access_token 过期)时,框架会自动获取新 access_token 并重试请求。
  • 无需手动操作:整个生命周期由框架自动管理,无需调用 SetAsync 手动设置初始 access_token

3. 错误重试说明

WeChatAccessTokenProvider 已重写 ShouldRefreshAsync,会同时检查 HTTP 状态码(401/403)和响应 JSON 中的 errcode。只有与 access_token 失效相关的错误码才会触发刷新,避免因临时网络问题或微信系统繁忙(如 -1)导致无意义的重试。


多节点集群部署#

当服务部署在多节点集群环境中时,默认的内存缓存会导致各节点独立管理 Access Token。一个节点获取或刷新 Token 后,其他节点的 Token 将随之失效,从而引发反复获取和刷新,甚至触发 API 限流。

为了解决这一问题,可以将 Access Token 的存储从内存迁移到分布式缓存(如 Redis)。只需实现 IHttpAccessTokenManager 接口并替换默认服务即可:

cs
public class RedisAccessTokenManager : IHttpAccessTokenManager{    // 实现接口方法,将 Access Token 存入 Redis 等分布式缓存}

然后在服务注册时替换默认实现:

cs
services.Replace(ServiceDescriptor.Singleton<IHttpAccessTokenManager, RedisAccessTokenManager>());

替换后,所有节点将共享同一份 Access Token,彻底避免多节点间的 Access Token 冲突和重复刷新问题。


通过上述配置,框架将自动处理 Access Token 的获取、刷新及注入,开发者无需关心 Access Token 过期时间、失效重试(如 401 自动重发)等细节,显著降低与第三方接口集成的复杂度。