随着各种设备的兴起,WebApi作为服务也越来越流行。而在无任何保护措施的情况下接口完全暴露在外面,将导致被恶意请求。最近项目的项目中由于提供给APP的接口未对接口进行时间防范导致短信接口被怒对造成一定的损失,临时的措施导致PC和app的防止措施不一样导致后来前端调用相当痛苦,选型过oauth,https,当然都被上级未通过,那就只能自己写了,就很,,???,,。下面就此次的方式做一次记录。最终的效果:传输过程中都是密文,别人拿到请求串不能更改请求参数,通过接口过期时间防止同一请求串一直被调用。
无论是APi还是Mvc请求管道都提供了我们很好的去扩展,本次说的是api,其实mvc大概意思也是差不多的。我们现在主要写出大致流程
从图中可以看出我们需要在MessageProcessingHandlder上做处理。我们继承MessageProcessingHandlder重写ProcessRequest和ProcessResponse方法,从方法名可以看出一个是针对请求值处理,一个是针对返回值处理代码如下:
1 public class CustomerMessageProcesssingHandler : MessageProcessingHandler 2 { 3 protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken) 4 { 5 var contentType = request.Content.Headers.ContentType; 6 7 if (!request.Headers.Contains("platformtype")) 8 { 9 return request; 10 } 11 //根据平台编号获得对应私钥 12 string privateKey = Encoding.UTF8.GetString(Convert.FromBase64String(ConfigurationManager.AppSettings["PlatformPrivateKey_" + request.Headers.GetValues("platformtype").FirstOrDefault()])); 13 if (request.Method == HttpMethod.Post) 14 { 15 // 读取请求body中的数据 16 string baseContent = request.Content.ReadAsStringAsync().Result; 17 // 获取加密的信息 18 // 兼容 body: 加密数据 和 body: sign=加密数据 19 baseContent = Regex.Match(baseContent, "(sign=)*(?<sign>[\\S]+)").Groups[2].Value; 20 // 用加密对象解密数据 21 baseContent = CommonHelper.RSADecrypt(privateKey, baseContent); 22 // 将解密后的BODY数据 重置 23 request.Content = new StringContent(baseContent); 24 //此contentType必须最后设置 否则会变成默认值 25 request.Content.Headers.ContentType = contentType; 26 } 27 if (request.Method == HttpMethod.Get) 28 { 29 string baseQuery = request.RequestUri.Query; 30 // 读取请求 url query数据 31 baseQuery = baseQuery.Substring(1); 32 baseQuery = Regex.Match(baseQuery, "(sign=)*(?<sign>[\\S]+)").Groups[2].Value; 33 baseQuery = CommonHelper.RSADecrypt(privateKey, baseQuery); 34 // 将解密后的 URL 重置URL请求 35 request.RequestUri = new Uri($"{request.RequestUri.AbsoluteUri.Split('?')[0]}?{baseQuery}"); 36 } 37 return request; 38 } 39 protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken) 40 { 41 return response; 42 } 43 }上面的代码大部分已经有注释了,但这里说明三点第一:platformtype用来针对不同的平台设置不同的公钥和私钥;第二:在post方法中ContentType一定要最后设置,否则会成为默认值,这个问题会导致webapi不能进行正确的参数绑定;第三:有人可能会问这里ProcessResponse是不是可以不用重写?答案是必须重写如果不想对结果操作直接返回就如上当然你也可以在此对返回值进行加密,但是个人认为意义不大,看具体情况因为大部分数据加密后前端还是需要解密然后展示所以此处不做任何处理。在这一步我们已经对前端请求的加密串在handler中处理成明文重新赋值给HttpRequestMessage。
1 public class CustomRequestAuthorizeAttribute : AuthorizeAttribute 2 { 3 4 public override void OnAuthorization(HttpActionContext actionContext) 5 { 6 //action具有[AllowAnonymous]特性不参与验证 7 if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().OfType<AllowAnonymousAttribute>().Any(x => x is AllowAnonymousAttribute)) 8 { 9 base.OnAuthorization(actionContext); 10 return; 11 } 12 var request = actionContext.Request; 13 string method = request.Method.Method, timeStamp = string.Empty, expireyTime = ConfigurationManager.AppSettings["UrlExpireTime"], timeSign = string.Empty, platformType = string.Empty; 14 if (!request.Headers.Contains("timesign") || !request.Headers.Contains("platformtype") || !request.Headers.Contains("timestamp") || !request.Headers.Contains("expiretime")) 15 { 16 HandleUnauthorizedRequest(actionContext); 17 return; 18 } 19 platformType = request.Headers.GetValues("platformtype").FirstOrDefault(); 20 timeSign = request.Headers.GetValues("timesign").FirstOrDefault(); 21 timeStamp = request.Headers.GetValues("timestamp").FirstOrDefault(); 22 var tempExpireyTime = request.Headers.GetValues("expiretime").FirstOrDefault(); 23 string privateKey = Encoding.UTF8.GetString(Convert.FromBase64String(ConfigurationManager.AppSettings[$"PlatformPrivateKey_{platformType}"])); 24 if (!SignValidate(tempExpireyTime, privateKey, timeStamp, timeSign)) 25 { 26 HandleUnauthorizedRequest(actionContext); 27 return; 28 } 29 if (tempExpireyTime != "0") 30 { 31 expireyTime = tempExpireyTime; 32 } 33 //判断timespan是否有效 34 double ts2 = ConvertHelper.ToDouble((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds, 2), ts = ts2 - ConvertHelper.ToDouble(timeStamp); 35 bool falg = ts > int.Parse(expireyTime) * 1000; 36 if (falg) 37 { 38 HandleUnauthorizedRequest(actionContext); 39 return; 40 } 41 base.IsAuthorized(actionContext); 42 } 43 protected override void HandleUnauthorizedRequest(HttpActionContext filterContext) 44 { 45 base.HandleUnauthorizedRequest(filterContext); 46 47 var response = filterContext.Response = filterContext.Response ?? new HttpResponseMessage(); 48 response.StatusCode = HttpStatusCode.Forbidden; 49 var content = new 50 { 51 BusinessStatus = -10403, 52 StatusMessage = "服务端拒绝访问" 53 }; 54 response.Content = new StringContent(JsonConvert.SerializeObject(content), Encoding.UTF8, "application/json"); 55 } 56 private bool SignValidate(string expiryTime, string privateKey, string timestamp, string sign) 57 { 58 bool isValidate = false; 59 var tempSign = CommonHelper.RSADecrypt(privateKey, sign); 60 if (CommonHelper.EncryptSHA256($"expiretime{expiryTime}" + $"timestamp{timestamp}") == tempSign) 61 { 62 isValidate = true; 63 } 64 return isValidate; 65 } 66 }
请求头部增加参数expiretime使用此参数作为本次接口的过期时间如果没有则表示使用平台默认的接口时间,是我们可以针对不同的接口设置不同的过期时间;timestamp请求时间戳来防止别人拿到接口后一直调用timesign是过期时间和时间戳通过hash然后在通过公钥加密的串来防止别人修改前两个参数。重写HandleUnauthorizedRequest来设置返回内容。
至此整个验证过程就结束了,我们在使用过程中可以建立BaseApi将特性标记上让其他APi继承,当然我们的接口中可能有的action不需要验证看OnAuthorization第一行代码 增加相应的特性跳过此验证。在整个过程中其实我们已经使用了两种加密方式。一是本文中的CustomerMessageProcesssingHandler;另外一种就是timestamp+QueryString然后hash 在公钥加密 这样就不需要CustomerMessageProcesssingHandler其实就是本文中的头部加密方式。