从一次解决Nancy参数绑定“bug”开始发布自己的第一个nuget包(上篇)


起因

      最近,同事跟我说,他们负责的一个Api程序出现了一些很奇怪的事情。这个Api是为环保局做的一个扬尘质控大屏提供数据的,底层是基于Nancy做的。因为发现有些接口的数据出现异常,他就去调试了一下,发现当前端传递的参数如果是空,后端反序列化的时候会出现参数值和参数名是一样的情况,这就会导致查询的数据错误。没有找到原因之前,只能通过nameof来判断做处理。具体情况,见下图。

 

      问题就是这么个问题,其实就是因为传递的参数不合规则导致的。正常情况下,参数应该是参数名1=参数值1&参数名2=参数值2,但是这里传递的空参数缺少了=,导致后端识别解析出了问题,只要按照正常的参数名1=参数值1&参数名2=参数值2即可解决问题,所以这里的bug是加了双引号,并不能完全是Nancy的锅。由于前端是基于公共的saas软件服务开发的,参数格式我们也无法修改的,于是就想在后端做一些处理来解决这个问题。

查源码

      我们在Module里通过下面的代码来定义一个Api,使用Bind<T>来反序列化请求的参数模型。 

/// <summary>
/// GET请求示例
/// </summary>
/// <param name="_"></param>
/// <returns></returns>
public Response GetSamp(dynamic _)
{
    var req = this.Bind<SampleInDto>();
    return Response.AsJson(req);
}

F12找到Bind<T>定义的位置,见下图

        //
        // 摘要:
        //     Bind the incoming request to a model
        //
        // 参数:
        //   module:
        //     Current module
        //
        // 类型参数:
        //   TModel:
        //     Model type
        //
        // 返回结果:
        //     Bound model instance
        public static TModel Bind<TModel>(this INancyModule module)
        {
            return module.Bind();
        }

 

这个方法是接口所定义的一个方法INancyModule,再次F12进入,可以发现INancyModule有一个上下文对象NancyContext

这个就是整个HTTP请求的上下文对象,进入NancyContext可以发现他包含HTTP请求中的的RequestResponse对象

我们再进入Request对象,可以发现它包含一些常见对象,比如form,cookies,header,Query,method等等。

我们可以发现,FormQuery的类型是dynamic动态类型,其中Form定义的是一个DynamicDictionary类型的动态类型,从字面理解也很容易想到,这两个对象保存的就是我们HTTP请求中通过地址和表单提交的参数键值对字典。因为我们的HTTP请求都是通过GET方式发起的地址传参,所以暂时不去理会Form,先去找到Query赋值的地方。最好的方式就是通过源码来查看,那就先去https://github.com/NancyFx/Nancy下载Nancy的源码吧。源码下载完毕,先去除一些无用的项目集,之后便是Nancy的源码吧。还是很意外的,没想到一个打着轻量级名号的WebApi框架,源码居然这么庞大。

源码在手,我们先找到Query定义的位置,通过查找引用,我们可以发现是通过一个AsQueryDictionary()的方法进行了赋值的。

接下来通过多次F12可以发现数据来自System.UriQuery对象,这个Query的类型是string说白了,这个就是url中从?开始的部分。比如?name=张三&age=20,相信大家都很容易理解这些吧。

        /// <summary>
        /// Initializes a new instance of the <see cref="Url" /> class, with
        /// the provided <paramref name="url"/>.
        /// </summary>
        /// <param name="url">A <see cref="string" /> containing a URL.</param>
        public Url(string url)
        {
            var uri = new Uri(url);
            this.HostName = uri.Host;
            this.Path = uri.LocalPath;
            this.Port = uri.Port;
            this.Query = uri.Query;
            this.Scheme = uri.Scheme;
        }

下面,重点就是这个AsQueryDictionary()的方法了,它的作用就是对字符串部分的参数进行重新组装,放到动态字典类型的Query中,此Query非彼Query。我猜测,出问题的地方就在这个AsQueryDictionary()里面,为了验证猜想,我决定把相关的代码扒下来,通过一个简单的控制台程序来验证。这个过程比较DT,一个方法调用另外一个方法,一个类引用另外一个类,只能一个一个尝试,把需要的代码拿过来,最终的结果就是下面的样子了。

测试方法,直接写在了Main方法中,如下所示。

class Program
    {
        static void Main(string[] args)
        {
            string format = "http://localhost:5050/hello?{0}";
            var context = new NancyContext();
            string[] arrNormal = new string[] {
                                                        string.Format(format, "name=张三&age=20"),
                                                        string.Format(format, "name=张三&age="),
                                                        string.Format(format, "name&age"),
                                                        string.Format(format, "name=&age")
                                                    };
            Console.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>明文参数>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
            foreach (string _url in arrNormal)
            {
                Console.WriteLine($"=============={_url}==============");
                var url = new Url(_url);
                context.Request = new Request(url);
                var dic = context.Request.Query as DynamicDictionary;
                foreach (string key in dic.Keys)
                {
                    Console.WriteLine($"{key}={dic[key]}");
                }
            }
            Console.ReadKey();
        }
    }

我准备了几种传参的方式,把最后得到的Query打印出来,如下图。

很明显,我们复现了这个Nancy的小“bug”。现在我们已经知道是因为AsQueryDictionary()的缘故了,接下来就是断点调试一下,看看到底是怎么回事。

最核心的两个方法是下面的这两个方法,代码很简单,相信大家都能理解,我简单说一下,第一个方法是根据参数的连接符&分组,然后遍历这个数组,将每个组的内容再根据键值对的连接符=来分组,分别取出参数名和参数值,放入到字典中。

internal static void ParseQueryString(string query, Encoding encoding, NameValueCollection result)
        {
            if (query.Length == 0)
                return;

            var decoded = HtmlDecode(query);

            var segments = decoded.Split(new[] { '&' }, StringSplitOptions.None);

            foreach (var segment in segments)
            {
                var keyValuePair = ParseQueryStringSegment(segment, encoding);
                if (!Equals(keyValuePair, default(KeyValuePair<string, string>)))
                    result.Add(keyValuePair.Key, keyValuePair.Value);
            }
        }

        private static KeyValuePair<string, string> ParseQueryStringSegment(string segment, Encoding encoding)
        {
            if (String.IsNullOrWhiteSpace(segment))
                return default(KeyValuePair<string, string>);

            var indexOfEquals = segment.IndexOf('=');
            if (indexOfEquals == -1)
            {
                var decoded = UrlDecode(segment, encoding);
                return new KeyValuePair<string, string>(decoded, decoded);
            }

            var key = UrlDecode(segment.Substring(0, indexOfEquals), encoding);
            var length = (segment.Length - indexOfEquals) - 1;
            var value = UrlDecode(segment.Substring(indexOfEquals + 1, length), encoding);
            return new KeyValuePair<string, string>(key, value);
        }

本来,这样操作是正常操作,没啥毛病。但是,我们注意看这一段代码。

var indexOfEquals = segment.IndexOf('=');
if (indexOfEquals == -1)
{
    var decoded = UrlDecode(segment, encoding);
    return new KeyValuePair<string, string>(decoded, decoded);
}

它发现参数分组中没有=连接符,会先进行一波url参数解码,然后将解码的内容既当key又作value放入了字典中,这也太骚了,我无法理解为什么会这么写,可能是我的段位太低了,始终无法理解其含义。但是这就是导致这个问题的根本原因,我们只需要对这一段代码进行一下稍微的改造即可。

var indexOfEquals = segment.IndexOf('=');
if (indexOfEquals == -1)
{
     segment = UrlDecode(segment, encoding);
     indexOfEquals = segment.IndexOf('=');
      if (indexOfEquals == -1)
      {
           return new KeyValuePair<string, string>(segment, "");
       }
}

我们在发现参数分组中没有正常的key=value组合时,将value部分置空即可。接下来,再次运行程序,我们会发现问题已经解决了。

问题虽然解决,但是我们发现源码中有很多对参数进行解码的操作。于是,我就在想如果参数编码之后,能否正常解析呢?于是,我就准备了几组不同格式的参数进行了验证。

我们会发现当参数部分进行url编码之后,已经是无法正常解析了。于是,再次对源码进行调试分析。很快,就定位到下面这一段代码。

var decoded = HtmlDecode(query);
var segments = decoded.Split(new[] { '&' }, StringSplitOptions.None);

断点之后,发现当经过url编码的参数字符串query到这里,无法查找到参数连接符&,导致所有的参数都变成了同一个参数。解决办法也很简单,就是不存在&连接符的时候,再次进行解码即可。

var decoded = HtmlDecode(query);
if (decoded.IndexOf('&') == -1)
{
    decoded = UrlDecode(decoded, encoding);
}
var segments = decoded.Split(new[] { '&' }, StringSplitOptions.None);

 

至此,问题得到解决。但是新的问题产生了,怎么解决这个问题

NNancy官方已经停止维护,不再更新了。

 

我们也没办法再提issue了。

解决办法

      很明显,这两个私有方法,我们是无法重写的,那我们怎么办呢?Nancy和ASP.NET MVC框架一样是有过滤器的,我们可以在拦截器中对上下文中的Query进行修改。

首先,我们来新建一个扩展方法类,拿来主义,直接把源码中HttpUtility.cs文件拿过来,新建两个修复方法:ParseQueryStringFixParseQueryStringSegmentFix,完整代码如下。

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using HttpUtility = Nancy.Helpers.HttpUtility;

namespace Nancy.FixQueryDictionary
{
    /// <summary>
    /// Nancy Http请求参数字典解析错误修复扩展方法
    /// </summary>
    public static class NancyFixQueryDictionaryExtensions
    {
        /// <summary>
        /// 修复Http请求参数字典解析错误
        /// </summary>
        /// <param name="ctx">NancyContext对象</param>
        /// <returns>NancyContext对象</returns>
        public static NancyContext FixQueryDictionary(this NancyContext ctx)
        {
            if (ctx == null)
            {
                return ctx;
            }
            ctx.Request.Query = ctx.Request.Url.Query.AsQueryDictionary();
            return ctx;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="queryString"></param>
        /// <returns></returns>
        public static DynamicDictionary AsQueryDictionary(this string queryString)
        {
            var coll = ParseQueryString(queryString);
            var ret = new DynamicDictionary();
            var found = 0;
            foreach (var key in coll.AllKeys.Where(key => key != null))
            {
                ret[key] = coll[key];
                found++;
                if (found >= StaticConfiguration.RequestQueryFormMultipartLimit)
                {
                    break;
                }
            }
            return ret;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public static NameValueCollection ParseQueryString(string query)
        {
            return ParseQueryString(query, Encoding.UTF8);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="query"></param>
        /// <param name="caseSensitive"></param>
        /// <returns></returns>
        public static NameValueCollection ParseQueryString(string query, bool caseSensitive)
        {
            return ParseQueryString(query, Encoding.UTF8, caseSensitive);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="query"></param>
        /// <param name="encoding"></param>
        /// <returns></returns>
        public static NameValueCollection ParseQueryString(string query, Encoding encoding)
        {
            return ParseQueryString(query, encoding, StaticConfiguration.CaseSensitive);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="query"></param>
        /// <param name="encoding"></param>
        /// <param name="caseSensitive"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException"></exception>
        public static NameValueCollection ParseQueryString(string query, Encoding encoding, bool caseSensitive)
        {
            if (query == null)
                throw new ArgumentNullException("query");
            if (encoding == null)
                throw new ArgumentNullException("encoding");
            if (query.Length == 0 || (query.Length == 1 && query[0] == '?'))
                return new NameValueCollection(StringComparer.Ordinal);
            if (query[0] == '?')
                query = query.Substring(1);

            NameValueCollection result = new NameValueCollection(StringComparer.Ordinal);
            ParseQueryStringFix(query, encoding, result);
            return result;
        }

        #region 原方法
        internal static void ParseQueryString(string query, Encoding encoding, NameValueCollection result)
        {
            if (query.Length == 0)
                return;

            var decoded = HttpUtility.HtmlDecode(query);

            var segments = decoded.Split(new[] { '&' }, StringSplitOptions.None);

            foreach (var segment in segments)
            {
                var keyValuePair = ParseQueryStringSegment(segment, encoding);
                if (!Equals(keyValuePair, default(KeyValuePair<string, string>)))
                    result.Add(keyValuePair.Key, keyValuePair.Value);
            }
        }

        private static KeyValuePair<string, string> ParseQueryStringSegment(string segment, Encoding encoding)
        {
            if (String.IsNullOrWhiteSpace(segment))
                return default(KeyValuePair<string, string>);

            var indexOfEquals = segment.IndexOf('=');
            if (indexOfEquals == -1)
            {
                var decoded = HttpUtility.UrlDecode(segment, encoding);
                return new KeyValuePair<string, string>(decoded, decoded);
            }

            var key = HttpUtility.UrlDecode(segment.Substring(0, indexOfEquals), encoding);
            var length = (segment.Length - indexOfEquals) - 1;
            var value = HttpUtility.UrlDecode(segment.Substring(indexOfEquals + 1, length), encoding);
            return new KeyValuePair<string, string>(key, value);
        }
        #endregion

        #region 修复方法
        internal static void ParseQueryStringFix(string query, Encoding encoding, NameValueCollection result)
        {
            if (query.Length == 0)
                return;

            var decoded = HttpUtility.HtmlDecode(query);
            if (decoded.IndexOf('&') == -1)
            {
                decoded = HttpUtility.UrlDecode(decoded, encoding);
            }
            var segments = decoded.Split(new[] { '&' }, StringSplitOptions.None);

            foreach (var segment in segments)
            {
                var keyValuePair = ParseQueryStringSegmentFix(segment, encoding);
                if (!Equals(keyValuePair, default(KeyValuePair<string, string>)))
                    result.Add(keyValuePair.Key, keyValuePair.Value);
            }
        }

        private static KeyValuePair<string, string> ParseQueryStringSegmentFix(string segment, Encoding encoding)
        {
            if (String.IsNullOrWhiteSpace(segment))
                return default(KeyValuePair<string, string>);

            var indexOfEquals = segment.IndexOf('=');
            if (indexOfEquals == -1)
            {
                segment = HttpUtility.UrlDecode(segment, encoding);
                indexOfEquals = segment.IndexOf('=');
                if (indexOfEquals == -1)
                {
                    return new KeyValuePair<string, string>(segment, "");
                }
            }
            var key = HttpUtility.UrlDecode(segment.Substring(0, indexOfEquals), encoding);
            var length = (segment.Length - indexOfEquals) - 1;
            var value = HttpUtility.UrlDecode(segment.Substring(indexOfEquals + 1, length), encoding);
            var res = new KeyValuePair<string, string>(key, value);
            return res;
        }
        #endregion
    }
}

接下来,我们在拦截器中进行参数拦截处理。

/// <summary>
/// 前置拦截器
/// </summary>
/// <param name="ctx">NancyContext上下文对象</param>
/// <returns></returns>
private Response BeforeRequest(NancyContext ctx)
{
   ctx.FixQueryDictionary();
   //TODO:

   return ctx.Response;
}

总结

      至此,我们的问题问题都得到了解决。本来到此应该结束了,但是心血来潮,想要把这个代码发布成nuget包,完成人生第一个nuget包的发布,想想还是挺激动的,经过一番百度操作,大概了解了nuget包的发布过程。本打算一次写完的,无奈时间不早了,该睡觉觉了,剩下的内容留着下篇再写吧。

如果各位大佬对此问题有什么更好的高见,欢迎留言!

文章来源:https://www.cnblogs.com/pinzi/p/15680782.html

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
管理员
上一篇:利用Windbg分析Magicodes.IE一次错误编写导致内存剧增
下一篇:C# 多线程入门系列(一)
评论列表

发表评论

评论内容
昵称:
验证码:
验证码
关联文章

解决Nancy参数bug开始发布自己第一nuget
解决Nancy参数bug开始发布自己第一nuget(下篇)
WPF_15_格式化数据
VS打nuget无法nuget官网
数据模式
使用.NET 6开发TodoList应用(26)——实现Configuration和Option强类型
.NET C#教程初级 1-1 基本数据类型及其存储方式
IIS初始化(预加载),解决第一访问慢,程序池被回收问题
YESWEB开发框架,账套域名
本地正常线接口报404
批处理nuget脚本
FastReport 合并多报表,打印多rpt文件
扫盲-什么是分布式任务调度
WPF 双向到非公开 set 方法属性在 NET 45 和 NET Core 行为不同
WPF Command并传参(以DataGrid示例)
vs2019+windows服务+nancy+打包
使用nuget-server搭建私有Nuget服务器后,nginx反向代理无法下载
解决winform使用EF6 第一查询会慢几秒钟
话说C#程序员人手ORM
支付宝支付报错:40003,三方应用未服务商账号

热门标签
.NET Core .NET Reactor ag-grid AI发布 api安全 ASP.NET Core C#DLL加密 C#播放声音 C#代码混淆 C#代码加密 ChromeDriver Codex DateTime DBeaver devexpress devTool DLL混淆 edge.js EF EFCore Electron element-ui el-form el-table excel FastReport FileStream FolderBrowerDialog FolderSelectDialog form提交 git gridcontrol gridview input javascript json字符串 JS转换对象JSON jwt JWT授权 linq log Math MCP mitmproxy MVC MySQL Navicat netstat nginx node_modules NSwag Nuget Nuget镜像 number PowerShell pyinstaller python pythoncom python爬虫 python抓包 pywin32 redis Requests-html RestSharp Selenium sql SQL Server Swagger to-cms Visual Studio VSCode vue VueRouter vue路由 VUE页面通讯 Webpack Windows Windows服务 winform wmi xlrd yaml YESCMS YESWEB开发框架 白象 表单提交 播放声音 打开URL 代码混淆 弹窗提醒 端口占用 对象转换 分布式 公共字典 机器码 进程排查 静态资源 开发指南 路由参数 密钥 配置教程 配置文件 权限 人工智能 任务 任务调度 日期间隔 日志 日志记录 省市区 授权验证 数据库 四舍五入 文案 文件读取 文件夹选择 文件目录选择 问题排查 行政区域数据 页面通讯 中间件 CSharp 事务锁 工单系统 并发控制 重复提交 CMS Markdig Markdown markdown-it marked 技术选型 VS Code 开发工具 源代码管理 版本控制 Docker PostgreSQL 时区 部署排查 CMS架构 EF Core 主题系统 二次开发 插件系统 容器 运维命令 镜像清理 Linux NAS 远程挂载 飞牛 fnOS S/4HANA SAP GUI SAP HANA SAP R/3 SAP入门 SAP版本 ERP SAP SAP MM 库存管理 物料管理 采购管理 入门教程 SAP S/4HANA SPRO 企业结构 采购组织 MM01 物料主数据 物料类型 BP分组 业务伙伴 供应商主数据 ME41 RFQ 库存物料 采购流程 ME51 消耗性物料 科目分配 采购申请 AC03 ML81N 外部服务 服务主数据 Business Partner SAP培训 ME51N MM模块 Lean Services MM-SRV 外部服务采购 PIR 供应来源 采购主数据 采购信息记录 ME31K 框架协议 计划协议 采购合同 ME01 供应来源确定 货源清单 MEQ1 供应源确定 配额安排 配额评分 MD04 MD21 MRP 计划文件 需求计划 批量程序 MD01N MD02 MRP Live MD05 MM 物料计划 优化采购 供应源 采购订单 ME2A 供应商确认 采购监控 Flexible Workflow 凭证释放 采购审批 释放策略 实地盘点 物料凭证 货物移动 MIGO 收货 移动类型 已撤回 供应商退货 货物发出 STO 库存转储 转移过账 生产订单 预留 GR/IR MIRO 供应商发票 物流发票校验 OMR2 税码 FI PP SD 实操教程 MRBR OMR6 发票差异 交货成本 后续借记 MI01 实物盘点 盘点差异 公司代码 工厂 组织结构 OMS2 主数据定制 自动科目确定 BP角色 CVI 伙伴确定 编号范围 凭证类型 字段选择 FBN1 OMBT OMC2 会计凭证 OMJJ BOM 委外加工 项目类别L MRKO 供应商寄售 特殊库存K MRKON PIPE Pipeline 特殊库存P ERS MRIS 发票计划 周期性结算 里程碑付款 变更追踪 版本管理 采购凭证 SFTP WebDAV 网盘 飞牛fnOS AMPL HERS MPN 中文教程 库存确定 可用性检查 缺件检查 Output Management 消息确定 输出确定 分割评估 库存计价 评估类别 评估类型 PB00 RM0000 条件技术 采购定价 MM-FI集成 OBYC 库存估价 文本类型 文本采用 EFB EVO MSV SU3 用户参数 发票校验 合同参照 履约保留款 特别总账 预付款 Fiori Launchpad SAP Fiori 应用导航 用户体验 LSMW LTMC Migration Cockpit 数据迁移 BRFplus OPD Output Control My Inbox 审批流程 灵活工作流 SAP PP 外部加工 SAP QM 检验批 质量信息记录 采购收货 SAP PM 维护BOM 维护订单 SAP SD SAP Service 端到端流程
联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码