使用 HttpClient 進行 JWT 身分驗證與呼叫需要授權的 API 和重新更新 Token 權杖的程式設計範例
設計符合 RESTful API 設計原則的回傳格式
#region APIResult
public class APIResult
{
public bool Status { get; set; } = false;
public string Message { get; set; } = "";
public object Payload { get; set; }
}
#endregion
設計一個進行身分驗證的 Login 服務類別
public class LoginManager : BaseWebAPI<LoginResponseDTO>
{
public LoginManager()
: base()
{
this.restURL = "/LoginShort";
this.host = Constants.HostAPI;
IsCollectionType = false;
EncodingType = EnctypeMethod.JSON;
NeedSaveData = true;
}
public async Task<APIResult> PostAsync(LoginRequestDTO loginRequestDTO, CancellationToken cancellationToken = default(CancellationToken))
{
#region 要傳遞的參數
HTTPPayloadDictionary dic = new HTTPPayloadDictionary();
dic.Add(Constants.JSONDataKeyName, JsonConvert.SerializeObject(loginRequestDTO));
#endregion
var mr = await this.SendAsync(dic, HttpMethod.Post, cancellationToken);
return mr;
}
}
public class LoginRequestDTO
{
public string Account { get; set; }
public string Password { get; set; }
}
public class LoginResponseDTO
{
public int Id { get; set; }
public string Account { get; set; }
public string Name { get; set; }
public string Token { get; set; }
public int TokenExpireMinutes { get; set; }
public string RefreshToken { get; set; }
public int RefreshTokenExpireDays { get; set; }
public string Image { get; set; }
public virtual DepartmentDTO Department { get; set; }
}
呼叫 RESTfule API 的基底服務類別
#region BaseWebAPI
/// <summary>
/// 存取Http服務的Base Class
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class BaseWebAPI<T>
{
#region Field
/// <summary>
/// WebAPI主機位置
/// </summary>
public string host = Constants.HostAPI;
/// <summary>
/// WebAPI方法網址
/// </summary>
public string restURL = "";
//public string AuthenticationHeaderBearerTokenValue = "";
/// <summary>
/// 指定 HTTP 標頭 Bearer 內,要放置的 JWT Token 值
/// </summary>
public string AuthenticationHeaderBearerTokenValue { get; set; }
/// <summary>
/// 要呼叫 REST API 的額外路由路徑
/// </summary>
public string RouteURL { get; set; } = "";
/// <summary>
/// 成功呼叫完成 API 之後,是否要儲存到本機檔案系統內
/// </summary>
public bool NeedSaveData { get; set; }
/// <summary>
/// 是否為集合型別的物件
/// </summary>
public bool IsCollectionType { get; set; } = true;
/// <summary>
/// 要傳遞的 HTTP Payload 使用的編碼格式
/// </summary>
public EnctypeMethod EncodingType;
/// <summary>
/// 資料夾名稱
/// </summary>
//public string CurrentFolderName = "";
public string CurrentFolderName { get; set; } = "";
public string SubFolderName { get; set; } = "";
public string TopDataFolderName { get; set; } = "Data";
/// <summary>
/// 檔案名稱
/// </summary>
public string DataFileName { get; set; } = "";
#region 系統用到的訊息字串
public static readonly string APIInternalError = "System Exception = null, Result = null";
#endregion
#endregion
// =========================================================================================================
#region protected
#endregion
// =========================================================================================================
#region Public
/// <summary>
/// 透過Http取得的資料,也許是一個物件,也許是List
/// </summary>
public List<T> Items { get; set; }
public T SingleItem { get; set; }
/// <summary>
/// 此次呼叫的處理結果
/// </summary>
public APIResult ManagerResult { get; set; }
#endregion
// =========================================================================================================
/// <summary>
/// 建構子,經由繼承後使用反射取得類別的名稱當作,檔案名稱及WebAPI的方法名稱
/// </summary>
public BaseWebAPI()
{
CurrentFolderName = TopDataFolderName;
restURL = "";
DataFileName = this.GetType().Name;
//子資料夾名稱 = 資料檔案名稱;
this.ManagerResult = new APIResult();
EncodingType = EnctypeMethod.JSON;
}
///// <summary>
///// 建立存取 Web 服務的參數
///// </summary>
///// <param name="_url">存取服務的URL</param>
///// <param name="_DataFileName">儲存資料的名稱</param>
///// <param name="_DataFolderName">資料要儲存的目錄</param>
///// <param name="_className">類別名稱</param>
//public void SetWebAccessCondition(string _url, string _DataFileName, string _DataFolderName, string _className = "")
//{
// string className = _className;
// this.restURL = string.Format("{0}{1}", _url, _className);
// this.資料檔案名稱 = _DataFileName;
// this.現在資料夾名稱 = _DataFolderName;
// this.managerResult = new APIResult();
//}
/// <summary>
/// 從網路取得相對應WebAPI的資料
/// </summary>
/// <param name="dic">所要傳遞的參數 Dictionary </param>
/// <param name="httpMethod">Get Or Post</param>
/// <returns></returns>
protected virtual async Task<APIResult> SendAsync(Dictionary<string, string> dic, HttpMethod httpMethod,
CancellationToken token = default(CancellationToken))
{
this.ManagerResult = new APIResult();
APIResult mr = this.ManagerResult;
string jsonPayload = "";
//檢查網路狀態
//if (UtilityHelper.IsConnected() == false)
//{
// mr.Status = false;
// mr.Message = "無網路連線可用,請檢查網路狀態";
// return mr;
//}
if (dic.ContainsKey(Constants.JSONDataKeyName))
{
jsonPayload = dic[Constants.JSONDataKeyName];
dic.Remove(Constants.JSONDataKeyName);
}
HttpClientHandler handler = new HttpClientHandler();
using (HttpClient client = new HttpClient(handler))
{
try
{
//client.Timeout = TimeSpan.FromMinutes(3);
string fooQueryString = dic.ToQueryString();
string fooUrl = $"{host}{restURL}{RouteURL}" + fooQueryString;
UriBuilder ub = new UriBuilder(fooUrl);
HttpResponseMessage response = null;
#region 檢查是否要將 JWT Token 放入 HTTP 標頭 Bearer 內
if (string.IsNullOrEmpty(this.AuthenticationHeaderBearerTokenValue) == false)
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", this.AuthenticationHeaderBearerTokenValue);
}
#endregion
#region 執行 HTTP 動詞 (Action) : Get, Post, Put, Delete
if (httpMethod == HttpMethod.Get)
{
// 使用 Get 方式來呼叫
response = await client.GetAsync(ub.Uri, token);
}
else if (httpMethod == HttpMethod.Post)
{
// 使用 Post 方式來呼叫
switch (EncodingType)
{
case EnctypeMethod.MULTIPART:
// 使用 MULTIPART 方式來進行傳遞資料的編碼
response = await client.PostAsync(ub.Uri, dic.ToMultipartFormDataContent(), token);
break;
case EnctypeMethod.FORMURLENCODED:
// 使用 FormUrlEncoded 方式來進行傳遞資料的編碼
response = await client.PostAsync(ub.Uri, dic.ToFormUrlEncodedContent(), token);
break;
case EnctypeMethod.JSON:
client.DefaultRequestHeaders.Accept.TryParseAdd("application/json");
response = await client.PostAsync(ub.Uri, new StringContent(jsonPayload, Encoding.UTF8, "application/json"));
break;
default:
throw new Exception("不正確的 HTTP Payload 編碼設定");
break;
}
}
else if (httpMethod == HttpMethod.Put)
{
// 使用 Post 方式來呼叫
switch (EncodingType)
{
case EnctypeMethod.MULTIPART:
// 使用 MULTIPART 方式來進行傳遞資料的編碼
response = await client.PutAsync(ub.Uri, dic.ToMultipartFormDataContent(), token);
break;
case EnctypeMethod.FORMURLENCODED:
// 使用 FormUrlEncoded 方式來進行傳遞資料的編碼
response = await client.PutAsync(ub.Uri, dic.ToFormUrlEncodedContent(), token);
break;
case EnctypeMethod.JSON:
client.DefaultRequestHeaders.Accept.TryParseAdd("application/json");
response = await client.PutAsync(ub.Uri, new StringContent(jsonPayload, Encoding.UTF8, "application/json"));
break;
default:
throw new Exception("不正確的 HTTP Payload 編碼設定");
break;
}
}
else if (httpMethod == HttpMethod.Delete)
{
response = await client.DeleteAsync(ub.Uri, token);
}
else
{
throw new NotImplementedException("Not Found HttpMethod");
}
#endregion
#region Response
if (response != null)
{
String strResult = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode == true)
{
mr = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
if (mr.Status == true)
{
var fooDataString = mr.Payload.ToString();
if (IsCollectionType == false)
{
SingleItem = JsonConvert.DeserializeObject<T>(fooDataString, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
if (NeedSaveData == true)
{
if (SingleItem == null)
{
SingleItem = (T)Activator.CreateInstance(typeof(T));
}
await this.WriteToFileAsync();
}
}
else
{
Items = JsonConvert.DeserializeObject<List<T>>(fooDataString, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
if (NeedSaveData == true)
{
if (Items == null)
{
Items = (List<T>)Activator.CreateInstance(typeof(List<T>));
}
await this.WriteToFileAsync();
}
}
}
}
else
{
APIResult fooAPIResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
if (fooAPIResult != null)
{
mr = fooAPIResult;
}
else
{
mr.Status = false;
mr.Message = string.Format("Error Code:{0}, Error Message:{1}", response.StatusCode, response.Content);
}
}
}
else
{
mr.Status = false;
mr.Message = APIInternalError;
}
#endregion
}
catch (Exception ex)
{
mr.Status = false;
mr.Message = ex.Message;
}
}
return mr;
}
/// <summary>
/// 將物件資料從檔案中讀取出來
/// </summary>
public virtual async Task ReadFromFileAsync()
{
#region 先將建立該資料模型的物件,避免檔案讀取不出來之後, Items / SingleItem 的物件值為 null
if (IsCollectionType == false)
{
SingleItem = (T)Activator.CreateInstance(typeof(T));
}
else
{
Items = (List<T>)Activator.CreateInstance(typeof(List<T>));
}
#endregion
string data = await StorageUtility.ReadFromDataFileAsync(this.CurrentFolderName, this.DataFileName);
if (string.IsNullOrEmpty(data) == true)
{
}
else
{
try
{
if (IsCollectionType == false)
{
this.SingleItem = JsonConvert.DeserializeObject<T>(data, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
}
else
{
this.Items = JsonConvert.DeserializeObject<List<T>>(data, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
/// <summary>
/// 將物件資料寫入到檔案中
/// </summary>
public virtual async Task WriteToFileAsync()
{
string data = "";
if (IsCollectionType == false)
{
data = JsonConvert.SerializeObject(this.SingleItem);
}
else
{
data = JsonConvert.SerializeObject(this.Items);
}
await StorageUtility.WriteToDataFileAsync(this.CurrentFolderName, this.DataFileName, data);
}
}
#endregion
開始進行測試
{
"id": 25,
"account": "user50",
"name": "Account50",
"token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiIyNSIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJ1c2VyNTAiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiVXNlciIsIkRlcHQxIl0sImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvdmVyc2lvbiI6IjAiLCJleHAiOjE1NjQzOTk5OTUsImlzcyI6IlhhbWFyaW5Gb3Jtc1dTLnZ1bGNhbi5uZXQiLCJhdWQiOiJYYW1hcmluLkZvcm1zIEFwcCJ9.XpO0wCvNVBvNpZT4AaBz83VK-xFMvVMjxTSQ0MMSOVycz-LwVk71BQT25c1OPRcnxhX4B0jBNBRlbmAJT4WldQ",
"tokenExpireMinutes": 15,
"refreshToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiIyNSIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJ1c2VyNTAiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiVXNlciIsIlJlZnJlc2hUb2tlbiJdLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3ZlcnNpb24iOiIwIiwiZXhwIjoxNTY1MDA1MDg1LCJpc3MiOiJYYW1hcmluRm9ybXNXUy52dWxjYW4ubmV0IiwiYXVkIjoiWGFtYXJpbi5Gb3JtcyBBcHAifQ.Gisq0bSRTRIzci19nNy83wfi0fMJyVPvBjyoRfWNWbca2maQwC0lWsvpBKKu5ZlZQBWWVH7JMrKH_CpvI5Tksw",
"refreshTokenExpireDays": 7,
"image": "",
"level": 0,
"department": {
"id": 1
}
}
錯誤代碼 1, 存取權杖可用期限已經逾期超過
{
"id": 25,
"account": "user50",
"name": "Account50",
"token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiIyNSIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJ1c2VyNTAiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiVXNlciIsIkRlcHQxIl0sImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvdmVyc2lvbiI6IjAiLCJleHAiOjE1NjQ0MDAwMTUsImlzcyI6IlhhbWFyaW5Gb3Jtc1dTLnZ1bGNhbi5uZXQiLCJhdWQiOiJYYW1hcmluLkZvcm1zIEFwcCJ9.0oJSRSu0zqFuE66HRRB_ulJlT8y8dcTJj01p-t7htngMtODmVCn8XUfvD3PMz0CN8F7tPOwJfqveJUFD3Xezvw",
"tokenExpireMinutes": 15,
"refreshToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiIyNSIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJ1c2VyNTAiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiVXNlciIsIlJlZnJlc2hUb2tlbiJdLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3ZlcnNpb24iOiIwIiwiZXhwIjoxNTY1MDA1MTA1LCJpc3MiOiJYYW1hcmluRm9ybXNXUy52dWxjYW4ubmV0IiwiYXVkIjoiWGFtYXJpbi5Gb3JtcyBBcHAifQ.Zxy_f5A3JY20c5XsQp5vUJlm901dy0jwW-rbOfOQXWld6TaCh4oPp-NuiavWdcjyt9oV8B8CtLHzxeZLVDsbhw",
"refreshTokenExpireDays": 7,
"image": "",
"level": 0,
"department": {
"id": 1
}
}
static async Task Main(string[] args)
{
LoginManager loginManager = new LoginManager();
Output("進行登入身分驗證");
APIResult result = await loginManager.PostAsync(new LoginRequestDTO()
{
Account = "user50",
Password = "password50"
});
if (result.Status == true)
{
Console.WriteLine($"登入成功");
Console.WriteLine($"{result.Payload}");
}
else
{
Console.WriteLine($"登入失敗");
Console.WriteLine($"{result.Message}");
}
Thread.Sleep(2000);
Output("利用取得的 JTW Token 呼叫取得部門資訊 Web API");
DepartmentsManager departmentsManager = new DepartmentsManager();
result = await departmentsManager.GetAsync(loginManager.SingleItem.Token);
if (result.Status == true)
{
Console.WriteLine($"取得部門資料成功");
Console.WriteLine($"{result.Payload}");
}
else
{
Console.WriteLine($"取得部門資料失敗");
Console.WriteLine($"{result.Message}");
}
Console.WriteLine("等候10秒鐘,等待 JWT Token 失效");
await Task.Delay(10000);
departmentsManager = new DepartmentsManager();
Output("再次呼叫取得部門資訊 Web API,不過,該 JWT Token已經失效了");
result = await departmentsManager.GetAsync(loginManager.SingleItem.Token);
if (result.Status == true)
{
Console.WriteLine($"取得部門資料成功");
Console.WriteLine($"{result.Payload}");
}
else
{
Console.WriteLine($"取得部門資料失敗");
Console.WriteLine($"{result.Message}");
}
Thread.Sleep(2000);
RefreshTokenService refreshTokenService = new RefreshTokenService();
Output("呼叫更新 JWT Token API,取得更新的 JWT Token");
result = await refreshTokenService.GetAsync(loginManager.SingleItem.RefreshToken);
if (result.Status == true)
{
Console.WriteLine($"更新 JWT Token 成功");
Console.WriteLine($"{result.Payload}");
}
else
{
Console.WriteLine($"更新 JWT Token 失敗");
Console.WriteLine($"{result.Message}");
}
Thread.Sleep(2000);
departmentsManager = new DepartmentsManager();
Output("再次呼叫取得部門資訊 Web API,不過,使用剛剛取得的更新 JWT Token");
result = await departmentsManager.GetAsync(refreshTokenService.SingleItem.Token);
if (result.Status == true)
{
Console.WriteLine($"取得部門資料成功");
Console.WriteLine($"{result.Payload}");
}
else
{
Console.WriteLine($"取得部門資料失敗");
Console.WriteLine($"{result.Message}");
}
Thread.Sleep(2000);
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
}