背景介绍

最近使用WebApi开发一套对外接口,主要是数据的外送以及结果回传,接口没什么难度,采用WebApi+EF的架构简单创建一个模板工程,使用template生成一套WebApi接口,去掉put、delete等操作,修改一下就可以上线。这些都不在话下,反正网上一大堆教程,随便找那个step by step做下来就可以了。

然后发布上线后,接口是放在外网,面临两个问题:

  1. 如何保证接口的调用的合法性
  2. 如何保证接口及数据的安全性

其实这两个问题是相互结合的,先保证合法,然后在合法基础上保证请求的唯一性,避免参数被篡改。

鉴于接口上线期限紧迫,结合众多案例,先解决掉接口调用数据的安全性问题,这里采用了RSA报文加解密的方案,保证数据安全和防止接口被恶意调用以及参数篡改的问题。

本文参考博客园多篇博文,内容多有引用,文末附有参照博文的地址。

以下为正文!

正文

首先,接口面临的问题:

  1. 请求来源(身份)是否合法(部分解决,后续在处理)?
  2. 请求参数被篡改?
  3. 请求的唯一性(不可复制),防止请求被恶意攻击

解决方案:

  1. 参数加密: 客户端和服务端参数采用RSA加密后传递,原则上只有持有私钥的服务端才能解密客户端公钥加密的参数,避免了参数篡改的问题
  2. 请求签名:采用一套签名算法,对请求进行签名验证,保证请求的唯一性

这里参照了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;
}

签名验证的过程如下:

  1. 获取到报文Header中的 nonce、timestamp、signature、token信息
  2. 将token、timestamp、nonce 三者合并数组中,然后进行顺序排序(排序为了保证后续三个字符串拼接后一致)
  3. 将数组拼接成字符串,然后进行sha256 哈希运算(这里随便什么运算都行,主要为了防止超长加密麻烦)
  4. 将上一步的哈希结果与[signature] RSA解密结果进行比对,一致则签名验证通过,否则则签名不一致,请求为伪造

然后,现在需要启用刚添加的方法过滤器,因为是继承与属性,可以全局启用,或者单个Controller中启用、或者为某个Action启用。全局启用代码如下:

下的WebApiConfig类添加如下代码:

config.Filters.Add(new ApiVerifyFilter());

OK,全部完成,最后附上两个前后的效果对比!

参考博文:

WebApi安全性 使用TOKEN+签名验证

WebAPi接口安全之公钥私钥加密

使用OAuth打造webapi认证服务供自己的客户端使用

Asp.Net WebAPI中Filter过滤器的使用以及执行顺序

微信 公众号开发文档

写博文太累了,回家吃螃蟹补补~

WebApi接口安全性 接口权限调用、参数防篡改防止恶意调用的更多相关文章

  1. 如何写出安全的API接口?接口参数加密签名设计思路

    开发中经常用到接口,尤其是在面向服务的soa架构中,数据交互全是用的接口. 几年以前我认为,我写个接口,不向任何人告知我的接口地址,我的接口就是安全的,现在回想真是too young,too simp ...

  2. ASP.NET WebAPI构建API接口服务实战演练

    一.课程介绍 一.王小二和他领导的第一次故事 有一天王小二和往常一下去上早班,刚吃完早餐刚一打开电脑没一会儿.王小二的领导宋大宝走到他的面前,我们现在的系统需要提供服务给其他内部业务系统,我看你平时喜 ...

  3. Spring Boot如何设计防篡改、防重放攻击接口

    Spring Boot 防篡改.防重放攻击 本示例要内容 请求参数防止篡改攻击 基于timestamp方案,防止重放攻击 使用swagger接口文档自动生成 API接口设计 API接口由于需要供第三方 ...

  4. AutoFac mvc和WebAPI 注册Service (接口和实现)

    AutoFac  mvc和WebAPI  注册Service (接口和实现) 1.准备组件版本:Autofac 3.5.0    Autofac.Integration.Mvc 3.3.0.0  (I ...

  5. c#代码 天气接口 一分钟搞懂你的博客为什么没人看 看完python这段爬虫代码,java流泪了c#沉默了 图片二进制转换与存入数据库相关 C#7.0--引用返回值和引用局部变量 JS直接调用C#后台方法(ajax调用) Linq To Json SqlServer 递归查询

    天气预报的程序.程序并不难. 看到这个需求第一个想法就是只要找到合适天气预报接口一切都是小意思,说干就干,立马跟学生沟通价格. ​ ​不过谈报价的过程中,差点没让我一口老血喷键盘上,话说我们程序猿的人 ...

  6. 给WebAPI的REST接口服务添加测试页面(一)

    当使用WebAPI提供REST服务的时候,一个经常进行的操作是对接口进行测试.Asp.net WebAPI框架本身并没有提供这一接口,不过由于提供的是标准的REST服务,是可以非常方便的使用一些第三方 ...

  7. ASP.NET Core WebApi构建API接口服务实战演练

    一.ASP.NET Core WebApi课程介绍 人生苦短,我用.NET Core!提到Api接口,一般会想到以前用到的WebService和WCF服务,这三个技术都是用来创建服务接口,只不过Web ...

  8. jeecg接口开发及权限实现原理

    接口开发使用的框架 jeecg本身是基于 Spring MVC 框架搭建的,因此,使用 Spring MVC 框架的 RESTful API 功能来进行接口开发就是顺理成章的事了. 接口的拦截与鉴权 ...

  9. 通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应, 请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法, 参数不同时,方法能重载吗?

    Dao 接口即 Mapper 接口.接口的全限名,就是映射文件中的 namespace 的值: 接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值:接口方法内的 参数,就 ...

随机推荐

  1. StringBuilder修改字符串内容,增,删,改,插

    package seday01;/** * 字符串不变对象特性只针对字符串重用,并没有考虑修改操作的性能.因此String不适合频繁修改内容. * 若有频繁修改操作,使用StringBuilder来完 ...

  2. Pycharm快捷键集合

    运行类:Alt + Shift + F10 运行模式配置Alt + Shift + F9 调试模式配置Shift + F10 运行Shift + F9 调试Ctrl + Shift + F10 运行编 ...

  3. JVM中优化指南

    JVM中优化指南 如何将新对象预留在年轻代 如何让大对象进入年老代 如何设置对象进入年老代的年龄 稳定的 Java 堆 VS 动荡的 Java 堆 增大吞吐量提升系统性能 尝试使用大的内存分页 使用非 ...

  4. PHP将字符串转数组

    explode(',',$arr_string) //将字符串转数组 $arr_string = '1,2,3'; $arr = explode(',',$arr_string); dump($arr ...

  5. FOLDER

    一.建noTab的Folder Form:1.创建数据库对象:  create table  和相应的view. 2.基于模板Template.fmb创建一个新的Form:****.fmb  添加一个 ...

  6. 渗透测试学习 十九、 XSS跨站脚本漏洞详解 续2

    二阶注入环境搭建 74cms 3.4 直接将源码放在PHPstudy的www路径下,在地址栏中输入127.0.0.1回车 然后进入网站首页,在填写简历里面存在二阶注入 先注册一个账号 创建简历 前面的 ...

  7. Ubuntu 根目录作用

    Ubuntu的根目录下存在着很多的文件夹,但你知道他们都存放着哪些文件呢?这些是深入了解Ubuntu系统必不缺少的知识,本文就关于此做一下介绍吧. /bin/    用以存储二进制可执行命令文件,/u ...

  8. Python入门基础学习(函数)

    Python基础学习笔记(三) 函数的概念: 所谓函数,就是把具有独立功能的代码块组织为一个小模块,在需要的时候调用 函数的使用包含两个步骤: 1.定义函数 --封装独立的功能 2.调用函数 --享受 ...

  9. 面向对象程序设计(Java) 第7周学习指导及要求

    2019面向对象程序设计(Java)第7周学习指导及要求 (2019.10.11-2019.10.14) 学习目标 掌握四种访问权限修饰符的使用特点: 掌握Object类的用途及常用API: 掌握Ar ...

  10. Springboot项目启动不了。也不打印任何日志信息。

    Springboot项目启动不了.也不打印任何日志信息. <!-- 在创建Spring Boot工程时,我们引入了spring-boot-starter,其中包含了spring-boot-sta ...