Mac OS下签发多域名证书

前言

本文介绍如何在Mac OS下方便的利用钥匙串程序制作自签名根证书,以及用它签发支持多个域名与泛域名的HTTPS证书。

制作自签名根证书

/Applications/Utilities里打开“钥匙串访问”这个程序,在菜单中选“钥匙串访问” -> “证书助理” -> “创建证书”。过程如图:

名称可以按自己的喜爱去写。


有效期添长一些,比如3650


这里的信息可写可不写。



勾选“证书签名”与“CRL签名”用来颁发与吊销证书。



这里一定要选“将此证书用作证书颁发机构”。


点击继续直到完成。


创建完成后,双击自签名的证书,将证书设置为始终信任。如图:

导出自签名根证书

之后将自签名的根证书导出,命名为ca.p12,存到随意位置。这里一定要选证书和密钥同时导出来,导出的时候可以不设置密码,但之后要及时删除掉ca.p12,以防信息泄露。

创建多域名配置文件

接下来创建配置文件san.cnf,按照要申请的域名去编辑alt_names配置节,其它配置节不用动。例如要申请 *.example.com example.com localhost的多域名证书,按如下配置。编辑完后与之前导出的ca.p12存到一个目录下。

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name
req_extensions     = req_ext
[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
stateOrProvinceName         = State or Province Name (full name)
localityName               = Locality Name (eg, city)
organizationName           = Organization Name (eg, company)
commonName                 = Common Name (e.g. server FQDN or YOUR name)
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
DNS.1   = *.example.com
DNS.2   = example.com
DNS.3   = localhost

签发多域名证书

然后进到ca.p12san.cnf存放的目录执行下列命令:

openssl pkcs12 -clcerts -nokeys -out ca-cert.pem -in ca.p12  #导出公钥
openssl pkcs12 -nocerts -nodes -out ca-key.pem -in ca.p12 #导出私钥

执行上面两行命令的时候,会提示输入导出ca.p12文件时设下的密码,如果没有设置,直接回车就行。然后继续执行下面的命令签发证书。

openssl req -out user-cert.csr -newkey rsa:2048 -nodes -keyout private.key -config san.cnf #创建多域名证书请求
openssl x509 -req -in user-cert.csr -CAcreateserial -CA ca-cert.pem -CAkey ca-key.pem -out cert.pem -days 500 -sha256 -extfile san.cnf -extensions req_ext #签发证书

执行的过程中提示输入信息,Common Name写成你要申请的主要域名,如:example.comwww.example.com,也可以写成localhost,其余的不知道怎么写的话可以回车跳过。

最后得到的cert.pem为证书,private.key为证书私钥。其它文件都要删除掉。最后将证书和私钥配置到server中,以nginx的为例:

listen  80;
listen  443 ssl;
server_name  example.com;
ssl_certificate      pki/cert.pem;
ssl_certificate_key  pki/private.key;

配置完server后,记得重启服务。用浏览器访问,可以看到最后的效果:

HTTPS化的第一步 —— 申请Let’s Encrypt证书

背景介绍

目前中国的网络环境十分恶劣,运营商劫持的行为比比皆是,最好的对应方式就是把你的站点加密,即启用HTTPS协议。而且下一代HTTP协议(http 2.0)也是建立在HTTPS基础之上的。所以启用HTTPS是一举多得的行为。关于HTTPS协议本身网上有很多介绍,这里不详细说明了,不了解的可以去google一下。

要启用HTTPS,首先要有一个服务器证书,本文会详细介绍一下如何在Let’s Encrypt上申请到免费证书。

相信有很多朋友已经申请过StartSSL提供的免费证书,但最近有人发现StartSSL涉及到了中国的某公司,这意味着你的网站有可能被冒充。

root@kali:~/# host www.startssl.com
www.startssl.com has address 97.74.232.97     # Godaddy
www.startssl.com has address 52.7.55.170      # Amazon Web Services
www.startssl.com has address 52.21.57.183     # Amazon Web Services
www.startssl.com has address 52.0.114.134     # Amazon Web Services
www.startssl.com has address 50.62.56.98      # Godaddy
www.startssl.com has address 104.192.110.222  # QiHU 360 Inc.
www.startssl.com has address 50.62.133.237    # Godaddy

下面是摘自微博的评论:

@ruanyf 如果你正在使用StartSSL提供的免费证书,请小心了。它的私钥服务器现在放在360的机房,意味着理论上360可以冒充你的网站。网页链接

这个消息一放出之后,也有用户禁用了系统的StartSSL根证书,这部分用户在访问使用StartSSL证书的网站时会提示“站点不可信”。所以之前用StartSSL证书的用户换新证书也是十分有必要的。

Let’s Encrypt介绍

Let's Encrypt是国外一个公共的免费SSL项目,由 Linux 基金会托管,它的来头不小,由Mozilla、思科、Akamai、IdenTrust和EFF等组织发起,目的就是向网站自动签发和管理免费证书,以便加速互联网由HTTP过渡到HTTPS,目前Facebook等大公司开始加入赞助行列。

Let's Encrypt已经得了 IdenTrust 的交叉签名,这意味着其证书现在已经可以被Mozilla、Google、Microsoft和Apple等主流的浏览器所信任,你只需要在Web 服务器证书链中配置交叉签名,浏览器客户端会自动处理好其它的一切,Let's Encrypt安装简单,未来大规模采用可能性非常大。

Let's Encrypt虽然还在测试当中,但是市场需求非常大,已经有非常多的朋友迫不及待地安装并用上了Let's Encrypt。Let's Encrypt向广大的网站提供免费SSL证书,不管是对于网站站长、互联网用户,还是对整个Web互联网,都是非常有利的,它有利于整个互联网的安全。

如何申请Let’s Encrypt的证书

Let’s Encrypt提供了很便捷的工具去申请证书。申请者首先要去获取工具。可以在本地机器上执行,也可以在网站服务器上执行。

git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
./letsencrypt-auto --help

执行上面的命令会自动下载缺失的依赖,没有问题的话会打印出来使用帮助。

帮助里提示了有几种获得证书的方式:

Choice of server plugins for obtaining and installing cert:

  --apache          Use the Apache plugin for authentication & installation
  --standalone      Run a standalone webserver for authentication
  (nginx support is experimental, buggy, and not installed by default)
  --webroot         Place files in a server's webroot folder for authentication

这里要介绍一下这个申请工具的运作原理,执行它的时候会产生类似1k8HnVu7aKIMcTm4XYzjYlmgLtMntkuhLCo8c8B3pyo.x24B1t7ILOHGEKSRUY3Wsg9OCNL2E7NJOff_xfZaa-s的字符串,然后接着它会请求http://www.zhoumingzhi.com/.well-known/acme-challenge/1k8HnVu7aKIMcTm4XYzjYlmgLtMntkuhLCo8c8B3pyo这个地址,再看返回的数据是不是上面那一堆字符串,如果是的话就说明申请者对这个域名有所有权,接下来就会发放证书文件。上面提到的那几种方式就是方便用户验证域名所有权的。为了方便理解,我们用通用性最好的方式——手动操作。

./letsencrypt-auto certonly --manual -d www.zhoumingzhi.com --email mingzhi22@gmail.com

执行上面的命令后会有几处让你确认的窗口,按OK就好。然后会出现类似这样的提示:

Make sure your web server displays the following content at
http://www.zhoumingzhi.com/.well-known/acme-challenge/ArUM149fkLfqBTg5cHw37_WRVGKpARnb1_fgpgNGhrw before continuing:

ArUM149fkLfqBTg5cHw37_WRVGKpARnb1_fgpgNGhrw.x24B1t7ILOHGEKSRUY3Wsg9OCNL2E7NJOff_xfZaa-s

现在需要配置你域名所对应的服务器,确保http://www.zhoumingzhi.com/.well-known/acme-challenge/ArUM149fkLfqBTg5cHw37_WRVGKpARnb1_fgpgNGhrw能返回ArUM149fkLfqBTg5cHw37_WRVGKpARnb1_fgpgNGhrw.x24B1t7ILOHGEKSRUY3Wsg9OCNL2E7NJOff_xfZaa-s,如果都准备好了的话按任意键继续,稍等几秒钟,证书就生成好了。

接下来按https://letsencrypt.readthedocs.org/en/latest/using.html#where-are-my-certificates说的去配置你的服务器。

SSH反向代理使用心得

有一天想在家里用ssh控制公司的电脑,但是公司的机器处在内网中没有办法直接连上。而我家里的路由器装了Linux系统,而且通过DDNS有独立的域名,这样就可以用ssh反向代理,用公司的电脑ssh反向连接到家里的路由器,然后用家里的电脑ssh到路由器,从而连上公司的电脑。这时家里的路由器相当于是做了个ssh中继。
数据流是:家里电脑->路由器->公司电脑

ssh反向代理简单的来讲,就是用公司电脑在路由器上开了个端口,发送到这个端口的数据都会被转发到公司机器上指定的端口。下面是具体的命令,假设路由器的域名是router.zhoumz.com

ssh -gNfR *:2222:localhost:22 root@router.zhoumz.com

上面的这个命令是在公司电脑上执行的,它表示的含义是:

  • 用root用户登录到router.zhoumz.com
  • 告诉router.zhoumz.com去监听2222端口上来自所有IP的数据
  • 将数据发送到执行此命令机器(公司电脑)上的22端口

参数的含义分别是:

-g    Allows remote hosts to connect to local forwarded ports.

-N    Do not execute a remote command.  This is useful for just 
      forwarding ports (protocol version 2 only).

-f    Requests ssh to go to background just before command execution.

-R    [bind_address:]port:host:hostport
      Specifies that the given port on the remote (server) host is to
      be forwarded to the given host and port on the local side.  This
      works by allocating a socket to listen to port on the remote
      side, and whenever a connection is made to this port, the
      connection is forwarded over the secure channel, and a connection
      is made to host port hostport from the local machine.

      Port forwardings can also be specified in the configuration file.
      Privileged ports can be forwarded only when logging in as root on
      the remote machine.  IPv6 addresses can be specified by enclosing
      the address in square brackets.

      By default, the listening socket on the server will be bound to
      the loopback interface only.  This may be overridden by
      specifying a bind_address.  An empty bind_address, or the address
      `*', indicates that the remote socket should listen on all
      interfaces. Specifying a remote bind_address will only succeed
      if the server's GatewayPorts option is enabled (sshd_config(5)).

      If the port argument is `0', the listen port will be dynamically
      allocated on the server and reported to the client at run time.
      When used together with -O forward the allocated port will be
      printed to the standard output.

值得注意的是:想监听来自所有IP的数据,要在远程机器(本例中是路由器)上把GatewayPorts配置项打开 (see sshd_config(5))。

做好上面的步骤之后,回家用电脑ssh路由器的2222端口就可以直接控制公司电脑了。

ssh -p 2222 michael@router.zhoumz.com

这里注意:michael为公司电脑的登录用户名。

MacOS的launchd使用

MacOS的launchd可以完成开机启动任务、文件监控、守护进程的有用的工作。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Disabled</key>
	<false/>
	<key>Label</key>
	<string>com.zhoumingzhi.watcher</string>
	<key>ProgramArguments</key>
	<array>
		<string>/command/to/be/executed</string>
	</array>
	<key>WatchPaths</key>
	<array>
		<string>/path/to/be/watched</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>WorkingDirectory</key>
	<string>/Users/Michael</string>
</dict>
</plist>

将上面的文件保存成.plist后缀名的文件,然后放在~/Library/LaunchAgents目录中,每次登录后可以监控文件或文件夹的改变,当有变化时执行ProgramArguments指定的命令。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Disabled</key>
	<false/>
	<key>Label</key>
	<string>com.zhoumz.ssh-tunnel</string>
	<key>ProgramArguments</key>
	<array>
		<string>ssh</string>
		<string>-qTnN</string>
		<string>-D</string>
		<string>7070</string>
		<string>you@yourdomain.com</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>KeepAlive</key>
	<dict>
		<key>NetworkState</key>
		<true/>
	</dict>
</dict>
</plist>

将上面的文件保存成.plist后缀名的文件,然后放在~/Library/LaunchAgents目录中,每次登录后可以自动开启SSH反向代理,而且会在网络畅通时自动重连。

用launchctl load 和 launchctl unload命令可以很方便的加载和卸载.plist文件,而不用重启系统。

关于上面文件的含义,请参考:https://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man5/launchd.plist.5.html

关于LaunchAgents目录的作用,请参考:https://developer.apple.com/library/mac/#documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html#//apple_ref/doc/uid/10000172i-SW7-BCIEDDBJ

修复在IE6下img标签引用半透明png图片的bug

<!--[if lte ie 6]>
<script type="text/javascript">
    function correctPNG() {
        var images = document.getElementsByTagName('IMG');
        for (var i = 0, len = images.length; i < len; i++) {
            var img = images[i];
            if (img.className && img.className.indexOf('alpha-opacity') !== -1) {
                var imgID = (img.id) ? "id='" + img.id + "' " : "";
                var imgClass = (img.className) ? "class='" + img.className + "' " : "";
                var imgTitle = (img.title) ? "title='" + img.title + "' " : "title='" + img.alt + "' ";
                var imgStyle = "display:inline-block;" + img.style.cssText;
                if (img.parentElement.href) {
                    imgStyle = "cursor:hand; " + imgStyle;
                }
                var strNewHTML = "<span " + imgID + imgClass + imgTitle
                        + " style=\"" + "width:" + img.width + "px; height:" + img.height + "px;" + imgStyle
                        + "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader"
                        + "(src=\'" + img.src + "\', sizingMethod='scale');\"></span>";
                img.outerHTML = strNewHTML;
                --i;
                --len;
            }
        }
    }
    window.attachEvent("onload", correctPNG);
</script>
<![endif]-->

关于IE下iframe里无法读取cookie的问题

在IE下的问题:

  1. http://webmail.mail.yeah.net/js5/main.jsp 页面用iframe嵌入 http://news.163.com/special/163mail_2012/
  2. http://news.163.com/special/163mail_2012/ 中引用 http://api.pr.163.com/mail/user 在.163.com域下发放cookie
  3. http://news.163.com/special/163mail_2012/ 用JavaScript获取不到步骤2中发放的cookie

由于IE特殊的安全策略,在特定的情况下拒绝iframe引用的页面对cookie进行读写。
(参考:http://msdn.microsoft.com/en-us/library/ms537343%28v=vs.85%29.aspx#privacy_preference_settings)

解决方法:在上述例子中的http://api.pr.163.com/mail/user 添加 P3P: “CP=CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR” 响应头。

结论:
如果再遇到无法访问iframe里发放cookie的问题,可以尝试在 cookie的发放地址添加P3P头调试解决。

让IE6支持min-width

最新更新,在IE7中_width: expression中的表达示还会被计算,所以要考虑好脚本的兼容性。

最近和同事在搞一个自适应布局的页面改版,样式里用到了一些min-width,这个CSS属性在IE6里不被支持。要解决这个问题,网上大多数都是用IE的expression去解决的(不了解expression的同学看下http://msdn.microsoft.com/en-us/library/ms537634%28v=vs.85%29.aspx)。expression有个特点就是,当元素reflow/repaint的时候,它都会重复运算一次。虽说可以解决min-width的兼容问题,但是性能损耗太大了,而且页面上用的min-width越多,这个性能损耗会随之变大。下面是用expression实现min-width的代码

<style type="text/css">
#target {
	min-width: 800px;
	_width: expression(this.parentNode.clientWidth > 800 ? 'auto' : '800px');
	height: 100px;
	background: red;
}
</style>

为了解决用expression的性能损耗问题,我又考虑到用JavaScript去实现。思路很简单,就是把在expression里面的逻辑搬到window.onresize这个事件处理函数中,以达到重新布局的目的。但是问题又来了,想重新布局页面元素,就要拿到这个元素的句柄,如果是带有id的元素事情到不是很复杂,如果是在类选择器里用到了min-width,事情就不是那么容易了。虽然有jQuery之类的类库帮我们选择元素,但这也是一笔不小的性能开销,而且代码变得难于维护,要是日后对min-width有所更改的话,还要同时去改JS。

总结一下,碰到的问题有以下两点:

  1. 在CSS中用expression实现min-width会造成大量不必要的计算
  2. 在JS中用window.onresize重新设定元素宽度会涉及到:1),如何取元素句柄的问题;2),JS代码不够简洁难于维护,修改样式的同时还要改JS;

结合上面的分析,最好的情况就是在CSS定义里面为window绑定onresize事件处理函数,然后在函数里面对当前DOM元素实现重新布局。听起来不可思议的事情,在IE6中变得很容易,因为expression的本质就是一段js代码,在里面可以调用页面中声明的其它JS函数,可以写成_width: expression(someFunc(min-width-value));的形式。此处的someFunc是通过JS声明的函数,在里面实现对window.onresize的绑定,每当窗口改变的时候,将元素的宽度重新设置成min-width-value。在CSS定义中可以这么写 #target {min-width: 800px; _width: expression(someFunc(‘800px’)); } 样式的定义都放在一处,维护起来也容易,这样就解决了上述第二点的问题。

到目前为止,当元素需要reflow/repaint的时候,expression还是不断的去调用someFunc,这也是第一点问题。其实解决的办法很容易,就是在someFunc的最后一行调用 this.style.width=’auto’;这样在以后的重绘中width的值恒为auto,就不会重新计算expression了。至此所以问题都已解决。下面就是真实的代码了。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html;charset=GBK" />
	<title>测试IE6的min-width</title>
	<style type="text/css">
	#target {
		min-width: 800px;
		_width: expression(minWidth(this, 800)); 
		height: 100px;
		background: red;
	}
	</style>
</head>
<body>
	<div id="target"></div>
	<script type="text/javascript">
	(function() {
		var isIE6=!-[1,]&&!window.XMLHttpRequest,
			elements = [];
		
		function reflow(elem, value) {
			elem.style.width = elem.parentNode.clientWidth > value ? 'auto': value + 'px';
		}
		
		window.minWidth = function(elem, value) {
			if(isIE6) {
				reflow(elem, value);
				elements.push({
					'elem': elem,
					'value': value
				});
			} else {
				elem.style.width = 'auto';
			}
		};
		
		if(isIE6) {
			var timer;
			window.attachEvent('onresize', function() {
				var handler = arguments.callee;
				
				clearTimeout(timer);
				timer = setTimeout(function() {
					// 注销掉事件,防止reflow里触发onresize而导致的死循环
					window.detachEvent('onresize', handler);
					for(var i = 0, len = elements.length; i < len; i++) {
						var element = elements[i];
						reflow(element.elem, element.value);
					}
					window.attachEvent('onresize', handler);
				}, 50);
			});
		}
	}());
	</script>
</body>
</html>

有一点值得注意,就是如何在expression中拿到元素的句柄。

用nginx测试AJAX接口

自从有了evernote后,好久没有更新这里了。打算日后把evernote记录的知识点陆续的搬上来。开始正题。

最近在做一个关于新闻推荐的内部项目,和之前的一些项目一样,也是富前端的应用,基本上都是后台提供数据接口,由前端这边完成页面的渲染。这次后台的接口多为json格式的。对比jsonp格式的数据,json格式只能用XHR的方式请求获取数据,在开发的时候就造成了跨域的问题。比如在开发时,页面的测试地址是htttp://localhost/page.html,而后台提供的接口地址是http://iread.ws.netease.com/article/reply,localhost和iread.ws.netease.com是两个不同的域,虽然高级的浏览器提供了XHR 2去解决跨域问题,但是老版本的IE不支持这个API,如果用XHR 2去实现的话,IE在开发时是没有办法测试到的,而且项目上线前还要将XHR 2改为XHR,这样就造成了不必要的工作量。最理想的解决办法就是在localhost做后台代理,将http://localhost/article/reply代理到http://iread.ws.netease.com/article/reply,这样我在请求数据接口的时候,只需要写相对路径就好了。比如在这个例子中,我直接请求/article/reply,在测试环境中就是请求http://localhost/article/reply,正式环境中变成请求http://iread.ws.netease.com/article/reply

做后台代理,最方便的就是用nginx了,只需要几行代码就能满足需求,配置的代码如下:

server {
	listen	*:80;

	location / {
		ssi	on;
		autoindex on;
		root  "/Users/Michael/制作/个性化新闻推荐/source";
		index  index.html index.htm;
	}

	location /article/reply {
		proxy_pass "http://iread.ws.netease.com/article/reply";
	}

}

这样我们请求http://localhost/article/reply的时候通过nginx的帮助就能从后台取到正确的数据了。但是问题又来了,http://iread.ws.netease.com/article/reply是一个需要cookie的接口,这就意味着nginx代理的时候需要将浏览器的cookie一同发送过去。浏览器向服务器发送cookie的时候,其实就是发送一个名为Cookie的request header。我的做法是打开浏览器,切换到firebug的“网络”选项卡,然后打开http://iread.ws.netease.com/article/reply,在“请求头信息”中将Cookie的值复制下来,然后加到nginx的配置节中,如下:

location /article/reply {
	proxy_pass "http://iread.ws.netease.com/article/reply";
	proxy_set_header Cookie 你刚才复制的内容;
}

如何制作拖放后的惯性效果(一)

现在很多交互都需要这样一个效果,在拖拽结束时,被拖拽的元素会因为惯性向前移动一段距离。接下来说一下如何实现这个效果。

这个运动的原理其实很简单,就是一个匀减速运动。匀减速运动也就是匀加速运动的一种,只是加速度的方向和速度方向符号相反。我们在中学的时候学过匀加速运动的公式为:S = vt + (1/2)at^2;用文字表达就是在t时刻,物体移动的距离S为:初始速度v * t + (1/2) * 加速度a * t的平方。在这个公式中,我们可以将加速度a和初始速度v视为常量,将t当做变量,就可以知道在t时刻物体移动的距离了。那么问题又转到了如何对a和v求解。加速度a在这个问题中其实只是一个系数而已,我们可以设定a为1秒钟减少10000像素/秒的运动速度。(a = 10000)。那么到目前为止只有初始速度v是未知的了。

在这个动画中初始速度为我拖拽时松开鼠标那一刻的瞬时速度。我们知道计算平均速度的公式为v = s / t,如果这个t你取得足够的小,那么在t时间段移动的距离s除以t就是物体在某一点上的瞬时速度。接下来的问题就是如何确定这个t,以及如何确定在t时间段中物体移动的距离s。

我们知道在移动鼠标的时候会触发mousemove事件,而在持续移动鼠标的过程中,前后相邻的两个mousemove事件的时间间隔非常小,也就符合我们上面说的t时间段,在主流浏览器中event对象会有个timeStamp属性来标识这个事件是什么时刻发生的(在IE中因为要用window.event来取得事件,window.event没有timeStamp属性,所以要用new Date来计算事件发生的时刻),那么对t的求解就变得显而易见了:var t = e1.timeStamp – e0.timeStamp; 同时在t时间段移动的距离在X轴方向上为var s = e1.clientX – e1.clientX;

有了上面的思路,在鼠标松开的瞬时速度也就变得迎刃而解了。鼠标松开的瞬时速度v即为(e1.timeStamp – e0.timeStamp) / (e1.clientX – e1.clientX); e1为最后一次mousemove事件,e0为倒数第二次mousemove事件。如何保存e1和e0可以用以下代码:

var tracker = [];
document.body.addEventListener('mousemove', function(e) {
	tracker.push(e);
	if(tracker.length > 2) {
		tracker.shift();
	}
});

待续……

本地存储的兼容解决方案

IE浏览器用userData,主流浏览器用LocalStorage来解决本地存储的需求。userData存储的数据对于同一目录下的地址是可见的,如http://www.zhoumingzhi.com/1/foo.html可以访问到http://www.zhoumingzhi.com/1/bar.html存的数据。而LocalStorage存储的数据对相同域名下的所有页面都是可见的。

var localStorageAdapter = {
    
     storeName: 'NTESBBS',

     isLocalStorage: window.localStorage? true: false,

     dataDOM: this.isLocalStorage? null: (function() {
          try{
               var dataDOM = document.createElement('input'),
                    expires = new Date();

               dataDOM.type = 'hidden';
               dataDOM.style.display = 'none';
               dataDOM.addBehavior('#default#userData');
               document.body.appendChild(dataDOM);
              
               expires.setDate(expires.getDate() + 30);
               dataDOM.expires = expires.toUTCString();

               return dataDOM;
          } catch(ex) {
               return null;
          }
     })(),

     set: function(key, value) {
          var dataDOM = this.dataDOM;

          if(this.isLocalStorage) {
               window.localStorage.setItem(key, value);
          } else {
               if(dataDOM) {
                    dataDOM.load(this.storeName);
                    dataDOM.setAttribute(key, value);
                    dataDOM.save(this.storeName);
               }
          }
     },

     get: function(key) {
          var dataDOM = this.dataDOM;

          if(this.isLocalStorage) {
               return window.localStorage.getItem(key);
          } else {
               if(dataDOM) {
                    dataDOM.load(this.storeName);
                    return dataDOM.getAttribute(key);
               }
          }
     },

     remove: function(key) {
          var dataDOM = this.dataDOM;

          if(this.isLocalStorage) {
               window.localStorage.removeItem(key);
          } else {
               if(dataDOM) {
                    dataDOM.load(this.storeName);
                    dataDOM.removeAttribute(key);
                    dataDOM.save(this.storeName);
               }
          }
     }
}