首先问你们一个问题,你在写开放的API接口时是如何保证数据的安全性的?先来看看有哪些安全性问题在开放的api接口中,咱们经过http Post或者Get方式请求服务器的时候,会面临着许多的安全性问题,例如:git
为了保证数据在通讯时的安全性,咱们能够采用TOKEN+参数签名的方式来进行相关验证。github
好比说咱们客户端须要查询产品信息这个操做来进行分析,客户端点击查询按钮==》调用服务器端api进行查询==》服务器端返回查询结果web
1、不进行验证的方式算法
api查询接口:json
客户端调用:http://api.XXX.com/getproduct?id=value1api
如上,这种方式简单粗暴,在浏览器直接输入"http://api.XXX.com/getproduct?id=value1",便可获取产品列表信息了,可是这样的方式会存在很严重的安全性问题,没有进行任何的验证,你们均可以经过这个方法获取到产品列表,致使产品信息泄露。
那么,如何验证调用者身份呢?如何防止参数被篡改呢?如何保证请求的惟一性? 如何保证请求的惟一性,防止请求被恶意攻击呢?浏览器
2、使用TOKEN+签名认证 保证请求安全性缓存
token+签名认证的主要原理是:1.作一个认证服务,提供一个认证的webapi,用户先访问它获取对应的token安全
2.用户拿着相应的token以及请求的参数和服务器端提供的签名算法计算出签名后再去访问指定的api服务器
3.服务器端每次接收到请求就获取对应用户的token和请求参数,服务器端再次计算签名和客户端签名作对比,若是验证经过则正常访问相应的api,验证失败则返回具体的失败信息
具体代码以下 :
1.用户请求认证服务GetToken,将TOKEN保存在服务器端缓存中,并返回对应的TOKEN到客户端(该请求不须要进行签名认证)
public HttpResponseMessage GetToken(string staffId) { ResultMsg resultMsg = null; int id = 0; //判断参数是否合法 if (string.IsNullOrEmpty(staffId) || (!int.TryParse(staffId, out id))) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError; resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText(); resultMsg.Data = ""; return HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); } //插入缓存 Token token =(Token)HttpRuntime.Cache.Get(id.ToString()); if (HttpRuntime.Cache.Get(id.ToString()) == null) { token = new Token(); token.StaffId = id; token.SignToken = Guid.NewGuid(); token.ExpireTime = DateTime.Now.AddDays(1); HttpRuntime.Cache.Insert(token.StaffId.ToString(), token, null, token.ExpireTime, TimeSpan.Zero); } //返回token信息 resultMsg =new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.Success; resultMsg.Info = ""; resultMsg.Data = token; return HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); }
2.客户端调用服务器端API,须要对请求进行签名认证,签名方式以下
(1) get请求:按照请求参数名称将全部请求参数按照字母前后顺序排序获得:keyvaluekeyvalue...keyvalue 字符串如:将arong=1,mrong=2,crong=3 排序为:arong=1, crong=3,mrong=2 而后将参数名和参数值进行拼接获得参数字符串:arong1crong3mrong2。
public static Tuple<string,string> GetQueryString(Dictionary<string, string> parames) { // 第一步:把字典按Key的字母顺序排序 IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parames); IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator(); // 第二步:把全部参数名和参数值串在一块儿 StringBuilder query = new StringBuilder(""); //签名字符串 StringBuilder queryStr = new StringBuilder(""); //url参数 if (parames == null || parames.Count == 0) return new Tuple<string,string>("",""); while (dem.MoveNext()) { string key = dem.Current.Key; string value = dem.Current.Value; if (!string.IsNullOrEmpty(key)) { query.Append(key).Append(value); queryStr.Append("&").Append(key).Append("=").Append(value); } } return new Tuple<string, string>(query.ToString(), queryStr.ToString().Substring(1, queryStr.Length - 1)); }
post请求:将请求的参数对象序列化为json格式字符串
Product product = new Product() { Id = 1, Name = "安慕希", Count = 10, Price = 58.8 }; var data=JsonConvert.SerializeObject(product);
(2)在请求头中添加timespan(时间戳),nonce(随机数),staffId(用户Id),signature(签名参数)
//加入头信息 request.Headers.Add("staffid", staffId.ToString()); //当前请求用户StaffId request.Headers.Add("timestamp", timeStamp); //发起请求时的时间戳(单位:毫秒) request.Headers.Add("nonce", nonce); //发起请求时的时间戳(单位:毫秒) request.Headers.Add("signature", GetSignature(timeStamp,nonce,staffId,data)); //当前请求内容的数字签名
(3)根据请求参数计算本次请求的签名,用timespan+nonc+staffId+token+data(请求参数字符串)获得signStr签名字符串,而后再进行排序和MD5加密获得最终的signature签名字符串,添加到请求头中
private static string GetSignature(string timeStamp,string nonce,int staffId,string data) { Token token = null; var resultMsg = GetSignToken(staffId); if (resultMsg != null) { if (resultMsg.StatusCode == (int)StatusCodeEnum.Success) { token = resultMsg.Result; } else { throw new Exception(resultMsg.Data.ToString()); } } else { throw new Exception("token为null,员工编号为:" +staffId); } var hash = System.Security.Cryptography.MD5.Create(); //拼接签名数据 var signStr = timeStamp +nonce+ staffId + token.SignToken.ToString() + data; //将字符串中字符按升序排序 var sortStr = string.Concat(signStr.OrderBy(c => c)); var bytes = Encoding.UTF8.GetBytes(sortStr); //使用MD5加密 var md5Val = hash.ComputeHash(bytes); //把二进制转化为大写的十六进制 StringBuilder result = new StringBuilder(); foreach (var c in md5Val) { result.Append(c.ToString("X2")); } return result.ToString().ToUpper(); }
(4) webapi接收到相应的请求,取出请求头中的timespan,nonc,staffid,signature 数据,根据timespan判断这次请求是否失效,根据staffid取出相应token判断token是否失效,根据请求类型取出对应的请求参数,而后服务器端按照一样的规则从新计算请求签名,判断和请求头中的signature数据是否相同,若是相同的话则是合法请求,正常返回数据,若是不相同的话,该请求可能被恶意篡改,禁止访问相应的数据,返回相应的错误信息
以下使用全局过滤器拦截全部api请求进行统一的处理
public class ApiSecurityFilter : ActionFilterAttribute { public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) { ResultMsg resultMsg = null; var request = actionContext.Request; string method = request.Method.Method; string staffid = String.Empty, timestamp = string.Empty, nonce = string.Empty, signature = string.Empty; int id = 0; if (request.Headers.Contains("staffid")) { staffid = HttpUtility.UrlDecode(request.Headers.GetValues("staffid").FirstOrDefault()); } if (request.Headers.Contains("timestamp")) { timestamp = HttpUtility.UrlDecode(request.Headers.GetValues("timestamp").FirstOrDefault()); } if (request.Headers.Contains("nonce")) { nonce = HttpUtility.UrlDecode(request.Headers.GetValues("nonce").FirstOrDefault()); } if (request.Headers.Contains("signature")) { signature = HttpUtility.UrlDecode(request.Headers.GetValues("signature").FirstOrDefault()); } //GetToken方法不须要进行签名验证 if (actionContext.ActionDescriptor.ActionName == "GetToken") { if (string.IsNullOrEmpty(staffid) || (!int.TryParse(staffid, out id) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce))) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError; resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } else { base.OnActionExecuting(actionContext); return; } } //判断请求头是否包含如下参数 if (string.IsNullOrEmpty(staffid) || (!int.TryParse(staffid, out id) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce) || string.IsNullOrEmpty(signature))) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError; resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } //判断timespan是否有效 double ts1 = 0; double ts2 = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds; bool timespanvalidate = double.TryParse(timestamp, out ts1); double ts = ts2 - ts1; bool falg = ts > int.Parse(WebSettingsConfig.UrlExpireTime) * 1000; if (falg || (!timespanvalidate)) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.URLExpireError; resultMsg.Info = StatusCodeEnum.URLExpireError.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } //判断token是否有效 Token token = (Token)HttpRuntime.Cache.Get(id.ToString()); string signtoken = string.Empty; if (HttpRuntime.Cache.Get(id.ToString()) == null) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.TokenInvalid; resultMsg.Info = StatusCodeEnum.TokenInvalid.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } else { signtoken = token.SignToken.ToString(); } //根据请求类型拼接参数 NameValueCollection form = HttpContext.Current.Request.QueryString; string data = string.Empty; switch (method) { case "POST": Stream stream = HttpContext.Current.Request.InputStream; string responseJson = string.Empty; StreamReader streamReader = new StreamReader(stream); data = streamReader.ReadToEnd(); break; case "GET": //第一步:取出全部get参数 IDictionary<string, string> parameters = new Dictionary<string, string>(); for (int f = 0; f < form.Count; f++) { string key = form.Keys[f]; parameters.Add(key, form[key]); } // 第二步:把字典按Key的字母顺序排序 IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters); IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator(); // 第三步:把全部参数名和参数值串在一块儿 StringBuilder query = new StringBuilder(); while (dem.MoveNext()) { string key = dem.Current.Key; string value = dem.Current.Value; if (!string.IsNullOrEmpty(key)) { query.Append(key).Append(value); } } data = query.ToString(); break; default: resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.HttpMehtodError; resultMsg.Info = StatusCodeEnum.HttpMehtodError.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } bool result = SignExtension.Validate(timestamp, nonce, id, signtoken,data, signature); if (!result) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.HttpRequestError; resultMsg.Info = StatusCodeEnum.HttpRequestError.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } else { base.OnActionExecuting(actionContext); } } public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { base.OnActionExecuted(actionExecutedContext); } }
而后咱们进行测试,检验api请求的合法性
Get请求:
1.获取产品数据,传递参数id=1,name="wahaha" ,完整请求为http://localhost:14826/api/product/getproduct?id=1&name=wahaha
2.请求头添加timespan,staffid,nonce,signature字段
3.如图当data里面的值为id1namewahaha的时候请求头中的signature和服务器端计算出来的result的值是彻底同样的,当我将data修改成id1namewahaha1以后,服务器端计算出来的签名result和请求头中提交的signature就不相同了,就表示为不合法的请求了
4.不合法的请求就会被识别为请求参数已被修改
合法的请求则会返回对应的商品信息
post请求:
1.post对象序列化为json字符串后提交到后台,后台返回相应产品信息
2.后台获取请求的参数信息
3.判断签名是否成功,第一次请求签名参数signature和服务器端计算result彻底相同, 而后当把请求参数中count的数量从10改为100以后服务器端计算的result和请求签名参数signature不一样,因此请求不合法,是非法请求,同理若是其余任何参数被修改最后计算的结果都会和签名参数不一样,请求一样识别为不合法请求
总结:
经过上面的案例,咱们能够看出,安全的关键在于参与签名的TOKEN,整个过程当中TOKEN是不参与通讯的,因此只要保证TOKEN不泄露,请求就不会被伪造。
而后咱们经过timestamp时间戳用来验证请求是否过时,这样就算被人拿走完整的请求连接也是无效的。
Sign签名的方式可以在必定程度上防止信息被篡改和伪造,保障通讯的安全