WebAPi使用公钥私钥加密介绍和使用
随着各种设备的兴起,WebApi作为服务也越来越流行。而在无任何保护措施的情况下接口完全暴露在外面,将导致被恶意请求。最近项目的项目中由于提供给APP的接口未对接口进行时间防范导致短信接口被怒对造成一定的损失,临时的措施导致PC和app的防止措施不一样导致后来前端调用相当痛苦,选型过oauth,https,当然都被上级未通过,那就只能自己写了,就很,ԾㅂԾ,。下面就此次的方式做一次记录。
最终的效果:传输过程中都是密文,别人拿到请求串不能更改请求参数,通过接口过期时间防止同一请求串一直被调用。
第一步重写MessageProcessingHandler中的ProcessRequest和ProcessResponse
无论是APi还是Mvc请求管道都提供了我们很好的去扩展,本次说的是api,其实mvc大概意思也是差不多的。我们现在主要写出大致流程
![](https://img-
blog.csdnimg.cn/img_convert/ee44bfb05709f58daa0688fe4048bdab.jpeg)
从图中可以看出我们需要在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。
第二步重写AuthorizeAttribute中OnAuthorization和HandleUnauthorizedRequest
![](https://img-
blog.csdnimg.cn/img_convert/05d39b07436d8de533147d81f8c457f3.jpeg)
上图是Api中Filter的请求顺序,我们在第一个Filter上处理,代码如下:
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请求如下:
![](https://img-
blog.csdnimg.cn/img_convert/f25a4f45f26c121912acadb80848111b.jpeg)
可以看到我们的请求串url已经是密文,头部时间sign也是密文,除非别人拿到我们的私钥不然是不能修改其参数的。然后请求到达我们的CustomerMessageProcesssingHandler中我们看下Get中得到的参数是:
![](https://img-
blog.csdnimg.cn/img_convert/3548820d885886be52a56f5c6b2341c8.jpeg)
这是我们得到的前端传过来的querystring的参数他的值就是我们前端加密后传过来的下一步我们解密应该要得到未加密之前的参数也就是客户端中id=1同时重新给requesturi赋值;
![](https://img-
blog.csdnimg.cn/img_convert/8e9793eefbba465a1019a75287101a70.jpeg)
结果中我们可以看到id=1已被正确解密得到。接下来进入我们的CustomRequestAuthorizeAttribute
![](https://img-
blog.csdnimg.cn/img_convert/72a1da5616715b4d3e70f97e953f52ca.jpeg)
在这一步我们进行对timeSign的解密对请求只进行hash对比然后验证时间戳是否在过期时间内最终我们到达相应的action:
![](https://img-
blog.csdnimg.cn/img_convert/437018bccceba7add92db737be7f57b2.jpeg)
这样整个请求也就完成了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 }
最后当验证不通过得到的返回值:
![](https://img-
blog.csdnimg.cn/img_convert/4dbd5d692880d51f075b0c2cf2b57e37.jpeg)
这也就是重写HandleUnauthorizedRequest的目的 当然你也可以不重写此方法那么返回的就是401 英文的未通过验证。
接下来我将给各位同学划分一张学习计划表!
学习计划
那么问题又来了,作为萌新小白,我应该先学什么,再学什么?
既然你都问的这么直白了,我就告诉你,零基础应该从什么开始学起:
阶段一:初级网络安全工程师
接下来我将给大家安排一个为期1个月的网络安全初级计划,当你学完后,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web渗透、安全服务、安全分析等岗位;其中,如果你等保模块学的好,还可以从事等保工程师。
综合薪资区间6k~15k
1、网络安全理论知识(2天)
①了解行业相关背景,前景,确定发展方向。
②学习网络安全相关法律法规。
③网络安全运营的概念。
④等保简介、等保规定、流程和规范。(非常重要)
2、渗透测试基础(1周)
①渗透测试的流程、分类、标准
②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking
③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察
④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等
3、操作系统基础(1周)
①Windows系统常见功能和命令
②Kali Linux系统常见功能和命令
③操作系统安全(系统入侵排查/系统加固基础)
4、计算机网络基础(1周)
①计算机网络基础、协议和架构
②网络通信原理、OSI模型、数据转发流程
③常见协议解析(HTTP、TCP/IP、ARP等)
④网络攻击技术与网络安全防御技术
⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现
5、数据库基础操作(2天)
①数据库基础
②SQL语言基础
③数据库安全加固
6、Web渗透(1周)
①HTML、CSS和JavaScript简介
②OWASP Top10
③Web漏洞扫描工具
④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)
那么,到此为止,已经耗时1个月左右。你已经成功成为了一名“脚本小子”。那么你还想接着往下探索吗?
阶段二:中级or高级网络安全工程师(看自己能力)
综合薪资区间15k~30k
7、脚本编程学习(4周)
在网络安全领域。是否具备编程能力是“脚本小子”和真正网络安全工程师的本质区别。在实际的渗透测试过程中,面对复杂多变的网络环境,当常用工具不能满足实际需求的时候,往往需要对现有工具进行扩展,或者编写符合我们要求的工具、自动化脚本,这个时候就需要具备一定的编程能力。在分秒必争的CTF竞赛中,想要高效地使用自制的脚本工具来实现各种目的,更是需要拥有编程能力。
零基础入门的同学,我建议选择脚本语言Python/PHP/Go/Java中的一种,对常用库进行编程学习
搭建开发环境和选择IDE,PHP环境推荐Wamp和XAMPP,IDE强烈推荐Sublime;
Python编程学习,学习内容包含:语法、正则、文件、 网络、多线程等常用库,推荐《Python核心编程》,没必要看完
用Python编写漏洞的exp,然后写一个简单的网络爬虫
PHP基本语法学习并书写一个简单的博客系统
熟悉MVC架构,并试着学习一个PHP框架或者Python框架 (可选)
了解Bootstrap的布局或者CSS。
阶段三:顶级网络安全工程师
如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!
学习资料分享
当然,只给予计划不给予学习资料的行为无异于耍流氓,这里给大家整理了一份【282G】的网络安全工程师从入门到精通的学习资料包,可点击下方二维码链接领取哦。