C# Nancy框架开发 WebApi 二:接口安全签名认证

上一章记录了创建一个Nancy框架的WebApi接口,这一章就在这个接口Demo上继续添加签名安全认证,保证接口的数据请求安全

  

一:创建一个MD5加密类,按照自己的加密方式来写

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Security;
namespace Security
{
    public class MD5
    {
     // 加密
public static string Encrypt(string str) { string result = string.Empty; string cl = DateTime.Now.Month + str + DateTime.Now.Day; var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider(); byte[] data = md5.ComputeHash(Encoding.Default.GetBytes(cl)); data.Reverse(); for (int i = 0; i < data.Length; i++) { result += data[i].ToString("X"); } return result; } } }

 

二:创建接口授权密钥 (这里用配置类来代替,实际可以配置在数据库中)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace NancyWebApiDemo.Security
{
    public class LicenceConfig
    {
        private static Dictionary<string, string> Licences = new Dictionary<string, string>();

        public LicenceConfig()
        {
            if (Licences.Count == 0)
            {
                Licences.Add("%%8795456$#@1198456451)(##@", "userOne"); //用户1的Api授权密钥
                Licences.Add("$984351321515##&*135131133#", "userTwo");  //用户2的Api授权密钥
            }
        }

        //获取拥有密钥系统用户
        public string GetLicencesUser(string Key)
        {
            return Licences[Key];
        }

        //检索密钥是否存在
        public bool CheckExistLicence(string Key)
        {
            return Licences.ContainsKey(Key);
        }
    }
}

 

创建一个缓存操作类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Caching;

namespace NancyWebApiDemo.Common
{
    public class CacheHelper
    {
        /// <summary>  
        /// 获取数据缓存  
        /// </summary>  
        /// <param name="cacheKey"></param>  
        public static object GetCache(string cacheKey)
        {
            var objCache = HttpRuntime.Cache.Get(cacheKey);
            return objCache;
        }
        /// <summary>  
        /// 设置数据缓存  
        /// </summary>  
        public static void SetCache(string cacheKey, object objObject)
        {
            var objCache = HttpRuntime.Cache;
            objCache.Insert(cacheKey, objObject);
        }
        /// <summary>  
        /// 设置数据缓存  
        /// </summary>  
        public static void SetCache(string cacheKey, object objObject, int timeout = 7200)
        {
            try
            {
                if (objObject == null) return;
                var objCache = HttpRuntime.Cache;
                //过期时间  
                objCache.Insert(cacheKey, objObject, null, DateTime.Now.AddSeconds(timeout), TimeSpan.Zero, CacheItemPriority.High, null);
            }
            catch (Exception)
            {
                //throw;  
            }
        }
        /// <summary>  
        /// 移除指定数据缓存  
        /// </summary>  
        public static void RemoveCache(string cacheKey)
        {
            var cache = HttpRuntime.Cache;
            cache.Remove(cacheKey);
        }
        /// <summary>  
        /// 移除全部缓存  
        /// </summary>  
        public static void RemoveAllCache()
        {
            var cache = HttpRuntime.Cache;
            var cacheEnum = cache.GetEnumerator();
            while (cacheEnum.MoveNext())
            {
                cache.Remove(cacheEnum.Key.ToString());
            }
        }
    }
}

 

 

三:在ApiModule.cs 创建签名获取接口

