Hello World
Spiga

一个较完整的关键字过滤解决方案(中)

2008-12-24 10:56 by 老赵, 16164 visits

问题远没结束

上篇文章提出的问题解决了没有?哦哦,我是指采取命名约定的方式来改变过滤行为。当然有问题,不过我这里提一下比较重要的两个:

首先,就是“改名”这种行为——究竟是否方便?还记得我们的需求吗(提示一下:方便、通用……)?如果采取上面的命名约定方案,我们可能就需要在页面的前端和后端都不断地改名,一会儿加-noffw,一会儿加-json。如果项目只由您来负责这还好办,只是麻烦一些,但是如果您的团队中的前台开发人员性格古怪,固执己见,不愿配合怎么办(打架我喜欢,可惜不能直接解决问题)?再者,假如您除了一个FilterForbiddenWordModule之外还有类似的“FilterScriptInjectionModule”怎么办(别真写这么一个HttpModule,不合适,老赵只是想不出一个恰当的例子了)?如果两个Module都采取命名约定的方式,那么如何制定一个两者能同时认同的约定就也是个麻烦事。

再者,命名真是我们可以控制的吗?某些情况下好说,但是假如您在使用WebForms中的控件怎么办?WebForm中的一个重要特性就是用过Naming Container来避免客户端ID的冲突。假设我们的页面是放在一个Master Page中ID为Main的ContentPlaceHolder中,那么ID为txtPassword的文本框在客户端里生成的HTML便会如下所示——那么我们又能有什么办法可以做到“命名约定”吗?

<input name="ctl00$Main$txtPassword" id="ctl00_Main_txtPassword"></input>

嘿,看来这种命名约定的方式有时候真不是那么通用啊。那么我就来设法解决WebForm这个问题。

其实如果要解决WebForm这个问题,说白了就是要设法可以让服务器端明确指定一些字段的处理方式。这种“特殊”则意味着对于过滤方式的判断必须与特定的Page——泛化一下,HttpHandler进行绑定。这里我先谈一下我的第一个想法:使用Custom Attribute进行标记的方式。我们构造一个FilterForbiddenWordAttribute,其中包含一个抽象GetFilterType方法根据key来指定过滤方式:

public enum FilterForbiddenWordType
{
    Ignored,
    Normal,
    Json,
    Html
}

public abstract class FilterForbiddenWordAttribute : Attribute
{
    public abstract FilterForbiddenWordType GetFilterType(string key);
}

我们如果有特别的需求,就可以通过定义一个FilterForbiddenWordHandlerAttribute的子类,重载GetFilterType方法,然后标记在HttpHandler上。如下:

public class DefaultFilterForbiddenWordAttribute :
    FilterForbiddenWordAttribute
{
    public override FilterForbiddenWordType GetFilterType(string key)
    {
        if (key.EndsWith("txtPassword"))
        {
            return FilterForbiddenWordType.Ignored;
        }

        return FilterForbiddenWordType.Normal;
    }
}

[DefaultFilterForbiddenWord]
public partial class Default : System.Web.UI.Page
{
    ...
}

当然,我们还需要对FilterForbiddenWordModule进行一些修改才能使之生效(朋友们可以先不要看代码,想想这次改变的关键在哪里?):

public class FilterForbiddenWordModule : IHttpModule
{
    ...

    void IHttpModule.Init(HttpApplication context)
    {
        context.PostMapRequestHandler += new EventHandler(OnPostMapRequestHandler);
    }

    private static void OnPostMapRequestHandler(object sender, EventArgs e)
    {
        var context = (sender as HttpApplication).Context;
        var handlerType = context.Handler.GetType();
        var filter = ((FilterForbiddenWordAttribute[])handlerType.GetCustomAttributes(
            typeof(FilterForbiddenWordAttribute), true)).FirstOrDefault(); 

        ProcessCollection(context.Request.QueryString, filter);
        ProcessCollection(context.Request.Form, filter);
    }

    private static void ProcessCollection(
        NameValueCollection collection,
        FilterForbiddenWordAttribute filter)
    {
        var copy = new NameValueCollection();

        foreach (string key in collection.AllKeys)
        {
            var filterType = (filter == null) ? FilterForbiddenWordType.Normal
                : filter.GetFilterType(key);

            Array.ForEach(
                collection.GetValues(key),
                v => copy.Add(key, ForbiddenWord.Filter(v, filterType)));
        }

        ...
    }
}

