C# winform自托管WebApi及身份信息加密、Basic驗證、Http Message Handler、跨域配置


1.介紹

1.1功能需求及介紹

由於具體情況需要,WebAPI不托管在IIS上,而是由Winform托管,且客戶端訪問服務端需要進行身份驗證,身份驗證的信息進行加密,另外需要允許由HTML頁面發出的跨域請求。

1.2內容分布說明

C# JS DES加密

首先研究數據加密算法,DES算法是最常用的對稱加密算法之一,雙方通過相同的秘鑰加解密。

C#提供System.Security.Cryptography進行加密,而JS有知名的crypto-js加密庫,提供有DES加密相關。

重點在於要讓兩種編程語言的加解密結果一致。

自托管WebAPI

在exe程序中運行WebAPI

Basic身份驗證

HTTP的基礎身份驗證方式

HTTPMessageHandler

防止API被惡意訪問,對無法驗證身份的訪問進行過濾

Web端跨域配置

訪問方式及配置

2.C# JS DES加密

2.1C# DES加密

首先添加相關引用using System.Security.Cryptography;

然后設置秘鑰private const string pKey = "12345678";注意秘鑰需要為8位,不可少,若多了需要看算法是否有截斷處理

2.1.1加密

        /// <summary>
        /// 加密
        /// </summary>
        /// <param name="StrOrign">待加密字符串,字符、數字、中文等</param>
        /// <returns>返回base64字符串</returns>
        public static string Encrypt(string StrOrign)
        {
            DESCryptoServiceProvider des = new DESCryptoServiceProvider();
            byte[] inputByteArray;
            inputByteArray = System.Text.Encoding.UTF8.GetBytes(StrOrign);
            // @#建立加密對象的密鑰和偏移量
            // @#原文使用ASCIIEncoding.ASCII方法的GetBytes方法
            // @#使得輸入密碼必須輸入英文文本
            des.Key = System.Text.Encoding.UTF8.GetBytes(pKey);
            des.IV = System.Text.Encoding.UTF8.GetBytes(pKey);
            des.Mode = CipherMode.ECB;
            des.Padding = PaddingMode.PKCS7;
            // @#寫二進制數組到加密流
            // @#(把內存流中的內容全部寫入)
            System.IO.MemoryStream ms = new System.IO.MemoryStream();
            CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(des.Key, des.IV), CryptoStreamMode.Write);
            // @#寫二進制數組到加密流
            // @#(把內存流中的內容全部寫入)
            cs.Write(inputByteArray, 0, inputByteArray.Length);
            cs.FlushFinalBlock();

            // '將8位無符號整數數組的值轉換成用Base64數字編碼的等效字符串表現形式,字符包含:A-Z a-z 0-9 + / 無值字符"="用於尾部空白
            byte[] bytes = ms.ToArray();
            string ret = Convert.ToBase64String(bytes);

            des.Dispose();
            cs.Dispose();
            ms.Dispose();
            return ret;
        }

2.1.2解密

        /// <summary>
        /// 解密。注意判斷傳入參數的有效性,非base64字符串會報錯,建議try-catch捕捉異常
        /// </summary>
        /// <param name="StrOrigin">待解密字符串,base64字符串</param>
        /// <returns>返回原始字符串</returns>
        public static string Decrypt(string StrOrigin)
        {
            DESCryptoServiceProvider des = new DESCryptoServiceProvider();           
            byte[] inputByteArray = Convert.FromBase64String(StrOrigin); // 將base64字符串轉換為16進制字節數組
            // @#建立加密對象的密鑰和偏移量,此值重要,不能修改
            des.Key = System.Text.Encoding.UTF8.GetBytes(pKey);
            des.IV = System.Text.Encoding.UTF8.GetBytes(pKey);
            des.Mode = CipherMode.ECB;
            des.Padding = PaddingMode.PKCS7;
            System.IO.MemoryStream ms = new System.IO.MemoryStream();
            CryptoStream cs = new CryptoStream(ms, des.CreateDecryptor(des.Key,des.IV), CryptoStreamMode.Write);
            cs.Write(inputByteArray, 0, inputByteArray.Length);
            cs.FlushFinalBlock();
            des.Dispose();
            cs.Dispose();
            ms.Dispose();
            return System.Text.Encoding.UTF8.GetString(ms.ToArray());
        }