//获取Api签名   
            Post["/getSign"] = p =>
            {
                CommResponse<object> response = new CommResponse<object>();
                response.Code = CodeConfig.CodeFailed;
                try
                {
                    string key = Request.Query["key"]; //获取
                    string data = Request.Query["data"];//请求的json数据
                    string type = Request.Query["type"]; //请求动作
                    bool flag = new Security.LicenceConfig().CheckExistLicence(key);
                    if (flag)
                    {
                        //创建签名
                        switch (type)
                        {
                            case "Query":
                                response.Message = "请求成功";
                                response.Code = CodeConfig.CodeSuccess;
                                response.Data = Security.MD5.Encrypt(type + key + data);
                                break;
                            case "Write":
                                response.Message = "请求成功";
                                response.Code = CodeConfig.CodeSuccess;
                                response.Data = Security.MD5.Encrypt(type + key + data);
                                break;
                            default:
                                response.Message = "接口操作类型错误";
                                break;
                        }
                        //获取签名成功
                        if (response.Code == CodeConfig.CodeSuccess)
                        {
                            //设置一个签名过期时间:120秒
                            CacheHelper.SetCache(response.Data as string, response.Data, 120);
                        }
                    }
                    else
                    {
                        response.Message = "接口授权密钥不存在";
                    }
                }
                catch (Exception ex)
                {
                    response.Message = ex.Message;
                }
                return Response.AsText(JsonHelper.ObjectConvertJson(response), "application/json");
            };

 

接下来把项目运行起来 用Postman 工具测试下签名接口

 

 

传入正确的密钥

 

在这里已经拿到了签名,自己的程序应该马上跟着请求数据接口 查询或者写入数据,因为我们设置了签名的120秒有效期。

 

四:在ApiModule.cs 中创建一个签名认证方法

     /// <summary>
        /// 验证签名
        /// </summary>
        /// <param name="type">操作类型</param>
        /// <param name="data">请求的源数据</param>
        /// <param name="sign">签名</param>
        /// <returns></returns>
        public CommResponse<object> VerificationSign(string type, string key, string data, string sign)
        {
            CommResponse<object> response = new CommResponse<object>();
            response.Code = CodeConfig.CodeFailed;

            //计算签名
            string old = Security.MD5.Encrypt(type + key + data);
            if (old.Equals(sign))
            {
                //继续判断签名是否过期
                object _data = CacheHelper.GetCache(sign);
                if (_data == null)
                {
                    response.Message = "签名已过有效期";
                }
                else
                {
                    response.Code = CodeConfig.CodeSuccess;
                    response.Message = "签名校验成功";
                }
            }
            else
            {
                response.Message = "签名校验未通过";
            }
            return response;
        }

创建几个类 :请求类和响应类和实体类

    /// <summary>
    /// 接口请求类
    /// </summary>
    public class CommRequest<T>
    { 
        //签名
        public string Sign { get; set; }

        //授权Key
        public string Key { get; set; }

        //操作类型:Query、Write
        public string Type { get; set; }

        //查询对象
        public T Data { get; set; }
    }

 

    /// <summary>
    /// 接口响应类
    /// </summary>
    public class CommResponse<T>
    {
        public int Code { get; set; }

        public string Message { get; set; }

        public T Data { get; set; }
    }

 

这个用户类我用来当查询条件和返回json

    /// <summary>
    /// 用户类
    /// </summary>
    public class UserInfo
    {
        public string ID { get; set; }

        public string Name { get; set; }

        public string Phone { get; set; }

        public string Address { get; set; }
    }

在ApiModule.cs中在定义一个初始化返回CommResponse的json方法

        /// <summary>
        /// 初始化一个Commresponse的Json
        /// </summary>
        /// <param name="code">返回代码</param>
        /// <param name="msg">描述</param>
        /// <param name="data">数据</param>
        /// <returns></returns>
        public string InitReturnResponseJson(int code, string msg, object data = null)
        {
            CommResponse<object> response = new CommResponse<object>();
            response.Code = code;
            response.Message = msg;
            response.Data = data;
            return JsonHelper.ObjectConvertJson(response);
        }

 

