一种更优的处理C#/Xamarin应用中授权后端服务并透明处理刷新令牌续期的方案





3.00/5 (1投票)
如何消费已授权的后端服务,同时透明地处理刷新令牌续订
大多数情况下,我们的移动(客户端)应用程序会处理后端服务以使用微服务,例如ASP .NET Core Web API。
调用这些资源无需身份验证非常容易,因为您无需担心Bearer令牌(JWT)或刷新令牌,您只需要发出一个简单的httpclient
请求,例如
using (var client = new HttpClient())
{
var responseMessage = await client.GetAsync(URL);
responseMessage.EnsureSuccessStatusCode();
var jsonResponse = await responseMessage.Content.ReadAsStringAsync();
var response = JsonConvert.DeserializeObject<IEnumerable<PersonInfo>>(jsonResponse);
return response;
}
在上面的代码中,我们只是向 WebAPI 发出一个HttpClient
请求并读取返回的数据。请注意,我们没有通过传递任何 JWT(Bearer 令牌)进行身份验证,也没有处理令牌过期和 http 请求重试。
使用刷新令牌处理 Bearer 令牌续期
出于安全原因,Bearer 令牌又名 JWT(JSON Web 令牌)不会永远有效,当它们过期时,我们必须获取一个新的令牌才能与后端服务通信。
有两种方法可以做到这一点
- 强制用户重新进行身份验证/再次登录,或者
- 使用您现有的刷新令牌检索新的 Bearer 令牌 + 刷新令牌对。
由于第一种选择会让用户感到厌烦,因此我们今天将在本文中介绍第二种选择。
在调用期间处理 Bearer 令牌(JWT)/刷新令牌续期时,使用WebApi
的典型工作流程如下所示
上述工作流程对于单个异步任务调用非常有效。
如果您同时发起多个异步任务调用,并且您的 Bearer 令牌在这些调用期间过期,那么所有调用都将尝试**独立地**使用它们拥有的刷新令牌来续期令牌,并且它们都将失败。
在这种情况下,您必须实现某种信号量,只允许第一个异步任务执行令牌续期,而其他异步任务则等待续期完成。
解决方案
我创建了一个名为RESTService
的Xamarin.Forms
服务类,负责处理 WebAPI 调用,这些类可以在我的Github上提供的示例代码中的 Xamarin Forms 项目中找到。
此类的主要目的是
- 对您的后端服务进行身份验证
- 执行您编写的任何
HttpClient
调用(GET
/POST
/PUT
/DELETE
)以使用您的 WebAPI - 优雅地处理 JWT 过期
- 透明地强制续期访问令牌/JWT/Bearer 令牌
- 在续期令牌时同步多个异步任务的执行
- 在令牌续期后立即恢复您的 WebAPI 调用
Xamarin 示例代码
在我的 Github 上,您可以下载完整的 C# 示例/可运行解决方案,它由一个Xamarin.Forms
项目和一个 ASP.NET Core 后端项目组成。
下面的屏幕截图来自示例项目。
后端项目假设
- 我不会更深入地介绍后端项目的细节,因为它仅仅是为了演示。
- 这个后端项目基本上是一个 ASPNET Core 3.1 项目,它使用内存中 Entity Framework Core,支持 JWT/刷新令牌,并包含两个 Web API,一个用于身份验证/刷新令牌,另一个用于演示正在检索的数据。
- 为简洁起见,我只实现了用户名/密码身份验证。如果您使用社交身份验证,我认为提供的代码是一个很好的起点,您可以自己实现。
- 为了测试目的,我将 Bearer 令牌 (JWT) 的过期时间配置为每 15 秒,刷新令牌的过期时间为 12 个月。您可以随意更改后端项目中的设置。
- 我在后端项目的 *Startup.cs* 中硬编码了一个用户,因此您必须使用**用户名 = “test”** 和**密码 = “test”** 进行测试。
Using the Code
如上所述,RESTService
类可在示例代码中找到,位于Xamarin.Forms
共享项目中的“_Services_”文件夹内。
身份验证
您必须先进行身份验证才能使用RESTService.ExecuteWithRetryAsync()
,因此让我们首先通过AuthWithCredentialsAsync()
方法进行身份验证
var response = await RESTService.AuthWithCredentialsAsync(Username, Password);
if (response.Success)
{
//Authenticated
}
else
{
//Authentication Failed
}
成功进行身份验证后,接收到的 Bearer 令牌 (JWT) 和刷新令牌将存储在本地缓存中,也可以通过RESTService.BearerToken
和RESTService.RefreshToken
访问,以便以后使用。
调用您的 WebAPI
RESTService.ExecuteWithRetryAsync()
接受一个 lambda 方法,该方法**应该是您自己的 httpclient 请求**,用于您想要检索/提交数据的WebAPI
控制器。
return RESTService.ExecuteWithRetryAsync(async () =>
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", $"bearer {RESTService.BearerToken}");
var responseMessage = await client.GetAsync(URL);
responseMessage.EnsureSuccessStatusCode();
var jsonResponse = await responseMessage.Content.ReadAsStringAsync();
var response = JsonConvert.DeserializeObject<IEnumerable<PersonInfo>>(jsonResponse);
return response;
}
});
几点说明
- 当前的
RESTService.BearerToken
应在您自己的httpclient
请求的标头(第 5 行)中提供。 - 您应该始终使用
EnsureSuccessStatusCode()
方法(第 9 行),以便在 Bearer 令牌 (JWT) 过期的情况下,抛出HttpRequestException
并内部处理它,以使用RefreshToken
开始令牌续期。
就是这样。
希望您喜欢!