API服務接口簽名代碼與設計,如果你的接口不走SSL的話?


在看下面文章之前,我們先問幾個問題

  • rest 服務為什么需要簽名?
  • 簽名的幾種方式?
  • 我認為的比較方便的快捷的簽名方式(如果有大神持不同意見,可以交流!)?
  • 怎么實現驗簽過程 ?
  • 開放式open api sign怎么設計 (openkey 和 openid 的設計) ?
  • 在一個服務中,有些接口不需要簽名,接口怎么濾過簽名 ?

我認為好的簽名設計,應該要解決以上問題。

 

  一: Rest 服務為什么需要簽名?

    在介紹簽名之前,我們先對服務進行分一分,我們的服務從內網以及外網角度分為:內網服務以及開放型外網服務兩大類

  1.     內網服務,我們認為它是可靠安全,受局域網的防火牆保護,內網型的服務,我們不開放出INTERNET 訪問。
  2.     暴露在外網型的服務,我們認為是它本質是提供到INTERNET 網絡允許訪問的服務。我們認為它是不可靠的,不安全的。

外網型的服務,我們通常面臨兩個問題:收到惡意請求和數據安全(如果你不是通過SSL走的話) 的問題。

在惡意請求方面,又涉及到惡意高頻請求以及數據攔截竄篡改請求。

數據安全方面涉及到,網絡傳輸的數據如果被攔截,涉及到客戶隱私數據被竊取等相關問題。

因此,為了解決上述問題,偉大的 服務 “簽名” 就誕生了。 你可以這么認為:簽名 就是 請求當前業務接口的 前提鑰匙。通過軟實施實現。

 

簽名如何解決上述問題:

  1. 惡意請求: 我們知道,簽名在設計上面具有防篡改性質,如果這一點沒有實現,那么就會失去簽名的意義。被攔截的請求,修改請求報文后,再次發送,將會被服務端 驗簽 過程中 檢測到,直接打回--我們通常說是驗簽失敗
  2. 客戶隱私:客戶隱私數據的保護,加簽后的接口,只能請求當前的相同請求報文的請求,而不能嘗試請求被篡改后報文的請求。如果數據被攔截,也只能是當前此條數據客戶隱私被泄露。因此,如果要絕對的保護客戶隱私的話,還有對報文數據進行加密。這樣,我們就可以做到數據安全級別較高的接口。下面的文章將對具體實現過程展開。
  3. 高頻請求的保護,如果簽名產生的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 }
View Code

 

服務端驗簽原理:

服務端通過  服務端定義接口攔截器或者全局過攔截器。 接口接收到的報文是上述表格的報文結構后,做如下事情

  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         }
    View Code
  • 然后構建一個攔截器,攔截器的工作如下所示 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         }
    View Code

     

  • 我們看一下接口定義- 給 接口 打上  OpenSign  標簽,並且使用  ResultRequset 來接收對方過來的報文數據。
    1 [HttpPost]
    2 
    3 [OpenSign]
    4  public ResultRequset test([FromBody]ResultRequset obj)
    5  {
    6 
    7  }
    View Code

     

上述我們基本上形成了 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     }
    View Code
  • 服務端接收對象改造:
     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     }
    View Code
  • 服務端攔截器改造
     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     }
    View Code
  • 服務端接口定義改造:
    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  }
    View Code

    上面接口定義 打上了  [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     }
    View Code

這樣我們就整體上完成了我們所需要的 框架性 服務接口簽名認證代碼。 上面的代碼 在Bitter.Frame 框架 服務簽名模塊中有, Bitter.Frame 代碼還在整理中 。后續會貼出來給大家。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM