欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

WebApi接口安全性 接口权限调用、参数防篡改防止恶意调用

程序员文章站 2022-06-29 08:39:51
背景介绍 最近使用WebApi开发一套对外接口,主要是数据的外送以及结果回传,接口没什么难度,采用WebApi+EF的架构简单创建一个模板工程,使用template生成一套WebApi接口,去掉put、delete等操作,修改一下就可以上线。这些都不在话下,反正网上一大堆教程,随便找那个step b... ......

背景介绍

最近使用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(1);
                    basequery = regex.match(basequery, "(sign=)*(?<sign>[\\s]+)").groups[2].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[2].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, 40301);
                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, 40302);
                return;
            }
            //检查接口调用是否超时
            var ts = common.datetime2timestamp(datetime.utcnow) - convert.todouble(timestamp);
            if (ts > int.parse(expireytime) * 1000)
            {
                setspecialresponsemessage(actioncontext, 40303);
                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 40301:
                    model.message = "没有设置签名、时间戳、随机字符串";
                    break;
                case 40302:
                    model.message = "签名无效";
                    break;
                case 40303:
                    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(1970, 1, 1, 0, 0, 0, 0);
            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过滤器的使用以及执行顺序

微信 公众号开发文档

 

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