上一章記錄了創建一個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地址在規定時間內的訪問次數。