2.22Access Token 自动管理
在与第三方服务(如微信公众号、企业微信等)对接时,通常需要先获取 Access Token,并在后续请求中携带该 Access Token 才能正常调用接口。Access Token 具有有效期(通常为两小时),过期后需重新获取并更新。
在与第三方服务(如微信公众号、企业微信等)对接时,通常需要先获取 Access Token,并在后续请求中携带该 Access Token 才能正常调用接口。Access Token 具有有效期(通常为两小时),过期后需重新获取并更新。
为简化这一流程,框架内置了 Access Token 自动管理机制:当 Access Token 不存在或已过期时,自动获取新 Access Token,并按配置将其注入到请求的 Header、Query、Cookie 等位置,同时支持在请求因 Access Token 失效(如返回 401)时自动重试。
HttpAccessToken 模型#
HttpAccessToken 表示一个 Access Token 信息,包含以下构造函数、属性和方法:
- 构造函数:
new(value, expiresAt):传入Access Token和绝对过期时间(UTC时间)。new(jwtToken):传入JWT Token字符串。
- 属性:
Value:Access Token值(string类型)。ExpiresAt:Access Token的绝对过期时间(DateTimeOffset类型)。Scheme:HTTP认证方案(string?类型)。RefreshToken:刷新令牌(string?类型),内部基于Items["refresh_token"]提供便捷访问。Items:共享数据字典(IDictionary<object, object?>类型),用于存储与Access Token相关的自定义数据(如refresh_token、用户标识等)。
- 静态属性:
None:表示没有可用的Access Token(HttpAccessToken?类型)。
- 方法:
IsExpired()检查Access Token是否过期。SetExpiresAt(expiresAt)设置Access Token的绝对过期时间。
启用步骤#
1. 实现 IHttpAccessTokenProvider 接口#
该接口负责定义如何获取和刷新 Access Token。所有方法均接收 HttpAccessTokenContext 参数,可通过 context.Items 获取请求时传入的自定义数据(如用户名、密码)。示例:
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 自动管理:#
// 配置默认客户端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。
配置注入位置(Header、Query、Cookie 等)#
默认情况下,Access Token 会以 Authorization 请求头形式发送。开发者可通过设置 HttpAccessToken.Scheme 属性指定认证方案(如 Bearer):
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 实现类上继承该接口,既减少类型定义,又便于集中管理:
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:
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); }}然后在服务容器中注册该实现:
services.TryAddSingleton<IHttpAccessTokenConfigurator, CustomHttpAccessTokenConfigurator>();自定义 Access Token 刷新触发条件#
默认情况下,当接收到 HTTP 401 Unauthorized 响应时,框架会强制刷新 Access Token 并重试请求。若您的 API 通过其他状态码(如 403)或响应内容内容标识 Access Token 失效,可重写 IHttpAccessTokenProvider 接口的 ShouldRefreshAsync 方法:
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 等方法使用。
var result = await httpRemoteService.SendAsync( HttpRequestBuilder.Get("https://api.furion.net/data") .SetHttpClientName("myapi") // 可选 .WithAccessTokenData("username", "admin") .WithAccessTokenData("password", "123456"));手动设置 Access Token(SetAsync)#
除了让框架自动调用 GetAsync 获取 Access Token,还支持在登录成功后手动设置 Access Token,然后重写 RefreshAsync 实现基于 RefreshToken 的刷新逻辑。
通过注入 IHttpAccessTokenManager 接口,调用 SetAsync 方法将 Token 存入框架缓存:
// 登录成功后,手动设置 Access Tokenvar token = new HttpAccessToken(accessToken, expiresAt) { RefreshToken = refreshToken };await httpAccessTokenManager.SetAsync("myapi", token);此后框架将从缓存中读取 Access Token,过期时自动调用您重写的 RefreshAsync 进行刷新。此时 GetAsync 可以返回 null 或抛出异常(不会被调用)。
一个典型的手动刷新实现如下:
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-token 和 x-access-token 响应头,实现无感滚动刷新。
1. 注册提供器
services.AddHttpClient("furion_api") .ConfigureOptions((options, serviceProvider) => { options.HttpAccessTokenProvider = ActivatorUtilities.CreateInstance<FurionAccessTokenProvider>(serviceProvider); });如果希望手动控制实例化,也可以显式传入依赖:
services.AddHttpClient("furion_api") .ConfigureOptions((options, serviceProvider) => { options.HttpAccessTokenProvider = new FurionAccessTokenProvider(serviceProvider.GetRequiredService<IHttpAccessTokenManager>()); });2. 登录成功后手动设置初始 Access Token
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-token 和 x-access-token 响应头自动更新缓存,无需任何额外代码。
注意:
FurionAccessTokenProvider不会根据HTTP 401触发刷新,因为它的刷新逻辑完全由响应头驱动。首次使用前务必调用SetAsync设置初始Access Token。
内置 WeChatAccessTokenProvider(微信开放平台专用)#
若您的项目需要调用微信公众号/小程序等微信服务端接口,可直接使用内置的 WeChatAccessTokenProvider。该提供器自动管理 access_token 的获取、缓存和刷新,并支持根据微信错误码自动重试。
1. 注册提供器
微信 access_token 的获取需要 appId 和 appSecret,使用 ActivatorUtilities.CreateInstance 传入:
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)、42001(access_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 接口并替换默认服务即可:
public class RedisAccessTokenManager : IHttpAccessTokenManager{ // 实现接口方法,将 Access Token 存入 Redis 等分布式缓存}然后在服务注册时替换默认实现:
services.Replace(ServiceDescriptor.Singleton<IHttpAccessTokenManager, RedisAccessTokenManager>());替换后,所有节点将共享同一份 Access Token,彻底避免多节点间的 Access Token 冲突和重复刷新问题。
通过上述配置,框架将自动处理 Access Token 的获取、刷新及注入,开发者无需关心 Access Token 过期时间、失效重试(如 401 自动重发)等细节,显著降低与第三方接口集成的复杂度。