查看原文
其他

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

DotNet 2021-09-23

(给DotNet加星标,提升.Net技能

转自:DavidChild
cnblogs.com/davidchildblog/p/fuwujiekqianming.html

前言


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


  • 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 加签原理:


我们假设有这么一个统一入参结构的请求报文



在我们构建传送报文的时候,我们看到有一个字段:sign 是 由 服务方分配给客户端一个 秘钥字符串 再加上 报文中 ( time 时间戳+ objcet 业务参序列化)相加后的字符串 后 MD5值。 


我们这里 sign 的形成有两个关键点:


  • sign  值形成的算法,我这边算法暂时是 :sign= MD5(openkey+ time+ JsonConvert.SerializeObject(object))


  • sign 分配给客户端的秘钥值—openkey


如下加签请求伪代码:


namespace T.API
{
/// <summary>
/// 请求的报文对象
/// </summary>
public class SendObject
{
/// <summary>
/// 发送实体对象
/// </summary>
public object @object { get; set; }

/// <summary>
/// 签名
/// </summary>
public string sign { get; set; }

/// <summary>
/// 当前请求的时间戳
/// </summary>
public long? time { get; set; }

/// <summary>
/// 用户id
/// </summary>
public int userId { get; set; }
}

/// <summary>
/// 接收到的报文对象
/// </summary>
public class ReciveObject
{
/// <summary>
/// 发送实体对象
/// </summary>
public object @object { get; set; }

/// <summary>
/// 服务请求响应值 code 为 1:请求成功 ,请求无异常
/// 当code 为 "1" 的情况下,下面的RevRep 对象中的 message 字段 90% 的场景为空,
/// 如果有必要赋值视双方业务场景而定;
/// code为 0:我方程序异常/业务性质失败/接口参数校验失败,
/// 当 code 为 "0"的情况下,下面message字段包装了异常/失败信息。
/// </summary>
public int code { get; set; }

/// <summary>
/// 请求响应的错误消息/或者其他业务场景响应提示信息
/// </summary>
public int message { get; set; }
}

/// <summary>
/// 上面 Req 对象中的object 封装字段具体实体定义
/// </summary>
public class ObjectEntity
{
public string orderNum { get; set; }

/// <summary>
/// 如果参数是浮点型,在实体中定义成字符串类型.
/// </summary>
public string orderMoney { get; set; }

/// <summary>
/// 如果参数是时间类型的,在实体中定义成long 时间戳类型
/// </summary>
public long? orderTime { get; set; }
}

/// <summary>
/// 请求示例代码
/// </summary>
public class RequestDemo
{
/// <summary>
/// 请求示例,调用方请求
/// </summary>
public static void Request()
{
//服务端分配给调用方:openkey
string openKey = "455853655-7dff-5585545-a1c3-7778887"; //

//定义发送对象
SendObject sendobject = new SendObject();
//定义请求时间戳
long? reqtime= DateTime.Now.ToSafeDateTime().ToSafeDataLong();// 赋值
sendobject.time = reqtime;
try
{
//定义以及赋值业务实体
ObjectEntity objectEntity = new ObjectEntity();
objectEntity.orderNum = "20200506071001";
objectEntity.orderTime = DateTime.Now.ToSafeDateTime().ToSafeDataLong();
objectEntity.orderMoney = "526.00";
//将定义好的业务实体塞入SendObject的object字段中.
sendobject.@object = objectEntity;
//加签并且赋值签名
sendobject.sign = sign(reqtime, openkey,JsonConvert.SerializeObject(sendobject.@object));
RestRequest rq = new RestRequest(Method.POST);
rq.Method = Method.POST; //请求设置为POST
rq.AddHeader(" Content-Type", "application/json;charset=utf-8"); //头部塞入Content-Type
rq.AddParameter("application/json", JsonConvert.SerializeObject(sendobject), ParameterType.RequestBody);
RestClient restclient = new RestClient { BaseUrl = new Uri("http://xx.xx.xx.xx:5021") }; //调用地址
TaskCompletionSource<IRestResponse> tcs = new TaskCompletionSource<IRestResponse>();
restclient.ExecuteAsync(rq, r =>
{
tcs.SetResult(r);
});
IRestResponse respones = tcs.Task.Result; // 请求返回的数据
//如果请求状态正常
if ((int)respones.StatusCode == 200)
{
ReciveObject recive = JsonConvert.DeserializeObject<ReciveObject>(respones.Content);
if (recive.code == 1)
{
//处理业务
}
else
{
//处理业务
}
}
else
{
throw new Exception("调用异常通讯状态:${respones.StatusCode}");
}
}
catch (Exception ex)
{
}
}
/// <summary>
/// Md5 方法
/// </summary>
public static string MD5(string md5orgincontent)
{
string md5result = string.Empty;
if (string.IsNullOrEmpty(md5result)) return md5result;
StringBuilder sb = new StringBuilder();
MD5 md5 = new MD5CryptoServiceProvider();
byte[] s = md5.ComputeHash(Encoding.UTF8.GetBytes(md5orgincontent));
md5.Clear();
for (int i = 0; i < s.Length; i++)
{
sb.Append(s[i].ToString("x2"));
}
md5result=sb.ToString();
return md5result;
}

/// <summary>
/// 加签
/// </summary>
/// <param name="time">时间戳</param>
/// <param name="openkey">服务端分配给调用方:openkey</param>
/// <param name="szobject">参与加签的object的json序列化字符串</param>
/// <returns></returns>
public static string sign(long? time, string openkey, string szobject)
{
string signresult = string.Empty;
var signcontent = openkey+time.ToSafeString()+szobject;
signresult = MD5(signcontent);
return signresult;
}
}

}


服务端验签原理:


服务端通过  服务端定义接口拦截器或者全局过拦截器。接口接收到的报文是上述表格的报文结构后,做如下事情


1、同样:接拦截器中,做同样的事情:秘钥字符串 再加上 报文传送过来的 ( time 时间戳+ objcet 业务参序列化)相加后的字符串 MD5值 ,我将此值 为 service_sign


2、将服务端的  service_sign 值跟 报文中的 sign 进行比对,如果发现不匹配(假设在双方算法一直,openkey 一致的情况下):报文被篡改,签名验证不通过


我在这里贴出服务端验签 C# 代码:其他语言可以参考:


服务端先定义一个接收报文的对象:


[JsonObject(MemberSerialization.OptIn)]
public class ResultRequset : BaseRequestEntity
{
[JsonProperty]
public object @object { get; set; }
public virtual string openKey{ get; set; }
/// <summary>
/// 服务端加签:此值将于传送过来的 sign 值最终进行比对/// </summary>
public override string checkedSign
{
get
{
var orgin =this.time.ToString() + openKey+ JsonConvert.SerializeObject(@object);
return EntitySign.To32Md5(orgin);
}
}

/// <summary>
/// 签名验证
/// </summary>
/// <returns></returns>
public Result CheckedSign()
{
Result r = new Result();
if (this.sign == checkedSign)
{
r.code = 1;
return r;
}
else
{
r.code = 0;
r.message = "延签失败!";
}
return r;
}
}


然后构建一个拦截器,拦截器的工作如下所示 NETFramework 代码,其他语言可以参考:


public class OpenSignAttribute : ActionFilterAttribute
{
public Type RequestType { get; set; }

public override void OnActionExecuting(HttpActionContext actionContext)
{
HttpContent content = actionContext.Request.Content;
var gloablkey = string.Empty;
ResultRequset resultRequset = new ResultRequset();
oreach (KeyValuePair<string, object> obj in actionContext.ActionArguments)
{

resultRequset = (ResultRequset)obj.Value; //第一步:获取报文数据,强制转换到 上面定义的 ResultRequset 报文接收对象
}
Resre = resultRequset.CheckedSign(); //第二步: 服务端进行加签并且验签
if (re.code == 1)
{
base.OnActionExecuting(actionContext);
}
else
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, re);
}
}
}
}


我们看一下接口定义- 给 接口 打上  OpenSign  标签,并且使用  ResultRequset 来接收对方过来的报文数据。


[HttpPost]
[OpenSign]
public ResultRequset test([FromBody]ResultRequset obj)
{
}


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


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


我们先定义一个查找方式的接口:


public interface ISingSecret
{
string OpenId(Microsoft.AspNetCore.Http.HttpRequest request =null);
string OpenKey(string OpenId);
}


服务端接收对象改造:


[JsonObject(MemberSerialization.OptIn)]
public class ResultRequset
{
[JsonProperty]
public object @object { get; set; }
/// <summary>
/// 可以覆盖此KEY的方式
/// </summary>
public virtual string openKey{ get; set; }

/// <summary>
/// 开放平台所使用的分配给客户的OPENID
/// </summary>
[JsonProperty]
public string openId
{
get;
set;
}

/// <summary>
/// 获取签名
/// </summary>
public override string checkedSign
{
get
{
var orgin = singContent;
return EntitySign.To32Md5(orgin);
}
}

/// <summary>
/// 用户ID 登入人ID
/// </summary>
[JsonIgnore]
public string singContent
{
get { return openKey+ this.time.ToString() + JsonConvert.SerializeObject(@object); }

}
/// <summary>
/// 签名验证
/// </summary>
/// <returns></returns>
public Result CheckedSign()
{
Result r = new Result();
if (this.sign == checkedSign)
{
r.code = 1;
return r;
}
else
{
r.code = 0;
r.message = "签名验证失败!";
LogService.Default.Debug("签名验证失败---"+"框架签名" + checkedSign.ToSafeString("")+"-------网络签名:"+ sign.ToSafeString("") + "--------签名信息:" + singContent);
}
return r;
}
}


