API服务接口签名代码与设计,如果你的接口不走SSL的话?

  • A+
所属分类:.NET技术
摘要

在看下面文章之前,我们先问几个问题我认为好的签名设计,应该要解决以上问题。

在看下面文章之前,我们先问几个问题

  • 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

 

如下加签请求伪代码:

API服务接口签名代码与设计,如果你的接口不走SSL的话?API服务接口签名代码与设计,如果你的接口不走SSL的话?

  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# 代码:其他语言可以参考:

  • 服务端先定义一个接收报文的对象:
    API服务接口签名代码与设计,如果你的接口不走SSL的话?API服务接口签名代码与设计,如果你的接口不走SSL的话?

     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 代码,其他语言可以参考:
    API服务接口签名代码与设计,如果你的接口不走SSL的话?API服务接口签名代码与设计,如果你的接口不走SSL的话?

     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 来接收对方过来的报文数据。
    API服务接口签名代码与设计,如果你的接口不走SSL的话?API服务接口签名代码与设计,如果你的接口不走SSL的话?

    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 配置对 怎么配置在服务端(有可能存库,有可能放在配置文件中)可能每个服务端都不太一样,我们把这层也抽象出来。让接口标签指定。

我们代码再次改造,如下所示:

  • 我们先定义一个查找方式的接口:
    API服务接口签名代码与设计,如果你的接口不走SSL的话?API服务接口签名代码与设计,如果你的接口不走SSL的话?

    1 public interface ISingSecret 2     { 3         string OpenId(Microsoft.AspNetCore.Http.HttpRequest request =null); 4          5         string OpenKey(string OpenId); 6     }

    View Code

  • 服务端接收对象改造:
    API服务接口签名代码与设计,如果你的接口不走SSL的话?API服务接口签名代码与设计,如果你的接口不走SSL的话?

     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

  • 服务端拦截器改造
    API服务接口签名代码与设计,如果你的接口不走SSL的话?API服务接口签名代码与设计,如果你的接口不走SSL的话?

     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

  • 服务端接口定义改造:
    API服务接口签名代码与设计,如果你的接口不走SSL的话?API服务接口签名代码与设计,如果你的接口不走SSL的话?

    1 [HttpPost] 2 [CentralSign(typeof(OpenSign))] 3 public Result SignatureSample([FromBody]ResultRequset result) 4   { 5             var str = [email protected](""); 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 逻辑代码:
    API服务接口签名代码与设计,如果你的接口不走SSL的话?API服务接口签名代码与设计,如果你的接口不走SSL的话?

     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 代码还在整理中 。后续会贴出来给大家。