zoukankan      html  css  js  c++  java
  • 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下载。

  • 相关阅读:
    Windows python 鼠标键盘监控及控制
    python 执行adb shell 命令
    python Windows提示框
    判断function属于函数或方法
    生成不同时间下的LOG
    pyqt5 QCalendar类中常用的方法
    python字符串大小写转换
    configparser 模块的基本方法
    QGridLayout类中常用的方法
    Day049--jQuery的文档操作和事件介绍
  • 原文地址:https://www.cnblogs.com/ygxddxc/p/14140806.html
Copyright © 2011-2022 走看看