OAuth的改变(转载)

作者:郭无心
链接:http://www.zhihu.com/question/19851243/answer/75070070
来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

OAuth1.0

OAuth诞生前,Web安全方面的标准协议只有OpenID,不过它关注的是验证,即WHO的问题,而不是授权,即WHAT的问题。好在FlickrAuth和GoogleAuthSub等私有协议在授权方面做了不少有益的尝试,从而为OAuth的诞生奠定了基础。

OAuth1.0定义了三种角色:User、Service Provider、Consumer。如何理解?假设我们做了一个SNS,它有一个功能,可以让会员把他们在Google上的联系人导入到SNS上,那么此时的会员是User,Google是Service Providere,而SNS则是Consumer。

+----------+                                           +----------+
 |          |--(A)- Obtaining a Request Token --------->|          |
 |          |                                           |          |
 |          |<-(B)- Request Token ----------------------|          |
 |          |       (Unauthorized)                      |          |
 |          |                                           |          |
 |          |      +--------+                           |          |
 |          |>-(C)-|       -+-(C)- Directing ---------->|          |
 |          |      |       -+-(D)- User authenticates ->|          |
 |          |      |        |      +----------+         | Service  |
 | Consumer |      | User-  |      |          |         | Provider |
 |          |      | Agent -+-(D)->|   User   |         |          |
 |          |      |        |      |          |         |          |
 |          |      |        |      +----------+         |          |
 |          |<-(E)-|       -+-(E)- Request Token ------<|          |
 |          |      +--------+      (Authorized)         |          |
 |          |                                           |          |
 |          |--(F)- Obtaining a Access Token ---------->|          |
 |          |                                           |          |
 |          |<-(G)- Access Token -----------------------|          |
 +----------+                                           +----------+

花絮:OAuth1.0的RFC没有ASCII流程图,于是我敲了几百下键盘自己画了一个,后经网友提示,Emacs可以很轻松的搞定ASCII图:Emacs Screencast: Artist Mode,VIM当然也可以搞定,不过要借助一个插件:DrawIt,可惜我的键盘都要坏了。

Consumer申请Request Token(/oauth/1.0/request_token):

oauth_consumer_key
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonceoauth_version

Service Provider返回Request Token:

oauth_token
oauth_token_secret

Consumer重定向User到Service Provider(/oauth/1.0/authorize):

oauth_token
oauth_callback

Service Provider在用户授权后重定向User到Consumer:

oauth_token

Consumer申请Access Token(/oauth/1.0/access_token):

oauth_consumer_key
oauth_token
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonceoauth_version

Service Provider返回Access Token:

oauth_token
oauth_token_secret

注:整个操作流程中,需要注意涉及两种Token,分别是Request Token和Access Token,其中Request Token又涉及两种状态,分别是未授权和已授权。

OAuth1.0a

OAuth1.0存在安全漏洞,详细介绍:Explaining the OAuth Session Fixation Attack,还有这篇:How the OAuth Security Battle Was Won, Open Web Style

简单点来说,这是一种会话固化攻击,和常见的会话劫持攻击不同的是,在会话固化攻击中,攻击者会初始化一个合法的会话,然后诱使用户在这个会话上完成后续操作,从而达到攻击的目的。反映到OAuth1.0上,攻击者会先申请Request Token,然后诱使用户授权这个Request Token,接着针对回调地址的使用,又存在以下几种攻击手段:

  • 如果Service Provider没有限制回调地址(应用设置没有限定根域名一致),那么攻击者可以把oauth_callback设置成成自己的URL,当User完成授权后,通过这个URL自然就能拿到User的Access Token。
  • 如果Consumer不使用回调地址(桌面或手机程序),而是通过User手动拷贝粘贴Request Token完成授权的话,那么就存在一个竞争关系,只要攻击者在User授权后,抢在User前面发起请求,就能拿到User的Access Token。

为了修复安全问题,OAuth1.0a出现了(RFC5849),主要修改了以下细节:

  • Consumer申请Request Token时,必须传递oauth_callback,而Consumer申请Access Token时,不需要传递oauth_callback。通过前置oauth_callback的传递时机,让oauth_callback参与签名,从而避免攻击者假冒oauth_callback。
  • Service Provider获得User授权后重定向User到Consumer时,返回oauth_verifier,它会被用在Consumer申请Access Token的过程中。攻击者无法猜测它的值。

Consumer申请Request Token(/oauth/1.0a/request_token):

oauth_consumer_key
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonceoauth_version
oauth_callback

Service Provider返回Request Token:

oauth_token
oauth_token_secret
oauth_callback_confirmed

Consumer重定向User到Service Provider(/oauth/1.0a/authorize):

oauth_token

Service Provider在用户授权后重定向User到Consumer:

oauth_token
oauth_verifier

Consumer申请Access Token(/oauth/1.0a/access_token):

oauth_consumer_key
oauth_token
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonceoauth_version
oauth_verifier

Service Provider返回Access Token:

oauth_token
oauth_token_secret

注:Service Provider返回Request Token时,附带返回的oauth_callback_confirmed是为了说明Service Provider是否支持OAuth1.0a版本。

签名参数中,oauth_timestamp表示客户端发起请求的时间,如未验证会带来安全问题。

在探讨oauth_timestamp之前,先聊聊oauth_nonce,它是用来防止重放攻击的,Service Provider应该验证唯一性,不过保存所有的oauth_nonce并不现实,所以一般只保存一段时间(比如最近一小时)内的数据。

如果不验证oauth_timestamp,那么一旦攻击者拦截到某个请求后,只要等到限定时间到了,oauth_nonce再次生效后就可以把请求原样重发,签名自然也能通过,完全是一个合法请求,所以说Service Provider必须验证oauth_timestamp和系统时钟的偏差是否在可接受范围内(比如十分钟),如此才能彻底杜绝重放攻击。

需要单独说一下桌面或手机应用应该如何使用OAuth1.0a。此类应用通常没有服务端,无法设置Web形式的oauth_callback地址,此时应该把它设置成oob(out-of-band),当用户选择授权后,Service Provider在页面上显示PIN码(也就是oauth_verifier),并引导用户把它粘贴到应用里完成授权。

一个问题是应用如何打开用户授权页面呢?很容易想到的做法是使用内嵌浏览器,说它是个错误的做法或许有点偏激,但它至少是个对用户不友好的做法,因为一旦浏览器内嵌到程序里,那么用户输入的用户名密码就有被监听的可能;对用户友好的做法应该是打开新窗口,弹出系统默认的浏览器,让用户在可信赖的上下文环境中完成授权流程。

不过这样的方式需要用户在浏览器和应用间手动切换,才能完成授权流程,某种程度上说,影响了用户体验,好在可以通过一些其它的技巧来规避这个问题,其中一个行之有效的办法是Monitor web-browser title-bar,简单点说,操作系统一般提供相应的API可以让应用监听桌面上所有窗口的标题,应用一旦发现某个窗口标题符合预定义格式,就可以认为它是我们要的PIN码,无需用户参与就可以完成授权流程。Google支持这种方式,并且有资料专门描述了细节:Auto-Detecting Approval(注:墙!)。

还有一点需要注意的是对桌面或移动应用来说,consumer_key和consumer_secret通常都是直接保存在应用里的,所以对攻击者而言,理论上可以通过反编译之类的手段解出来。进而通过consumer_key和consumer_secret签名一个伪造的请求,并且在请求中把oauth_callback设置成自己控制的URL,来骗取用户授权。为了屏蔽此类问题,Service Provider需要强制开发者必须预定义回调地址:如果预定义的回调地址是URL方式的,则需要验证请求中的回调地址和预定义的回调地址是否主域名一致;如果预定义的回调地址是oob方式的,则禁止请求以URL的方式回调。

OAuth2.0

OAuth1.0虽然在安全性上经过修补已经没有问题了,但还存在其它的缺点,其中最主要的莫过于以下两点:其一,签名逻辑过于复杂,对开发者不够友好;其二,授权流程太过单一,除了Web应用以外,对桌面、移动应用来说不够友好。

为了弥补这些短板,OAuth2.0做了以下改变:

首先,去掉签名,改用SSL(HTTPS)确保安全性,所有的token不再有对应的secret存在,这也直接导致OAuth2.0不兼容老版本。

其次,针对不同的情况使用不同的授权流程,和老版本只有一种授权流程相比,新版本提供了四种授权流程,可依据客观情况选择。

在详细说明授权流程之前,我们需要先了解一下OAuth2.0中的角色:

OAuth1.0定义了三种角色:User、Service Provider、Consumer。而OAuth2.0则定义了四种角色:Resource Owner、Resource Server、Client、Authorization Server:

  • Resource Owner:User
  • Resource Server:Service Provider
  • Client:Consumer
  • Authorization Server:Service Provider

也就是说,OAuth2.0把原本OAuth1.0里的Service Provider角色分拆成Resource Server和Authorization Server两个角色,在授权时交互的是Authorization Server,在请求资源时交互的是Resource Server,当然,有时候他们是合二为一的。

下面我们具体介绍一下OAuth2.0提供的四种授权流程:

Authorization Code

可用范围:此类型可用于有服务端的应用,是最贴近老版本的方式。

+----------+
 | resource |
 |   owner  |
 |          |
 +----------+
      ^
      |
     (B)
 +----|-----+          Client Identifier      +---------------+
 |         -+----(A)-- & Redirection URI ---->|               |
 |  User-   |                                 | Authorization |
 |  Agent  -+----(B)-- User authenticates --->|     Server    |
 |          |                                 |               |
 |         -+----(C)-- Authorization Code ---<|               |
 +-|----|---+                                 +---------------+
   |    |                                         ^      v
  (A)  (C)                                        |      |
   |    |                                         |      |
   ^    v                                         |      |
 +---------+                                      |      |
 |         |>---(D)-- Authorization Code ---------'      |
 |  Client |          & Redirection URI                  |
 |         |                                             |
 |         |<---(E)----- Access Token -------------------'
 +---------+       (w/ Optional Refresh Token)

Client向Authorization Server发出申请(/oauth/2.0/authorize):

response_type = code
client_id
redirect_uri
scope
state

Authorization Server在Resource Owner授权后给Client返回Authorization Code:

code
state

Client向Authorization Server发出申请(/oauth/2.0/token):

grant_type = authorization_code
code
client_id
client_secret
redirect_uri

Authorization Server在Resource Owner授权后给Client返回Access Token:

access_token
token_type
expires_in
refresh_token

说明:基本流程就是拿Authorization Code换Access Token。

Implicit Grant

可用范围:此类型可用于没有服务端的应用,比如Javascript应用。

+----------+
 | Resource |
 |  Owner   |
 |          |
 +----------+
      ^
      |
     (B)
 +----|-----+          Client Identifier     +---------------+
 |         -+----(A)-- & Redirection URI --->|               |
 |  User-   |                                | Authorization |
 |  Agent  -|----(B)-- User authenticates -->|     Server    |
 |          |                                |               |
 |          |<---(C)--- Redirection URI ----<|               |
 |          |          with Access Token     +---------------+
 |          |            in Fragment
 |          |                                +---------------+
 |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
 |          |          without Fragment      |     Client    |
 |          |                                |    Resource   |
 |     (F)  |<---(E)------- Script ---------<|               |
 |          |                                +---------------+
 +-|--------+
   |    |
  (A)  (G) Access Token
   |    |
   ^    v
 +---------+
 |         |
 |  Client |
 |         |
 +---------+

Client向Authorization Server发出申请(/oauth/2.0/authorize):

response_type = token
client_id
redirect_uri
scope
state

Authorization Server在Resource Owner授权后给Client返回Access Token:

access_token
token_type
expires_in
scope
state

说明:没有服务端的应用,其信息只能保存在客户端,如果使用Authorization Code授权方式的话,无法保证client_secret的安全。BTW:不返回Refresh Token。

Resource Owner Password Credentials

可用范围:不管有无服务端,此类型都可用。

+----------+
 | Resource |
 |  Owner   |
 |          |
 +----------+
      v
      |    Resource Owner
     (A) Password Credentials
      |
      v
 +---------+                                  +---------------+
 |         |>--(B)---- Resource Owner ------->|               |
 |         |         Password Credentials     | Authorization |
 | Client  |                                  |     Server    |
 |         |<--(C)---- Access Token ---------<|               |
 |         |    (w/ Optional Refresh Token)   |               |
 +---------+                                  +---------------+

Clien向Authorization Server发出申请(/oauth/2.0/token):

grant_type = password
username
password
scope

AuthorizationServer给Client返回AccessToken:

access_token
token_type
expires_in
refresh_token

说明:因为涉及用户名和密码,所以此授权类型仅适用于可信赖的应用。

Client Credentials

可用范围:不管有无服务端,此类型都可用。

+---------+                                  +---------------+
 |         |                                  |               |
 |         |>--(A)- Client Authentication --->| Authorization |
 | Client  |                                  |     Server    |
 |         |<--(B)---- Access Token ---------<|               |
 |         |                                  |               |
 +---------+                                  +---------------+

Client向Authorization Server发出申请(/oauth/2.0/token):

grant_type = client_credentials
client_id
client_secret
scope

Authorization Server给Client返回Access Token:

access_token
token_type
expires_in

说明:此授权类型仅适用于获取与用户无关的公共信息。BTW:不返回Refresh Token。

流程中涉及两种Token,分别是Access Token和Refresh Token。通常,Access Token的有效期比较短,而Refresh Token的有效期比较长,如此一来,当Access Token失效的时候,就需要用Refresh Token刷新出有效的Access Token:

+--------+                                         +---------------+
 |        |--(A)------- Authorization Grant ------->|               |
 |        |                                         |               |
 |        |<-(B)----------- Access Token -----------|               |
 |        |               & Refresh Token           |               |
 |        |                                         |               |
 |        |                            +----------+ |               |
 |        |--(C)---- Access Token ---->|          | |               |
 |        |                            |          | |               |
 |        |<-(D)- Protected Resource --| Resource | | Authorization |
 | Client |                            |  Server  | |     Server    |
 |        |--(E)---- Access Token ---->|          | |               |
 |        |                            |          | |               |
 |        |<-(F)- Invalid Token Error -|          | |               |
 |        |                            +----------+ |               |
 |        |                                         |               |
 |        |--(G)----------- Refresh Token --------->|               |
 |        |                                         |               |
 |        |<-(H)----------- Access Token -----------|               |
 +--------+           & Optional Refresh Token      +---------------+

Client向Authorization Server发出申请(/oauth/2.0/token):

grant_type = refresh_token
refresh_token
client_id
client_secret
scope

Authorization Server给Client返回Access Token:

access_token
expires_in
refresh_token
scope

CentOS系统时间同步

在使用CentOS系统的时候,我们可能会遇到时间不准的问题,那我们如何解决这个我问题呢,下面就来教大家一个CentOS系统时间同步的方法,希望大家可以解决自己所存在的疑问。
CentOS系统时间同步的步骤如下:
新装的CentOS系统服务器可能设置了错误的,需要调整时区并调整时间.
如下是CentOS系统使用NTP来从一个时间服务器同步

cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
ntpdate us.pool.ntp.org

很简单吧!

下面解析一下,第一句是把当前时区调整为上海就是+8区,想改其他时区也可以去看看/usr/share/zoneinfo目录;
然后第二句是利用ntpdate同步标准时间.
没有安装ntpdate的可以yum一下:

yum install -y ntpdate

加入定时计划任务,每隔10分钟同步一下时钟

crontab -e
0-59/10 * * * * /usr/sbin/ntpdate us.pool.ntp.org | logger -t NTP

这样,我们就可以来解决在CentOS系统中时间不准确的问题了。

更新定时器

service crond restart

简单的 JavaScript 类继承框架(转载)

在写work-in-progress JavaScript book一书时,对于javascript继承体系,我花费了相当的时间,并在该过程中研究了各种不同的模拟经典类继承的方案。这些技术方案中,我最为推崇的是base2Prototype的实现。

从这些方案中,应该能提炼出一个具有其思想内涵的框架,该框架须具有简单、可重用、易于理解并无依赖等特点,其中简单性与可用性是重点。以下是使用示例:

var Person = Class. extend ( {
  init: function (isDancing ) {
    this. dancing = isDancing;
  },
  dance: function ( ) {
    return this. dancing;
  }
} );
var Ninja = Person.extend({
  init: function(){
    this._super( false );
  },
  dance: function(){
    // Call the inherited version of dance()
    return this._super();
  },
  swingSword: function(){
    return true;
  }
});
var p = new Person(true);
p.dance(); // => true
var n = new Ninja();
n.dance(); // => false
n.swingSword(); // => true
// Should all be true
p instanceof Person && p instanceof Class &&
n instanceof Ninja && n instanceof Person && n instanceof Class

有几点需要留意:

  • 构造函数须简单(通过init函数来实现),
  • 新定义的类比须继承于已有的类,
  • 所有的‘类’都继承于始祖类:Class,因此如果要创建一个全新的类,该类必须为Class的子类,
  • 最具挑战的一点:父类的被覆写方法必须能访问到(通过配置上下文环境)。
    在上面的示例中,你能发现通过this._super()来调用Person父类的init()和dance()方法。

对结果相当满意:使类的定义结构化,保持单一继承,并且能够调用超类方法。

简单的类创建与继承

下面为其实现(便于阅读并有注释),大概25行左右。欢迎并感谢提出建议。

/* Simple JavaScript Inheritance
 * By John Resig http://ejohn.org/
 * MIT Licensed.
 */
// Inspired by base2 and Prototype
( function ( ) {
  var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
  // The base Class implementation (does nothing)
  this.Class = function(){};
   
  // Create a new Class that inherits from this class
  Class.extend = function(prop) {
    var _super = this.prototype;
     
    // Instantiate a base class (but only create the instance,
    // don't run the init constructor)
    initializing = true;
    var prototype = new this();
    initializing = false;
     
    // Copy the properties over onto the new prototype
    for (var name in prop) {
      // Check if we're overwriting an existing function
      prototype[name] = typeof prop[name] == "function" &&
        typeof _super[name] == "function" && fnTest.test(prop[name]) ?
        (function(name, fn){
          return function() {
            var tmp = this._super;
             
            // Add a new ._super() method that is the same method
            // but on the super-class
            this._super = _super[name];
             
            // The method only need to be bound temporarily, so we
            // remove it when we're done executing
            var ret = fn.apply(this, arguments);       
            this._super = tmp;
             
            return ret;
          };
        })(name, prop[name]) :
        prop[name];
    }
     
    // The dummy class constructor
    function Class() {
      // All construction is actually done in the init method
      if ( !initializing && this.init )
        this.init.apply(this, arguments);
    }
     
    // Populate our constructed prototype object
    Class.prototype = prototype;
     
    // Enforce the constructor to be what we expect
    Class.prototype.constructor = Class;
    // And make this class extendable
    Class.extend = arguments.callee;
     
    return Class;
  };
})();

其中  “初始化(initializing/don’t call init)”与“创建_super方法”最为棘手。接下来,我会对此做简要的介绍,使得大家对其实现机制能更好的理解。

初始化

为了说明函数原型式的继承方式,首先来看传统的实现过程,即将子类的prototype属性指向父类的一个实例。如下所示:

function Person ( ) { }
function Ninja ( ) { }
Ninja. prototype = new Person ( );
// Allows for instanceof to work:
(new Ninja()) instanceof Person

然而,这里具有挑战性的一点,便是我们只想要得到‘是否实例(instatnceOf)’的效果,而不需要实例一个 Person并调用其构造函数所带来的后果。为防止这一点,在代码中设置一个bool参数initializing,只有在实例化父类并将其配置到子类的prototype属性时, 其值才为true。这样处理的目的是区分开真正的实例化与设计继承时这两种调用构造函数之间的区别,进而在真正实例化时调用init方法:

if ( !initializing )
  this.init.apply(this, arguments);

值得特别注意的是,因为在init函数中可能会运行相当费资源的代码(如连接服务器,创建DOM元素等,谁也无法预测),所以做出区分是完全必要的。

超类方法(Super Method)

当使用继承时,最常见的需求便是子类能访问超类被覆写的方法。在该实现下,最终的方案便是提供一个临时方法(._super),该方法指向超类方法,并且只能在子类方法中访问。

var Person = Class. extend ( {
  init: function (isDancing ) {
    this. dancing = isDancing;
  }
} );
var Ninja = Person.extend({
  init: function(){
    this._super( false );
  }
});
var p = new Person(true);
p.dancing; // => true
var n = new Ninja();
n.dancing; // => false

实现这一功能需要几步处理。首先,我们使用extend来合并基本的Person实例(类实例,上面我们提到过其构造过程)与字面对象(Person.extend()的函数参数)。在合并过程中,做了简单的检查:首先检查将被合并的的属性是否为函数,如为函数,然后检查将被覆写的超类属性是否也为函数?如果这两个检查都为true,则需要为该属性准备_super方法。

注意,在这里创建了一个匿名闭包(返回的是函数对象)来封装增加的super方法。基于维护运行环境的需要,我们应该将旧的this._super(不管其是否存在)保存起来以备函数运行后重置,这有助于在有相同名称(不想偶然丢失对象指针)的情况下发生不可预知的问题。

然后,创建新的_super方法,该方法对象仅指向超类中被覆写的方法。谢天谢地,不用对_super做任何改动或变更作用域,因为函数的执行环境会随着函数调用对象自动变更(指针this会指向超类).

最后,调用字面量对象的方法,方法执行中可能会使用this._super(),方法执行后,将属性_super重置回其原来状态,之后return退出函数。

以上可以有许多种方案能达到相同的效果(我之前曾见过将super绑定到其自身,然后用arguments.callee访问),但是感觉还是这种方法最能能体现可用性与简洁性的特点。

在我已完成的多个基于javascript原型的工作中,只有这个类继承实现方案是我发表出来与大家分享的。我认为,简洁的代码(易于学习,易于继承,更少下载)更需要提出来让大家探讨,因此,对于学习javascript类构造与继承的人们来说,这套实现方案是一个好的开始。

JAMES 垃圾邮件过滤

最近公司发布了一个JAMES邮件服务器, 第二天早一来, 发现spool里面有几万的垃圾邮件.
在网上仔细查了一下, 一般都是自定义一个matcher和一个mailet, 如:

package com.easynet.mailet;

import java.util.Collection;
import java.util.Iterator;
import java.util.Vector;
import javax.mail.MessagingException;
import org.apache.mailet.GenericMatcher;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;

public class FilterSenderMatcher extends GenericMatcher {
	private static  String[] hosts = null;
	@Override
	public Collection match(Mail mail) throws MessagingException {
		//System.out.println("getCondition:" + getCondition());
		if (hosts == null && getCondition() != null) {
			hosts = getCondition().split(",");
		}
		if (hosts == null || hosts.length == 0 || getCondition() == null) {
			return mail.getRecipients();
		}
		MailAddress mailAddress = mail.getSender();
		//System.out.println("mailAddress.toString():" + mailAddress.toString());
		for (String strTemp : hosts) {
			if (mailAddress.toString().toLowerCase().indexOf(strTemp) != -1) {
				//System.out.println(mailAddress.toString().toLowerCase().indexOf(strTemp));
				return mail.getRecipients();
			}
		}
		//mail.getRecipients().clear();//code1
        return null;
	}
}

可发现这种做法根本就过滤不了后缀是本服务器域名的邮件, 但JAMES包下的:org\apache\james\transport\matchers很多matche是这种写法. 经过尝试, 得把code1处的注释打开才可以过滤.
正确的操作步骤:
1.编写自己的Matcher, 上面一段code(把code1处的注释打开), 和Mailet.

package com.easynet.mailet;
import javax.mail.MessagingException;
import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
public class FilterSenderMailet extends GenericMailet {
	@Override
	public void service(Mail mail) throws MessagingException {
//		MailAddress ma = mail.getSender();   
//        System.out.println("sender:"+ma.toInternetAddress().toString()); 
	}
}

2. 把这两个JAVA类打包成JAR包, 放到james-2.3.2\apps\james\SAR-INF\lib目录下, 同时也要把james-2.3.2\apps\james.sar里面的james-2.3.2.jar,mail-1.4.1.jar,mailet-2.3.jar,mailet-api-2.3.jar这些包解压出来也放到这个目录下面, 不然会报找不到类的异常.
3.修改config.xml文件, 在<mailetpackages>后面添加:<mailetpackage>com.easynet.mailet</mailetpackage>, 在<matcherpackages>后面添加:<matcherpackage>com.easynet.mailet</matcherpackage>, 即是刚才的那两个类的包名.
4.修改config.xml文件, 添加过滤,在<spoolmanager>下面,<mailet match=”All” class=”PostmasterAlias”/>的后面添加:<mailet match=”FilterSenderMatcher=@easynet.com,@easynet.cn”  class=”FilterSenderMailet”/>, FilterSenderMatcher即为Mathcer的类的名字, FilterSenderMailet也mailet的类的名字, 它会在上面配置的包下面去找这两个类, 先通过matcher, 如果matcher返回的不是null的collection, 刚会下一步调用mailet; 如果matcher返回null, 但mail.getRecipients()不为null, 邮件会发送成功, 但不调用mailet, 所以code1处的注释要打开才不会发送.
OK,大功告成!