服务端拦截器改造:


[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class CentralSign: ActionFilterAttribute
{
private Type ISingRealization { get; set; }
private ISingSecret singRealization { get; set; } //关键代码:由服务端实现通过openid 查出openkey 的具体逻辑.
public CentralSign(Type ISingSecret) // 关键代码: 定义带构造函数的 接口标签属性 .
{
this.ISingRealization = ISingSecret;
if (ISingRealization != null)
{
//获取类的初始化参数信息
ConstructorInfo obj = ISingRealization.GetConstructor(System.Type.EmptyTypes);
singRealization = (ISingSecret)Activator.CreateInstance(ISingRealization); //实例化对象
}
}
public override void OnActionExecuting(ActionExecutingContext actionContext)
{
var content = actionContext.HttpContext.Request;
var gloablkey = string.Empty;
ResultRequset resultRequset = new ResultRequset();
foreach (KeyValuePair<string, object> obj in actionContext.ActionArguments)
{
resultRequset = (ResultRequset)obj.Value;
}
Result re = new Result();
if (resultRequset == null)
{
re.code = 0;
re.message = "传值不能为空";
actionContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
actionContext.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(re));

}
else
{
if (singRealization != null)
{
var openId = singRealization.OpenId(actionContext.HttpContext.Request); // 关键代码: 通过 ISingSecret.OpenId() 方法,获取到对应调用方传送过来的 openid

resultRequset.publicApikey = singRealization.OpenKey(openId); // 关键代码: 通过 ISingSecret.OpenId() 方法,获取到对应调用方传送过来的 openid

}
re = resultRequset.CheckedSign();
if (re.code == 1)
{
base.OnActionExecuting(actionContext);
}
else
{
HandleUnauthorizedRequest(actionContext);
}
}
}
protected void HandleUnauthorizedRequest(ActionExecutingContext actionContext)
{
var r = new JsonResult("签名失败,访问受限.");
r.StatusCode = (int)HttpStatusCode.BadRequest;
actionContext.Result =r;
return;
}
}


服务端接口定义改造:


[HttpPost]
[CentralSign(typeof(OpenSign))]
public Result SignatureSample([FromBody]ResultRequset result)
{
var str = result.@object.ToSafeString("");
Result re = new Result() { code = 1,message="签名验证成功!"};
re.@object = str;
return re;
}


上面接口定义 打上了  [CentralSign(typeof(OpenSign))] 标签,CentralSign 接收了一个 OpenSign Type  对象类型。根据上面的代码,我们知道,OpenSign 实现了  ISingSecret 逻辑。我们具体看下 OpenSign  具体实现:


OpenSign  实现 ISingSecret 逻辑代码:


public class OpenSign : ISingSecret
{
public string OpenId(HttpRequest request)
{
return Header.GetHeaderValue(request,"openId");
}
public string OpenKey(string OpenId)
{
return ConfigManage.JsonConfigMange.GetInstance().AppSettings[OpenId];
}
}


这样我们就整体上完成了我们所需要的 框架性 服务接口签名认证代码。上面的代码 在Bitter.Frame 框架 服务签名模块中有, Bitter.Frame 代码还在整理中 。后续会贴出来给大家。


- EOF -


推荐阅读  点击标题可跳转
.NET Core 中实现健康检查WinForm调用摄像头扫码识别二维码.NET Core 部署到 Linux(CentOS) 最全解决方案 (进阶篇)


看完本文有收获?请转发分享给更多人

推荐关注「DotNet」,提升.Net技能 

点赞和在看就是最大的支持❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存