【发布时间】:2019-08-30 11:37:48
【问题描述】:
我有多个线程并行执行 API 调用。每当 jwt 令牌过期时,我希望我的所有线程等到刷新令牌 API 被调用并返回有效更新的 jwt 令牌。我有一个单例类,它有一个刷新令牌方法,它将进行调用并更新令牌。如何确保我的所有其他线程都等到令牌提取完成?
public class JWTTokenManager
{
private static JWTTokenManager _tokenManager;
private string _token;
private bool _refreshingToken;
public static JWTTokenManager GetManager()
{
if (_tokenManager == null)
_tokenManager = new JWTTokenManager();
return _tokenManager;
}
public void UpdateToken(string token)
{
_token = token;
}
public string GetToken()
{
return _token;
}
public async Task<bool> ValidateRefreshTocken()
{
UserInfo userdata = JsonConvert.DeserializeObject<UserInfo>(GetUserInfo(_token), new Helper.DefaultJsonSetting());
if (!string.IsNullOrWhiteSpace(userdata.Exp) && TokenExpired(long.Parse(userdata.Exp)))
{
_refreshingToken = true;
JWTToken jwtToken = Database.DBService.GetDB().FetchJWTToken();
RefreshToken requestRefresh = new RefreshToken
{
ExpiredTocken = jwtToken.Token,
RefreshTocken = jwtToken.RefreshToken
};
HttpClient httpClient = CloudService.GetCloudService().GetHttpClient();
HttpResponseMessage response = await httpClient.PostAsync($"account/v1/tokenRefresh", new StringContent(JsonConvert.SerializeObject(requestRefresh), Encoding.UTF8, "application/json"));
bool responseStatus = await ParseTokenResponseAsync(response);
_refreshingToken = false;
return responseStatus;
}
else
{
return true;
}
}
private string GetUserInfo(string key)
{
string[] base64Url = key.Split('.');
if (base64Url.Length > 1)
{
string userinfo = base64Url[1];
userinfo = userinfo.Replace(" ", "+");
int mod4 = userinfo.Length % 4;
if (mod4 > 0)
{
userinfo += new string('=', 4 - mod4);
}
var base64EncodedBytes = System.Convert.FromBase64String(userinfo);
return Encoding.UTF8.GetString(base64EncodedBytes);
}
else
{
return "";
}
}
public bool TokenExpired(long unixTimeStamp)
{
DateTime tokenExpiryDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
tokenExpiryDateTime = tokenExpiryDateTime.AddSeconds(unixTimeStamp).ToLocalTime();
DateTime currentDateTime = DateTime.Now;
return DateTime.Compare(tokenExpiryDateTime, currentDateTime) <= 0;
}
public async Task<bool> ParseTokenResponseAsync(HttpResponseMessage httpResponse)
{
if (httpResponse.IsSuccessStatusCode == true)
{
string responseString = await httpResponse.Content.ReadAsStringAsync();
Newtonsoft.Json.Linq.JObject responsedataObject = Newtonsoft.Json.Linq.JObject.Parse(responseString);
string token = responsedataObject["data"]["token"].ToString();
string refreshToken = responsedataObject["data"]["refreshToken"].ToString();
_token = token;
JWTToken updatedToken = new JWTToken()
{
Token = _token,
RefreshToken = refreshToken
};
Database.DBService.GetDB().InsertOrUpdateJWTToken(updatedToken);
return true;
}
else
{
return false;
}
}
}
public class CloudService
{
private const int TIME_OUT = 50;
private const int HTTP_GET = 0;
private const int HTTP_PUT = 1;
private const int HTTP_POST = 2;
private const int HTTP_DELETE = 3;
private static CloudService _serviceInstance;
public static CloudService GetCloudService()
{
if (_serviceInstance == null)
_serviceInstance = new CloudService();
return _serviceInstance;
}
private async Task<HttpResponseMessage> ExecuteHttpTask(int taskType, string url, StringContent content = null)
{
HttpClient httpClient = GetHttpClient();
switch (taskType)
{
case HTTP_GET:
return await httpClient.GetAsync(url);
case HTTP_PUT:
return await httpClient.PutAsync(url, content);
case HTTP_POST:
return await httpClient.PostAsync(url, content);
case HTTP_DELETE:
return await httpClient.DeleteAsync(url);
default:
return null;
}
}
public async Task<Response> HTTPTask(string url, int taskType, StringContent content = null, bool login = false)
{
bool refreshTocken = await JWTTokenManager.GetManager().ValidateRefreshTocken();
Response httpResponse = new Response();
try
{
HttpResponseMessage response = await ExecuteHttpTask(taskType, url, content);
string responseString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
httpResponse.status = "error";
else
httpResponse.status = "data";
httpResponse.data = ParseResponseData(httpResponse.status, responseString);
}
catch (Exception e)
{
httpResponse = GenericErrorResponse(e.Message);
}
return httpResponse;
}
public async Task<Response> GetSectionAsync(string id)
{
string url = $"catalog/v2/homepageSections/{id}?order-by=name,asc";
return await HTTPTask(url, Constants.HTTP_GET);
}
public async Task<Response> GetProductAsync(string id)
{
string url = $"catalog/v2/products/{id}";
return await HTTPTask(url, Constants.HTTP_GET);
}
public async Task<Response> GetCourseDetailsAsync(string id)
{
string url = $"catalog/v2/products/{id}/courseDetails";
return await HTTPTask(url, Constants.HTTP_GET);
}
}
不同的线程会调用 ClouService 中的方法,这些方法依次并行调用不同的 API,所有这些都通过 HTTPTask 方法,在该方法中验证令牌,如果无效,则调用 API 以获取更新的令牌。如何让所有 API(线程)从令牌无效的那一刻起一直等待,直到 API 返回有效令牌?
基于 cmets,我更新了这两个类。请看一下。
public sealed class JWTTokenManager
{
private static readonly JWTTokenManager _tokenManager = new JWTTokenManager();
private static SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private string _token;
// Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
static JWTTokenManager()
{
}
private JWTTokenManager()
{
}
public static JWTTokenManager GetManager()
{
return _tokenManager;
}
public void UpdateToken(string token)
{
_token = token;
}
public string GetToken()
{
return _token;
}
public async Task<bool> ValidateRefreshTocken(bool forceRefresh = false)
{
bool validToken;
await _semaphore.WaitAsync();
try
{
UserInfo userdata = JsonConvert.DeserializeObject<UserInfo>(GetUserInfo(_token), new DefaultJsonSetting());
if (forceRefresh || !string.IsNullOrWhiteSpace(userdata.Exp) && TokenExpired(long.Parse(userdata.Exp)))
{
validToken = await RefreshToken();
}
else
{
validToken = true;
}
}
catch (Exception exception)
{
Console.WriteLine(exception);
validToken = false;
}
finally
{
_semaphore.Release();
}
return validToken;
}
private async Task<bool> RefreshToken()
{
JWTToken jwtToken = Database.DBService.GetDB().FetchJWTToken();
RefreshToken requestRefresh = new RefreshToken
{
ExpiredTocken = jwtToken.Token,
RefreshTocken = jwtToken.RefreshToken
};
HttpClient httpClient = CloudService.GetCloudService().GetHttpClient();
HttpResponseMessage response = await httpClient.PostAsync($"account/v1/tokenRefresh", new StringContent(JsonConvert.SerializeObject(requestRefresh), Encoding.UTF8, "application/json"));
bool status = await ParseTokenResponseAsync(response);
return status;
}
private string GetUserInfo(string key)
{
string[] base64Url = key.Split('.');
if (base64Url.Length > 1)
{
string userinfo = base64Url[1];
userinfo = userinfo.Replace(" ", "+");
int mod4 = userinfo.Length % 4;
if (mod4 > 0)
{
userinfo += new string('=', 4 - mod4);
}
var base64EncodedBytes = System.Convert.FromBase64String(userinfo);
return Encoding.UTF8.GetString(base64EncodedBytes);
}
else
{
return "";
}
}
public bool TokenExpired(long unixTimeStamp)
{
DateTime tokenExpiryDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
tokenExpiryDateTime = tokenExpiryDateTime.AddSeconds(unixTimeStamp).ToLocalTime();
DateTime currentDateTime = DateTime.Now;
return DateTime.Compare(tokenExpiryDateTime, currentDateTime) <= 0;
}
public async Task<bool> ParseTokenResponseAsync(HttpResponseMessage httpResponse)
{
if (httpResponse.IsSuccessStatusCode == true)
{
string responseString = await httpResponse.Content.ReadAsStringAsync();
Newtonsoft.Json.Linq.JObject responsedataObject = Newtonsoft.Json.Linq.JObject.Parse(responseString);
string token = responsedataObject["data"]["token"].ToString();
string refreshToken = responsedataObject["data"]["refreshToken"].ToString();
_token = token;
JWTToken updatedToken = new JWTToken()
{
Token = _token,
RefreshToken = refreshToken
};
Database.DBService.GetDB().InsertOrUpdateJWTToken(updatedToken);
return true;
}
else
{
return false;
}
}
}
public sealed class CloudService
{
private const int TIME_OUT = 50;
private const int HTTP_GET = 0;
private const int HTTP_PUT = 1;
private const int HTTP_POST = 2;
private const int HTTP_DELETE = 3;
private static readonly CloudService _serviceInstance = new CloudService();
static CloudService()
{
}
private CloudService()
{
}
public static CloudService GetCloudService()
{
return _serviceInstance;
}
public HttpClient GetHttpClient()
{
HttpClient httpClient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(TIME_OUT),
BaseAddress = new Uri($"{AppConst.ServerBaseURL}/"),
};
httpClient.DefaultRequestHeaders.Add("X-Jwt-Token", JWTTokenManager.GetManager().GetToken());
httpClient.DefaultRequestHeaders.Add("tenantId", AppConst.TenanatID);
return httpClient;
}
private async Task<HttpResponseMessage> ExecuteHttpTask(int taskType, string url, StringContent content = null)
{
HttpClient httpClient = GetHttpClient();
switch (taskType)
{
case HTTP_GET:
return await httpClient.GetAsync(url);
case HTTP_PUT:
return await httpClient.PutAsync(url, content);
case HTTP_POST:
return await httpClient.PostAsync(url, content);
case HTTP_DELETE:
return await httpClient.DeleteAsync(url);
default:
return null;
}
}
public async Task<Response> HTTPTask(string url, int taskType, StringContent content = null, bool login = false)
{
Response httpResponse = new Response();
try
{
HttpResponseMessage response = await ExecuteHttpTask(taskType, url, content);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && !login)
{
bool refreshTocken = await JWTTokenManager.GetManager().ValidateRefreshTocken(true);
if (refreshTocken == true)
{
response = await ExecuteHttpTask(taskType, url, content);
}
else
{
httpResponse = GenericErrorResponse();
return httpResponse;
}
}
string responseString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
httpResponse.status = "error";
else
httpResponse.status = "data";
httpResponse.data = ParseResponseData(httpResponse.status, responseString);
}
catch (Exception e)
{
httpResponse = GenericErrorResponse(e.Message);
}
return httpResponse;
}
}
【问题讨论】:
-
无关:您的单例实现不是线程安全的。你确定你没有任何问题吗?
-
@Fildor 请您详细说明一下。
-
我的意思是理论上可能有不止一个 JWTTokenManager 实例。这可能会导致问题。尤其是在您尝试同步线程时。
-
您很可能忽略了隐式线程竞争错误。您无法确保令牌不会过期线程正忙于尝试使用它。所以这不是阻塞线程,你必须重新启动它们。
标签: c# multithreading asynchronous async-await thread-synchronization