修改示例。例如我们在页面上放置两个文本框txtPassword和txtNormal:

<asp:TextBox ID="txtPassword" runat="server" TextMode="MultiLine" />
<asp:TextBox ID="txtNormal" runat="server" TextMode="MultiLine" />
<asp:Button ID="Button1" runat="server" Text="Click" />

点击,效果不言而喻:

公布答案:因为我们需要等到确认了HttpHandler类型才能获得FilterForbiddenWordAttribute标记信息,所以这次更新的关键是我们必须推迟进行过滤的时机。推迟到哪个阶段?自然是能够确定HttpHandler类型的最早时机,PostMapRequestHandler。我们通过反射来获取Handler类型上的FilterForbiddenWordAttribute子类的信息,作为Filter传入带有额外参数的ProcessCollection方法中。ProcessCollection方法内部会调用根据filter参数来确定某个key的过滤方式:正常(当作纯文本进行过滤)、忽略(不过滤)、JSON(只过滤JSON内元素的值)以及HTML(忽视tag和attribute,并考虑文字内的HTML Encode)。其余不变。

顺便说一句,以上代码其实只是为了写这些内容而在10分钟内写好的,不考虑性能、缓存、同步、边界等情况——因为我相信看了下面的文字您一定会抛弃这种做法。

继续改进

上面的做法(相对使用命名约定的方式)改进了什么地方?很简单,之前提到的命名约定的缺点就是上述做法的优点:

  1. 不同Page(Http Handler)可以自行指定字段所需要的过滤逻辑。
  2. 无需前端改名,只需后端标记。
  3. 避免复杂的命名约定,使多种横切型的过滤功能可以轻易共存。

真是美妙地嗷嗷的,但是有没有朋友看出问题来?我提示一下:GetFilterType方法中使用了一个常量字符串txtPassword。

估计有朋友会问:“咦,这有什么问题?”粗看似乎没有,不过老赵看到代码中出现常量总是要警惕一番(自觉是个好喜欢):为啥要是txtPassword而不是txtPassWord(一个常见的拼写错误)?为啥代码中用0而不用-1?这里的问题倒不是说一个常量在代码中到处使用时最好使用一个const——不不,是readonly字段来代替(为啥用const不太好?)。而是……再提示一下,如果某人将页面上的txtName文本框改为txtUserName那会出现何种情况?

嗯嗯,那么Attribute中的GetFilterType方法当然还是在判断一个key是否由txtName结尾,而我们修改后的页面中Post内容中已经变成了txtUserName,咋整?但是可悲的是,我们尊敬的Attribute,就算你拿刀威胁它它也没法知道该替换什么啊。唉,那又有谁才能知道呢?不用多想,当然是页面本身了。

.NET中Custom Attribute的特性深入人心,大大增强了.NET中反射机制的可用性,也因此Kent Beck认为NUnit的设计和使用较JUnit更为优雅。老赵的项目中也到处可见Custom Attribute的存在,写出的代码也简单优美强大地很。不过用多了Custom Attribute也造成了一种思维定势,一些“附加功能”往往都喜欢往上靠,很多问题往往一个功能出来三秒不到脑子里就浮出一个利用Custom Attribute的解决方案。古语有云,“世界如此美好,我却如此浮躁,这样不好,不好……”。事实上ASP.NET框架中已经有了不使用Custom Attribute进行“标记”的现成示例。例如,您知道IRequiresSessionState接口和INamingContainer接口的作用吗?

如果您翻过IRequriedSessionState和INamingContainer接口的文档,就会发现它们有个共同的特点——没有任何成员。这意味着什么呢?这意味着实现了这样的接口的类,唯一的作用就是“别人知道你实现了这个接口”。有点拗口,对吧?其实就是指,这两个接口只起到了标记的作用。使用Custom Attribute或使用接口对一个类进行标记和扩展的优劣取舍,我打算用额外的一篇文章来讨论这个问题(要不现在大家来Brain Storm一下如何?)。目前,朋友们只需关心一点,如果不用Custom Attirubte而使用接口,我们该如何改写上面的程序。并且,这种改变带来了什么好处?

