23种设计模式

23种设计模式

目录

创建型

  1. Factory Method(工厂方法)
  2. Abstract Factory(抽象工厂)
  3. Builder(建造者)
  4. Prototype(原型)
  5. Singleton(单例)

结构型

  1. Adapter Class/Object(适配器)
  2. Bridge(桥接)
  3. Composite(组合)
  4. Decorator(装饰)
  5. Facade(外观)
  6. Flyweight(享元)
  7. Proxy(代理)

行为型

  1. Interpreter(解释器)
  2. Template Method(模板方法)
  3. Chain of Responsibility(责任链)
  4. Command(命令)
  5. Iterator(迭代器)
  6. Mediator(中介者)
  7. Memento(备忘录)
  8. Observer(观察者)
  9. State(状态)
  10. Strategy(策略)
  11. Visitor(访问者)

创建型

1. Factory Method(工厂方法)

意图:

定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使个类的实例化延迟到其子类。

适用性:

当一个类不知道它所必须创建的对象的类的时候。

当一个类希望由它的子类来指定它所创建的对象的时候。

当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。

2. Abstract Factory(抽象工厂)

意图:

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
适用性:

一个系统要独立于它的产品的创建、组合和表示时。

一个系统要由多个产品系列中的一个来配置时。

当你要强调一系列相关的产品对象的设计以便进行联合使用时。

当你提供一个产品类库,而只想显示它们的接口而不是实现时。

3. Builder(建造者)

意图:

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

适用性:

当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。

当构造过程必须允许被构造的对象有不同的表示时。

4. Prototype(原型)

意图:

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

适用性:

当要实例化的类是在运行时刻指定时,例如,通过动态装载;或者

为了避免创建一个与产品类层次平行的工厂类层次时;或者

当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。

5. Singleton(单例)

意图:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

适用性:

当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。

当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。

结构型

6. Adapter Class/Object(适配器)


意图:

将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适用性:

你想使用一个已经存在的类,而它的接口不符合你的需求。

你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。

(仅适用于对象Adapter )你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。

7. Bridge(桥接)

意图:

将抽象部分与它的实现部分分离,使它们都可以独立地变化。

适用性:

你不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如这种情况可能是因为,在程序运行时刻实现部分应可以被选择或者切换。

类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这时Bridge 模式使你可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。

对一个抽象的实现部分的修改应对客户不产生影响,即客户的代码不必重新编译。

(C++)你想对客户完全隐藏抽象的实现部分。在C++中,类的表示在类接口中是可见的。

有许多类要生成。这样一种类层次结构说明你必须将一个对象分解成两个部分。Rumbaugh 称这种类层次结构为“嵌套的普化”(nested generalizations )。

你想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。一个简单的例子便是Coplien 的String 类[ Cop92 ],在这个类中多个对象可以共享同一个字符串表示(StringRep )。

8. Composite(组合)


意图:

将对象组合成树形结构以表示“部分-整体”的层次结构。C o m p o s i t e 使得用户对单个对象和组合对象的使用具有一致性。
适用性:

你想表示对象的部分-整体层次结构。

你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

9. Decorator(装饰)


意图:
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator 模式相比生成子类更为灵活。
适用性:

在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。

处理那些可以撤消的职责。

当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。

10. Facade(外观)

意图:

为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

适用性:

当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类。这使得子系统更具可重用性,也更容易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难。Facade 可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过facade层。

客户程序与抽象类的实现部分之间存在着很大的依赖性。引入facade 将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。

当你需要构建一个层次结构的子系统时,使用facade模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过facade进行通讯,从而简化了它们之间的依赖关系。

11. Flyweight(享元)

意图:

运用共享技术有效地支持大量细粒度的对象。

适用性:

一个应用程序使用了大量的对象。

完全由于使用大量的对象,造成很大的存储开销。

对象的大多数状态都可变为外部状态。

如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。

应用程序不依赖于对象标识。由于Flyweight 对象可以被共享,对于概念上明显有别的对象,标识测试将返回真值。

12. Proxy(代理)

意图:

为其他对象提供一种代理以控制对这个对象的访问。

适用性:

在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用Proxy模式。下面是一 些可以使用Proxy模式常见情况:
1) 远程代理(Remote Proxy )为一个对象在不同的地址空间提供局部代表。 NEXTSTEP[Add94] 使用NXProxy 类实现了这一目的。Coplien[Cop92] 称这种代理为“大使” (Ambassador )。
2 )虚代理(Virtual Proxy )根据需要创建开销很大的对象。在动机一节描述的ImageProxy 就是这样一种代理的例子。
3) 保护代理(Protection Proxy )控制对原始对象的访问。保护代理用于对象应该有不同 的访问权限的时候。例如,在Choices 操作系统[ CIRM93]中KemelProxies为操作系统对象提供 了访问保护。
4 )智能指引(Smart Reference )取代了简单的指针,它在访问对象时执行一些附加操作。 它的典型用途包括:

对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为SmartPointers[Ede92 ] )。

当第一次引用一个持久对象时,将它装入内存。

在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。

行为型

13. Interpreter(解释器)

意图:

给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

适用性:

当有一个语言需要解释执行, 并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。而当存在以下情况时该模式效果最好:

该文法简单对于复杂的文法, 文法的类层次变得庞大而无法管理。此时语法分析程序生成器这样的工具是更好的选择。它们无需构建抽象语法树即可解释表达式, 这样可以节省空间而且还可能节省时间。

效率不是一个关键问题最高效的解释器通常不是通过直接解释语法分析树实现的, 而是首先将它们转换成另一种形式。例如,正则表达式通常被转换成状态机。但即使在这种情况下, 转换器仍可用解释器模式实现,该模式仍是有用的。

14. Template Method(模板方法)

意图:

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

适用性:

一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。

各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。这是Opdyke 和Johnson 所描述过的“重分解以一般化”的一个很好的例子[ OJ93 ]。首先识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。

控制子类扩展。模板方法只在特定点调用“hook ”操作(参见效果一节),这样就只允许在这些点进行扩展。

15. Chain of Responsibility(责任链)

意图:

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

适用性:

有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。

你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。

可处理一个请求的对象集合应被动态指定。

16. Command(命令)

意图:

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。

适用性:

抽象出待执行的动作以参数化某对象,你可用过程语言中的回调(call back)函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。Command 模式是回调机制的一个面向对象的替代品。

在不同的时刻指定、排列和执行请求。一个Command对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。

支持取消操作。Command的Excute 操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。Command 接口必须添加一个Unexecute操作,该操作取消上一次Execute调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用Unexecute和Execute来实现重数不限的“取消”和“重做”。

支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在Command接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用Execute操作重新执行它们。

用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务( transaction)的信息系统中很常见。一个事务封装了对数据的一组变动。Command模式提供了对事务进行建模的方法。Command有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。

17. Iterator(迭代器)

意图:

提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。

适用性:

访问一个聚合对象的内容而无需暴露它的内部表示。

支持对聚合对象的多种遍历。

为遍历不同的聚合结构提供一个统一的接口(即, 支持多态迭代)。

18. Mediator(中介者)

意图:

用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

适用性:

一组对象以定义良好但是复杂的方式进行通信。产生的相互依赖关系结构混乱且难以理解。

一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象。

想定制一个分布在多个类中的行为,而又不想生成太多的子类。

19. Memento(备忘录)

意图:

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

适用性:

必须保存一个对象在某一个时刻的(部分)状态, 这样以后需要时它才能恢复到先前的状态。

如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。

20. Observer(观察者)

意图:

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。

适用性:

当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。

当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。

当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。

21. State(状态)

意图:

允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

适用性:

一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。

一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常, 有多个操作包含这一相同的条件结构。State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。

22. Strategy(策略)

意图:

定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

适用性:

许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。

需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时[H087] ,可以使用策略模式。

算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。

一个类定义了多种行为, 并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。

23. Visitor(访问者)

意图:

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

适用性:

一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。

各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。这是Opdyke和Johnson所描述过的“重分解以一般化”的一个很好的例子[OJ93]。首先识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。

控制子类扩展。模板方法只在特定点调用“hook ”操作(参见效果一节),这样就只允许在这些点进行扩展。

设计模式的六大原则

设计模式的六大原则

总原则、开闭原则(Open Close Principle)

开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

 

定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。以前,如果有人告诉我“你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。

在仔细思考以及仔细阅读很多设计模式的文章后,终于对开闭原则有了一点认识。其实,我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。

 

1、里氏代换原则(Liskov Substitution Principle)

里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

2、依赖倒转原则(Dependence Inversion Principle)

这个原则是开闭原则的基础,具体内容:针对对接口编程,依赖于抽象而不依赖于具体。

A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
B.抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

意图:

面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。
面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。
3、接口隔离原则(Interface Segregation Principle)

这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。本文例子中,将一个庞大的接口变更为3个专用的接口所采用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

采用接口隔离原则对接口进行约束时,要注意以下几点:

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

4、迪米特法则,又称最少知道原则(Demeter Principle)

最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

定义:一个对象应该对其他对象保持最少的了解。

问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

解决方案:尽量降低类与类之间的耦合。

自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。

迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

 

5、合成复用原则(Composite Reuse Principle)

合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。

一:合成/聚合复用原则

  尽量使用合成/聚合,尽量不要使用类继承

 二:什么是合成?

. 合成表示一种强的拥有关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样,打个比方:人有两个胳膊,胳膊和人就是部分和整体的关系,人去世了,那么胳膊也就没用了,也就是说胳膊和人的生命周期是相同的

. 合成关系用实心的菱形+实线来表示

三:什么是聚合?

  . 聚合表示一种弱的拥有关系,体现的是A对象可以包含B对象,但是B对象并不是A对象的一部分,打个比方:人是群居动物,所以每个人属于一个人群,一个人群可以有多个人,所以人群和人是聚合的关系

. 聚合关系用空心的菱形+实线来表示

 四:为什么尽量不要使用类继承而使用合成/聚合?

  . 对象的继承关系在编译时就定义好了,所以无法在运行时改变从父类继承的子类的实现

  . 子类的实现和它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化

. 当你复用子类的时候,如果继承下来的实现不适合解决新的问题,则父类必须重写或者被其它更适合的类所替换

这种依赖关系限制了灵活性,并最终限制了复用性

  五:合成/聚合复用原则结构图

  1和2以及n,被称为基数,表示这一端的类可以有几个实例,很显然,一个人有两个胳膊,如果一个类可能有无数个实例,则用n表示,一人群可能有无数个人

 

6、单一职责原则

定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。

问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。

解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

说到单一职责原则,很多人都会不屑一顾。因为它太简单了。稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。

比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)

数据库设计原则

1. 原始单据与实体之间的关系
可以是一对一、一对多、多对多的关系。在一般情况下,它们是一对一的关系:即一张原始单据对应且只对应一个实体。
在特殊情况下,它们可能是一对多或多对一的关系,即一张原始单证对应多个实体,或多张原始单证对应一个实体。
这里的实体可以理解为基本表。明确这种对应关系后,对我们设计录入界面大有好处。

〖例1〗:一份员工履历资料,在人力资源信息系统中,就对应三个基本表:员工基本情况表、社会关系表、工作简历表。
这就是“一张原始单证对应多个实体”的典型例子。

2. 主键与外键
一般而言,一个实体不能既无主键又无外键。在E—R 图中, 处于叶子部位的实体, 可以定义主键,也可以不定义主键
(因为它无子孙), 但必须要有外键(因为它有父亲)。

主键与外键的设计,在全局数据库的设计中,占有重要地位。当全局数据库的设计完成以后,有个美国数据库设计专
家说:“键,到处都是键,除了键之外,什么也没有”,这就是他的数据库设计经验之谈,也反映了他对信息系统核
心(数据模型)的高度抽象思想。因为:主键是实体的高度抽象,主键与外键的配对,表示实体之间的连接。

3. 基本表的性质
  基本表与中间表、临时表不同,因为它具有如下四个特性:
(1) 原子性。基本表中的字段是不可再分解的。
(2) 原始性。基本表中的记录是原始数据(基础数据)的记录。
(3) 演绎性。由基本表与代码表中的数据,可以派生出所有的输出数据。
(4) 稳定性。基本表的结构是相对稳定的,表中的记录是要长期保存的。
理解基本表的性质后,在设计数据库时,就能将基本表与中间表、临时表区分开来。

4. 范式标准
  基本表及其字段之间的关系, 应尽量满足第三范式。但是,满足第三范式的数据库设计,往往不是最好的设计。
为了提高数据库的运行效率,常常需要降低范式标准:适当增加冗余,达到以空间换时间的目的。

〖例2〗:有一张存放商品的基本表,如表1所示。“金额”这个字段的存在,表明该表的设计不满足第三范式,
因为“金额”可以由“单价”乘以“数量”得到,说明“金额”是冗余字段。但是,增加“金额”这个冗余字段,
可以提高查询统计的速度,这就是以空间换时间的作法。
在Rose 2002中,规定列有两种类型:数据列和计算列。“金额”这样的列被称为“计算列”,而“单价”和
“数量”这样的列被称为“数据列”。

表1 商品表的表结构
商品名称 商品型号 单价 数量 金额
电视机 29吋 2,500 40 100,000

5. 通俗地理解三个范式
通俗地理解三个范式,对于数据库设计大有好处。在数据库设计中,为了更好地应用三个范式,就必须通俗地理解
三个范式(通俗地理解是够用的理解,并不是最科学最准确的理解):
第一范式:1NF是对属性的原子性约束,要求属性具有原子性,不可再分解;
第二范式:2NF是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性;
第三范式:3NF是对字段冗余性的约束,即任何字段不能由其他字段派生出来,它要求字段没有冗余。

没有冗余的数据库设计可以做到。但是,没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降
低范式标准,适当保留冗余数据。具体做法是:在概念数据模型设计时遵守第三范式,降低范式标准的工作放到物理
数据模型设计时考虑。降低范式就是增加字段,允许冗余。

6. 要善于识别与正确处理多对多的关系
  若两个实体之间存在多对多的关系,则应消除这种关系。消除的办法是,在两者之间增加第三个实体。这样,原来一
个多对多的关系,现在变为两个一对多的关系。要将原来两个实体的属性合理地分配到三个实体中去。这里的第三个
实体,实质上是一个较复杂的关系,它对应一张基本表。一般来讲,数据库设计工具不能识别多对多的关系,但能处
理多对多的关系。

〖例3〗:在“图书馆信息系统”中,“图书”是一个实体,“读者”也是一个实体。这两个实体之间的关系,是一
个典型的多对多关系:一本图书在不同时间可以被多个读者借阅,一个读者又可以借多本图书。为此,要在二者之
间增加第三个实体,该实体取名为“借还书”,它的属性为:借还时间、借还标志(0表示借书,1表示还书),另外,
它还应该有两个外键(“图书”的主键,“读者”的主键),使它能与“图书”和“读者”连接。

7. 主键PK的取值方法
   PK是供程序员使用的表间连接工具,可以是一无物理意义的数字串, 由程序自动加1来实现。也可以是有物理意义
的字段名或字段名的组合。不过前者比后者好。当PK是字段名的组合时,建议字段的个数不要太多,多了不但索引
占用空间大,而且速度也慢。

8. 正确认识数据冗余
主键与外键在多表中的重复出现, 不属于数据冗余,这个概念必须清楚,事实上有许多人还不清楚。非键字段的重
复出现, 才是数据冗余!而且是一种低级冗余,即重复性的冗余。高级冗余不是字段的重复出现,而是字段的派生出现。

〖例4〗:商品中的“单价、数量、金额”三个字段,“金额”就是由“单价”乘以“数量”派生出来的,它就是冗余,
而且是一种高级冗余。冗余的目的是为了提高处理速度。只有低级冗余才会增加数据的不一致性,因为同一数据,可
能从不同时间、地点、角色上多次录入。因此,我们提倡高级冗余(派生性冗余),反对低级冗余(重复性冗余)。

9. E–R图没有标准答案
信息系统的E–R图没有标准答案,因为它的设计与画法不是惟一的,只要它覆盖了系统需求的业务范围和功能内容,
就是可行的。反之要修改E–R图。尽管它没有惟一的标准答案,并不意味着可以随意设计。好的E—R图的标准是:
结构清晰、关联简洁、实体个数适中、属性分配合理、没有低级冗余。

10 . 视图技术在数据库设计中很有用
  与基本表、代码表、中间表不同,视图是一种虚表,它依赖数据源的实表而存在。视图是供程序员使用数据库的
一个窗口,是基表数据综合的一种形式, 是数据处理的一种方法,是用户数据保密的一种手段。为了进行复杂处理、
提高运算速度和节省存储空间, 视图的定义深度一般不得超过三层。 若三层视图仍不够用, 则应在视图上定义临时表,
在临时表上再定义视图。这样反复交迭定义, 视图的深度就不受限制了。

对于某些与国家政治、经济、技术、军事和安全利益有关的信息系统,视图的作用更加重要。这些系统的基本表完
成物理设计之后,立即在基本表上建立第一层视图,这层视图的个数和结构,与基本表的个数和结构是完全相同。
并且规定,所有的程序员,一律只准在视图上操作。只有数据库管理员,带着多个人员共同掌握的“安全钥匙”,
才能直接在基本表上操作。请读者想想:这是为什么?

11. 中间表、报表和临时表
中间表是存放统计数据的表,它是为数据仓库、输出报表或查询结果而设计的,有时它没有主键与外键(数据仓
库除外)。临时表是程序员个人设计的,存放临时记录,为个人所用。基表和中间表由DBA维护,临时表由程序员
自己用程序自动维护。

12. 完整性约束表现在三个方面
  域的完整性:用Check来实现约束,在数据库设计工具中,对字段的取值范围进行定义时,有一个Check按钮,通
过它定义字段的值城。
参照完整性:用PK、FK、表级触发器来实现。
用户定义完整性:它是一些业务规则,用存储过程和触发器来实现。

13. 防止数据库设计打补丁的方法是“三少原则”
   (1) 一个数据库中表的个数越少越好。只有表的个数少了,才能说明系统的E–R图少而精,去掉了重复的多余的
实体,形成了对客观世界的高度抽象,进行了系统的数据集成,防止了打补丁式的设计;

(2) 一个表中组合主键的字段个数越少越好。因为主键的作用,一是建主键索引,二是做为子表的外键,所以组
合主键的字段个数少了,不仅节省了运行时间,而且节省了索引存储空间;

(3) 一个表中的字段个数越少越好。只有字段的个数少了,才能说明在系统中不存在数据重复,且很少有数据冗
余,更重要的是督促读者学会“列变行”,这样就防止了将子表中的字段拉入到主表中去,在主表中留下许
多空余的字段。所谓“列变行”,就是将主表中的一部分内容拉出去,另外单独建一个子表。这个方法很简
单,有的人就是不习惯、不采纳、不执行。

数据库设计的实用原则是:在数据冗余和处理速度之间找到合适的平衡点。“三少”是一个整体概念,综合观点,
不能孤立某一个原则。该原则是相对的,不是绝对的。“三多”原则肯定是错误的。试想:若覆盖系统同样的功
能,一百个实体(共一千个属性) 的E–R图,肯定比二百个实体(共二千个属性) 的E–R图,要好得多。

提倡“三少”原则,是叫读者学会利用数据库设计技术进行系统的数据集成。数据集成的步骤是将文件系统集成
为应用数据库,将应用数据库集成为主题数据库,将主题数据库集成为全局综合数据库。集成的程度越高,数据
共享性就越强,信息孤岛现象就越少,整个企业信息系统的全局E—R图中实体的个数、主键的个数、属性的个数
就会越少。

提倡“三少”原则的目的,是防止读者利用打补丁技术,不断地对数据库进行增删改,使企业数据库变成了随意
设计数据库表的“垃圾堆”,或数据库表的“大杂院”,最后造成数据库中的基本表、代码表、中间表、临时表
杂乱无章,不计其数,导致企事业单位的信息系统无法维护而瘫痪。

“三多”原则任何人都可以做到,该原则是“打补丁方法”设计数据库的歪理学说。“三少”原则是少而精的
原则,它要求有较高的数据库设计技巧与艺术,不是任何人都能做到的,因为该原则是杜绝用“打补丁方法”
设计数据库的理论依据。

14. 提高数据库运行效率的办法
在给定的系统硬件和系统软件条件下,提高数据库系统的运行效率的办法是:
(1) 在数据库物理设计时,降低范式,增加冗余, 少用触发器, 多用存储过程。
(2) 当计算非常复杂、而且记录条数非常巨大时(例如一千万条),复杂计算要先在数据库外面,以文件系统方
式用C++语言计算处理完成之后,最后才入库追加到表中去。这是电信计费系统设计的经验。
(3) 发现某个表的记录太多,例如超过一千万条,则要对该表进行水平分割。水平分割的做法是,以该表主键
PK的某个值为界线,将该表的记录水平分割为两个表。若发现某个表的字段太多,例如超过八十个,则
垂直分割该表,将原来的一个表分解为两个表。
(4) 对数据库管理系统DBMS进行系统优化,即优化各种系统参数,如缓冲区个数。
(5) 在使用面向数据的SQL语言进行程序设计时,尽量采取优化算法。
总之,要提高数据库的运行效率,必须从数据库系统级优化、数据库设计级优化、程序实现级优化,这三
个层次上同时下功夫。

上述十四个技巧,是许多人在大量的数据库分析与设计实践中,逐步总结出来的。对于这些经验的运用,读者不能生帮硬套,死记硬背,而要消化理解,实事求是,灵活掌握。并逐步做到:在应用中发展,在发展中应用。

PHP漏洞全解(九)文件上传漏洞

一套web应用程序,一般都会提供文件上传的功能,方便来访者上传一些文件。

下面是一个简单的文件上传表单

  1. <form action=“upload.php” method=“post” enctype=“multipart/form-data” name=“form1”>
  2. <input type=“file” name=“file1” /><br />
  3. <input type=“submit” value=“上传文件” />
  4. <input type=“hidden” name=“MAX_FILE_SIZE” value=“1024” />
  5. form> 

php的配置文件php.ini,其中选项upload_max_filesize指定允许上传的文件大小,默认是2M

$_FILES数组变量

PHP使用变量$_FILES来上传文件,$_FILES是一个数组。如果上传test.txt,那么$_FILES数组的内容为:

  1. $FILES
  2. Array
  3. {
  4. [file] => Array
  5. {
  6. [name] => test.txt //文件名称
  7. [type] => text/plain //MIME类型
  8. [tmp_name] => /tmp/php5D.tmp //临时文件
  9. [error] => 0 //错误信息
  10. [size] => 536 //文件大小,单位字节
  11. }
  12. }

如果上传文件按钮的name属性值为file

  1. <input type=“file” name=“file” />

那么使用$_FILES[‘file’][‘name’]来获得客户端上传文件名称,不包含路径。使用$_FILES[‘file’][‘tmp_name’]来获得服务端保存上传文件的临时文件路径

存放上传文件的文件夹

PHP不会直接将上传文件放到网站根目录中,而是保存为一个临时文件,名称就是$_FILES[‘file’][‘tmp_name’]的值,开发者必须把这个临时文件复制到存放的网站文件夹中。

$_FILES[‘file’][‘tmp_name’]的值是由PHP设置的,与文件原始名称不一样,开发者必须使用$_FILES[‘file’][‘name’]来取得上传文件的原始名称。

上传文件时的错误信息

$_FILES[‘file’][‘error’]变量用来保存上传文件时的错误信息,它的值如下:

错误信息 数值 说 明
UPLOAD_ERR_OK 0 没有错误
UPLOAD_ERR_INI_SIZE 1 上传文件的大小超过php.ini的设置
UPLOAD_ERR_FROM_SIZE 2 上传文件的大小超过HTML表单中MAX_FILE_SIZE的值
UPLOAD_ERR_PARTIAL 3 只上传部分的文件
UPLOAD_ERR_NO_FILE 4 没有文件上传

文件上传漏洞

如果提供给网站访问者上传图片的功能,那必须小心访问者上传的实际可能不是图片,而是可以指定的PHP程序。如果存放图片的目录是一个开放的文件夹,则入侵者就可以远程执行上传的PHP文件来进行攻击。

下面是一个简单的文件上传例子:

  1. php 
  2. // 设置上传文件的目录
  3. $uploaddir = “D:/www/images/”;
  4. // 检查file是否存在
  5. if (isset($_FILES[‘file1’]))
  6. {
  7. // 要放在网站目录中的完整路径,包含文件名
  8. $uploadfile = $uploaddir . $_FILES[‘file1’][‘name’];
  9. // 将服务器存放的路径,移动到真实文件名
  10. move_uploaded_file($_FILES[‘file1’][‘tmp_name’], $uploadfile);
  11. }
  12. ?>
  13. ……
  14. <form method=“post” enctype=“multipart/form-data” name=“form1”>
  15. <input type=“file” name=“file1” /><br />
  16. <input type=“submit” value=“上传文件” />
  17. <input type=“hidden” name=“MAX_FILE_SIZE” value=“1024” />
  18. form> 

这个例子没有检验文件后缀,可以上传任意文件,很明显的上传漏洞

PHP漏洞全解(八)HTTP响应拆分

HTTP请求的格式

1)请求信息:例如“Get /index.php HTTP/1.1”,请求index.php文件

2)表头:例如“Host: localhost”,表示服务器地址

3)空白行

4)信息正文

“请求信息”和“表头”都必须使用换行字符(CRLF)来结尾,空白行只能包含换行符,不可以有其他空格符。

下面例子发送HTTP请求给服务器www.yhsafe.com

GET /index.php HTTP/1.1↙ //请求信息

Host:www.yhsafe.com↙ //表头

↙ //空格行
↙符号表示回车键,在空白行之后还要在按一个空格才会发送HTTP请求,HTTP请求的表头中只有Host表头是必要的饿,其余的HTTP表头则是根据HTTP请求的内容而定。
HTTP请求的方法
1)GET:请求响应
2)HEAD:与GET相同的响应,只要求响应表头
3)POST:发送数据给服务器处理,数据包含在HTTP信息正文中
4)PUT:上传文件
5)DELETE:删除文件
6)TRACE:追踪收到的请求
7)OPTIONS:返回服务器所支持的HTTP请求的方法
8)CONNECT:将HTTP请求的连接转换成透明的TCP/IP通道
HTTP响应的格式
服务器在处理完客户端所提出的HTTP请求后,会发送下列响应。
1)第一行是状态码
2)第二行开始是其他信息
状态码包含一个标识状态的数字和一个描述状态的单词。例如:
HTTP/1.1 200 OK
200是标识状态的是数字,OK则是描述状态的单词,这个状态码标识请求成功。

HTTP请求和响应的例子

打开cmd输入telnet,输入open www.00aq.com 80

打开连接后输入

GET /index.php HTTP/1.1↙

Host:www.00aq.com↙

返回HTTP响应的表头

返回的首页内容

使用PHP来发送HTTP请求

header函数可以用来发送HTTP请求和响应的表头

函数原型

void header(string string [, bool replace [, int http_response_code]])

string是HTTP表头的字符串

如果replace为TRUE,表示要用目前的表头替换之前相似的表头;如果replace为FALSE,表示要使用多个相似的表头,默认值为TRUE

http_response_code用来强制HTTP响应码使用http_response_code的值

实例:

  1. // 打开Internet socket连接
  2. $fp = fsockopen(www.00aq.com, 80);
  3. // 写入HTTP请求表头
  4. fputs($fp“GET / HTTP/1.1\r\n”);
  5. fputs($fp“Host: www.00aq.com\r\n\r\n”);
  6. // HTTP响应的字符串
  7. $http_response = “”;
  8. while (!feof($fp))
  9. {
  10. // 读取256位的HTTP响应字符串
  11. $http_response .= fgets($fp, );
  12. }
  13. // 关闭Internet socket连接
  14. fclose($fp);
  15. // 显示HTTP响应信息
  16. echo nl2br(htmlentities($http_response));
  17. ?>

HTTP响应拆分攻击

HTTP响应拆分是由于攻击者经过精心设计利用电子邮件或者链接,让目标用户利用一个请求产生两个响应,前一个响应是服务器的响应,而后一个则是攻击者设计的响应。此攻击之所以会发生,是因为WEB程序将使用者的数据置于HTTP响应表头中,这些使用者的数据是有攻击者精心设计的。

可能遭受HTTP请求响应拆分的函数包括以下几个:

header(); setcookie(); session_id(); setrawcookie();

HTTP响应拆分通常发生在:

Location表头:将使用者的数据写入重定向的URL地址内

Set-Cookie表头:将使用者的数据写入cookies内

实例:

  1.     header(“Location: “ . $_GET[‘page’]);
  2. ?>

请求

GET /location.php?page=http://www.00aq.com HTTP/1.1↙

Host: localhost↙

返回

HTTP/1.1 302 Found

Date: Wed, 13 Jan 2010 03:44:24 GMT

Server: Apache/2.2.8 (Win32) PHP/5.2.6

X-Powered-By: PHP/5.2.6

Location: http://www.00aq.com

Content-Length: 0

Keep-Alive: timeout=5, max=100

Connection: Keep-Alive

Content-Type: text/html

访问下面的链接,会直接出现一个登陆窗口

http://localhost/location.php?page=%0d%0aContent-Type:%20text/html%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0aContent-Length:%20158%0d%0a%0d%0a<form%20method=post%20name=form1>帐号%20<input%20type=text%20name=username%20><br%20>密码%20<input%20name=password%20type=password%20><br%20><input%20type=submit%20name=login%20value=登录%20>

转换成可读字符串为:

Content-Type: text/html

HTTP/1.1 200 OK

Content-Type: text/html

Content-Length: 158

帐号
密码

一个HTTP请求产生了两个响应

防范的方法:

1)替换CRLF换行字符

  1.     header(“Location: “ . strtr($_GET[‘page’], array(“\r”=>“”,    “\n”=>“”)));
  2. ?>

2)使用最新版本的PHP

PHP最新版中,已经不允许在HTTP表头内出现换行字符

隐藏HTTP响应表头

apache中httpd.conf,选项ServerTokens = Prod, ServerSignature = Off

php中php.ini,选项expose_php = Off

PHP漏洞全解(七)Session劫持

服务端和客户端之间是通过session(会话)来连接沟通。当客户端的浏览器连接到服务器后,服务器就会建立一个该用户的session。每个用户的session都是独立的,并且由服务器来维护。每个用户的session是由一个独特的字符串来识别,成为session id。用户发出请求时,所发送的http表头内包含session id 的值。服务器使用http表头内的session id来识别时哪个用户提交的请求。

session保存的是每个用户的个人数据,一般的web应用程序会使用session来保存通过验证的用户账号和密码。在转换不同的网页时,如果需要验证用户身份,就是用session内所保存的账号和密码来比较。session的生命周期从用户连上服务器后开始,在用户关掉浏览器或是注销时用户session_destroy函数删除session数据时结束。如果用户在20分钟内没有使用计算机的动作,session也会自动结束。

php处理session的应用架构

会话劫持

会话劫持是指攻击者利用各种手段来获取目标用户的session id。一旦获取到session id,那么攻击者可以利用目标用户的身份来登录网站,获取目标用户的操作权限。

攻击者获取目标用户session id的方法:

1)暴力破解:尝试各种session id,直到破解为止。

2)计算:如果session id使用非随机的方式产生,那么就有可能计算出来

3)窃取:使用网络截获,xss攻击等方法获得

会话劫持的攻击步骤

实例

  1. //login.php
  2. session_start();
  3. if (isset($_POST[“login”]))
  4. {
  5. $link = mysql_connect(“localhost”“root”“root”)
  6. or die(“无法建立MySQL数据库连接:” . mysql_error());
  7. mysql_select_db(“cms”or die(“无法选择MySQL数据库”);
  8. if (!get_magic_quotes_gpc())
  9. {
  10. $query = “select * from member where username=’” . addslashes($_POST[“username”]) .
  11. “’ and password=’” . addslashes($_POST[“password”]) . “’”;
  12. }
  13. else
  14. {
  15. $query = “select * from member where username=’” . $_POST[“username”] .
  16. “’ and password=’” . $_POST[“password”] . “’”;
  17. }
  18. $result = mysql_query($query)
  19. or die(“执行MySQL查询语句失败:” . mysql_error());
  20. $match_count = mysql_num_rows($result);
  21. if ($match_count)
  22. {
  23. $_SESSION[“username”] = $_POST[“username”];
  24. $_SESSION[“password”] = $_POST[“password”];
  25. $_SESSION[“book”] = 1;
  26. mysql_free_result($result);
  27. mysql_close($link);
  28. header(“Location: http://localhost/index.php?user=” .
  29. $_POST[“username”]);
  30. }

…..

  1. //index.php
  2. // 打开Session
  3. session_start();
  4. 访客的 Session ID 是:echo session_id(); ?>
  5. 访客:echo htmlspecialchars($_GET[“user”], ENT_QUOTES); ?>
  6. book商品的数量:echo htmlspecialchars($_SESSION[“book”], ENT_QUOTES); ?>
  7. 如果登录成功,使用
  8. $_SESSION[“username”] 保存账号
  9. $_SESSION[“password”] 保存密码
  10. #_SESSION[“book”] 保存购买商品数目

登录以后显示

开始攻击

  1. //attack.php
  2. php 
  3. // 打开Session
  4. session_start();
  5. echo “目标用户的Session ID是:” . session_id() . “<br />“;
  6. echo “目标用户的username是:” . $_SESSION[“username”] . “<br />“;
  7. echo “目标用户的password是:” . $_SESSION[“password”] . “<br />“;
  8. // 将book的数量设置为2000
  9. $_SESSION[“book”] = 2000;
  10. ?>

提交 http://localhost/attack.php?PHPSESSID=5a6kqe7cufhstuhcmhgr9nsg45 此ID为获取到的客户session id,刷新客户页面以后

客户购买的商品变成了2000

session固定攻击

黑客可以使用把session id发给用户的方式,来完成攻击

http://localhost/index.php?user=dodo&PHPSESSID=1234 把此链接发送给dodo这个用户显示

然后攻击者再访问 http://localhost/attack.php?PHPSESSID=1234 后,客户页面刷新,发现

商品数量已经成了2000

防范方法

1)定期更改session id

函数 bool session_regenerate_id([bool delete_old_session])

delete_old_session为true,则删除旧的session文件;为false,则保留旧的session,默认false,可选

在index.php开头加上

session_start();

session_regenerate_id(TRUE);

……

这样每次从新加载都会产生一个新的session id

2)更改session的名称

session的默认名称是PHPSESSID,此变量会保存在cookie中,如果黑客不抓包分析,就不能猜到这个名称,阻挡部分攻击

session_start();

session_name(“mysessionid”);

……

3)关闭透明化session id

透明化session id指当浏览器中的http请求没有使用cookies来制定session id时,sessioin id使用链接来传递;打开php.ini,编辑

session.use_trans_sid = 0

代码中

int_set(“session.use_trans_sid”, 0);

session_start();

……

4)只从cookie检查session id

session.use_cookies = 1 表示使用cookies存放session id

session.use_only_cookies = 1 表示只使用cookies存放session id,这可以避免session固定攻击

代码中

int_set(“session.use_cookies”, 1);

int_set(“session.use_only_cookies”, 1); p>

5)使用URL传递隐藏参数

session_start();

$seid = md5(uniqid(rand()), TRUE));

$_SESSION[“seid”] = $seid;

攻击者虽然能获取session数据,但是无法得知$seid的值,只要检查seid的值,就可以确认当前页面是否是web程序自己调用的。

PHP漏洞全解(六)跨网站请求伪造

CSRF(Cross Site Request Forgeries),意为跨网站请求伪造,也有写为XSRF。攻击者伪造目标用户的HTTP请求,然后此请求发送到有CSRF漏洞的网站,网站执行此请求后,引发跨站请求伪造攻击。攻击者利用隐蔽的HTTP连接,让目标用户在不注意的情况下单击这个链接,由于是用户自己点击的,而他又是合法用户拥有合法权限,所以目标用户能够在网站内执行特定的HTTP链接,从而达到攻击者的目的。

例如:某个购物网站购买商品时,采用http://www.shop.com/buy.php?item=watch&num=1,item参数确定要购买什么物品,num参数确定要购买数量,如果攻击者以隐藏的方式发送给目标用户链接

,那么如果目标用户不小心访问以后,购买的数量就成了1000个

实例

随缘网络PHP留言板V1.0

任意删除留言

//delbook.php 此页面用于删除留言

  1. include_once(“dlyz.php”); //dlyz.php用户验证权限,当权限是admin的时候方可删除留言
  2. include_once(“../conn.php”);
  3. $del=$_GET[“del”];
  4. $id=$_GET[“id”];
  5. if ($del==“data”)
  6. {
  7. $ID_Dele= implode(“,”,$_POST[‘adid’]);
  8. $sql=“delete from book where id in (“.$ID_Dele.“)”;
  9. mysql_query($sql);
  10. }
  11. else
  12. {
  13. $sql=“delete from book where id=”.$id//传递要删除的留言ID
  14. mysql_query($sql);
  15. }
  16. mysql_close($conn);
  17. echo “<script language=’javascript’>”;
  18. echo “alert(‘删除成功!’);”;
  19. echo ” location=’book.php’;”;
  20. echo “”;
  21. ?>

当我们具有admin权限,提交http://localhost/manage/delbook.php?id=2 时,就会删除id为2的留言

利用方法:

我们使用普通用户留言(源代码方式),内容为

  1. “delbook.php?id=2” />
  2. “delbook.php?id=3” />
  3. “delbook.php?id=4” />
  4. “delbook.php?id=5” />

插入4张图片链接分别删除4个id留言,然后我们返回首页浏览看,没有什么变化。。图片显示不了

现在我们再用管理员账号登陆后,来刷新首页,会发现留言就剩一条,其他在图片链接中指定的ID号的留言,全部都被删除。

攻击者在留言中插入隐藏的图片链接,此链接具有删除留言的作用,而攻击者自己访问这些图片链接的时候,是不具有权限的,所以看不到任何效果,但是当管理员登陆后,查看此留言,就会执行隐藏的链接,而他的权限又是足够大的,从而这些留言就被删除了

修改管理员密码

  1. //pass.php
  2. if($_GET[“act”])
  3. {
  4. $username=$_POST[“username”];
  5. $sh=$_POST[“sh”];
  6. $gg=$_POST[“gg”];
  7. $title=$_POST[“title”];
  8. $copyright=$_POST[“copyright”].
    设计制作:<a href=http: www.115cn.cn=””>厦门随缘网络科技”
    ;
  9. $password=md5($_POST[“password”]);
  10. if(emptyempty($_POST[“password”]))
  11. {
  12. $sql=“update gly set username=’”.$username.“’,sh=”.$sh.“,gg=’”.$gg.“’,title=’”.$title.“’,copyright=’”.$copyright.“’ where id=1”;
  13. }
  14. else
  15. {
  16. $sql=“update gly set username=’”.$username.“’,password=’”.$password.“’,sh=”.$sh.“,gg=’”.$gg.“’,title=’”.$title.“’,copyright=’”.$copyright.“’ where id=1”;
  17. }
  18. mysql_query($sql);
  19. mysql_close($conn);
  20. echo “<script language=’javascript’>”;
  21. echo “alert(‘修改成功!’);”;
  22. echo ” location=’pass.php’;”;
  23. echo “”;
  24. }

这个文件用于修改管理密码和网站设置的一些信息,我们可以直接构造如下表单:

  1. <body>
  2. <form action=“http://localhost/manage/pass.php?act=xg” method=“post” name=“form1” id=“form1”>
  3. <input type=“radio” value=“1” name=“sh”>
  4. <input type=“radio” name=“sh” checked value=“0”>
  5. <input type=“text” name=“username” value=“root”>
  6. <input type=“password” name=“password” value=“root”>
  7. <input type=“text” name=“title” value=“随缘网络PHP留言板V1.0(带审核功能)” >
  8. <textarea name=“gg” rows=“6” cols=“80” >欢迎您安装使用随缘网络PHP留言板V1.0(带审核功能)!textarea> 
  9. <textarea name=“copyright” rows=“6” cols=“80” >随缘网络PHP留言本V1.0 版权所有:厦门随缘网络科技 2005-2009<br/>承接网站建设及系统定制 提供优惠主机域名textarea> 
  10. form> 
  11. body> 

存为attack.html,放到自己网站上http://www.sectop.com/attack.html,此页面访问后会自动向目标程序的pass.php提交参数,用户名修改为root,密码修改为root,然后我们去留言板发一条留言,隐藏这个链接,管理访问以后,他的用户名和密码全部修改成了root

防范方法

防范CSRF要比防范其他攻击更加困难,因为CSRF的HTTP请求虽然是攻击者伪造的,但是却是由目标用户发出的,一般常见的防范方法有下面几种:

1、检查网页的来源

2、检查内置的隐藏变量

3、使用POST,不要使用GET

检查网页来源

在//pass.php头部加入以下红色字体代码,验证数据提交

  1. if($_GET[“act”])
  2. {
  3. if(isset($_SERVER[“HTTP_REFERER”]))
  4. {
  5. $serverhost = $_SERVER[“SERVER_NAME”];
  6. $strurl = str_replace(“http://”,“”,$_SERVER[“HTTP_REFERER”]);
  7. $strdomain = explode(“/”,$strurl);
  8. $sourcehost = $strdomain[0];
  9. if(strncmp($sourcehost$serverhoststrlen($serverhost)))
  10. {
  11. unset($_POST);
  12. echo “<script language=’javascript’>”;
  13. echo “alert(‘数据来源异常!’);”;
  14. echo ” location=’index.php’;”;
  15. echo “”;
  16. }
  17. }
  18. $username=$_POST[“username”];
  19. $sh=$_POST[“sh”];
  20. $gg=$_POST[“gg”];
  21. $title=$_POST[“title”];
  22. $copyright=$_POST[“copyright”].
    设计制作:<a href=http: www.115cn.cn=””>厦门随缘网络科技”
    ;
  23. $password=md5($_POST[“password”]);
  24. if(emptyempty($_POST[“password”]))
  25. {
  26. $sql=“update gly set username=’”.$username.“’,sh=”.$sh.“,gg=’”.$gg.“’,title=’”.$title.“’,copyright=’”.$copyright.“’ where id=1”;
  27. }
  28. else
  29. {
  30. $sql=“update gly set username=’”.$username.“’,password=’”.$password.“’,sh=”.$sh.“,gg=’”.$gg.“’,title=’”.$title.“’,copyright=’”.$copyright.“’ where id=1”;
  31. }
  32. mysql_query($sql);
  33. mysql_close($conn);
  34. echo “<script language=’javascript’>”;
  35. echo “alert(‘修改成功!’);”;
  36. echo ” location=’pass.php’;”;
  37. echo “”;
  38. }

检查内置隐藏变量

我们在表单中内置一个隐藏变量和一个session变量,然后检查这个隐藏变量和session变量是否相等,以此来判断是否同一个网页所调用

  1. php 
  2. include_once(“dlyz.php”);
  3. include_once(“../conn.php”);
  4. if($_GET[“act”])
  5. {
  6. if (!isset($_SESSION[“post_id”]))
  7. {
  8. // 生成唯一的ID,并使用MD5来加密
  9. $post_id = md5(uniqid(rand(), true));
  10. // 创建Session变量
  11. $_SESSION[“post_id”] = $post_id;
  12. }
  13. // 检查是否相等
  14. if (isset($_SESSION[“post_id”]))
  15. {
  16. // 不相等
  17. if ($_SESSION[“post_id”] != $_POST[“post_id”])
  18. {
  19. // 清除POST变量
  20. unset($_POST);
  21. echo “<script language=’javascript’>“;
  22. echo “alert(‘数据来源异常!’);”;
  23. echo ” location=’index.php’;”;
  24. echo “script>“; 
  25. }
  26. }
  27. ……
  28. <input type=“reset” name=“Submit2” value=“重 置”>
  29. <input type=“hidden” name=“post_id” value=”php echo $_SESSION[“post_id“];?>”> 
  30. td>tr> 
  31. table> 
  32. form> 
  33. php 
  34. }
  35. mysql_close($conn);
  36. ?>
  37. body> 
  38. html> 

使用POST,不要使用GET

传递表单字段时,一定要是用POST,不要使用GET,处理变量也不要直接使用$_REQUEST

PHP漏洞全解(五)SQL注入攻击

SQL注入攻击(SQL Injection),是攻击者在表单中提交精心构造的sql语句,改动原来的sql语句,如果web程序没有对提交的数据经过检查,那么就会造成sql注入攻击。

SQL注入攻击的一般步骤:

1、攻击者访问有SQL注入漏洞的站点,寻找注入点

2、攻击者构造注入语句,注入语句和程序中的SQL语句结合生成新的sql语句

3、新的sql语句被提交到数据库中执行 处理

4、数据库执行了新的SQL语句,引发SQL注入攻击

实例

数据库

  1. CREATE TABLE `postmessage` (
  2. `id` int(11) NOT NULL auto_increment,
  3. `subject` varchar(60) NOT NULL default ”,
  4. `namevarchar(40) NOT NULL default ”,
  5. `email` varchar(25) NOT NULL default ”,
  6. `question` mediumtext NOT NULL,
  7. `postdate` datetime NOT NULL default ’0000-00-00 00:00:00′,
  8. PRIMARY KEY (`id`)
  9. ) ENGINE=MyISAM DEFAULT CHARSET=gb2312 COMMENT=’运用者的留言’ AUTO_INCREMENT=69 ;
  10. grant all privileges on ch3.* to ‘sectop’@localhost identified by ’123456′;
  11. //add.php 插入留言
  12. //list.php 留言列表
  13. //show.php 显示留言

页面 http://www.netsos.com.cn/show.php?id=71 可能存在注入点,我们来测试

http://www.netsos.com.cn/show.php?id=71 and 1=1

返回页面

一次查询到记录,一次没有,我们来看看源码

//show.php 12-15行

// 执行mysql查询语句

$query = “select * from postmessage where id = “.$_GET[“id”];

$result = mysql_query($query)

or die(“执行ySQL查询语句失败:” . mysql_error());

参数id传递进来后,和前面的字符串结合的sql语句放入数据库执行 查询

提交 and 1=1,语句变成select * from postmessage where id = 71 and 1=1 这语句前值后值都为真,and以后也为真,返回查询到的数据

提交 and 1=2,语句变成select * from postmessage where id = 71 and 1=2 这语句前值为真,后值为假,and以后为假,查询不到任何数据

正常的SQL查询,经过我们构造的语句之后,形成了SQL注入攻击。通过这个注入点,我们还可以进一步拿到权限,比如说运用 union读取管理密码,读取数据库信息,或者用mysql的load_file,into outfile等函数进一步渗透。

防范方法

整型参数:

运用 intval函数将数据转换成整数

函数原型

int intval(mixed var, int base)

var是要转换成整形的变量

base,可选,是基础数,默认是10

浮点型参数:

运用 floatval或doubleval函数分别转换单精度和双精度浮点型参数

函数原型

int floatval(mixed var)

var是要转换的变量

int doubleval(mixed var)

var是要转换的变量

字符型参数:

运用 addslashes函数来将单引号“’”转换成“\’”,双引号“””转换成“\””,反斜杠“\”转换成“\\”,NULL字符加上反斜杠“\”

函数原型

string addslashes (string str)

str是要检查的字符串

那么刚才出现的代码漏洞,我们可以这样修补

// 执行mysql查询语句

$query = “select * from postmessage where id = “.intval($_GET[“id”]);

$result = mysql_query($query)

or die(“执行ySQL查询语句失败:” . mysql_error());

如果是字符型,先判断magic_quotes_gpc能无法 为On,当不为On的时候运用 addslashes转义特殊字符

  1. if(get_magic_quotes_gpc())
  2. {
  3.     $var = $_GET[“var”];
  4. }
  5.  else
  6.  {
  7.   $var = addslashes($_GET[“var”]);
  8. }

再次测试,漏洞已经修补

PHP漏洞全解(四)xss跨站脚本攻击

XSS(Cross Site Scripting),意为跨网站脚本攻击,为了和样式表css(Cascading Style Sheet)区别,缩写为XSS

跨站脚本主要被攻击者利用来读取网站用户的cookies或者其他个人数据,一旦攻击者得到这些数据,那么他就可以伪装成此用户来登录网站,获得此用户的权限。

跨站脚本攻击的一般步骤:

1、攻击者以某种方式发送xss的http链接给目标用户

2、目标用户登录此网站,在登陆期间打开了攻击者发送的xss链接

3、网站执行了此xss攻击脚本

4、目标用户页面跳转到攻击者的网站,攻击者取得了目标用户的信息

5、攻击者使用目标用户的信息登录网站,完成攻击

当有存在跨站漏洞的程序出现的时候,攻击者可以构造类似http://www.sectop.com/search.php?key=” method=”POST”>

跨站脚本被插进去了

防御方法还是使用htmlspecialchars过滤输出的变量,或者提交给自身文件的表单使用

 

这样直接避免了$_SERVER[“PHP_SELF”]变量被跨站

PHP漏洞全解(三)客户端脚本植入

客户端脚本植入(Script Insertion),是指将可以执行的脚本插入到表单、图片、动画或超链接文字等对象内。当用户打开这些对象后,攻击者所植入的脚本就会被执行,进而开始攻击。

可以被用作脚本植入的HTML标签一般包括以下几种:

1、<script>标签标记的javascript和vbscript等页面脚本程序。在<script>标签内可以指定js程序代码,也可以在src属性内指定js文件的URL路径

2、<object>标签标记的对象。这些对象是java applet、多媒体文件和ActiveX控件等。通常在data属性内指定对象的URL路径

3、<embed>标签标记的对象。这些对象是多媒体文件,例如:swf文件。通常在src属性内指定对象的URL路径

4、<applet>标签标记的对象。这些对象是java applet,通常在codebase属性内指定对象的URL路径

5、<form>标签标记的对象。通常在action属性内指定要处理表单数据的web应用程序的URL路径

客户端脚本植入的攻击步骤

1、攻击者注册普通用户后登陆网站

2、打开留言页面,插入攻击的js代码

3、其他用户登录网站(包括管理员),浏览此留言的内容

4、隐藏在留言内容中的js代码被执行,攻击成功

实例

数据库

CREATE TABLE `postmessage` (

`id` int(11) NOT NULL auto_increment,

`subject` varchar(60) NOT NULL default ”,

`name` varchar(40) NOT NULL default ”,

`email` varchar(25) NOT NULL default ”,

`question` mediumtext NOT NULL,

`postdate` datetime NOT NULL default ’0000-00-00 00:00:00′,

PRIMARY KEY (`id`)

) ENGINE=MyISAM DEFAULT CHARSET=gb2312 COMMENT=’使用者的留言’ AUTO_INCREMENT=69 ;

//add.php 插入留言

//list.php 留言列表

//show.php 显示留言

提交下图的留言

浏览此留言的时候会执行js脚本

插入 <script>while(1){windows.open();}</script> 无限弹框

插入<script>location.href=”http://www.sectop.com”;</script> 跳转钓鱼页面

或者使用其他自行构造的js代码进行攻击

防范的方法

一般使用htmlspecialchars函数来将特殊字符转换成HTML编码

函数原型

string htmlspecialchars (string string, int quote_style, string charset)

string 是要编码的字符串

quote_style 可选,值可为ENT_COMPAT、ENT_QUOTES、ENT_NOQUOTES,默认值ENT_COMPAT,表示只转换双引号不转换单引号。ENT_QUOTES,表示双引号和单引号都要转换。ENT_NOQUOTES,表示双引号和单引号都不转换

charset 可选,表示使用的字符集

函数会将下列特殊字符转换成html编码:

& —-> &

” —-> ”

‘ —-> ‘

< —-> <

> —-> >

把show.php的第98行改成

<?php echo htmlspecialchars(nl2br($row[‘question’]), ENT_QUOTES); ?>

然后再查看插入js的漏洞页面