五:在正式的数据访问接口中 调用验证签名的方法

            //查询方法
            Post["queryUser"] = p =>
            {
                string param = Request.Query["param"];
                string json = Request.Query["json"];
                string result = string.Empty;
                CommRequest<UserInfo> request = null;
                CommResponse<object> response;
                try
                {
                    request = JsonHelper.JsonConvertObject<CommRequest<UserInfo>>(param);
                    request.Data = JsonHelper.JsonConvertObject<UserInfo>(json);
                    //验证签名
                    response = VerificationSign(request.Type, request.Key, json, request.Sign);
                    if (response.Code == CodeConfig.CodeFailed)
                    {
                        return Response.AsText(JsonHelper.ObjectConvertJson(response), "application/json");
                    }
                }
                catch
                {
                    result = InitReturnResponseJson(CodeConfig.CodeFailed, "Json参数格式错误");
                    return Response.AsText(result, "application/json");
                }

                //进入接口,开始进行数据操作
                //response = QueryUserInfo(request.Data.ID);
                //result = JsonHelper.ObjectConvertJson(response);


                //返回数据
                response.Code = CodeConfig.CodeSuccess;
                response.Message = "请求成功";
                response.Data= new { Id = request.Data.ID, Name = "Tom", Address = "四川省成都市" };
                result = JsonHelper.ObjectConvertJson(response);

                return Response.AsText(result, "application/json");
            };

 

六:测试查询接口

  1.当输入错误的签名或者当发送的json查询参数被抓取后篡改  都是无法通过服务器签名验证的  (在调用此数据接口时,应先获取sign签名,见上面!!)

 

  2.或者当签名正确,但签名已过设置的2分钟有效期。也是无法正常访问接口的

 

  3.当参数完全正确和签名通过后  则可以拿到数据

 

 

 

 

附: 写一个测试Demo 

class Program
    {
        static string url = "http://localhost:56157/";  //接口地址
        static string method = string.Empty; //接口方法

        static void Main(string[] args)
        {
            string key = "T8951SLI02UTY983CVBAX03"; //接口的授权Key
            Dictionary<string, string> pars = new Dictionary<string, string>(); //参数

            //请求接口
            //  ①创建查询参数Json对象
            Console.WriteLine("创建接口查询参数...");
            var dataJson = JsonHelper.ObjectConvertJson(new { ID = "10001" });
            //  ②获取此次请求签名
            Console.WriteLine("开始获取签名...");
            CommResponse<string> sign = getApiSign("Query", dataJson);
            if (sign.Code == 1)
            {
                Console.WriteLine("签名获取成功:" + sign.Data);
                //获取签名成功则继续请求数据接口
                //  ③创建签名Json
                var signJson = JsonHelper.ObjectConvertJson(new { Key = key, Sign = sign.Data, Type = "Query" });
                //  ④封装参数开始请求
                pars.Add("param", signJson);
                pars.Add("json", dataJson);
                method = "queryUser";
                Console.WriteLine("开始发起查询请求...");
                string result = Http.SendRequest(url + method, pars);

                Console.WriteLine("请求结果:" + result);
            }
            else
            {
                //获取签名失败
                Console.WriteLine("签名获取失败:" + sign.Message);
            }


            Console.ReadKey();
        }

        /// <summary>
        /// 获取请求签名
        /// </summary>
        /// <param name="type">操作类型</param>
        /// <param name="data">要请求的数据</param>
        /// <returns></returns>
        public static CommResponse<string> getApiSign(string type, string data)
        {
            Dictionary<string, string> pars = new Dictionary<string, string>(); //参数
            string key = "T8951SLI02UTY983CVBAX03"; //接口的授权Key
            method = "getSign";
            pars.Add("key", key);
            pars.Add("type", type);
            pars.Add("data", data);
            string result = Http.SendRequest(url + method, pars);
            if (!string.IsNullOrWhiteSpace(result))
            {
                return JsonHelper.JsonConvertObject<CommResponse<string>>(result);
            }
            else
            {
                return new CommResponse<string>() { Code = 2, Message = "获取失败" };
            }
        }
    }

 

Http请求类