如果在某些情况中,我们也可以把对象本身作为参数传入Custom Attribute的方法中,Attribute方法内部根据参数的属性来实现逻辑,可惜的是,Page类内部的控件成员是protected变量,无法从外部访问。对于我们来说,使Http Handler(即页面)直接实现某个接口的最大(唯一?)好处,就是让该接口的成员可以访问页面内部的非公开成员了。这点就是问题关键,我们现在不必直接使用txtPassword这个常量,而是能够访问页面中的txtPassword控件来获取它相关的属性(ID)。不再赘述,修改如下:

public interface IForbiddenWordFilter
{
    FilterForbiddenWordType GetFilterType(string key);
}

public partial class Default : System.Web.UI.Page, IForbiddenWordFilter
{
    ...

    FilterForbiddenWordType IForbiddenWordFilter.GetFilterType(string key)
    {
        if (key.EndsWith(this.txtPassword.ID)) return FilterForbiddenWordType.Ignored;
        return FilterForbiddenWordType.Normal;
    }
}

至于HttpModule上的修改,相信不会难道您,老赵就不在这里说帖太多代码浪费带宽了。可以看出,现在的代码中已经没有了txtPassword这个常量,取而代之的是对txtPassword对象ID的访问。现在如果在aspx中修改了这个控件的ID,那么在aspx.cs中的变量也会被重构至对应名字,这大大提高了开发效率,降低了出错可能。

差点忘说了一句,大家千万不要忘了对于WebForms模型,有几个特定的key是不能替换的例如“__VIEWSTATE”和“__VIEWSTATEENCRYPTED”。关于这点,老赵的作法是忽略所有以两条下划线作为开头的Key以保护WebForms模型内部需求。

总结

结合上一篇文章,这似乎就是个较为完整的解决方案,不过这个话题结束了吗?当然没有。在下一篇文章里,我们将讨论几个额外的话题,例如:

  • 这个解决方案的适用场合?不适用场合?
  • 输入过滤?输出过滤?
  • 我们一定要使用HttpModule进行过滤吗?
  • 性能?

此外,我想大家在看了这篇文章后来一起思考一些问题,而我对于这些问题的看法也会在下一篇文章中谈到:

  • 在WebForms模型中,Page即是一个Handler,于是可以实现IForbiddenWordFilter。那么Page里Control所需要过滤的内容呢?动态加载的Control呢?
  • 这篇文章的示例里有个陷阱,您看的出是在哪里吗?

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

39 条回复

  1. 老赵
    admin
    链接

    老赵 2008-12-24 10:59:00

    还是那么长,只能拆成3段了

  2. 老赵
    admin
    链接

    老赵 2008-12-24 11:22:00

    @holywolf
    您是指什么?

  3. songcan
    *.*.*.*
    链接

    songcan 2008-12-24 11:22:00

  4. holywolf
    *.*.*.*
    链接

    holywolf 2008-12-24 11:22:00

    莫不成是递归

  5. Astar
    *.*.*.*
    链接

    Astar 2008-12-24 11:26:00

    ^_^支持,高手应该多写文章嘛

  6. 上不了岸的鱼{ttzhang}
    *.*.*.*
    链接

    上不了岸的鱼{ttzhang} 2008-12-24 11:40:00

    呵呵,占个位先

  7. hahahehe[未注册用户]
    *.*.*.*
    链接

    hahahehe[未注册用户] 2008-12-24 11:40:00

    又来了。。。^_^

  8. volnet(可以叫我大V)
    *.*.*.*
    链接

    volnet(可以叫我大V) 2008-12-24 11:49:00

    原来在下之前还有个中字~

  9. 老赵
    admin
    链接

    老赵 2008-12-24 11:56:00

    @volnet(可以叫我大V)
    本没打算过

  10. Microshaoft
    *.*.*.*
    链接

    Microshaoft 2008-12-24 12:04:00

    enable viewstate 下的
    viewstate(hidden field input) 里的webcontrol textBox.text???

    不过请求时已经过滤掉了 应该就不会有这种情况发生

  11. 青羽
    *.*.*.*
    链接

    青羽 2008-12-24 12:04:00

    期待的下变成了中,占个位置慢慢读~~

  12. Microshaoft
    *.*.*.*
    链接

    Microshaoft 2008-12-24 12:10:00

    如果能在输出做 保证性能就最好了
    这样 脏话 还是 入库 保留证据 可以做为呈堂证供 防抵赖
    override write 方法

  13. 老赵
    admin
    链接

    老赵 2008-12-24 12:18:00

    @Microshaoft
    关键就是保证性能不容易。
    如果用了输出过滤,那么势必要作很多其他优化。

  14. Kevin-moon
    *.*.*.*
    链接

    Kevin-moon 2008-12-24 12:53:00

    期待了两天 终于出来了.....
    可是还要期待...!!!

  15. volnet(可以叫我大V)
    *.*.*.*
    链接

    volnet(可以叫我大V) 2008-12-24 13:27:00

    @Jeffrey Zhao
    优化不可少,可咱也不能忽视这里面的好处呀,呵呵,程序员嘛,没事的时候就多折腾一下,哈,不能因为存在难度就坐视不理……态度决定一切……

  16. volnet(可以叫我大V)
    *.*.*.*
    链接

    volnet(可以叫我大V) 2008-12-24 13:48:00

    你背离了最初的初衷,就是不用修改代码就实现过滤,现在你修改了页面的代码,这意味着你需要修改很多的页面,又或者你正面临着巨大复杂的工作量,甚至还要重新编译部署…… 哈,被点名了,还不带链接,哈…… 至于ScriptInjection,是在之前就要做的,而不是在我输出前做的…… 更好的方式应该是在数据库,咱就整数据,啥也不整,跟asp.net没啥关系,咱政府关心的是内容…… 希望快快出(下),加油加油噢~

  17. 老赵
    admin
    链接

    老赵 2008-12-24 14:02:00

    @volnet(可以叫我大V)
    好处当然有,关键是太难,你可以想一下,至少我一直没有想到合适的作法。

  18. 老赵
    admin
    链接

    老赵 2008-12-24 14:04:00

    @volnet(可以叫我大V)
    事实证明,不需要有太多例外,因为大部分东西让它过滤也没事,我们有足够的时间去修改它。我提一下只是为了保证一个完整的解决方案,要严谨么,hoho。
    编译部署的代价很小,放心吧。
    // 啥东西被点名了?
    // 关ScriptInjection什么事情?

  19. volnet(可以叫我大V)
    *.*.*.*
    链接

    volnet(可以叫我大V) 2008-12-24 14:17:00

    @Jeffrey Zhao
    其实编译部署也很麻烦的,如果都你一人负责也就没问题了,如果是涉及到别人,就有一个代价的问题,沟通成本,如果是正规一点的,还要审批流程,万一上去了,结果系统瘫痪了,谁负责?没编译肯定比编译要好。
    其实不是时间的问题,为一百个页面增加接口,或者在自定义的Page基类上加一个接口都是没有问题的,少几分钟,多也不过几钟头,关键是这种增量带来的风险……
    哈,还以为FilterForbiddenWordModule是在说我,哈

  20. 老赵
    admin
    链接

    老赵 2008-12-24 14:21:00

    @volnet(可以叫我大V)
    那真是你说的这样的系统,我觉得更要用我这个模块了。
    // 再没能具体化量化的情况下莫谈风险、流程等虚的东西,没底的。

  21. TerryLee
    *.*.*.*
    链接

    TerryLee 2008-12-24 14:30:00

    --引用--------------------------------------------------
    Jeffrey Zhao: @volnet(可以叫我大V)
    那真是你说的这样的系统,我觉得更要用我这个模块了。
    // 再没能具体化量化的情况下莫谈风险、流程等虚的东西,没底的。
    --------------------------------------------------------
    编译部署在某些系统中是非常致命的,尤其涉及到政府部门的一些项目……

  22. 老赵
    admin
    链接

    老赵 2008-12-24 14:33:00

    @TerryLee
    这个我理解,但是有什么方法是做修改但是不用编译部署的?

  23. volnet(可以叫我大V)
    *.*.*.*
    链接

    volnet(可以叫我大V) 2008-12-24 14:34:00

    @Jeffrey Zhao 赵弟弟好,哈

  24. 老赵
    admin
    链接

    老赵 2008-12-24 14:37:00

    --引用--------------------------------------------------
    volnet(可以叫我大V): @Jeffrey Zhao
    我说的流程是指办事流程,正如TerryLee所说的,政府很难办,人家就是不鸟你,你也没办法,要你办事,但是完全不配合……
    --------------------------------------------------------
    哥哥,我真要叫你们哥哥了。
    不是在谈技术么?怎么开始谈流程复杂度了?
    除非你能提出一种不用“涉及流程”就能解决问题的方法,否则你比较两个方法在流程上是否方便,代价是否高低不是纯粹在扯淡么……

  25. 老赵
    admin
    链接

    老赵 2008-12-24 14:39:00

    @volnet(可以叫我大V)
    兄弟你写篇文章来谈这块吧,你说的这些东西已经超出我能接受的范围了,呵呵。

  26. TerryLee
    *.*.*.*
    链接

    TerryLee 2008-12-24 14:50:00

    @volnet(可以叫我大V)
    @Jeffrey Zhao
    讨论的有点跑题了,考虑到实际项目中的各种具体业务情况,太复杂了……

    // 没仔细看你俩的讨论过程,只是跑出来插一句,关于编译部署的事,因为我遇到这样的情况,很麻烦。。。

  27. volnet(可以叫我大V)
    *.*.*.*
    链接

    volnet(可以叫我大V) 2008-12-24 15:02:00

    看来我这边评论顺序出问题了,挺严重……

  28. 老赵
    admin
    链接

    老赵 2008-12-24 15:04:00

    @volnet(可以叫我大V)
    从时间上看……没错阿,真奇怪,难道博客园忽然改过时间了?哈哈

  29. yzlhccdec
    *.*.*.*
    链接

    yzlhccdec 2008-12-25 10:09:00

    介个,貌似不适用于ASP.NET MVC,而且是用NVelocity引擎的MVC....
    确实会有前台页面改了后台也要相应修改的情况,但是这个在MVC中应该是无解的,不然BindAttribute就不会提供一个Prefix的属性用于数据绑定了。

  30. 老赵
    admin
    链接

    老赵 2008-12-25 10:50:00

    @yzlhccdec
    嗯嗯,总归是可以的,不过的确有点不合适,下次我们讨论讨论吧。

  31. Indigo Dai
    *.*.*.*
    链接

    Indigo Dai 2008-12-26 13:14:00

    一切都在用反射呀……

  32. Tony Zhou
    *.*.*.*
    链接

    Tony Zhou 2008-12-31 18:00:00

    --引用--------------------------------------------------
    volnet(可以叫我大V): 你背离了最初的初衷,就是不用修改代码就实现过滤,现在你修改了页面的代码,这意味着你需要修改很多的页面,又或者你正面临着巨大复杂的工作量,甚至还要重新编译部署……

    哈,被点名了,还不带链接,哈……
    至于ScriptInjection,是在之前就要做的,而不是在我输出前做的……

    更好的方式应该是在数据库,咱就整数据,啥也不整,跟asp.net没啥关系,咱政府关心的是内容……

    希望快快出(下),加油加油噢~
    --------------------------------------------------------

  33. aaaaaaa[未注册用户]
    *.*.*.*
    链接

    aaaaaaa[未注册用户] 2009-06-25 17:55:00

    你好,我想问一下

    当使用接口处理的时候,以下这部分代码应该怎么写

    才能获取到filter 的类型?

    var filter = ((FilterForbiddenWordAttribute[])handlerType.GetCustomAttributes(
    typeof(FilterForbiddenWordAttribute), true)).FirstOrDefault();

    直接改IForbiddenWordFilter,返回null

    用接口的话,就没法使用 [DefaultFilterForbiddenWord]

    这样的方式了把?

    不好意思,我比较菜,希望你能详细的说一下,谢谢了。

  34. 老赵
    admin
    链接

    老赵 2009-06-25 21:36:00

    获得所有的CustomAttribute再遍历吧。

  35. Nero.Pang
    *.*.*.*
    链接

    Nero.Pang 2009-08-23 15:33:00

    老赵的文章能不能简洁明了一点啊,罗嗦话太多!

  36. 老赵
    admin
    链接

    老赵 2009-08-23 15:45:00

    @Nero.Pang
    哪些是罗嗦话?

  37. KKcat
    *.*.*.*
    链接

    KKcat 2009-10-30 11:05:00

    赵老师,下篇,还没出下篇。苦苦等待中

  38. 老赵
    admin
    链接

    老赵 2009-10-30 11:06:00

    @KKcat
    已经有了。

  39. KKcat
    *.*.*.*
    链接

    KKcat 2009-10-30 11:12:00

    @Jeffrey Zhao
    哦,在这篇“中”里
    "至于HttpModule上的修改,相信不会难道您"
    难道=>难倒
    然后麻烦再加个下一篇的链接吧
    这样就比较完美了,也符合赵老师一贯严谨的风格,谢谢

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我