接口安全之公钥私钥加密
WebAPi使用公钥私钥加密介绍和使用
随着各种设备的兴起,WebApi作为服务也越来越流行。而在无任何保护措施的情况下接口完全暴露在外面,将导致被恶意请求。最近项目的项目中由于提供给APP的接口未对接口进行时间防范导致短信接口被怒对造成一定的损失,临时的措施导致PC和app的防止措施不一样导致后来前端调用相当痛苦,选型过oauth,https,当然都被上级未通过,那就只能自己写了,就很,,ԾㅂԾ,,。下面就此次的方式做一次记录。最终的效果:传输过程中都是密文,别人拿到请求串不能更改请求参数,通过接口过期时间防止同一请求串一直被调用。
第一步重写MessageProcessingHandler中的ProcessRequest和ProcessResponse
无论是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 }
第二步重写AuthorizeAttribute中OnAuthorization和HandleUnauthorizedRequest
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其实就是本文中的头部加密方式。
补充:园友建议增加请求端实例,确实是昨天有所遗漏。趁不忙补充上:
本次以HttpClient调用方式为例,展示Get,Post请求加密到执行的相应的action的过程;首先看一下Get请求如下:
可以看到我们的请求串url已经是密文,头部时间sign也是密文,除非别人拿到我们的私钥不然是不能修改其参数的。然后请求到达我们的CustomerMessageProcesssingHandler中我们看下Get中得到的参数是:
这是我们得到的前端传过来的querystring的参数他的值就是我们前端加密后传过来的下一步我们解密应该要得到未加密之前的参数也就是客户端中id=1同时重新给requesturi赋值;
结果中我们可以看到id=1已被正确解密得到。接下来进入我们的CustomRequestAuthorizeAttribute
在这一步我们进行对timeSign的解密对请求只进行hash对比然后验证时间戳是否在过期时间内最终我们到达相应的action:
这样整个请求也就完成了Post跟Get区别不大重要的在于拿到传递参数的地方不一样这里我只贴一下调用的代码过程同上:
1 public static void PostTestByModel() { 2 3 HttpClient http = new HttpClient(); 4 var timestamp = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds; 5 var expiretime = "600"; 6 var timesign = RSAEncrypt(publicKey, EncryptSHA256($"expiretime{expiretime}timestamp{timestamp}")); 7 var codeValue = RSAEncrypt(publicKey, JsonConvert.SerializeObject(new Tenmp { Id = 1, Name = "cl" })); 8 http.DefaultRequestHeaders.Add("platformtype", "Web"); 9 http.DefaultRequestHeaders.Add("timesign", $"{timesign}"); 10 http.DefaultRequestHeaders.Add("timestamp", $"{string.Format("{0:N2}", timestamp.ToString()) }"); 11 http.DefaultRequestHeaders.Add("expiretime", expiretime); 12 var url1 = string.Format($"{host}api/Values/PostTestByModel"); 13 HttpContent content = new StringContent(codeValue); 14 MediaTypeHeaderValue typeHeader = new MediaTypeHeaderValue("application/json"); 15 typeHeader.CharSet = "UTF-8"; 16 content.Headers.ContentType = typeHeader; 17 var response1 = http.PostAsync(url1, content).Result; 18 }
最后当验证不通过得到的返回值:
这也就是重写HandleUnauthorizedRequest的目的 当然你也可以不重写此方法那么返回的就是401 英文的未通过验证。