在看下面文章之前,我們先問幾個問題
- rest 服務為什么需要簽名?
- 簽名的幾種方式?
- 我認為的比較方便的快捷的簽名方式(如果有大神持不同意見,可以交流!)?
- 怎么實現驗簽過程 ?
- 開放式open api sign怎么設計 (openkey 和 openid 的設計) ?
- 在一個服務中,有些接口不需要簽名,接口怎么濾過簽名 ?
我認為好的簽名設計,應該要解決以上問題。
一: Rest 服務為什么需要簽名?
在介紹簽名之前,我們先對服務進行分一分,我們的服務從內網以及外網角度分為:內網服務以及開放型外網服務兩大類
- 內網服務,我們認為它是可靠安全,受局域網的防火牆保護,內網型的服務,我們不開放出INTERNET 訪問。
- 暴露在外網型的服務,我們認為是它本質是提供到INTERNET 網絡允許訪問的服務。我們認為它是不可靠的,不安全的。
外網型的服務,我們通常面臨兩個問題:收到惡意請求和數據安全(如果你不是通過SSL走的話) 的問題。
在惡意請求方面,又涉及到惡意高頻請求以及數據攔截竄篡改請求。
數據安全方面涉及到,網絡傳輸的數據如果被攔截,涉及到客戶隱私數據被竊取等相關問題。
因此,為了解決上述問題,偉大的 服務 “簽名” 就誕生了。 你可以這么認為:簽名 就是 請求當前業務接口的 前提鑰匙。通過軟實施實現。
簽名如何解決上述問題:
- 惡意請求: 我們知道,簽名在設計上面具有防篡改性質,如果這一點沒有實現,那么就會失去簽名的意義。被攔截的請求,修改請求報文后,再次發送,將會被服務端 驗簽 過程中 檢測到,直接打回--我們通常說是驗簽失敗
- 客戶隱私:客戶隱私數據的保護,加簽后的接口,只能請求當前的相同請求報文的請求,而不能嘗試請求被篡改后報文的請求。如果數據被攔截,也只能是當前此條數據客戶隱私被泄露。因此,如果要絕對的保護客戶隱私的話,還有對報文數據進行加密。這樣,我們就可以做到數據安全級別較高的接口。下面的文章將對具體實現過程展開。
- 高頻請求的保護,如果簽名產生的uuid 加上 時間戳,就可以解決高頻請求的容錯限流等問題.
因此簽名尤其變為重要。
我上面的標題,如果你的接口不走SSL的話,你的外網接口就需要走上述這些事情,為了你接口安全而考慮。
二:簽名的幾種方式
簽名的幾種方式:我們通常見到的有 SHA 加密簽名,MD5 簽名。我個人比較推崇的是 MD5 加簽簽名。
原因:簡單,易懂,跨語言平台型強,通用性強。尤其是.NET 與 JAVA 跨語言的的接口簽名對接時。因為JAVA 的 SHA 版本有很多中,而更甚的是,有些 SHA 在某些銀行還被改過,形成自己私有的版本。如果:
你要對接他們的 他們的接口,你必須使用JAVA 語言. 然而 MD5 的算法比較統一。只要 確認 對方的最簡單的 字符串 123 MD5 值跟 你 這邊的 MD5 值一樣。就可以保證 底層算法 的一致 性,就 可以采用上述的加簽方式。
MD5 加簽原理:
我們假設有這么一個統一入參結構的請求報文
請求對象協議結構 |
類型 |
說明 |
object |
object |
說明:請求的業務參數(包裝對象),各接口不同的參數 二:包裝對象中實體中特殊業務字段中的具體格式要求: ① 如果業務對象參數是時間類型的, 將時間參數轉成時間戳(當前時間與'1970-01-01'精確到毫秒,類型Long) ② 業務中的浮點型使用字符串定義傳送(避免不同跨語言造成序列化形成的浮點位數不一致性) |
time |
long |
當前時間的時間戳:datekong(當前時間與'1970-01-01'相對值,精確到毫秒) |
sign |
string |
sign=MD5(openkey+ time+ JsonConvert.SerializeObject(object)) 備注:OpenKey: 分配給調用方的key值,此值無需暴露在網絡中傳輸。
|
在我們構建傳送報文的時候,我們看到有一個字段: sign 是 由 服務方分配給客戶端一個 秘鑰字符串 再加上 報文中 ( time 時間戳+ objcet 業務參序列化)相加后的字符串 后 MD5值。
我們這里 sign 的形成有兩個關鍵點:
第一: sign 值形成的算法,我這邊算法暫時是 :sign= MD5(openkey+ time+ JsonConvert.SerializeObject(object))
第二: sign 分配給客戶端的秘鑰值—openkey
如下加簽請求偽代碼:

