2019年5月21日 星期二

將圖片檔案上傳到遠端伺服器

將圖片檔案上傳到遠端伺服器

由於最近正在進行 Xamarin.Forms 的拍照與檔案上傳的練習,因此,特別設計一個 RESTful 上傳圖片的 API,不過,該 API 是不需要經過身分驗證,而是可以直接將圖片檔案上傳到遠端伺服器,所以,撰寫出一篇文章,可以使用 Console 類型的專案,將一個特定的圖片檔案,使用 HttpClient 配合 Multipart 編碼方式,將圖片檔案上傳到遠端伺服器上,若成功上傳之後,將會取得該圖片的 URL 網址,最後,可以透過該 URL 網址來取得該圖片。不過,該上傳 URL 僅用於練習之用,因此,上傳上來的圖片將會隨時有可能的不經通告,就會直接刪除。
在這篇文章所提到的範例程式碼,可以從這裡 GitHub 取得
首先,先要設計呼叫這個上傳圖片 Web API 會用到的相關類別,因為這個練習用的 ASP.NET Core 設計的 Web API 專案,是遵循 REST 規範而設計出來的,因此,當呼叫這台伺服器上的 RESTful Web API 的時候,都會回傳 APIResult 格式的 JSON 物件,透過該物件可以了解到此次呼叫 Web API 是否成功或者失敗,失敗的原因為何?而該失敗原因的說明內容,將會定義在遠端伺服器上的錯誤訊息對應表中。
若此次呼叫 Web API 成功,則可以從 APIResult.Payload 這個屬性中取得遠端伺服器回傳的 JSON 物件內容,現在,便可以根據所呼叫的 Web API 當初設計的規格,使用相對應的類別,反序列化該 JSON 物件成為 .NET 物件即可;最後,透過 UploadImageResponseDTO.ImageUrl 屬性,便可以得到該圖片在遠端伺服器上的 URL 囉。
C Sharp / C#
/// <summary>
/// 呼叫 API 回傳的制式格式
/// </summary>
public class APIResult
{
    /// <summary>
    /// 此次呼叫 API 是否成功
    /// </summary>
    public bool Status { get; set; } = true;
    public int HTTPStatus { get; set; } = 200;
    public int ErrorCode { get; set; }
    /// <summary>
    /// 呼叫 API 失敗的錯誤訊息
    /// </summary>
    public string Message { get; set; } = "";
    /// <summary>
    /// 呼叫此API所得到的其他內容
    /// </summary>
    public object Payload { get; set; }
}
public class UploadImageResponseDTO
{
    public string ImageUrl { get; set; }
}
當要進行圖片上傳的時候,當然需要先取得該圖片檔案,在這裡需要取得該圖片檔案的絕對路徑名稱(含檔案名稱) imageFileName 、該圖片檔案的名稱(沒有路徑) fileName 。
現在,可以建立一個 HttpClient 物件,這個練習專案使用的上傳圖片 API URL 為 http://lobworkshop.azurewebsites.net/api/UploadImage 。
為了要能夠上傳圖片檔案,不是一般的文字內容,而是二進位 Binary 的內容,此時,需要使用 MultipartFormDataContent 類別,將二進位的圖片檔案進行編碼,這樣就可以上傳到遠端伺服器上,而遠端伺服器上也可以透過相同的編碼方式,還原到原先的圖片檔案,對於這樣的編碼規範,可以參考 https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html 。當要上傳圖片的時候,將會使用 StreamContent 這個類別,取得該上傳檔案的 Stream 物件,便可以得到該圖片的檔案內容。
當完成圖片檔案的編碼物件產生之後,就可以使用 await client.PostAsync(url, content); 將該圖片上傳到遠端伺服器上。
當呼叫完成上傳圖片 API 之後,將會得到一個 JSON 文字物件,這個時候僅需使用 JsonConvert.DeserializeObject 將這個 JSON 物件反序列化成為 .NET 物件,這裡使用的方法將會是強型別的泛型方法。最後,上傳圖片的網址可以透過 uploadImageResponseDTO.ImageUrl 屬性來取得。
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("使用 HttpClient 上傳檔案範例");
        string path = Directory.GetCurrentDirectory();
        string fileName = "XamarinForms.jpg";
        string imageFileName = Path.Combine(path, fileName);

        HttpClient client = new HttpClient();
        string url = $"http://lobworkshop.azurewebsites.net/api/UploadImage";
        #region 將圖片檔案,上傳到網路伺服器上(使用 Multipart 的規範)
        // 規格說明請參考 https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
        using (var content = new MultipartFormDataContent())
        {
            // 開啟這個圖片檔案,並且讀取其內容
            using (var fs = File.Open(imageFileName, FileMode.Open))
            {
                var streamContent = new StreamContent(fs);
                streamContent.Headers.Add("Content-Type", "application/octet-stream");
                streamContent.Headers.Add("Content-Disposition", "form-data; name=\"file\"; filename=\"" + fileName + "\"");
                content.Add(streamContent, "file", fileName);

                // 上傳到遠端伺服器上
                HttpResponseMessage response = await client.PostAsync(url, content);

                if (response != null)
                {
                    if (response.IsSuccessStatusCode == true)
                    {
                        // 取得呼叫完成 API 後的回報內容
                        String strResult = await response.Content.ReadAsStringAsync();
                        APIResult apiResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
                        if (apiResult?.Status == true)
                        {
                            UploadImageResponseDTO uploadImageResponseDTO = JsonConvert.DeserializeObject<UploadImageResponseDTO>(apiResult.Payload.ToString());
                            Console.WriteLine("上傳圖片的網址");
                            Console.WriteLine(uploadImageResponseDTO.ImageUrl);
                        }
                    }
                }
            }
        }
        #endregion

        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
}
C Sharp / C#
csharp



