起因

最近,同事跟我说,他们负责的一个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请求中的的Request和Response对象

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

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

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

接下来通过多次F12可以发现数据来自System.Uri的Query对象,这个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文件拿过来,新建两个修复方法:ParseQueryStringFix,ParseQueryStringSegmentFix,完整代码如下。

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包的发布过程。本打算一次写完的,无奈时间不早了,该睡觉觉了,剩下的内容留着下篇再写吧。

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

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

  1. SpringMVC(三) —— 参数绑定和数据回显

    参数绑定的过程:就是页面向后台传递参数,后台接受的一个过程. 默认支持的参数类型:(就是你在方法上以形参的形式去定义一下的类型,就可以直接使用它) HttpServletRequest HttpSer ...

  2. Spring MVC-学习笔记(3)参数绑定注解、HttpMessageConverter<T>信息转换、jackson、fastjson、XML

    1.参数绑定注解 1>@RequestParam: 用于将指定的请求参数赋值给方法中的指定参数.支持的属性: 2>@PathVariable:可以方便的获得URL中的动态参数,只支持一个属 ...

  3. SpringMVC 完美解决PUT请求参数绑定问题(普通表单和文件表单)

    一 解决方案 修改web.xml配置文件 将下面配置拷贝进去(在原有的web-app节点里面配置 其它配置不变) <!-- 处理PUT提交参数(只对基础表单生效) --> <filt ...

  4. SpringMvc参数绑定出现乱码解决方法

    在SpringMvc参数绑定过程中出现乱码的解决方法 1.post参数乱码的解决方法 在web.xml中添加过滤器 <!-- 过滤器 处理post乱码 --> <filter> ...

  5. SpringMVC参数绑定、Post乱码解决方法

    从客户端请求key/value数据,经过参数绑定,将key/value数据绑定到controller方法的形参上. springmvc中,接收页面提交的数据是通过方法形参来接收.而不是在control ...

  6. spring mvc参数绑定

    spring绑定参数的过程 从客户端请求key/value数据,经过参数绑定,将key/value数据绑定到controller方法的形参上.springmvc中,接收页面提交的数据是通过方法形参来接 ...

  7. 细说 Web API参数绑定和模型绑定

    今天跟大家分享下在Asp.NET Web API中Controller是如何解析从客户端传递过来的数据,然后赋值给Controller的参数的,也就是参数绑定和模型绑定. Web API参数绑定就是简 ...

  8. 使用ASP.Net WebAPI构建REST服务(四)——参数绑定

    默认绑定方式 WebAPI把参数分成了简单类型和复杂类型: 简单类型主要包括CLR的primitive types,(int.double.bool等),系统内置的几个strcut类型(TimeSpa ...

  9. springmvc(三) 参数绑定、

    前面两章就介绍了什么是springmvc,springmvc的框架原理,并且会简单的使用springmvc以及ssm的整合,从这一章节来看,就开始讲解springmvc的各种功能实现,慢慢消化 --W ...

随机推荐

  1. Python如何格式化输出

    目录 Python中的格式化输出 1.旧格式化 2.新格式format( ) 函数 Python中的格式化输出 格式化输出就是将字符串中的某些内容替换掉再输出就是格式化输出 旧格式化输出常用的有%d( ...

  2. php 数组(2)

    数组排序算法 冒泡排序,是一种计算机科学领域的较简单的排序算法.它重复地访问要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们减缓过来.走访数列的工作室重复的进行直到没有再需要交换,也就是说该 ...

  3. Springboot 加载配置文件源码分析

    Springboot 加载配置文件源码分析 本文的分析是基于springboot 2.2.0.RELEASE. 本篇文章的相关源码位置:https://github.com/wbo112/blogde ...

  4. [hdu7032]Command and Conquer: Red Alert 2

    令$(x,y,z)$为狙击手的坐标,其攻击范围即以$(x,y,z)$为中心的$(2k)^{3}$​的立方体 为了避免$k$的影响(二分答案会多一个$\log$),不妨将其变为以$(x,y,z)$为左下 ...

  5. go程序不停机重启

    让我们给http服务写一个版本更新接口,让它自动更新版本并重启服务吧. 初步例子 注:为了精简,文中代码都去除了err处理 main.go var Version = "1.0" ...

  6. COS 音视频实践 | 多种姿势让你的视频“跑”起来

    导语 随着4G/5G时代的到来,短视频/直播行业开始流行,音视频逐渐成为信息传播中流量占比最大的部分.腾讯云对象存储(COS)作为可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务, ...

  7. 【2020五校联考NOIP #3】序列

    题面传送门 原题题号:Codeforces Gym 101821B 题意: 给出一个排列 \(p\),要你找出一个最长上升子序列(LIS)和一个最长下降子序列(LDS),满足它们没有公共元素.或告知无 ...

  8. Codeforces 809C - Find a car(找性质)

    Codeforces 题目传送门 & 洛谷题目传送门 首先拿到这类题第一步肯定要分析题目给出的矩阵有什么性质.稍微打个表即可发现题目要求的矩形是一个分形.形式化地说,该矩形可以通过以下方式生成 ...

  9. spring security 授权方式(自定义)及源码跟踪

    spring security 授权方式(自定义)及源码跟踪 ​ 这节我们来看看spring security的几种授权方式,及简要的源码跟踪.在初步接触spring security时,为了实现它的 ...

  10. R语言矩阵相关性计算及其可视化?

    目录 1. 矩阵相关性计算方法 base::cor/cor.test psych::corr.test Hmisc::rcorr 其他工具 2. 相关性矩阵转化为两两相关 3. 可视化 corrplo ...