public class Http
    {
        /// <summary>
        /// 发起请求
        /// </summary>
        /// <param name="url">接口地址</param>
        /// <param name="pars">字典类型的参数集合</param>
        /// <param name="timeout"></param>
        /// <returns></returns>
        public static string SendRequest(string url, IDictionary<string, string> pars, int timeout = 120)
        {
            HttpWebRequest request = CreateRequest(url);

            byte[] pdata = Encoding.UTF8.GetBytes(BuildQuery(pars, "utf-8"));

            request.ContentLength = pdata.Length;

            Stream writer = null;
            try
            {
                writer = request.GetRequestStream();
            }
            catch (Exception ex)
            {
                return ex.Message;
            }

            writer.Write(pdata, 0, pdata.Length);
            writer.Close();

            HttpWebResponse response = null;
            try
            {
                //获得响应流
                response = (HttpWebResponse)request.GetResponse();
            }
            catch (Exception ex)
            {
                return ex.Message;
            }
            StreamReader sRead = new StreamReader(response.GetResponseStream());

            string postContent = sRead.ReadToEnd();
            sRead.Close();

            return postContent;
        }


        /// <summary>
        /// 创建一个Http请求
        /// </summary>
        /// <param name="url">接口地址</param>
        /// <param name="timeout">超时时间:秒</param>
        /// <returns></returns>
        public static HttpWebRequest CreateRequest(string url, int timeout = 30)
        {
            HttpWebRequest mRequest = (HttpWebRequest)WebRequest.Create(url);

            mRequest.Proxy = null;
            mRequest.UseDefaultCredentials = false;
            mRequest.AllowWriteStreamBuffering = true;
            mRequest.Headers[HttpRequestHeader.AcceptLanguage] = "zh-CN";
            mRequest.Headers[HttpRequestHeader.AcceptEncoding] = "gzip, deflate";
            mRequest.Headers[HttpRequestHeader.CacheControl] = "no-cache";
            mRequest.Accept = "*/*";
            mRequest.Method = "POST";
            mRequest.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
            mRequest.ProtocolVersion = HttpVersion.Version11;
            mRequest.ServicePoint.Expect100Continue = false;
            mRequest.ServicePoint.ConnectionLimit = 20000;
            mRequest.ServicePoint.MaxIdleTime = 20000;
            mRequest.ServicePoint.ReceiveBufferSize = 16384;
            mRequest.PreAuthenticate = true;
            mRequest.AllowAutoRedirect = false;
            mRequest.Timeout = timeout * 1000;
            mRequest.ReadWriteTimeout = timeout * 1000;
            mRequest.KeepAlive = true;
            return mRequest;
        }

        /// <summary>
        /// 组装普通文本请求参数。
        /// </summary>
        /// <param name="parameters">Key-Value形式请求参数字典</param>
        /// <returns>URL编码后的请求数据</returns>
        static string BuildQuery(IDictionary<string, string> parameters, string encode)
        {
            StringBuilder postData = new StringBuilder();
            bool hasParam = false;
            IEnumerator<KeyValuePair<string, string>> dem = parameters.GetEnumerator();
            while (dem.MoveNext())
            {
                string name = dem.Current.Key;
                string value = dem.Current.Value;
                // 忽略参数名或参数值为空的参数
                if (!string.IsNullOrEmpty(name))
                {
                    if (hasParam)
                    {
                        postData.Append("&");
                    }
                    postData.Append(name);
                    postData.Append("=");
                    if (encode == "gb2312")
                    {
                        postData.Append(System.Web.HttpUtility.UrlEncode(value, Encoding.GetEncoding("gb2312")));
                    }
                    else if (encode == "utf8")
                    {
                        postData.Append(System.Web.HttpUtility.UrlEncode(value, Encoding.UTF8));
                    }
                    else
                    {
                        postData.Append(value);
                    }
                    hasParam = true;
                }
            }
            return postData.ToString();
        }
    }

 

开始断点发起请求    (直接按正规流程先执行一次看看结果)

接口是正常请求拿到了数据的

 

下面我们来模拟下当发起请求时,查询参数被恶意修改 (看上面代码能知道 我们请求签名时的数据 ID是10001  而我们数据请求时改成了10002)

修改后  请求结果

 

 

在断点情况下模拟下签名过有效期

 

 

接口请求安全大概就到这里,另外除此之外 还可以引用一些 限流框架,限制某个IP地址在规定时间内的访问次数。

  

posted @ 2020-04-23 17:23  阿东呢  阅读(1779)  评论(0编辑  收藏  举报