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地址在规定时间内的访问次数。