背景介绍
最近使用WebApi开发一套对外接口,主要是数据的外送以及结果回传,接口没什么难度,采用WebApi+EF的架构简单创建一个模板工程,使用template生成一套WebApi接口,去掉put、delete等操作,修改一下就可以上线。这些都不在话下,反正网上一大堆教程,随便找那个step by step做下来就可以了。
然后发布上线后,接口是放在外网,面临两个问题:
- 如何保证接口的调用的合法性
- 如何保证接口及数据的安全性
其实这两个问题是相互结合的,先保证合法,然后在合法基础上保证请求的唯一性,避免参数被篡改。
鉴于接口上线期限紧迫,结合众多案例,先解决掉接口调用数据的安全性问题,这里采用了RSA报文加解密的方案,保证数据安全和防止接口被恶意调用以及参数篡改的问题。
本文参考博客园多篇博文,内容多有引用,文末附有参照博文的地址。
以下为正文!
正文
首先,接口面临的问题:
- 请求来源(身份)是否合法(部分解决,后续在处理)?
- 请求参数被篡改?
- 请求的唯一性(不可复制),防止请求被恶意攻击
解决方案:
- 参数加密: 客户端和服务端参数采用RSA加密后传递,原则上只有持有私钥的服务端才能解密客户端公钥加密的参数,避免了参数篡改的问题
- 请求签名:采用一套签名算法,对请求进行签名验证,保证请求的唯一性
这里参照了WebAPi使用公钥私钥加密介绍和使用 一文,进行公钥私钥加解密的处理
先说服务端:
扩展 MessageProcessingHandler
先看一下MessageProcessingHandler的介绍:
#region 程序集 System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
// C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Net.Http.dll
#endregion using System.Threading;
using System.Threading.Tasks; namespace System.Net.Http
{
//
// 摘要:
// 仅对请求和/或响应消息进行一些小型处理的处理程序的基类。
public abstract class MessageProcessingHandler : DelegatingHandler
{
//
// 摘要:
// 创建的一个实例 System.Net.Http.MessageProcessingHandler 类。
protected MessageProcessingHandler();
//
// 摘要:
// 创建的一个实例 System.Net.Http.MessageProcessingHandler 具有特定的内部处理程序类。
//
// 参数:
// innerHandler:
// 内部处理程序负责处理 HTTP 响应消息。
protected MessageProcessingHandler(HttpMessageHandler innerHandler); //
// 摘要:
// 处理每个发送到服务器的请求。
//
// 参数:
// request:
// 要处理的 HTTP 请求消息。
//
// cancellationToken:
// 可由其他对象或线程用以接收取消通知的取消标记。
//
// 返回结果:
// 已处理的 HTTP 请求消息。
protected abstract HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken);
//
// 摘要:
// 处理来自服务器的每个响应。
//
// 参数:
// response:
// 要处理的 HTTP 响应消息。
//
// cancellationToken:
// 可由其他对象或线程用以接收取消通知的取消标记。
//
// 返回结果:
// 已处理的 HTTP 响应消息。
protected abstract HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken);
//
// 摘要:
// 异步发送 HTTP 请求到要发送到服务器的内部处理程序。
//
// 参数:
// request:
// 要发送到服务器的 HTTP 请求消息。
//
// cancellationToken:
// 可由其他对象或线程用以接收取消通知的取消标记。
//
// 返回结果:
// 表示异步操作的任务对象。
//
// 异常:
// T:System.ArgumentNullException:
// request 是 null。
protected internal sealed override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}
}
扩展这个类的目的是解密参数,其实也可以推迟到Action过滤器中做,但是还是觉得时机上在这里处理比较合适。具体的建议了解一下WebApi消息管道以及扩展过滤器的相关文章,本文不再延伸。
下面是扩展的实现代码:
/// <summary>
/// 请求预处理,报文解密
/// </summary>
/// <seealso cref="System.Net.Http.MessageProcessingHandler"/>
public class ArgDecryptMessageProcesssingHandler : MessageProcessingHandler
{ /// <summary>
/// 处理每个发送到服务器的请求。
/// </summary>
/// <param name="request"> 要处理的 HTTP 请求消息。</param>
/// <param name="cancellationToken">可由其他对象或线程用以接收取消通知的取消标记。</param>
/// <returns>已处理的 HTTP 请求消息。</returns>
protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken)
{
var contentType = request.Content.Headers.ContentType; //swagger请求直接跳过不予处理
if (request.RequestUri.AbsolutePath.Contains("/swagger"))
{
return request;
} //获得平台私钥
string privateKey = Common.GetRsaPrivateKey(); //获取Get中的Query信息,解密后重置请求上下文
if (request.Method == HttpMethod.Get)
{
string baseQuery = request.RequestUri.Query;
if (!string.IsNullOrEmpty(baseQuery))
{
baseQuery = baseQuery.Substring();
baseQuery = Regex.Match(baseQuery, "(sign=)*(?<sign>[\\S]+)").Groups[].Value;
baseQuery = RsaHelper.RSADecrypt(privateKey, baseQuery);
var requestUrl = $"{request.RequestUri.AbsoluteUri.Split('?')[0]}?{baseQuery}";
request.RequestUri = new Uri(requestUrl);
} } //获取Post请求中body中的报文信息,解密后重置请求上下文
if (request.Method == HttpMethod.Post)
{
string baseContent = request.Content.ReadAsStringAsync().Result;
baseContent = Regex.Match(baseContent, "(sign=)*(?<sign>[\\S]+)").Groups[].Value;
baseContent = RsaHelper.RSADecrypt(privateKey, baseContent);
request.Content = new StringContent(baseContent);
//此contentType必须最后设置 否则会变成默认值
request.Content.Headers.ContentType = contentType;
} return request;
} /// <summary>
/// 处理来自服务器的每个响应。
/// </summary>
/// <param name="response"> 要处理的 HTTP 响应消息。</param>
/// <param name="cancellationToken">可由其他对象或线程用以接收取消通知的取消标记。</param>
/// <returns>已处理的 HTTP 响应消息。</returns>
protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken)
{
return response;
}
}
获取平台私钥那里,实际上可以针对不同的接口调用方单独一个,另起一篇在介绍。
然后找到解决方案【App_Start】目录下的WebApiConfig类,在里面添加如下代码,启用消息处理扩展类:
public static void Register(HttpConfiguration config)
{ // Web API 路由
config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.MessageHandlers.Add(new ArgDecryptMessageProcesssingHandler()); }
扩展 ActionFilterAttribute
注意!注意!注意!
原博文中是扩展的 AuthorizeAttribute,即认证和授权过滤器,代码实现上是没有多大差别的;在时机上认证和授权过滤器要比方法过滤器执行的要早,更适合做认证和授权的操作。而我们扩展这个过滤器的目的是对报文进行签名验证以及超时验证,所以使用方法过滤器更恰当些。
下面是扩展过滤器的代码:
/// <summary>
/// 扩展方法过滤器,进入方法前验证签名
/// </summary>
public class ApiVerifyFilter : ActionFilterAttribute
{ public override void OnActionExecuting(HttpActionContext actionContext)
{
base.OnActionExecuting(actionContext); //获取平台私钥
string privateKey = Common.GetRsaPrivateKey(); //获取请求的超时时间,为了测试设置为100秒,即两次调用间隔不能超过100秒
string expireyTime = ConfigurationManager.AppSettings["UrlExpireTime"];
var request = actionContext.Request; //验证签名所需header内容
if (!request.Headers.Contains("signature") || !request.Headers.Contains("timestamp") || !request.Headers.Contains("nonce"))
{
SetSpecialResponseMessage(actionContext, );
return;
}
var token = string.Empty;
var signature = request.Headers.GetValues("signature").FirstOrDefault();
var timeStamp = request.Headers.GetValues("timestamp").FirstOrDefault();
var nonce = request.Headers.GetValues("nonce").FirstOrDefault(); //验证签名
if (!Common.SignValidate(privateKey, nonce, timeStamp, signature, token))
{
SetSpecialResponseMessage(actionContext, );
return;
}
//检查接口调用是否超时
var ts = Common.DateTime2TimeStamp(DateTime.UtcNow) - Convert.ToDouble(timeStamp);
if (ts > int.Parse(expireyTime) * )
{
SetSpecialResponseMessage(actionContext, );
return;
}
} /// <summary>
/// 设置签名验证异常返回状态
/// </summary>
/// <param name="actionContext">当前请求上下文</param>
/// <param name="statusCode">异常状态码</param>
private static void SetSpecialResponseMessage(HttpActionContext actionContext, int statusCode)
{
BizResponseModel model = new BizResponseModel
{
Status = statusCode,
Date = DateTime.Now.ToString("yyyyMMddhhmmssfff"),
Message = "服务端拒绝访问"
};
switch (statusCode)
{
case :
model.Message = "没有设置签名、时间戳、随机字符串";
break;
case :
model.Message = "签名无效";
break;
case :
model.Message = "无效的请求";
break;
default:
break;
}
actionContext.Response = new HttpResponseMessage
{
Content = new StringContent(JsonConvert.SerializeObject(model))
};
} public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
}
}
这里为了方便写了个ResponseModel,代码如下:
/// <summary>
/// 特殊状态
/// </summary>
public class BizResponseModel
{
public int Status { get; set; }
public string Message { get; set; }
public string Date { get; set; }
}
然后下面是用的公共方法:
/// <summary>
/// 获取时间戳毫秒数
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static long DateTime2TimeStamp(DateTime dateTime)
{
TimeSpan ts = DateTime.UtcNow - new DateTime(, , , , , , );
return Convert.ToInt64(ts.TotalMilliseconds);
} public static bool SignValidate(string privateKey, string nonce, string timestamp, string signature, string token)
{
bool isValidate = false;
var tempSign = RsaHelper.RSADecrypt(privateKey, signature);
string[] arr = new[] { token, timestamp, nonce }.OrderBy(z => z).ToArray();
string arrString = string.Join("", arr);
var sha256Result = arrString.EncryptSha256();
if (sha256Result == tempSign)
{
isValidate = true;
}
return isValidate;
}
签名验证的过程如下:
- 获取到报文Header中的 nonce、timestamp、signature、token信息
- 将token、timestamp、nonce 三者合并数组中,然后进行顺序排序(排序为了保证后续三个字符串拼接后一致)
- 将数组拼接成字符串,然后进行sha256 哈希运算(这里随便什么运算都行,主要为了防止超长加密麻烦)
- 将上一步的哈希结果与[signature] RSA解密结果进行比对,一致则签名验证通过,否则则签名不一致,请求为伪造
然后,现在需要启用刚添加的方法过滤器,因为是继承与属性,可以全局启用,或者单个Controller中启用、或者为某个Action启用。全局启用代码如下:
下的WebApiConfig类添加如下代码:
config.Filters.Add(new ApiVerifyFilter());
OK,全部完成,最后附上两个前后的效果对比!
参考博文:
Asp.Net WebAPI中Filter过滤器的使用以及执行顺序
写博文太累了,回家吃螃蟹补补~
WebApi接口安全性 接口权限调用、参数防篡改防止恶意调用的更多相关文章
-
如何写出安全的API接口?接口参数加密签名设计思路
开发中经常用到接口,尤其是在面向服务的soa架构中,数据交互全是用的接口. 几年以前我认为,我写个接口,不向任何人告知我的接口地址,我的接口就是安全的,现在回想真是too young,too simp ...
-
ASP.NET WebAPI构建API接口服务实战演练
一.课程介绍 一.王小二和他领导的第一次故事 有一天王小二和往常一下去上早班,刚吃完早餐刚一打开电脑没一会儿.王小二的领导宋大宝走到他的面前,我们现在的系统需要提供服务给其他内部业务系统,我看你平时喜 ...
-
Spring Boot如何设计防篡改、防重放攻击接口
Spring Boot 防篡改.防重放攻击 本示例要内容 请求参数防止篡改攻击 基于timestamp方案,防止重放攻击 使用swagger接口文档自动生成 API接口设计 API接口由于需要供第三方 ...
-
AutoFac mvc和WebAPI 注册Service (接口和实现)
AutoFac mvc和WebAPI 注册Service (接口和实现) 1.准备组件版本:Autofac 3.5.0 Autofac.Integration.Mvc 3.3.0.0 (I ...
-
c#代码 天气接口 一分钟搞懂你的博客为什么没人看 看完python这段爬虫代码,java流泪了c#沉默了 图片二进制转换与存入数据库相关 C#7.0--引用返回值和引用局部变量 JS直接调用C#后台方法(ajax调用) Linq To Json SqlServer 递归查询
天气预报的程序.程序并不难. 看到这个需求第一个想法就是只要找到合适天气预报接口一切都是小意思,说干就干,立马跟学生沟通价格. 不过谈报价的过程中,差点没让我一口老血喷键盘上,话说我们程序猿的人 ...
-
给WebAPI的REST接口服务添加测试页面(一)
当使用WebAPI提供REST服务的时候,一个经常进行的操作是对接口进行测试.Asp.net WebAPI框架本身并没有提供这一接口,不过由于提供的是标准的REST服务,是可以非常方便的使用一些第三方 ...
-
ASP.NET Core WebApi构建API接口服务实战演练
一.ASP.NET Core WebApi课程介绍 人生苦短,我用.NET Core!提到Api接口,一般会想到以前用到的WebService和WCF服务,这三个技术都是用来创建服务接口,只不过Web ...
-
jeecg接口开发及权限实现原理
接口开发使用的框架 jeecg本身是基于 Spring MVC 框架搭建的,因此,使用 Spring MVC 框架的 RESTful API 功能来进行接口开发就是顺理成章的事了. 接口的拦截与鉴权 ...
-
在webapi中为Action使用dynamic参数实现Post方式调用
1.在webapi中使用controller/action/id的路径配置,打开文件[App_Start] -[WebApiConfig] config.Routes.MapHttpRoute( na ...
随机推荐
-
一周试用yii开发一个带各种该有功能的web程序(二)
上篇随笔写完的是yii能使用简单的命令创建出一个基本的架构,我们只需要在这个架构上进行代码编写,扩展功能.而生成的一个小型系统是可以操作的,但是不是我们想要的,所以,这篇结合源码讲如何创建出我们自己的 ...
-
hibnate 创建表的时候type=innodb报错
这个原因是在MYSQL5.5及以后版本中type=InnoDB 由ENGINE=InnoDB 代替. 解决办法,自己定义一个方言: package com.hotusm.dialect; /** * ...
-
【BZOJ 3083】遥远的国度
这道题很简单的连剖+分类讨论,但是SDOI Round2要来了,不会手动栈怎么办呢?只好用一下这道题练习一下手动栈了,结果调了一天多QwQ 链剖的第一个dfs用bfs水过就行,但是我自以为是地把倍增写 ...
-
jquery网页字体变大小
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
-
HDU 1494 跑跑卡丁车 (DP)
题目链接 题意 : 中文题不详述. 思路 : sum = L*N 段,每走过一段如果不用加速卡的话,能量会增20%,将20%看作1,也就是说每涨到15就要变为10,所以最多是14才不浪费. dp[i] ...
-
careercup-C和C++ 13.6
13.6 基类的析构函数为何要声明为virtual? 解答: 用对象指针来调用一个函数,有以下两种情况: 如果是虚函数,会调用派生类中的版本. 如果是非虚函数,会调用指针所指类型的实现版本. 析构函数 ...
-
JS拖拽原理
实现拖拽效果主要跟鼠标的三个事件有关: onmousedown : 选择要拖拽的元素 onmousemove : 移动元素 onmouseup : 释放元素 三个事件的关系: obj.onmoused ...
-
asp.net webapi中helppage
今天研究了下webapi,发现还有自动生成接口说明文档提供测试的功能 参考:https://docs.microsoft.com/en-us/aspnet/web-api/overview/getti ...
-
axis调用Web服务报axis unexpected wrapper element{XXXX}XXX错误的解决
使用axis调用WebService时报错:axis unexpected wrapper element{XXXX}YYY .... expected {XXXX}. 经查,XXXX为wsdl文件中 ...
-
使用h5py操作hdf5文件
HDF(Hierarchical Data Format)指一种为存储和处理大容量科学数据设计的文件格式及相应库文件.HDF 最早由美国国家超级计算应用中心 NCSA 开发,目前在非盈利组织 HDF ...