2.1.3重要參數

算法中有幾個重要參數如des.Keydes.IVdes.Modedes.Padding需要將其顯式設置,否則會使用默認值而不一定與Js參數配置相同。

2.1.4格式編碼統一

需要注意的是,在加解密時,需要將原始字符串、秘鑰、偏移向量等參數都以相同方式進行編碼與解碼,另外加密完成后將密文轉換為base64類型。

解密時,由於接受base64類型密文,在處理前需要特別驗證是否為base64類型,否則算法崩潰。

加解密完成后需要將流關閉。

2.2JS DES加密

2.2.1crypto-js加密庫

DES加密需要添加這個庫里的幾個文件:tripledes.jsmode-ecb.js,另外為了方便操作,我使用了jQuery。

以下為相關代碼,完整源代碼在文末下載。

2.2.2加密

function encryptByDES(message, key) {
            var keyHex = CryptoJS.enc.Utf8.parse(key);
            var encrypted = CryptoJS.DES.encrypt(message, keyHex, {
                mode: CryptoJS.mode.ECB,
                padding: CryptoJS.pad.Pkcs7
            });
            // return base64toHEX(encrypted.toString());//base64轉16進制字符串
            return encrypted.toString();
        }

2.2.3解密

function decryptByDES(ciphertext, key) {
            // ciphertext=HexToBase64(ciphertext);//16進制轉base64
            var keyHex = CryptoJS.enc.Utf8.parse(key);
            // direct decrypt ciphertext
            var decrypted = CryptoJS.DES.decrypt({
                ciphertext: CryptoJS.enc.Base64.parse(ciphertext)
            }, keyHex, {
                mode: CryptoJS.mode.ECB,
                padding: CryptoJS.pad.Pkcs7
            });
            return decrypted.toString(CryptoJS.enc.Utf8);
        }

2.2.4 base64轉16進制

此代碼為網上資料

function base64toHEX(base64) {
            var raw = atob(base64);
            var HEX = '';
            for (i = 0; i < raw.length; i++) {
                var _hex = raw.charCodeAt(i).toString(16)
                HEX += (_hex.length == 2 ? _hex : '0' + _hex);
            }
            return HEX.toUpperCase();
        }

2.2.5 16進制轉base64

此代碼為網上資料

        function HexToBase64(sha1) {
            var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
            var base64_rep = "";
            var cnt = 0;
            var bit_arr = 0;
            var bit_num = 0;

            for (var n = 0; n < sha1.length; ++n) {
                if (sha1[n] >= 'A' && sha1[n] <= 'Z') {
                    ascv = sha1.charCodeAt(n) - 55;
                }
                else if (sha1[n] >= 'a' && sha1[n] <= 'z') {
                    ascv = sha1.charCodeAt(n) - 87;
                }
                else {
                    ascv = sha1.charCodeAt(n) - 48;
                }

                bit_arr = (bit_arr << 4) | ascv;
                bit_num += 4;
                if (bit_num >= 6) {
                    bit_num -= 6;

                    base64_rep += digits[bit_arr >>> bit_num];
                    bit_arr &= ~(-1 << bit_num);
                }
            }

            if (bit_num > 0) {
                bit_arr <<= 6 - bit_num;
                base64_rep += digits[bit_arr];
            }
            var padding = base64_rep.length % 4;

            if (padding > 0) {
                for (var n = 0; n < 4 - padding; ++n) {
                    base64_rep += "=";
                }
            }
            return base64_rep;

3.自托管WebAPI及Basic驗證、HTTP Message Handler

WebAPI托管運行在winform程序中,HTTP請求中需要附帶身份驗證header,通過消息處理程序提前批量過濾未驗證請求,而無需在每個API接口處驗證。

3.1自托管WebAPI

3.1.1引用

//引用自托管包
//Install-Package Microsoft.AspNet.WebApi.SelfHost -Version 5.2.7

3.3.2建立服務

        private HttpSelfHostServer server;//服務器對象

            try
            {
                HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(sUrl);
                config.MapHttpAttributeRoutes();              
                config.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = RouteParameter.Optional });

                server = new HttpSelfHostServer(config);
                server.OpenAsync().Wait();
                lblState.Text = "web服務已經啟動並運行中...";
                btnStop.Enabled = true;
                btnStart.Enabled = false;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }

3.3.3API控制器

建立一個AddController.cs的文件,並繼承ApiController。注意文件名一定要以Controller為后半部分,否則無法識別,不會生效。

一個簡單的接口,發送get請求,帶數字參數,服務端會將其乘10后返回。

    public class AddController : ApiController
    {
        [HttpGet]
        public string Add(int id)
        {
            return (id * 10).ToString();
        }
        [HttpGet]
        public string Add()
        {
            return "hello";
        }
    }

3.3.4注意

webapi服務端需要監聽端口,直接運行程序會報錯,需要將vs先以管理員權限運行再打開項目,如果只要運行程序,也可以管理員身份打開exe程序。

3.2Basic驗證

3.2.1Basic驗證方式

HTTP請求中有身份驗證頭Authorization,格式為:Authorization: ,其中type有多種類型,basic是其中一種,credentials是憑證信息明文,需要用戶自己加密。

basic驗證只是提供一種身份驗證規范,具體的驗證方式需要自己構造。

3.2.2客戶端構造

客戶端構造basic驗證只需要為Authorization標頭設置scheme和parameter兩個參數,scheme是類型,parameter是身份信息。

            reqMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(scheme, parameter);

客戶端發送GET請求完整代碼為:

        public static string HttpWithAuthorize(string url, string scheme,string parameter)
        {
            HttpClient client = new HttpClient();
            HttpRequestMessage reqMessage = new HttpRequestMessage();
            Uri thisUri = new Uri(url);
            reqMessage.Method = HttpMethod.Get;
            reqMessage.RequestUri = thisUri;
            reqMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(scheme, parameter);

            ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
            HttpResponseMessage resp = client.SendAsync(reqMessage).Result;
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("響應碼:" + resp.StatusCode);
            sb.AppendLine("返回值:" + resp.Content.ReadAsStringAsync().Result);
            return sb.ToString();
        }

3.2.3服務端解析

檢查HTTP請求的header,讀取數據並解密,與服務端保存的身份信息對比,判斷是否通過。

        private bool ValidateRequest(HttpRequestMessage message)//驗證信息解密並對比
        {
            var authorization = message.Headers.Authorization;
            //如果此header為空或不是basic方式則返回未授權
            if (authorization != null && authorization.Scheme == "Basic" && authorization.Parameter != null)
            {
                string Parameter = DES1.Decrypt(authorization.Parameter);
System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(authorization.Parameter));//將base64轉為字符串形式
                return (Parameter == "lbh:123456");
            }
            else
            {
                return false;
            }
        }

3.3HTTP Message Handler

3.3.1建立驗證文件

新建BasicAuthorizationHandler.cs文件,繼承DelegatingHandler

    public class BasicAuthorizationHandler:DelegatingHandler