1 namespace T.API 2 { 3 4 5 /// <summary> 6 /// 請求的報文對象 7 /// </summary> 8 public class SendObject 9 { 10 /// <summary> 11 /// 發送實體對象 12 /// </summary> 13 public object @object { get; set; } 14 15 /// <summary> 16 /// 簽名 17 /// </summary> 18 19 public string sign { get; set; } 20 /// <summary> 21 /// 當前請求的時間戳 22 /// </summary> 23 public long? time { get; set; } 24 25 /// <summary> 26 /// 用戶id 27 /// </summary> 28 public int userId { get; set; } 29 } 30 31 /// <summary> 32 /// 接收到的報文對象 33 /// </summary> 34 public class ReciveObject 35 { 36 /// <summary> 37 /// 發送實體對象 38 /// </summary> 39 public object @object { get; set; } 40 41 /// <summary> 42 /// 服務請求響應值 code 為 1:請求成功 ,請求無異常 43 /// 當code 為 "1" 的情況下,下面的RevRep 對象中的 message 字段 90% 的場景為空, 44 /// 如果有必要賦值視雙方業務場景而定; 45 /// code為 0:我方程序異常/業務性質失敗/接口參數校驗失敗, 46 /// 當 code 為 "0"的情況下,下面message字段包裝了異常/失敗信息。 47 /// </summary> 48 49 public int code { get; set; } 50 51 52 /// <summary> 53 /// 請求響應的錯誤消息/或者其他業務場景響應提示信息 54 /// </summary> 55 public int message { get; set; } 56 } 57 58 59 /// <summary> 60 /// 上面 Req 對象中的object 封裝字段具體實體定義 61 /// </summary> 62 public class ObjectEntity 63 { 64 65 public string orderNum { get; set; } 66 67 /// <summary> 68 /// 如果參數是浮點型,在實體中定義成字符串類型. 69 /// </summary> 70 public string orderMoney { get; set; } 71 72 /// <summary> 73 /// 如果參數是時間類型的,在實體中定義成long 時間戳類型 74 /// </summary> 75 public long? orderTime { get; set; } 76 } 77 78 79 80 81 /// <summary> 82 /// 請求示例代碼 83 /// </summary> 84 public class RequestDemo 85 { 86 87 /// <summary> 88 /// 請求示例,調用方請求 89 /// </summary> 90 91 public static void Request() 92 { 93 94 //服務端分配給調用方:openkey 95 string openKey = "455853655-7dff-5585545-a1c3-7778887"; // 96 97 //定義發送對象 98 SendObject sendobject = new SendObject(); 99 //定義請求時間戳 100 long? reqtime= DateTime.Now.ToSafeDateTime().ToSafeDataLong();// 賦值 101 sendobject.time = reqtime; 102 try 103 { 104 //定義以及賦值業務實體 105 ObjectEntity objectEntity = new ObjectEntity(); 106 objectEntity.orderNum = "20200506071001"; 107 objectEntity.orderTime = DateTime.Now.ToSafeDateTime().ToSafeDataLong(); 108 objectEntity.orderMoney = "526.00"; 109 110 //將定義好的業務實體塞入SendObject的object字段中. 111 sendobject.@object = objectEntity; 112 113 //加簽並且賦值簽名 114 sendobject.sign = sign(reqtime, openkey,JsonConvert.SerializeObject(sendobject.@object)); 115 116 117 RestRequest rq = new RestRequest(Method.POST); 118 119 rq.Method = Method.POST; //請求設置為POST 120 121 rq.AddHeader(" Content-Type", "application/json;charset=utf-8"); //頭部塞入Content-Type 122 rq.AddParameter("application/json", JsonConvert.SerializeObject(sendobject), ParameterType.RequestBody); 123 124 125 RestClient restclient = new RestClient { BaseUrl = new Uri("http://xx.xx.xx.xx:5021") }; //調用地址 126 TaskCompletionSource<IRestResponse> tcs = new TaskCompletionSource<IRestResponse>(); 127 restclient.ExecuteAsync(rq, r => 128 { 129 tcs.SetResult(r); 130 }); 131 IRestResponse respones = tcs.Task.Result; // 請求返回的數據 132 133 //如果請求狀態正常 134 if ((int)respones.StatusCode == 200) 135 { 136 ReciveObject recive = JsonConvert.DeserializeObject<ReciveObject>(respones.Content); 137 if (recive.code == 1) 138 { 139 //處理業務 140 } 141 else 142 { 143 //處理業務 144 } 145 146 147 148 } 149 else 150 { 151 throw new Exception("調用異常通訊狀態:${respones.StatusCode}"); 152 153 } 154 155 156 157 158 159 160 } 161 catch (Exception ex) 162 { 163 164 } 165 166 } 167 168 169 /// <summary> 170 /// Md5 方法 171 /// </summary> 172 public static string MD5(string md5orgincontent) 173 { 174 175 string md5result = string.Empty; 176 if (string.IsNullOrEmpty(md5result)) return md5result; 177 StringBuilder sb = new StringBuilder(); 178 179 MD5 md5 = new MD5CryptoServiceProvider(); 180 byte[] s = md5.ComputeHash(Encoding.UTF8.GetBytes(md5orgincontent)); 181 md5.Clear(); 182 for (int i = 0; i < s.Length; i++) 183 { 184 sb.Append(s[i].ToString("x2")); 185 } 186 md5result=sb.ToString(); 187 return md5result; 188 189 190 } 191 192 193 /// <summary> 194 /// 加簽 195 /// </summary> 196 /// <param name="time">時間戳</param> 197 /// <param name="openkey">服務端分配給調用方:openkey</param> 198 /// <param name="szobject">參與加簽的object的json序列化字符串</param> 199 /// <returns></returns> 200 public static string sign(long? time, string openkey, string szobject) 201 { 202 203 204 string signresult = string.Empty; 205 var signcontent = openkey+time.ToSafeString()+szobject; 206 signresult = MD5(signcontent); 207 return signresult; 208 209 } 210 211 212 213 214 } 215 216 }
服務端驗簽原理:
服務端通過 服務端定義接口攔截器或者全局過攔截器。 接口接收到的報文是上述表格的報文結構后,做如下事情
1: 同樣:接攔截器中,做同樣的事情: 秘鑰字符串 再加上 報文傳送過來的 ( time 時間戳+ objcet 業務參序列化)相加后的字符串 MD5值 ,我將此值 為 service_sign
2:將服務端的 service_sign 值跟 報文中的 sign 進行比對,如果發現不匹配(假設在雙方算法一直,openkey 一致的情況下):報文被篡改,簽名驗證不通過
我在這里貼出服務端驗簽 C# 代碼:其他語言可以參考:
- 服務端先定義一個接收報文的對象:
1 [JsonObject(MemberSerialization.OptIn)] 2 public class ResultRequset : BaseRequestEntity 3 { 4 [JsonProperty] 5 public object @object { get; set; } 6 public virtual string openKey{ get; set; } 7 /// <summary> 8 /// 服務端加簽:此值將於傳送過來的 sign 值最終進行比對/// </summary> 9 public override string checkedSign 10 { 11 get 12 { 13 var orgin =this.time.ToString() + openKey+ JsonConvert.SerializeObject(@object); 14 return EntitySign.To32Md5(orgin); 15 } 16 } 17 18 /// <summary> 19 /// 簽名驗證 20 /// </summary> 21 /// <returns></returns> 22 public Result CheckedSign() 23 { 24 Result r = new Result(); 25 if (this.sign == checkedSign) 26 { 27 r.code = 1; 28 return r; 29 } 30 else 31 { 32 r.code = 0; 33 r.message = "延簽失敗!"; 34 } 35 return r; 36 }
- 然后構建一個攔截器,攔截器的工作如下所示 NETFramework 代碼,其他語言可以參考:
1 public class OpenSignAttribute : ActionFilterAttribute 2 { 3 public Type RequestType { get; set; } 4 5 public override void OnActionExecuting(HttpActionContext actionContext) 6 { 7 HttpContent content = actionContext.Request.Content; 8 var gloablkey = string.Empty; 9 ResultRequset resultRequset = new ResultRequset(); 10 foreach (KeyValuePair<string, object> obj in actionContext.ActionArguments) 11 { 12 13 resultRequset = (ResultRequset)obj.Value; //第一步:獲取報文數據,強制轉換到 上面定義的 ResultRequset 報文接收對象 14 } 15 Result re = resultRequset.CheckedSign(); //第二步: 服務端進行加簽並且驗簽 16 if (re.code == 1) 17 { 18 base.OnActionExecuting(actionContext); 19 } 20 else 21 { 22 actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, re); 23 } 24 } 25 }
- 我們看一下接口定義- 給 接口 打上 OpenSign 標簽,並且使用 ResultRequset 來接收對方過來的報文數據。
1 [HttpPost] 2 3 [OpenSign] 4 public ResultRequset test([FromBody]ResultRequset obj) 5 { 6 7 }
上述我們基本上形成了 MD5 加簽和驗簽的邏輯過程。那么上述的這這個過程還是有個缺陷,就是文章一開頭要解決的一個問題,開放式open api sign怎么設計 (openkey 和 openid 的設計) ?
也就是說:上述的 demo 的openkey 在是死的,如果我們想 服務端分配給每個調用方的openkey 都不一樣,怎么辦?
其實原理很簡單:我們在增加一個 openid 概念:openid 是服務端分配給對調用方的唯一標識,openkey 是我們分配調用方參與加簽的 鑰匙。
怎么做呢:
1:調用方: openid 一定要讓對方 放入 HTTP HEADER 里面 傳送到服務端。openkey 是參與加密,不需要傳送。
2:服務端:在接收到 調用方 傳送過來的 openid后,通過查庫或者其他方式 查出 openid 對應的 openkey, 然后將查到的openkey 參與服務端驗簽算法。
上述原理,也就是我們通常看到的ALI,騰訊,或者其他第三方提供出來的 API 為什么需要分配一個OPENID,OPENKEY 的原因,或許有些廠商不是這種叫法。但是原理都是這樣。
在貼出改造代碼之前,我們還需要解決一個問題:就是 服務端在“接收到 調用方 傳送過來的 openid后,通過查庫或者其他方式 查出 openid 對應的 openkey, 然后將查到的openkey 參與服務端驗簽算法” 這里的藍色字體標注的具體怎么查,這對
openid,openkey 配置對 怎么配置在服務端(有可能存庫,有可能放在配置文件中)可能每個服務端都不太一樣,我們把這層也抽象出來。讓接口標簽指定。
我們代碼再次改造,如下所示:
- 我們先定義一個查找方式的接口:
1 public interface ISingSecret 2 { 3 string OpenId(Microsoft.AspNetCore.Http.HttpRequest request =null); 4 5 string OpenKey(string OpenId); 6 }
- 服務端接收對象改造:
1 [JsonObject(MemberSerialization.OptIn)] 2 public class ResultRequset 3 { 4 [JsonProperty] 5 public object @object { get; set; } 6 7 8 /// <summary> 9 /// 可以覆蓋此KEY的方式 10 /// </summary> 11 public virtual string openKey{ get; set; } 12 /// <summary> 13 /// 開放平台所使用的分配給客戶的OPENID 14 /// </summary> 15 [JsonProperty] 16 public string openId 17 { 18 19 get; 20 21 set; 22 } 23 /// <summary> 24 /// 獲取簽名 25 /// </summary> 26 public override string checkedSign 27 { 28 get 29 { 30 var orgin = singContent; 31 32 return EntitySign.To32Md5(orgin); 33 } 34 } 35 36 37 /// <summary> 38 /// 用戶ID 登入人ID 39 /// </summary> 40 [JsonIgnore] 41 public string singContent 42 { 43 get { return openKey+ this.time.ToString() + JsonConvert.SerializeObject(@object); } 44 }/// <summary> 45 /// 簽名驗證 46 /// </summary> 47 /// <returns></returns> 48 public Result CheckedSign() 49 { 50 Result r = new Result(); 51 52 if (this.sign == checkedSign) 53 { 54 r.code = 1; 55 return r; 56 } 57 else 58 { 59 r.code = 0; 60 61 r.message = "簽名驗證失敗!"; 62 LogService.Default.Debug("簽名驗證失敗---"+"框架簽名" + checkedSign.ToSafeString("")+"-------網絡簽名:"+ sign.ToSafeString("") + "--------簽名信息:" + singContent); 63 } 64 return r; 65 } 66 }
- 服務端攔截器改造:
1 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] 2 public class CentralSign: ActionFilterAttribute 3 { 4 5 6 7 private Type ISingRealization { get; set; } 8 9 private ISingSecret singRealization { get; set; } //關鍵代碼:由服務端實現通過openid 查出openkey 的具體邏輯. 10 11 public CentralSign(Type ISingSecret) // 關鍵代碼: 定義帶構造函數的 接口標簽屬性 . 12 { 13 this.ISingRealization = ISingSecret; 14 if (ISingRealization != null) 15 { 16 //獲取類的初始化參數信息 17 ConstructorInfo obj = ISingRealization.GetConstructor(System.Type.EmptyTypes); 18 singRealization = (ISingSecret)Activator.CreateInstance(ISingRealization); //實例化對象 19 20 } 21 } 22 23 public override void OnActionExecuting(ActionExecutingContext actionContext) 24 { 25 var content = actionContext.HttpContext.Request; 26 var gloablkey = string.Empty; 27 28 ResultRequset resultRequset = new ResultRequset(); 29 foreach (KeyValuePair<string, object> obj in actionContext.ActionArguments) 30 { 31 resultRequset = (ResultRequset)obj.Value; 32 } 33 34 35 Result re = new Result(); 36 if (resultRequset == null) 37 { 38 re.code = 0; 39 re.message = "傳值不能為空"; 40 actionContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; 41 actionContext.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(re)); 42 43 } 44 else 45 { 46 if (singRealization != null) 47 { 48 var openId = singRealization.OpenId(actionContext.HttpContext.Request); // 關鍵代碼: 通過 ISingSecret.OpenId() 方法,獲取到對應調用方傳送過來的 openid 49 50 resultRequset.publicApikey = singRealization.OpenKey(openId); // 關鍵代碼: 通過 ISingSecret.OpenId() 方法,獲取到對應調用方傳送過來的 openid 51 52 } 53 re = resultRequset.CheckedSign(); 54 if (re.code == 1) 55 { 56 base.OnActionExecuting(actionContext); 57 } 58 else 59 { 60 61 HandleUnauthorizedRequest(actionContext); 62 63 } 64 } 65 66 67 } 68 69 protected void HandleUnauthorizedRequest(ActionExecutingContext actionContext) 70 { 71 var r = new JsonResult("簽名失敗,訪問受限."); 72 73 r.StatusCode = (int)HttpStatusCode.BadRequest; 74 actionContext.Result =r; 75 return; 76 } 77 }
- 服務端接口定義改造:
1 [HttpPost] 2 [CentralSign(typeof(OpenSign))] 3 public Result SignatureSample([FromBody]ResultRequset result) 4 { 5 var str = result.@object.ToSafeString(""); 6 Result re = new Result() { code = 1,message="簽名驗證成功!"}; 7 re.@object = str; 8 return re; 9 }
上面接口定義 打上了 [CentralSign(typeof(OpenSign))] 標簽,CentralSign 接收了一個 OpenSign Type 對象類型。根據上面的代碼,我們知道,OpenSign 實現了 ISingSecret 邏輯。我們具體看下 OpenSign 具體實現:
- OpenSign 實現 ISingSecret 邏輯代碼:
1 public class OpenSign : ISingSecret 2 { 3 public string OpenId(HttpRequest request) 4 { 5 return Header.GetHeaderValue(request,"openId"); 6 } 7 8 public string OpenKey(string OpenId) 9 { 10 return ConfigManage.JsonConfigMange.GetInstance().AppSettings[OpenId]; 11 } 12 }
這樣我們就整體上完成了我們所需要的 框架性 服務接口簽名認證代碼。 上面的代碼 在Bitter.Frame 框架 服務簽名模塊中有, Bitter.Frame 代碼還在整理中 。后續會貼出來給大家。