2019年5月20日 星期一

取得後端 Web API 的 JWT Token ,並且呼叫相關 API 的做法

取得後端 Web API 的 JWT Token ,並且呼叫相關 API 的做法

在上一篇文章 
使用 C# HttpClient 追蹤與顯示 HTTP Request / Response 封包內容
 文章中,有說明如何呼叫遠端 Web API,進行使用者帳號身分驗證的工作,一旦身分驗證成功之後,將會得到遠端 API 伺服器產生的 JWT 權杖 Token,不過,該權杖有效期限僅有 15 分鐘有效時間。
在這篇文章中,將會使用登入驗證完成之後取得的權杖,呼叫 https://lobworkshop.azurewebsites.net/api/Invoices 這個發票 API,說明如何進行發票的 CRUD ,也就是 Create 新增、Retrive 查詢、Update 更新、Delete 刪除的作業。
在這篇文章所提到的範例程式碼,可以從這裡 GitHub 取得
為了要能夠完成這樣的練習,首先需要建立發票 API 會用到的 DTO 類別,這些類別如下所示:
C Sharp / C#
#region DTO 型別宣告
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 class InvoiceRequestDTO
{
    public int Id { get; set; }
    public string InvoiceNo { get; set; }
    public UserDTO user { get; set; }
    public DateTime Date { get; set; }
    public string Memo { get; set; }
}
public class InvoiceResponseDTO
{
    public int Id { get; set; }
    public string InvoiceNo { get; set; }
    public UserDTO user { get; set; }
    public DateTime Date { get; set; }
    public string Memo { get; set; }
}
public class UserDTO
{
    public int Id { get; set; }
}
/// <summary>
/// 呼叫 API 回傳的制式格式
/// </summary>
public class APIResult
{
    /// <summary>
    /// 此次呼叫 API 是否成功
    /// </summary>
    public bool Status { get; set; } = false;
    /// <summary>
    /// 呼叫 API 失敗的錯誤訊息
    /// </summary>
    public string Message { get; set; } = "";
    /// <summary>
    /// 呼叫此API所得到的其他內容
    /// </summary>
    public object Payload { get; set; }
}
#endregion
首先,來看看如何呼叫新增發票 API,在這裡要先建立要新增發票的物件 InvoiceRequestDTO invoiceRequestDTO ,接著,使用 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token); 敘述,把登入驗證成功後取得的 JWT Token,放入此次新增發票的請求的 HTTP 標頭 Header 內,如此,對於後端 Web Server 收到這個新增發票請求的時候,將會先檢查 JWT 權杖是否存,若存在,將會檢查該權杖是否正確、還在有效期限等等狀況,若沒有問題發生,將會觸發新增發票的 程式碼。
在這裡,將會使用 await client.PostAsync(url, new StringContent(httpJsonPayload, System.Text.Encoding.UTF8, "application/json")); 表示式,呼叫遠端的新增發票 API,第二個參數將會是此次要新增發票的內容。
C Sharp / C#
private static async Task<InvoiceResponseDTO> CreateInvoiceAsync(LoginResponseDTO loginResponseDTO)
{
    InvoiceResponseDTO invoiceResponseDTO = new InvoiceResponseDTO();
    string url = "https://lobworkshop.azurewebsites.net/api/Invoices";
    InvoiceRequestDTO invoiceRequestDTO = new InvoiceRequestDTO()
    {
        InvoiceNo = "123",
        Memo = "查理王",
        Date = new DateTime(2019, 05, 20),
        user = new UserDTO()
        {
            Id = loginResponseDTO.Id
        }
    };
    var httpJsonPayload = JsonConvert.SerializeObject(invoiceRequestDTO);
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token);
    HttpResponseMessage response = await client.PostAsync(url,
        new StringContent(httpJsonPayload, System.Text.Encoding.UTF8, "application/json"));

    if (response.IsSuccessStatusCode)
    {
        String strResult = await response.Content.ReadAsStringAsync();
        APIResult apiResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        if (apiResult.Status == true)
        {
            string itemJsonContent = apiResult.Payload.ToString();
            Console.WriteLine($"成功新增一筆發票 : {itemJsonContent}");
            invoiceResponseDTO = JsonConvert.DeserializeObject<InvoiceResponseDTO>(itemJsonContent, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        }
    }
    return invoiceResponseDTO;
}
對於想要修改遠端 Web API 資料庫內的發票紀錄,當然需要先取得該發票紀錄內容,在此,將會透過剛剛呼叫玩新增發票 API 後所得到的該發票紀錄,緊接著來呼叫修改 API。
在底下的呼叫修改發票方法中,第二個參數將會這次要修改發票的物件,而根據 REST 規範,當我們要進行修改某個紀錄的時候,需要提供一個 URI,該 URI 可以指向到該筆發票紀錄,因此,當要進行修改發票的時候,所要使用的 URL ,將會設定為 string url = $"https://lobworkshop.azurewebsites.net/api/Invoices/{UpdateItem.Id}"; ,也就是要把該發票紀錄的 ID 放到 URL 路徑。
接下來將會進行任意修改該發票紀錄,當然也需要將 JWT Token,使用這個敘述 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token); ,設定到此次 HTTP REST API 呼叫過程中,如此,就可以接著使用 await client.PutAsync(url, new StringContent(httpJsonPayload, System.Text.Encoding.UTF8, "application/json")); 敘述,呼叫遠端的發票更新 API。
C Sharp / C#
private static async Task<InvoiceResponseDTO> UpdateInvoiceAsync(LoginResponseDTO loginResponseDTO, InvoiceResponseDTO UpdateItem)
{
    InvoiceResponseDTO invoiceResponseDTO = new InvoiceResponseDTO();
    string url = $"https://lobworkshop.azurewebsites.net/api/Invoices/{UpdateItem.Id}";
    InvoiceRequestDTO invoiceRequestDTO = new InvoiceRequestDTO()
    {
        Id = UpdateItem.Id,
        InvoiceNo = UpdateItem.InvoiceNo,
        Memo = "修正" +UpdateItem.Memo,
        Date = UpdateItem.Date.AddDays(5),
        user = UpdateItem.user
    };
    var httpJsonPayload = JsonConvert.SerializeObject(invoiceRequestDTO);
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token);
    HttpResponseMessage response = await client.PutAsync(url,
        new StringContent(httpJsonPayload, System.Text.Encoding.UTF8, "application/json"));

    String strResult = await response.Content.ReadAsStringAsync();
    if (response.IsSuccessStatusCode)
    {
        APIResult apiResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        if (apiResult.Status == true)
        {
            string itemJsonContent = apiResult.Payload.ToString();
            Console.WriteLine($"成功修改一筆發票 : {itemJsonContent}");
            invoiceResponseDTO = JsonConvert.DeserializeObject<InvoiceResponseDTO>(itemJsonContent, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        }
    }
    return invoiceResponseDTO;
}
當想要查詢遠端 Web API 上的發票紀錄,可以使用 HTTP Get 方法,當然,也是要將 JWT Token 放到 HTTP 標頭 Header 上;在這個發票查詢的 API,將會回傳 List ,也就是 InvoiceResponseDTO 的集合物件。
C Sharp / C#
private static async Task<List<InvoiceResponseDTO>> QueryInvoiceAsync(LoginResponseDTO loginResponseDTO)
{
    List<InvoiceResponseDTO> invoiceResponseDTOs = new List<InvoiceResponseDTO>();
    string url = "https://lobworkshop.azurewebsites.net/api/Invoices";
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token);
    HttpResponseMessage response = await client.GetAsync(url);

    if (response.IsSuccessStatusCode)
    {
        String strResult = await response.Content.ReadAsStringAsync();
        APIResult apiResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        String strResult = await response.Content.ReadAsStringAsync();
        if (apiResult.Status == true)
        {
            Console.WriteLine($"成功查詢發票 : {itemJsonContent}");
            invoiceResponseDTOs = JsonConvert.DeserializeObject<List<InvoiceResponseDTO>>(itemJsonContent, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        }
    }
    return invoiceResponseDTOs;
}
當要刪除某個發票紀錄的時候,根據 REST 規範,一樣需要使用 URI 標示出該發票紀錄所在的地方,在這裡將會使用這樣的敘述 string url = $"https://lobworkshop.azurewebsites.net/api/Invoices/{Id}"; ,當然,同樣也需要把 JWT Token 放到此次的 HTTP 呼叫標頭上。
最後,使用 await client.DeleteAsync(url); 敘述,呼叫遠端刪除的 API。
C Sharp / C#
private static async Task<InvoiceResponseDTO> DeleteInvoiceAsync(LoginResponseDTO loginResponseDTO, int Id)
{
    InvoiceResponseDTO invoiceResponseDTO = new InvoiceResponseDTO();
    string url = $"https://lobworkshop.azurewebsites.net/api/Invoices/{Id}";
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token);
    HttpResponseMessage response = await client.DeleteAsync(url);

    String strResult = await response.Content.ReadAsStringAsync();
    if (response.IsSuccessStatusCode)
    {
        APIResult apiResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        if (apiResult.Status == true)
        {
            string itemJsonContent = apiResult.Payload.ToString();
            Console.WriteLine($"成功刪除一筆發票 : {itemJsonContent}");
            invoiceResponseDTO = JsonConvert.DeserializeObject<InvoiceResponseDTO>(itemJsonContent, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        }
    }
    return invoiceResponseDTO;
}
現在,可以在 Main 方法內,使用底下的方法來進行發票的 CRUD 呼叫
C Sharp / C#
static async Task Main(string[] args)
{
    // 登入系統,取得 JTW Token
    await LoginAsync();

    // 從檔案中取得 JWT 權杖 Token
    string fileContent = await StorageUtility.ReadFromDataFileAsync("", "MyDataFolder", "MyFilename.txt");
    LoginResponseDTO loginResponseDTO = JsonConvert.DeserializeObject<LoginResponseDTO>(fileContent);

    #region CRUD => Retrive 取得該使用者的發票資料
    List<InvoiceResponseDTO> invoiceResponseDTOs = await QueryInvoiceAsync(loginResponseDTO);
    PrintAllInvoice(invoiceResponseDTOs);
    #endregion

    #region CRUD => Create 新增發票資料
    InvoiceResponseDTO invoiceResponseDTO = await CreateInvoiceAsync(loginResponseDTO);
    #endregion

    #region CRUD => Retrive 取得該使用者的發票資料
    invoiceResponseDTOs = await QueryInvoiceAsync(loginResponseDTO);
    PrintAllInvoice(invoiceResponseDTOs);
    #endregion

    #region CRUD => Update 修改發票資料
    invoiceResponseDTO = await UpdateInvoiceAsync(loginResponseDTO, invoiceResponseDTO);
    #endregion

    #region CRUD => Retrive 取得該使用者的發票資料
    invoiceResponseDTOs = await QueryInvoiceAsync(loginResponseDTO);
    PrintAllInvoice(invoiceResponseDTOs);
    #endregion

    #region CRUD => Delete 刪除發票資料
    foreach (var item in invoiceResponseDTOs)
    {
        await DeleteInvoiceAsync(loginResponseDTO, item.Id);
    }
    #endregion

    #region CRUD => Retrive 取得該使用者的發票資料
    invoiceResponseDTOs = await QueryInvoiceAsync(loginResponseDTO);
    PrintAllInvoice(invoiceResponseDTOs);
    #endregion

    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}
由於後端 Web API 所發出的 JWT Token 僅僅會有 15 分鐘的有效期限,因此,當您成功執行過該專案後,等候 15 分鐘之後,可以把 Main 方法內的 await LoginAsync(); 註解起來,再度執行看看,不過,先在 QueryInvoiceAsync 方法內的 if (apiResult.Status == true) 敘述上設定一個中斷點,當執行到這個中斷點,先查詢看看 apiResult.Status 屬性值,應該是 false,而在該敘述的上一行,那就是 String strResult = await response.Content.ReadAsStringAsync(); 這個敘述,請檢查看看 strResult 變數值,將會看到底下的內容,後端 Web API 將會因為這次呼叫查詢發票 API,因為所提供的 JWT 權杖 Token 已經逾期,因此,將會得到 401 的錯誤訊息。
{"Status":false,"HTTPStatus":401,"ErrorCode":1,"Message":"錯誤代碼 1, 存取權杖可用期限已經逾期超過","Payload":null}