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下载。

原文地址:https://www.cnblogs.com/ygxddxc/p/14140806.html