對傳入服務端的請求進行處理:

        protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            //包含自定義header字段的跨域請求,瀏覽器會先向服務器發送OPTIONS請求,探測該服務器是否允許自定義的跨域字段。如果允許,則繼續實際的POST/GET正常請求,否則,返回錯誤。https://blog.csdn.net/xuedapeng/article/details/79076704
            //必須理解在正常請求前會收到option請求,如果不讓其通過message handler,則既不能通過身份驗證(沒有authorization頭),也不能通過跨域訪問控制(在本類中即被返回response,根本到達不了路由配置處),因此在前端頁面會出現兩種錯誤讓人無法理解
            if (request.Method == HttpMethod.Options)
            {
                //如果是前端用於驗證跨域允許的option請求,則將其轉到路由配置處處理,路由跨域配置處理后前端會發送真正的請求,由下面的身份驗證代碼處理
                var optRes = await base.SendAsync(request, cancellationToken);
                return optRes;
            }
            if (!ValidateRequest(request))
            {
                //注意:和上面同樣道理,假如身份驗證失敗,該請求沒有進入路由跨域配置(真正的請求而非option),直接返回后前端會出現跨域錯誤。不過問題不大,此時是有403和cors兩種錯誤,前端能據此定位錯誤。

                //沒有通過則創建response,code為401
                var response = new HttpResponseMessage(HttpStatusCode.Forbidden);//用403拒絕訪問。如果用401未授權且有頁面的話會被重定向到登錄頁
                var content=new Result{
                    success=false,
                    errs=new []{"服務端拒絕訪問:你沒有權限"}
                };
                //添加響應頭返回給跨域請求
                //response.Headers.Add("Access-Control-Allow-Origin", "*");
                //response.Headers.Add("Cache-Control", "no-cache");
                //response.Headers.Add("Access-Control-Allow-Headers", "Content-Type,Authorization,token");//允許自定義頭
                //response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
              
                response.Content = new StringContent(JsonConvert.SerializeObject(content), Encoding.UTF8, "application/json");//content添加錯誤信息
                return response;//返回響應
            }
            var res = await base.SendAsync(request, cancellationToken);
            return res;
            //以下為官方文檔用法,這種方法會使被拒絕方沒有任何提示信息
            //var tsc = new TaskCompletionSource<HttpResponseMessage>();
            //tsc.SetResult(response); // Also sets the task state to "RanToCompletion"
            //return tsc.Task;
        }

	public class Result//構建用於返回錯誤信息的對象
    {
        public bool success { get; set; }
        public string[] errs { get; set; }
    }

3.3.2服務器配置

可以為指定路由配置,也可以全局配置

				config.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = RouteParameter.Optional });
                    //constraints:null,
                    //handler:new BasicAuthorizationHandler());//可以為指定路由添加message handler。注意重載參數順序

                config.MessageHandlers.Add(new BasicAuthorizationHandler());//全局HTTP Message Handler

4.Web端跨域訪問配置

HTML頁面訪問webapi會受到跨域訪問策略的阻止,因此需要進行配置

4.1服務端配置

4.1.1引用

//引入跨域包
//Install-Package Microsoft.AspNet.WebApi.Cors -Version 5.2.7 

4.1.2config配置

                //配置跨域訪問。一定要添加這個,才能使用EnableCorsAttribute,否則,在Contoler或者Action上面添加這個特性無效
                config.EnableCors(new System.Web.Http.Cors.EnableCorsAttribute("*", "*", "*"));//配置全局跨域
                //config.EnableCors();//為特定控制器或方法添加跨域特性

或者在控制器處添加:

    //[EnableCors(origins: "*", headers: "*", methods: "*")]
	public class AddController : ApiController
    {
  		 [HttpGet]
        public string Add(int id)
        {
            return (id * 10).ToString();
        }
    }

4.2客戶端訪問

        function WebApi() {
            var ServerUrl = $.trim($('#ServerBase').val() + $('#ServerRelative').val());//webapi接口地址
            var strKey = $.trim($('#key').val());//key
            var strMsg = $.trim($('#Parameter').val());//需要加密的身份信息
            var ciphertext = encryptByDES(strMsg, strKey);//加密后結果
            console.log('Basic ' + ciphertext);
            $.ajax({
                url: ServerUrl,
                //crossDomain:true,
                type: "GET",
                headers: {
                    'Accept': "text/html, application/xhtml+xml, */*",
                    'Content-Type': "application/json",
                    'Authorization': 'Basic ' + ciphertext,
                    'token':"hello"
                },
                success: function (data,textStatus) {
                    console.log(textStatus + data);
                    document.getElementById('txtInfo').innerHTML += data + '<br/>';
                },
                error: function (XMLHttpRequest, textStatus, errorThrown) {
                    console.log(textStatus + errorThrown);
                    var errdata = textStatus + errorThrown
                    document.getElementById('txtInfo').innerHTML += errdata + '<br/>';
                }
            })
        }

5.完整代碼

https://files.cnblogs.com/files/ygxddxc/WebApi身份驗證.zip
為了減小體積,文件里的自托管、跨域相關庫已經刪除,可以自行通過nuget下載。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM