LVS和Nginx的一些杂记

LVS(Linux Virtual Server)是章文嵩博士创立的开源项目,可以帮助我们建立高可用性和高扩展性的Linux集群,LVS性能非常好,在暂时不能花费巨资购买F5、NetDispatcher之类硬件的时期,LVS还是能够很好的满足需要的。

LVS的Linux组件IPVS在新版本的Linux内核里已经支持,不需要单独再安装,可以通过命令

lsmod |grep ip_vs

检查内核是否支持IPVS。

ipvsadm是LVS的管理软件,在编译安装的时候要注意,先做一个ln连接,类似

ln -s /usr/src/kernels/2.6.9-42.EL-smp-i686 /usr/src/linux

ln -s /usr/src/kernels/2.6.18-8.el5-x86_64 /usr/src/linux

keepalived是一个很方便的LVS管理工具,只要需要配置keepalived的配置文件就可以了,不用一条一条的执行ipvsadm命令了。

将keepalived加为服务的方法(keepalived默认安装在/usr/local/etc下)

cp /usr/local/etc/rc.d/init.d/keepalived /etc/rc.d/init.d/
cp /usr/local/etc/sysconfig/keepalived /etc/sysconfig/
mkdir /etc/keepalived
cp /usr/local/etc/keepalived/keepalived.conf /etc/keepalived/
cp /usr/local/sbin/keepalived /usr/sbin/

关于虚拟IP与真实服务器IP的端口对应问题
在测试中,发现虚拟IP的端口只能和真实服务器IP的端口对应,比如虚拟IP的端口是80,后面对应的真实服务器的端口也只能是80,不能任意配置,真实服务器的端口,让我非常的不解,是我配置错误?还是LVS或keepalived得问题呢?

在Nginx服务器里也可以实现简单的Load Balance,举例

upstream tomcats {
ip_hash;
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8280 max_fails=3 fail_timeout=30s;
}
server {
location / {
proxy_pass http://tomcats;
}
}

其中,ip_hash的作用在于很据用IP做hash算法,分配到相应的后端服务上,这样的话,用可以基本上固定的分配到某台机子上,就不用担心Session的问题,fail_timeout和max_fails是指在单位时间重试的次数,如果连接不成功,则不会再向其转发请求。

Tomcat中request.getContextPath()值引发的问题

今天是生日,一大早上社区就发现了一个问题,是一个网友报的错误(http://bbs.laoer.com/main-read-15-ff808081205a54e001205d2565901041.html),我一看还真是,连接地址有错误,成了http://post.bbscs?action=re&bid=15&parentID=……,前面的服务器地址没有了,程序没有修改过,为什么会出现这样的问题呢,我看了一下代码,在BBSCSUtil.java里有个getActionMappingURL(String action, HttpServletRequest request)方法,其中要取request.getContextPath(),当应用根路径下运行的时候,request.getContextPath()的值为“/”,所以getActionMappingURL方法返回的值前面多了一个“/”,在Tomcat5.5中对于//uri的连接解析似乎不正确,而在用Resin时候却没有问题,看来在request.getContextPath()的问题上还是要多注意。

生日在愚人节

我的生日是4月1号,就是明天,我想这篇博客还是今天写吧,至少在今天,大家不会认为我是欺骗,知道4月1日是愚人节,好像是在上初中的时候,一个同学在英语课本的附加文章找到了April Fool’s Day,至此,我的生日再也逃不脱一顿捉弄,所以也是从这以后我也不太愿意将自己的生日告诉别人,不过在10多年之后,每到这个特殊的节日,那些曾经的同学、朋友便会想起我了。

关于愚人节(来自百度百科)

愚人节也称万愚节,是西方也是美国民间传统节日,节期在每年4月1日。愚人节与古罗马的嬉乐节(Hilaria,3月25日)和印度的欢悦节(Holi,到3月31日为止)有相似之处。在时间的选择,看来与“春分”(3月21日)有关。在这时间天气常常突然变化,恰似是大自然在愚弄人类。对于愚人节的起源众说纷纭。一种说法认为这一习俗源自印度的“诠俚节”。该节规定,每年三月三十一日的节日这天,不分男女老幼,可以互开玩笑、互相愚弄欺骗以换得娱乐。愚人节较普遍的说法是起源于法国。1564年,法国首先采用新改革的纪年法格里历(即目前通用的公历),以1月1日为一年之始。但一些因循守旧的人反对这种改革,依然按照旧历固执地在4月1日这一天送礼品,庆祝新年。主张改革的人对这些守旧者的做法大加嘲弄。聪明滑稽的人在4月1日就给他们送假礼品,邀请他们参加假招待会.并把上当受骗的保守分子称为“四月傻瓜”或“上钩的鱼”。从此人们在4月1日便互相愚弄,成为法国流行的风俗。18世纪初,愚人节习俗传到英国,接着又被英国的早期移民带到了美国。
起初,任何美国人都可以炮制骇人听闻的消息,而且不负丝毫的道德和法律责任,政府和司法部门也不会追究。相反,谁编造的谎言最离奇、最能骗取人们相信,谁还会荣膺桂冠。这种做法给社会带来不少混乱,因而引起人们的不满。现在,人们在节日期间的愚弄欺骗已不再像过去那样离谱,而是以轻松欢乐为目的。

记忆中的几个生日

2001年4月1日,我刚到上海,开始一份新的工作,全新的环境、生活方式,似乎与24年过去的生活做了一个割裂,到现在我也感觉,在上海之后的这几年生活,和以前完全的不同,几乎和以前的朋友、同学失去了联系,2004年4月1日,新的同事送给我一副SONY的耳机,直到今天我依然用它来听音乐。

2003年4月1日,我当时在网易工作,张国荣在这一天在香港文华酒店坠楼身亡,2009年3月30日,张国荣主演的电影《东邪西毒终极版》在香港首映。

2004年4月1日,Google推出Gmail,容量达到2G,原本以为是Google在愚人节推出的一个笑话,但真实的2G邮箱摆在自己面前的时候,让当时主流的Web Mail服务商们一身冷汗,从此Web Mail进入G时代。

PHP SOAP实践

我现在公司里的系统都是SOA/面向服务架构的,用Java编写的Web Service,理论上Web Service支持异构语言的,我想测试一下用PHP来调用Java的Web Service是否方便。

首先用Java建立一个Web Service服务端(用CXF,JAX-WS实现),下面是公布出来的WSDL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version='1.0' encoding='UTF-8'?><wsdl:definitions name="SampleWebService" targetNamespace="http://www.abc.net/WS/2008" xmlns:ns1="http://webservice.passport.abc.net/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://www.abc.net/WS/2008" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <wsdl:import location="http://192.168.19.42:8080/sample/service/SampleWebService?wsdl=SampleWebService.wsdl" namespace="http://webservice.passport.abc.net/">
    </wsdl:import>
  <wsdl:message name="helloUser">
    <wsdl:part element="ns1:helloUser" name="parameters">
    </wsdl:part>
  </wsdl:message>
  <wsdl:message name="helloUserResponse">
    <wsdl:part element="ns1:helloUserResponse" name="parameters">
    </wsdl:part>
  </wsdl:message>
  <wsdl:binding name="SampleWebServiceSoapBinding" type="ns1:SampleWebService">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
    <wsdl:operation name="helloUser">
      <soap:operation soapAction="" style="document" />
      <wsdl:input name="helloUser">
        <soap:body use="literal" />
      </wsdl:input>
      <wsdl:output name="helloUserResponse">
        <soap:body use="literal" />
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:service name="SampleWebService">
    <wsdl:port binding="tns:SampleWebServiceSoapBinding" name="SampleWebServicePort">
      <soap:address location="http://192.168.19.42:8080/sample/service/SampleWebService" />
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

这个Web Service有个“helloUser”方法,传入一个参数,它返回一段文本“Hello 参数!”,

PHP5之后已经加入了SOAP的支持,我最开始用PHP5的SOAP函数来调用,下面是代码

1
2
$client = new SoapClient('http://192.168.19.42:8080/sample/service/SampleWebService?wsdl');
var_dump($client->__getFunctions());

但报错Fatal error: Uncaught SoapFault exception: [WSDL] SOAP-ERROR: Parsing WSDL: <message> ‘helloUser’ already defined in …,我搜了PHP网站上的相关文档也没找到答案,没办法,我只能找其他SOAP的组件,找到了NuSOAP,它最近发布的版本是0.7.3,是在2007年发布的,现在已经2009了,用用试试吧,参考nusoap的例子,写好自己代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require_once('nusoap.php');
$client = new nusoap_client('http://192.168.19.42:8080/sample/service/SampleWebService?wsdl', 'wsdl');
$err = $client->getError();
if ($err) {
	echo '<h2>Constructor error</h2><p>' . $err . '</p>';
}
$param = array('arg0'=>'laoer');
$result = $client->call('helloUser', array('parameters' => $param),"http://webservice.passport.abc.net/",'', false, false,"document","literal");
 
if ($client->fault) {
	echo '<h2>Fault</h2><p>';
	print_r($result);
	echo '</p>';
} else {
	// Check for errors
	$err = $client->getError();
	if ($err) {
		// Display the error
		echo '<h2>Error</h2><p>' . $err . '</p>';
	} else {
		// Display the result
		echo '<h2>Result</h2><p>';
		//print_r($result);
		echo $result["return"];
		echo '</p>';
	}
}
 
echo '<h2>Request</h2><p>' . htmlspecialchars($client->request, ENT_QUOTES) . '</p>';
echo '<h2>Response</h2><p>' . htmlspecialchars($client->response, ENT_QUOTES) . '</p>';

执行结果

Result
Hello laoer!

Request
POST /sample/service/SampleWebService HTTP/1.0
Host: 192.168.19.42:8080
User-Agent: NuSOAP/0.7.3 (1.114)
Content-Type: text/xml; charset=UTF-8
SOAPAction: “”
Content-Length: 436

我们可以看到Request的SOAP文本

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:ns1="http://webservice.passport.abc.net/">
	<SOAP-ENV:Body>
		<helloUser xmlns="">
			<arg0 xmlns="">laoer</arg0>
		</helloUser>
	</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Response
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
SOAPAction: “”
Content-Type: text/xml;charset=UTF-8
Content-Length: 237
Date: Fri, 27 Mar 2009 04:27:11 GMT
Connection: close

Response的SOAP文本

1
2
3
4
5
6
7
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
	<soap:Body>
		<ns2:helloUserResponse xmlns:ns2="http://webservice.passport.abc.net/">
			<return>Hello laoer!</return>
		</ns2:helloUserResponse>
	</soap:Body>
</soap:Envelope>

碰到一个小问题就是PHP给方法传参数,看是我不知道怎么填,后来我用soapUI,做了一个SOAP Request的例子,发现参数的Name是arg0,如果是多个参数,则arg0、arg1、arg2…以此类推,所以我们定义的参数是$param = array(‘arg0’=>’laoer’),还有就是nusoap里对返回信息的编码有点问题,如果你Web Service放回的编码是UTF-8,则nusoap多转换了一次,在nusoap.php文件nusoap_client类里把$decode_utf8置为false就可以了。

Java RSA研究

前几天我做了PHP RSA的测试,想想Java来做RSA应该更简单,就在网上搜了一下Java RSA,发现资料是有一些,但却不是很完整,也不是很严谨,都是转来转去,看来还是自己要测试一下,两个文件RSAUtil.java和EncryptException.java,注意,JDK里没有RSA的provider,所以我们要用第三方的provider,在http://www.bouncycastle.org/下载最新的包,加入工程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.io.*;
import java.math.BigInteger;
 
public class RSAUtil {
 
    /**
     *   * 生成密钥对
     *   * @return KeyPair
     *   * @throws EncryptException
     */
    public static KeyPair generateKeyPair() throws EncryptException {
        try {
            KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA",
                    new org.bouncycastle.jce.provider.BouncyCastleProvider());
            final int KEY_SIZE = 1024;//没什么好说的了,这个值关系到块加密的大小,可以更改,但是不要太大,否则效率会低
            keyPairGen.initialize(KEY_SIZE, new SecureRandom());
            KeyPair keyPair = keyPairGen.genKeyPair();
            return keyPair;
        } catch (Exception e) {
            throw new EncryptException(e.getMessage());
        }
    }
 
    /**
     *   * 生成公钥
     *   * @param modulus
     *   * @param publicExponent
     *   * @return RSAPublicKey
     *   * @throws EncryptException
     */
    public static RSAPublicKey generateRSAPublicKey(byte[] modulus, byte[] publicExponent) throws EncryptException {
        KeyFactory keyFac = null;
        try {
            keyFac = KeyFactory.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
        } catch (NoSuchAlgorithmException ex) {
            throw new EncryptException(ex.getMessage());
        }
 
        RSAPublicKeySpec pubKeySpec = new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(publicExponent));
        try {
            return (RSAPublicKey) keyFac.generatePublic(pubKeySpec);
        } catch (InvalidKeySpecException ex) {
            throw new EncryptException(ex.getMessage());
        }
    }
 
    /**
     *   * 生成私钥
     *   * @param modulus
     *   * @param privateExponent
     *   * @return RSAPrivateKey
     *   * @throws EncryptException
     */
    public static RSAPrivateKey generateRSAPrivateKey(byte[] modulus, byte[] privateExponent) throws EncryptException {
        KeyFactory keyFac = null;
        try {
            keyFac = KeyFactory.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
        } catch (NoSuchAlgorithmException ex) {
            throw new EncryptException(ex.getMessage());
        }
 
        RSAPrivateKeySpec priKeySpec = new RSAPrivateKeySpec(new BigInteger(modulus), new BigInteger(privateExponent));
        try {
            return (RSAPrivateKey) keyFac.generatePrivate(priKeySpec);
        } catch (InvalidKeySpecException ex) {
            throw new EncryptException(ex.getMessage());
        }
    }
 
    /**
     *   * 加密
     *   * @param key 加密的密钥
     *   * @param data 待加密的明文数据
     *   * @return 加密后的数据
     *   * @throws EncryptException
     */
    public static byte[] encrypt(Key key, byte[] data) throws EncryptException {
        try {
            Cipher cipher = Cipher.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
            cipher.init(Cipher.ENCRYPT_MODE, key);
            int blockSize = cipher.getBlockSize();//获得加密块大小,如:加密前数据为128个byte,而key_size=1024 加密块大小为127 byte,加密后为128个byte;因此共有2个加密块,第一个127 byte第二个为1个byte
            int outputSize = cipher.getOutputSize(data.length);//获得加密块加密后块大小
            int leavedSize = data.length % blockSize;
            int blocksSize = leavedSize != 0 ? data.length / blockSize + 1 : data.length / blockSize;
            byte[] raw = new byte[outputSize * blocksSize];
            int i = 0;
            while (data.length - i * blockSize > 0) {
                if (data.length - i * blockSize > blockSize)
                    cipher.doFinal(data, i * blockSize, blockSize, raw, i * outputSize);
                else
                    cipher.doFinal(data, i * blockSize, data.length - i * blockSize, raw, i * outputSize);
//这里面doUpdate方法不可用,查看源代码后发现每次doUpdate后并没有什么实际动作除了把byte[]放到ByteArrayOutputStream中,而最后doFinal的时候才将所有的byte[]进行加密,可是到了此时加密块大小很可能已经超出了OutputSize所以只好用dofinal方法。
 
                i++;
            }
            return raw;
        } catch (Exception e) {
            throw new EncryptException(e.getMessage());
        }
    }
 
 
 
    /**
     *   * 解密
     *   * @param key 解密的密钥
     *   * @param raw 已经加密的数据
     *   * @return 解密后的明文
     *   * @throws EncryptException
     */
    public static byte[] decrypt(Key key, byte[] raw) throws EncryptException {
        try {
            Cipher cipher = Cipher.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
            cipher.init(cipher.DECRYPT_MODE, key);
            int blockSize = cipher.getBlockSize();
            ByteArrayOutputStream bout = new ByteArrayOutputStream(64);
            int j = 0;
 
            while (raw.length - j * blockSize > 0) {
                bout.write(cipher.doFinal(raw, j * blockSize, blockSize));
                j++;
            }
            return bout.toByteArray();
        } catch (Exception e) {
            throw new EncryptException(e.getMessage());
        }
    }
 
    public static String byte2hex(byte[] b) {
        String hs = "";
        String stmp = "";
        for (int i = 0; i < b.length; i++) {
            stmp = Integer.toHexString(b[i] & 0xFF);
            if (stmp.length() == 1) {
                hs += "0" + stmp;
            } else {
                hs += stmp;
            }
        }
        //return hs;
        return hs.toUpperCase();
    }
 
 
    public static byte[] hex2byte(String hex) throws IllegalArgumentException {
        if (hex.length() % 2 != 0) {
            throw new IllegalArgumentException();
        }
        char[] arr = hex.toCharArray();
        byte[] b = new byte[hex.length() / 2];
        for (int i = 0, j = 0, l = hex.length(); i < l; i++, j++) {
            String swap = "" + arr[i++] + arr[i];
            int byteint = Integer.parseInt(swap, 16) & 0xFF;
            b[j] = new Integer(byteint).byteValue();
        }
        return b;
    }
 
    /**
     *   *
     *   * @param args
     *   * @throws Exception
     */
    public static void main(String[] args) throws Exception {
 
        byte[] orgData = "test".getBytes();
        KeyPair keyPair = RSAUtil.generateKeyPair();
        RSAPublicKey pubKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey priKey = (RSAPrivateKey) keyPair.getPrivate();
 
        byte[] pubModBytes = pubKey.getModulus().toByteArray();
        System.out.println("PubKey Modulus:"+new BigInteger(pubModBytes).toString(16));
        byte[] pubPubExpBytes = pubKey.getPublicExponent().toByteArray();
        System.out.println("publicExponent:"+new BigInteger(pubPubExpBytes).toString(16));
        byte[] priModBytes = priKey.getModulus().toByteArray();
        byte[] priPriExpBytes = priKey.getPrivateExponent().toByteArray();
        RSAPublicKey recoveryPubKey = RSAUtil.generateRSAPublicKey(pubModBytes, pubPubExpBytes);
        RSAPrivateKey recoveryPriKey = RSAUtil.generateRSAPrivateKey(priModBytes, priPriExpBytes);
 
        byte[] raw = RSAUtil.encrypt(priKey, orgData);
        System.out.println("Encrypt:"+byte2hex(raw));
        byte[] data = RSAUtil.decrypt(recoveryPubKey, raw);
        System.out.println("Decrypt:"+new String(data));
 
    }
 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class EncryptException extends Exception {
 
    public EncryptException() {
        super();
    }
 
    public EncryptException(String message) {
        super(message);
    }
 
    public EncryptException(String message, Throwable cause) {
        super(message, cause);
    }
 
    public EncryptException(Throwable cause) {
        super(cause);
    }
 
}

我们看一下运行结果:

1
2
3
4
PubKey Modulus:a907de8b5789b1df66c8a4ea90f99c9b00bbad520d487a7e218cd1ee2a1cafcaff2dd03a70cc61d8ccdfe0557b9132dd163a5a6c287d94790fc8b573a154ba0cd799e2cc73fc44c03083274760664125cafc33c647c44df300968665b6dbc9e553c59f8180de0ded3ae2163aab499c1ec0688ed7468fb816cdf05db501cbba19
publicExponent:10001
Encrypt:71521BBA91D871AAA8FF99D7E9D6E44DAE6218FAFDC07CE7C34ABACC1357854BF8F5D94F17FD106346B0916CC81A85B7031F9421810F1E5A568EE408E6DFDB219201CB1DE2AF259A516F3B930130D6AD4FFCB26072EB2BD9CFC11CA727B181A640311BC3023B2D1E54EBC52454C669389C24D0F7AD316B0B24257D53384E1446
Decrypt:test

使用已有的Modulus加密的话

1
2
3
4
5
6
7
8
String modulus = "D192471B8699640F931FE6F4FACC3E990B894F894CEA5BEE0DCBD7A4B76752F7345CF9B5F1271001B724F7A0ABF0A6E911E309536F4BE4749E92DCC531B8E36B95969D206649C9DD2371B413A8DFD9B92569660B1499A5CD310B86A8FDE24988E456897A416D2E7B0B649F0714F322C57EF92563B21A448D1072FF3806C34C75";
byte[] b_modulus = new BigInteger(modulus,16).toByteArray();
String publicExponent = "10001";
byte[] b_publicExponent = new BigInteger(publicExponent,16).toByteArray();
String text = "test";
RSAPublicKey recoveryPubKey = RSAUtil.generateRSAPublicKey(b_modulus, b_publicExponent);        
byte[] ss = RSAUtil.encrypt(recoveryPubKey,text.getBytes());
System.out.println(byte2hex(ss));

PHP RSA研究

最近研究了一下QQ邮箱的登录过程,发现QQ邮箱登录比较严谨,加了一些小技巧,其中一点就是用JavaScript对用户的密码做了一下RSA的加密,在它的登录页面里有一段

1
2
3
4
5
6
7
8
var PublicKey = "CF87D7B4C864F4842F1D337491A48FFF54B73A17300E8E42FA365420393AC0346AE55D8AFAD975DFA175FAF0106CBA81AF1DDE4ACEC284DAC6ED9A0D8FEB1CC070733C58213EFFED46529C54CEA06D774E3CC7E073346AEBD6C66FC973F299EB74738E400B22B1E7CDC54E71AED059D228DFEB5B29C530FF341502AE56DDCFE9";
var RSA = new RSAKey();
RSA.setPublic(PublicKey, "10001");
var Res = RSA.encrypt(document.form1.pp.value + '\n' + document.form1.ts.value + '\n');
if (Res)
{
document.form1.p.value = hex2b64(Res);
}

再看看RSAKey的相关源码,应该是在http://m367.mail.qq.com/zh_CN/htmledition/js/safeauth.js里,看了一下还是蛮复杂的,我就在Google上搜一下有没有相关的资料,找到了这个网站《BigIntegers and RSA in JavaScript》,看了一下他们的代码,和QQ里的基本一样嘛,估计QQ也是用了人家的代码,这篇文章里有密钥对的生成、加密解密的测试页面,由此推断QQ代码里的PublicKey,应该是密钥对的modulus(也可以理解为公钥吧),并且QQ用的是1024位的密钥,那我们是否可以用这个modulus,使用别的语言(比如PHP)来做密码加密呢?

想到就做,我在Google上搜索一下“PHP RSA”,找到了这个网站http://www.edsko.net/misc/,里面有PHP RSA的实现,不过我对它的rsa_encrypt($message, $public_key, $modulus, $keylength)方法产生了疑惑,它有4个参数,第一个是要加密的字串,那后面三个怎么填,我们现在仅知道的就是modulus了,而$public_key和$keylength从何而来呢,真有点摸不着头脑了,还是看看它带的例子吧,原来它例子里是从密钥对文件中取得相关的信息,QQ的密钥对文件自然取不到,那我只能自己生成一个密钥对文件,来看看有什么规律吧,在Linux下用openssl做

openssl genrsa -out key.pem 1024

生成了一个1024位的密钥对文件,可以打开看看,里面应该是Base64编码的,之后我们通过下面的命令可以得到modulus

1
2
openssl rsa -in key.pem -noout -modulus
Modulus=D192471B8699640F931FE6F4FACC3E990B894F894CEA5BEE0DCBD7A4B76752F7345CF9B5F1271001B724F7A0ABF0A6E911E309536F4BE4749E92DCC531B8E36B95969D206649C9DD2371B413A8DFD9B92569660B1499A5CD310B86A8FDE24988E456897A416D2E7B0B649F0714F322C57EF92563B21A448D1072FF3806C34C75

比照QQ的,位数是一样的,接下来我们用命令

openssl rsa -in key.pem -text -noout

输出的内容如下:
Private-Key: (1024 bit)
modulus:
00:d1:92:47:1b:86:99:64:0f:93:1f:e6:f4:fa:cc:
3e:99:0b:89:4f:89:4c:ea:5b:ee:0d:cb:d7:a4:b7:
67:52:f7:34:5c:f9:b5:f1:27:10:01:b7:24:f7:a0:
ab:f0:a6:e9:11:e3:09:53:6f:4b:e4:74:9e:92:dc:
c5:31:b8:e3:6b:95:96:9d:20:66:49:c9:dd:23:71:
b4:13:a8:df:d9:b9:25:69:66:0b:14:99:a5:cd:31:
0b:86:a8:fd:e2:49:88:e4:56:89:7a:41:6d:2e:7b:
0b:64:9f:07:14:f3:22:c5:7e:f9:25:63:b2:1a:44:
8d:10:72:ff:38:06:c3:4c:75
publicExponent: 65537 (0x10001)
privateExponent:
00:83:d3:d9:08:f6:95:3c:bd:13:56:29:09:07:4e:
3d:3e:36:64:8c:74:98:be:7f:4f:72:bc:3c:0c:f0:
15:7d:b9:e4:e5:6b:6a:c8:a4:42:cc:61:71:4e:97:
72:30:f2:3d:80:33:e9:a4:e3:48:c1:0f:9e:c4:51:
3d:75:f6:90:8e:f3:c3:f8:ce:45:59:2a:67:42:a8:
c6:d0:4c:1d:12:c4:cf:53:f8:b1:58:b4:e1:23:71:
0e:e9:e9:e0:40:3d:9a:99:e3:5f:e1:93:04:e2:0a:
60:34:77:56:be:f9:8f:e6:4e:87:23:46:48:ba:38:
9d:dd:46:ce:20:b7:82:27:cd
prime1:
00:ee:a9:e4:70:9c:d4:fe:bf:cd:87:5c:00:cb:ea:
ef:82:92:e1:88:f7:99:6a:42:09:f4:fd:78:93:bd:
30:28:1f:2e:ed:c1:cd:d3:60:8b:34:52:89:a7:ac:
98:37:cd:96:81:1e:57:2f:46:08:0e:8d:fb:13:92:
8d:f5:7a:50:5f
prime2:
00:e0:cb:65:5e:31:f2:3b:c0:7f:93:ae:d9:6c:35:
75:e5:ce:8b:37:7d:39:ce:82:dd:9b:43:00:09:a6:
d8:c1:ab:bc:10:fe:3d:56:34:fe:bd:38:fe:fc:6c:
f2:74:a8:d6:40:25:e5:5a:35:7b:d0:24:71:44:8d:
53:23:71:83:ab
exponent1:
4b:d5:7f:d8:a8:7c:a5:55:9c:a0:de:03:02:c8:6b:
c2:39:99:a0:43:cc:63:8f:08:4a:e8:1f:60:12:45:
32:fa:75:96:e6:75:d8:2c:5d:0f:0b:0a:e2:54:5d:
29:9e:11:ac:85:4f:7e:9d:ea:01:75:eb:c9:94:4f:
b7:28:5e:51
exponent2:
00:9b:9f:d4:56:a8:e7:55:3c:88:55:fa:97:a5:55:
41:80:ce:44:0d:2f:51:a4:c9:6e:97:fd:83:7a:2b:
1b:26:c1:38:da:de:d8:21:e5:60:72:29:92:45:b9:
3b:05:4e:99:bd:21:3f:2d:fb:96:f2:db:37:db:48:
a7:c5:02:e2:2f
coefficient:
00:c2:75:38:a5:02:24:39:1e:0e:e9:ec:56:6a:31:
5d:38:82:ca:3e:9b:67:cb:40:7e:7b:2f:91:26:bb:
4e:64:3d:60:53:f1:21:67:8b:b7:af:f8:2e:95:f7:
af:cf:42:75:ab:6c:5c:42:97:42:17:94:17:ff:e0:
b9:cb:c9:e8:6d

通过它例子的代码,我明白了,$public_key应该是1024,$keylength就是65537,$modulus不能直接用这段文字,要先转成BigInteger,再转成文本传进去,BigInteger实现在PEAR里有,http://pear.php.net/package/Math_BigInteger

我们接下来就写程序吧

1
2
3
4
5
6
7
8
9
10
11
include('rsa.php');
include('BigInteger.php');
 
$public = 65537;
$modulus = "D192471B8699640F931FE6F4FACC3E990B894F894CEA5BEE0DCBD7A4B76752F7345CF9B5F1271001B724F7A0ABF0A6E911E309536F4BE4749E92DCC531B8E36B95969D206649C9DD2371B413A8DFD9B92569660B1499A5CD310B86A8FDE24988E456897A416D2E7B0B649F0714F322C57EF92563B21A448D1072FF3806C34C75";
$keylength = 1024;
$modulus_16 = new Math_BigInteger($modulus,16);
$mend = $modulus_16->toString();
 
$encrypted = rsa_encrypt("test", $public, $mend, $keylength);
echo bin2hex($encrypted); //这里也可以用Base64,QQ就是Base64

最后说一下我对于RSA的理解,首先生成了公钥/私钥的密钥对,之后把公钥发布出去,外部系统用公钥加密,传给内部系统用私钥解密。

SNS中好友动态功能的设计思路

现在大部分SNS网站都有一个功能,就是显示好友的活动状态,比如你的好友上传了一张照片、分享了一篇文章等等动作,都可以显示在你的页面里,这样大大增强了社区的互动性,也成为现在SNS网站的主要特征,对于这样一个功能,从设计角度,还是值得思考的,并不简单,特别是用户越来越多,信息海量增长的时候,我未必能提出十全十美的方案,但我们可以由简如繁梳理一下思路。

首先我们要定义用户的活动消息,也可以理解为一个事件,就是我们举的例子:用户上传照片、与别人结为好友、修改了个人资料等等,动作各有不同,但需要在结构上通用,我们先设计一个

ID //消息ID
UserID //用户ID
MsgType //消息类型,比如加好友、上传照片等不同的类型
EventMsg //消息的内容,这里我们可以用Json的数据格式来描述出不同的活动内容
CreateTime //消息创建时间

这个结构也是个数据库的结构,当用户做个一个动作之后,就会创建这样一个消息,并保存在数据库中,当显示好友的活动信息时,就从这张表里查询自己好友id的数据,并按时间显示,这个做法是一个最简单的实现,但会出现一些问题,当你与一个用户成为好友之后,该好友之前发生的动作会显示出来,而不是在成为好友时点之后的动作,同样,切断好友关系之后也有类似的问题,如果从业务角度和用户体验上可以接受的话,也没什么,但由于信息是按时间排序,有时候会给用户错乱的感觉,还有,这个信息不能删除,如果删除了所有好友就看不到这条信息了,但在Facebook里是则是可以删除好友的动作信息的,这个方法还有一个问题是,所有信息都放在一张大表里,在信息爆炸增长,个人好友也很多的情况下,查询效率会非常低,产生严重的性能障碍,如果对这张表做水平切分,则在实现上复杂了许多,性能也未必好很多,接下来我们再思考是否有更好的解决办法。

在SNS的理论里,个人好友的合理数量在150个左右(最近有文章说Facebook的人均好友数是120人),SNS网站应该是有好友数量的限制的,我们就按人均150个好友来设想,是否可以在用户发生某些动作之后,针对他的所有好友都写入一条信息,所能解决的是,信息是在用户成为好友时点之后写入,用户可以删除好友的活动信息,不影响其他用户的显示,在显示时查询效率要高很多,但是负面效应也十分明显,一个用户的动作有平均150个写入,对于数据库来说开销也非常大,我们想想在这样的设计下,是否可以提高性能。

首先看数据库设计,我们要把信息表和信息与用户的对应表分开,我们上面定义的数据结构保留,我们定义它的表名为Event,我们再新建一张表EventUser,结构如下

ID  //主键
EventID //Event表的ID
FriendUserID //好友的ID
CreateTime //消息创建时间

对FriendUserID做索引,当用户发生动作时,首先将动作信息写入Event表,之后查找用户的所有好友,将EventID、好友ID逐条写入EventUser表,当要显示自己的好友活动信息是,查询EventUser中FriendUserID等于自己ID的信息,并和Event表做一个Join查询就可以了。

进一步提升性能的方法,将Event里的信息缓存,比如用Memcached,EventID做为Key,内容做为Value,这样,查询EventUser是就不用Join Event表,而是从缓存里读取,由于要记录给每个好友的信息,所以EventUser会变得非常大,平均要比我们最初设计的数据容量大150倍,我们对EventUser表做水平切分,根据用户ID做Hash算法,基本上能均匀的分配到所有的表中,至于EventUser水平切分的算法还有多种,根据实际情况来定,总的来说就是把数据分摊掉,同时EventUser里的数据可以不永久保存,做定期删除,可以保持数据容量在一个合理的范围内。

对于用户动作之后的数据写入,可以采用异步的方法,在发生动作时,抛出一个消息,由另外一个线程在做写入操作,如果对每个动作平均150次的写入仍存顾虑,我们可以针对每个用户开出一块内存空间,或是缓存,里面保存最后50条最新的好友动作,并在每条记录里做一个持久化标志,当有50条信息都标志是“未持久化”时,做一次数据库的写入,然后把信息置为“已持久化”,这种非实时写入的方式,可以提高一定的数据库效率,显示时,先从内存中取出,再查数据库。

还有一些问题,对不不同消息类型的处理方式不同,比如用户修改个人资料,并不是每次发生这样的动作都要做一次给朋友做一次“群发”的操作,如果遇到这个人在短时间内做了多次修改个人资料的操作,就给朋友发出多个消息,会产生垃圾信息,让人觉得很怪异,所以对于这样的操作会有一定的时效性,比如在一天之内的修改个人资料,就是一个消息,这时候的处理是更新,而不是插入。

以上是我对SNS中好友动态功能的设计思路,可能还有一些未想到的问题,还需要认真思考。

IDEA中使用Tomcat不能启动的问题

今天用IDEA做个Java的Web工程,想运行一下,在IDEA配置好Tomcat(8180端口),部署上去,运行,竟然报Address localhost:8180 is already in use,我检查了一下本地没有在8180上的服务,奇怪了,前一段用IDEA的时候没有这样的问题,我尝试将Tomcat换到其他的端口,依旧报错,还是Google一下吧,找到了http://www.notionzone.com/2008/11/19/intellij-idea-eclipse-tomcat-deploy-58.html,原来是NOD32的问题,我就是最近才换到NOD32的,将NOD32中“启用HTTP检查”关闭就好了。

用Google API来取得Google帐户的联系人列表

昨天写了一篇从Web Mail里取得用户通讯录的方法的文章,里面提到了Google的Account Authentication API,今天我们就用Account Authentication APIGoogle Contacts Data API来做一个取得Google帐户联系人的测试。

首先我们要看一下Account Authentication API,对于网络应用来说我们选择对网络应用程序的验证,对网络应用程序的验证也提供了 OAuthAuthSub 两种认证方式,我们选择AuthSub的认证方式,认证过程如下图

Google Authsub Diagram

用户在第三方Web应用上向Google Accounts Authentication发送AuthSub的HTTP请求,如果用户没有登录Google,则会显示登录页面,用户登录之后,会提示用户是否接受或拒绝这个第三方Web应用的访问请求,如果用户同意,Google就会生成一个token,转回第三方Web应用,第三方Web应用凭此token,可以请求Google的相关Sevice,比如联系人的服务,从而取得相关数据。

AuthSub有两种接口,一个是AuthSubRequest(A call to this method sends the user to a Google Accounts web page, where the user is given the opportunity to log in and grant Google account access to the web application. If successful, Google provides a single-use authentication token, which the web application can use to access the user’s Google service data.)另一个是AuthSubSessionToken(A call to this method allows the web application to exchange a single-use token for a session token),按照Google文档的理解,AuthSubRequest是一个单次的认证,AuthSubSessionToken应该是带会话(Session)的。

我们就举Google提供的例子

https://www.google.com/accounts/AuthSubRequest?
   next=http%3A%2F%2Fwww.yourwebapp.com%2Fshowcalendar.html
   &scope=http%3A%2F%2Fwww.google.com%2Fcalendar%2Ffeeds%2F
   &session=1
   &secure=1

https://www.google.com/accounts/AuthSubRequest就是AuthSub请求的地址,next表示认证之后要转回的地址,一般就是第三方Web应用的地址,也就是你网站的一个地址,Google会把token附带到这个地址后面,scope是你要请求的Google服务地址,这个例子里是要访问Google日历的数据,另外两个参数看Google的文档吧。

接下来,我们要取得Google帐户的联系人,我们先看看Google提供了多少可以访问的服务吧,访问Google数据API,Google提供的数据还着不少,有日历、文档、图书搜索、网路相册等等,当然也包括我们所需要的联系人的API,Google数据API要好好了解一下,总体来说Google提供一个Gdata的数据格式,和RSS的feed类似的格式,通过相关服务的访问地址,就可以返回Gdata数据,至于Gdata的读取,已经有了很多程序语言的封装好的程序(http://code.google.com/intl/zh-CN/apis/gdata/clientlibs.html),直接用就可以了,我们用PHP举例,PHP对Gdata的封装,是Zend Framework里的Gdata包,在http://framework.zend.com/download/gdata下载就可以了,但是现在Zend Gdata的包里没有直接的Google contacts的组件,但不要紧,通过Zend Gdata里基础数据的访问,可以取得Google contacts。

我们看看Google Contacts Data API的开发人员指南吧,取得联系人的Feed URL是

http://www.google.com/m8/feeds/contacts/userEmail/full 
或 
http://www.google.com/m8/feeds/contacts/default/full

那我们就用PHP来写一个取得联系人的程序吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
require_once 'Zend/Loader.php';
Zend_Loader::loadClass ( 'Zend_Gdata' );
Zend_Loader::loadClass ( 'Zend_Gdata_AuthSub' );
Zend_Loader::loadClass ( 'Zend_Gdata_ClientLogin' );
Zend_Loader::loadClass ( 'Zend_Gdata_Query' );
Zend_Loader::loadClass ( 'Zend_Gdata_Feed' );
 
$my_contacts = 'http://www.google.com/m8/feeds/contacts/default/full';
 
if (! isset ( $_SESSION ['cal_token'] )) {
	if (isset ( $_GET ['token'] )) {
		// You can convert the single-use token to a session token.
		$session_token = Zend_Gdata_AuthSub::getAuthSubSessionToken ( $_GET ['token'] );
		// Store the session token in our session.
		$_SESSION ['cal_token'] = $session_token;
	} else {
		// Display link to generate single-use token
		$googleUri = Zend_Gdata_AuthSub::getAuthSubTokenUri ( 'http://' . $_SERVER ['SERVER_NAME'] . $_SERVER ['REQUEST_URI'], $my_contacts, 0, 1 );
		echo "Click <a href='$googleUri'>here</a> " . "to authorize this application.";
		exit ();
	}
}
 
// Create an authenticated HTTP Client to talk to Google.
$client = Zend_Gdata_AuthSub::getHttpClient ( $_SESSION ['cal_token'] );
 
$gdata = new Zend_Gdata ( $client );
$query = new Zend_Gdata_Query ( $my_contacts );
//$query->setMaxResults(10);
$query->setMaxResults ( 2000 );
$feed = $gdata->getFeed ( $query );
 
foreach ( $feed as $entry ) {
 
	$parts = $entry->getExtensionElements ();
	foreach ( $parts as $p ) {
		$element = $p->getDOM ();
		switch ($element->tagName) {
			case 'email' :
				print ( "Email: " . $element->getAttribute ( 'address' ) . "<br/>" );
				break;
			case 'phoneNumber' :
				print ( "Phone: " . $element->nodeValue . "<br/>" );
				break;
			default :
				continue;
		}
	}
 
}

放在你服务器上运行一下吧(别忘了Zend Gdata包要加进去)。

从邮件服务商取得用户通讯录的方法

我们在使用facebook开心网这些SNS网站的时候,里面有一项功能,就是通过邮件邀请好友,它可以导出你在Hotmail、Gmail、Yahoo、网易、新浪等Web Mail里的通讯录,然后给这些好友发送邀请信,这种病毒式营销推广方式起到了非常好的效果,facebook、开心网这些网站得以迅速壮大,这项功能也成了SNS网站的标配,从技术角度,我们就来探讨一下实现这项功能的原理和方法。

先说Gmail、Hotmail,Google和MS还是非常强大的,Google提供了Account Authentication API,它允许第三方的网站通过它的认证服务后,通过Google的Service取得用户的数据,包括通讯录等,MS也有Windows Live ID服务,不过我对微软的东西研究的不深,MSDN上找到的Windows Live ID SDK的资料,http://msdn.microsoft.com/en-us/library/bb404787.aspx

接下来说说其他邮件系统的取得方式,主要的原理就是通过模拟用户行为,登陆Web Mail系统,取得用户通讯录的信息,很多Web Mail都提供通讯录CSV格式的下载服务,我们取得这个CSV文件就可以了,网易的邮件系统似乎已经不提供CSV文件的下载,那只能通过通讯录页面的HTML分析,来取得信息,所以相对复杂一些,而Sohu的通讯录虽然前端采用了Ajax,但后端取得通讯返回的格式是Json,应该是最方便。

接下来说模拟用户行为,其实就是通过在自己服务器端向Web Mail发送相对的HTTP请求,而模拟出用户登陆的行为,在Linux下,我们用cURL这个组件,在编译PHP的时候把cURL编译进去,使PHP支持cURL,cURL非常好用,也非常强大,在libcurl里有大多数开发语言的支持,如果使用JAVA的话,还可以使用Apache的HttpClient

我们要模拟出用户登陆Web Mail的过程,我们就要对通常情况下,登陆Web Mail的HTTP请求过程有所了解,HttpWatch是一个非常好的工具,在做为插件安装倒IE和FireFox上,在IE或是FireFox里就可以看到所有的HTTP请求的信息了。

httpwatch

我们看看网易163的邮件的登陆过程

00:00:00.000    跳转提示
+ 0.000        0.231    1543    2352    POST    200    text/html; charset=UTF-8    http://reg.163.com/login.jsp?type=1&url=http://entry.mail.163.com/coremail/fcg/ntesdoor2?lightweight%3D1%26verifycookie%3D1%26language%3D-1%26style%3D35
0.231    1543    2352    1 request
00:00:00.462        0.177    0    0    GET    (History Cache)    image/x-icon    http://reg.163.com/favicon.ico
00:00:00.469    网易电子邮箱 – 简约3.0Beta
+ 0.000        0.118    1703    604    GET    302    Redirect to http://g4a30.mail.163.com/jy3/main.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz    http://entry.mail.163.com/coremail/fcg/ntesdoor2?lightweight=1&verifycookie=1&language=-1&style=35&username=xxx
+ 0.245        0.366    1664    6788    GET    200    text/html;charset=GBK    http://g4a30.mail.163.com/jy3/main.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz
+ 0.422        0.158    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/jscss/globle.css
+ 0.659        0.087    0    0    GET    (Cache)    application/x-javascript    http://mimg.126.net/p/jy3style/js/0903090940/tools.js
+ 0.816        0.067    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/cmcss/skin_blue.css
+ 0.998        0.578    1586    5521    GET    200    text/html;charset=GBK    http://g4a30.mail.163.com/jy3/top.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz
+ 1.015        0.624    1589    24844    GET    200    text/html;charset=GBK    http://g4a30.mail.163.com/jy3/folder.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz
+ 1.025        0.316    1590    10490    GET    200    text/html;charset=GBK    http://g4a30.mail.163.com/jy3/welcome.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz
+ 1.034        3.174    1592    2043    GET    200    text/html;charset=GBK    http://g4a30.mail.163.com/jy3/justfresh.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz
+ 1.280        0.439    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/jscss/globle.css
+ 1.632        0.407    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/jscss/globle.css
+ 1.647        0.432    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/jscss/welcome2.css
+ 1.876        0.235    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/jscss/globle.css
+ 2.179        0.117    0    0    GET    (Cache)    application/x-javascript    http://mimg.126.net/p/jy3style/js/0903090940/tools.js
+ 2.263        0.167    0    0    GET    (Cache)    application/x-javascript    http://mimg.126.net/p/jy3style/js/0903090940/tools.js
+ 2.354        0.207    0    0    GET    (Cache)    application/x-javascript    http://mimg.126.net/p/jy3style/js/0903090940/tools.js
+ 2.488        0.209    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/cmcss/skin_blue.css
+ 2.614        0.152    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/cmcss/skin_blue.css
+ 2.619        0.192    0    0    GET    (Cache)    application/x-javascript    http://mimg.126.net/p/jy3style/js/0903090940/welcome.js
+ 2.740        0.325    0    0    GET    (Cache)    text/css    http://mimg.126.net/p/jy3style/lib/0903090940/cmcss/skin_blue.css
4.208    9724    50290    20 requests
00:00:00.878        0.124    0    0    GET    (History Cache)    image/x-icon    http://www.163.com/favicon.ico
00:00:03.572    网易电子邮箱 – 简约3.0Beta
+ 0.000        0.541    0    0    GET    (Cache)    text/html; charset=GB2312    http://mimg.163.com/tianqi/city_simple/58367_090311.html
+ 0.531        0.715    1424    250    GET    304    text/css    http://mimg.163.com/jy3style/lib/jscss/ifr_weather2.css
+ 1.614        0.245    0    0    GET    (Cache)    image/gif    http://mimg.163.com/jy3style/lib/images/weather2.gif
1.859    1424    250    3 requests

首先,将用户名和密码POST到http://reg.163.com/login.jsp?type=1&url=http://entry.mail.163.com/coremail/fcg/ntesdoor2?lightweight%3D1%26verifycookie%3D1%26language%3D-1%26style%3D35,reg.163.com是网易通行证,后面的url是用户登陆网易通行证后要转向的地址,在这里我们看到就是http://entry.mail.163.com/coremail/fcg/ntesdoor2,这就是网易邮件的入口,在我们模拟登陆的时候,可以不传这个url过去,至于用户名和密码表单的name,看mail.163.com的HTML代码就知道了。相关错误处理,在POST到http://reg.163.com/login.jsp这个地址之后,返回的信息要取得的,因为如果用户输错了用户名或是密码,还是应该要提示用户的,而且在错误的情况下,也不用往下进行了,reg.163.com返回的错误页面里有“您的密码不正确”或是“用户名不正确”这样的信息,以此就可以判断用户是否成功登陆了。

之后我们要模拟用户访问http://entry.mail.163.com/coremail/fcg/ntesdoor2?lightweight=1&verifycookie=1&language=-1&style=35&username=xxx,就是登陆邮件的过程,注意,这里是个GET方法,用POST方法是不行的,访问entry.mail.163.com这个URL之后,会返回一个302码,就是要Redirect转向,到http://g4a30.mail.163.com/jy3/main.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz这个地址,这也就是大家看到的邮件登陆进去的首页了,注意这个sid要抓出来,应该是登陆的一个token,还有http://g4a30.mail.163.com这个地址,网易邮件是个集群,有非常多的服务器,你们每次上去的服务器是随随机的,用你的sid访问其他服务器也是没问题的,比如http://g1a98.mail.163.com/jy3/main.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz,所以这个服务器地址你可以写死。

解析来我们要看看网易邮件通讯录的URL,在UI看到是http://g4a30.mail.163.com/jy3/address/addrlist.jsp?sid=gAJQIqzzbBwvsaVoDhzztmnXQvBRiMuz&gid=all,我们模拟用户访问这个URL,然后抓取这个页面的内容HTML,分析一下,就可以了,在PHP里有个很好的HTML Parser工具PHP Simple HTML DOM Parser,可以像jQuery一样取得页面元素,这样就很方便的取得页面信息了。

其他一些Web Mail的登陆过程分析也是类似的,如果是取得CSV文件就更简单了,通过取到的通讯录内容,就可以在SNS网站上邀请了。

在这里我向国内这些大的门户网站或是Mail服务商提个建议,现在的互联网是一个开放化、平台化的趋势,网站应该开放自己的资源,可以让第三方的的应用更方便的访问,固步自封是不能发展的,试想如果QQ开放他的好友平台,引入第三方应用,那它将轻松超过Facebook,成为世界第一大SNS社区。