2013年7月16日星期二

阮一峰的网络日志

阮一峰的网络日志


如何让搜索引擎抓取AJAX内容?

Posted: 15 Jul 2013 08:32 PM PDT

越来越多的网站,开始采用"单页面结构"(Single-page application)。

整个网站只有一张网页,采用Ajax技术,根据用户的输入,加载不同的内容。

这种做法的好处是用户体验好、节省流量,缺点是AJAX内容无法被搜索引擎抓取。举例来说,你有一个网站。

  http://example.com

用户通过井号结构的URL,看到不同的内容。

  http://example.com#1

  http://example.com#2

  http://example.com#3

但是,搜索引擎只抓取example.com,不会理会井号,因此也就无法索引内容。

为了解决这个问题,Google提出了"井号+感叹号"的结构。

  http://example.com#!1

当Google发现上面这样的URL,就自动抓取另一个网址:

  http://example.com/?_escaped_fragment_=1

只要你把AJAX内容放在这个网址,Google就会收录。但是问题是,"井号+感叹号"非常难看且烦琐。Twitter曾经采用这种结构,它把

  http://twitter.com/ruanyf

改成

  http://twitter.com/#!/ruanyf

结果用户抱怨连连,只用了半年就废除了。

那么,有没有什么方法,可以在保持比较直观的URL的同时,还让搜索引擎能够抓取AJAX内容?

我一直以为没有办法做到,直到前两天看到了Discourse创始人之一的Robin Ward的解决方法,不禁拍案叫绝。

Discourse是一个论坛程序,严重依赖Ajax,但是又必须让Google收录内容。它的解决方法就是放弃井号结构,采用 History API

所谓 History API,指的是不刷新页面的情况下,改变浏览器地址栏显示的URL(准确说,是改变网页的当前状态)。这里有一个例子,你点击上方的按钮,开始播放音乐。然后,再点击下面的链接,看看发生了什么事?

地址栏的URL变了,但是音乐播放没有中断!

History API 的详细介绍,超出这篇文章的范围。这里只简单说,它的作用就是在浏览器的History对象中,添加一条记录。

  window.history.pushState(state object, title, url);

上面这行命令,可以让地址栏出现新的URL。History对象的pushState方法接受三个参数,新的URL就是第三个参数,前两个参数都可以是null。

  window.history.pushState(null, null, newURL);

目前,各大浏览器都支持这个方法:Chrome(26.0+),Firefox(20.0+),IE(10.0+),Safari(5.1+),Opera(12.1+)。

下面就是Robin Ward的方法。

首先,用History API替代井号结构,让每个井号都变成正常路径的URL,这样搜索引擎就会抓取每一个网页。

  example.com/1

  example.com/2

  example.com/3

然后,定义一个JavaScript函数,处理Ajax部分,根据网址抓取内容(假定使用jQuery)。

  function anchorClick(link) {
    var linkSplit = link.split('/').pop();
    $.get('api/' + linkSplit, function(data) {
      $('#content').html(data);
    });
  }

再定义鼠标的click事件。

  $('#container').on('click', 'a', function(e) {
    window.history.pushState(null, null, $(this).attr('href'));
    anchorClick($(this).attr('href'));
    e.preventDefault();
  });

还要考虑到用户点击浏览器的"前进 / 后退"按钮。这时会触发History对象的popstate事件。

  window.addEventListener('popstate', function(e) {
    anchorClick(location.pathname);
  });

定义完上面三段代码,就能在不刷新页面的情况下,显示正常路径URL和AJAX内容。

最后,设置服务器端。

因为不使用井号结构,每个URL都是一个不同的请求。所以,要求服务器端对所有这些请求,都返回如下结构的网页,防止出现404错误。

  <html>
    <body>
      <section id='container'></section>
      <noscript>
        ... ...
      </noscript>
    </body>
  </html>

仔细看上面这段代码,你会发现有一个noscript标签,这就是奥妙所在。

我们把所有要让搜索引擎收录的内容,都放在noscript标签之中。这样的话,用户依然可以执行AJAX操作,不用刷新页面,但是搜索引擎会收录每个网页的主要内容!

===================================

[通知]

接下来三周,我外出旅行,暂停更新网志。

大家可以从 http://www.ruanyifeng.com/tweets/,了解我的行程。欢迎关注。

(完)

文档信息

2013年7月12日星期五

阮一峰的网络日志

阮一峰的网络日志


GPG入门教程

Posted: 12 Jul 2013 03:44 AM PDT

前两篇文章,我介绍了RSA算法

今天,就接着来看,现实中怎么使用这个算法,对信息加密和解密。这要用到GnuPG软件(简称GPG),它是目前最流行、最好用的加密工具之一。

一、什么是GPG


要了解什么是GPG,就要先了解PGP

1991年,程序员Phil Zimmermann为了避开政府监视,开发了加密软件PGP。这个软件非常好用,迅速流传开来,成了许多程序员的必备工具。但是,它是商业软件,不能自由使用。所以,自由软件基金会决定,开发一个PGP的替代品,取名为GnuPG。这就是GPG的由来。

GPG有许多用途,本文主要介绍文件加密。至于邮件的加密,不同的邮件客户端有不同的设置,请参考Ubuntu网站的介绍

本文的使用环境为Linux命令行。如果掌握了命令行,WindowsMac OS 客户端,就非常容易掌握。GPG并不难学,学会了它,从此就能轻松传递加密信息。建议读者一步步跟着教程做,对每条命令都自行测试。

二、安装

GPG有两种安装方式。可以下载源码,自己编译安装。

  ./configure
  make
  make install

也可以安装编译好的二进制包。

  # Debian / Ubuntu 环境
  sudo apt-get install gnupg

  # Fedora 环境
  yum install gnupg

安装完成后,键入下面的命令:

  gpg --help

如果屏幕显示GPG的帮助,就表示安装成功。

三、生成密钥

安装成功后,使用gen-ken参数生成自己的密钥。

  gpg --gen-key

回车以后,会跳出一大段文字:

  gpg (GnuPG) 1.4.12; Copyright (C) 2012 Free Software Foundation, Inc.
  This is free software: you are free to change and redistribute it.
  There is NO WARRANTY, to the extent permitted by law.

  请选择您要使用的密钥种类:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (仅用于签名) 
   (4) RSA (仅用于签名)
  您的选择?

第一段是版权声明,然后让用户自己选择加密算法。默认选择第一个选项,表示加密和签名都使用RSA算法。

然后,系统就会问你密钥的长度。

  RSA 密钥长度应在 1024 位与 4096 位之间。
  您想要用多大的密钥尺寸?(2048)

密钥越长越安全,默认是2048位。

接着,设定密钥的有效期。

  请设定这把密钥的有效期限。
   0 = 密钥永不过期
   <n> = 密钥在 n 天后过期
   <n>w = 密钥在 n 周后过期
   <n>m = 密钥在 n 月后过期
   <n>y = 密钥在 n 年后过期
  密钥的有效期限是?(0)

如果密钥只是个人使用,并且你很确定可以有效保管私钥,建议选择第一个选项,即永不过期。回答完上面三个问题以后,系统让你确认。

  以上正确吗?(y/n)

输入y,系统就要求你提供个人信息。

  您需要一个用户标识来辨识您的密钥;本软件会用真实姓名、注释和电子邮件地址组合成用户标识,如下所示:
  "Heinrich Heine (Der Dichter) <heinrichh@duesseldorf.de>"

  真实姓名:
  电子邮件地址:
  注释:

"真实姓名"填入你姓名的英文写法,"电子邮件地址"填入你的邮件地址,"注释"这一栏可以空着。

然后,你的"用户ID"生成了。

  您选定了这个用户标识:
   "Ruan YiFeng <yifeng.ruan@gmail.com>"

我的"真实姓名"是Ruan YiFeng,"电子邮件地址"是yifeng.ruan@gmail.com,所以我的"用户ID"就是"Ruan YiFeng <yifeng.ruan@gmail.com>"。系统会让你最后确认一次。

  更改姓名(N)、注释(C)、电子邮件地址(E)或确定(O)/退出(Q)?

输入O表示"确定"。

接着,系统会让你设定一个私钥的密码。这是为了防止误操作,或者系统被侵入时有人擅自动用私钥。

  您需要一个密码来保护您的私钥:

然后,系统就开始生成密钥了,这时会要求你做一些随机的举动,以生成一个随机数。

  我们需要生成大量的随机字节。这个时候您可以多做些琐事(像是敲打键盘、移动鼠标、读写硬盘之类的),这会让随机数字发生器有更好的机会获得足够的熵数。

几分钟以后,系统提示密钥已经生成了。

  gpg: 密钥 EDDD6D76 被标记为绝对信任
  公钥和私钥已经生成并经签名。

请注意上面的字符串"EDDD6D76",这是"用户ID"的Hash字符串,可以用来替代"用户ID"。

这时,最好再生成一张"撤销证书",以备以后密钥作废时,可以请求外部的公钥服务器撤销你的公钥。

  gpg --gen-revoke [用户ID]

上面的"用户ID"部分,可以填入你的邮件地址或者Hash字符串(以下同)。

四、密钥管理

4.1 列出密钥

list-keys参数列出系统中已有的密钥.

  gpg --list-keys

显示结果如下:

  /home/ruanyf/.gnupg/pubring.gpg
  -------------------------------
  pub 4096R/EDDD6D76 2013-07-11
  uid Ruan YiFeng <yifeng.ruan@gmail.com>
  sub 4096R/3FA69BE4 2013-07-11

第一行显示密钥文件名(pubring.gpg),第二行显示公钥特征(4096位,Hash字符串和生成时间),第三行显示"用户ID",第四行显示私钥特征。

如果你要从密钥列表中删除某个密钥,可以使用delete-key参数。

  gpg --delete-key [用户ID]

4.2 输出密钥

密钥文件(.gnupg/pubring.gpg)储存的是二进制数据,而且公钥和私钥都放在同一个文件中。

export参数可以单独输出公钥。

  gpg --export [用户ID] --output public-key.txt

"用户ID"指定输出公钥的用户,output参数指定公钥的输出文件名(public-key.txt)。

所有密钥文件默认以二进制形式保存,不便于查看和传输,armor参数可以让密钥以ASCII码显示。

  gpg --armor --output public-key.txt --export [用户ID]

类似地,使用export-secret-keys参数,可以输出私钥。

  gpg --armor --output private-key.txt --export-secret-keys

4.3 上传公钥

公钥服务器是网络上专门储存用户公钥的服务器。send-keys参数可以将公钥上传到服务器。

  gpg --send-keys [用户ID] --keyserver hkp://subkeys.pgp.net

使用上面的命令,你的公钥就被传到了服务器subkeys.pgp.net,然后通过交换机制,所有的公钥服务器最终都会包含你的公钥。

由于公钥服务器没有检查机制,任何人都可以用你的名义上传公钥,所以没有办法保证服务器上的公钥的可靠性。通常,你可以在网站上公布一个公钥指纹,让其他人核对下载到的公钥是否为真。fingerprint参数生成公钥指纹。

  gpg --fingerprint [用户ID]

4.4 输入密钥

除了生成自己的密钥,还需要将他人的公钥或者你的其他密钥输入系统。这时可以使用import参数。

  gpg --import [密钥文件]

为了获得他人的公钥,可以让对方直接发给你,或者到公钥服务器上寻找。

  gpg --keyserver hkp://subkeys.pgp.net --search-keys [用户ID]

正如前面提到的,我们无法保证服务器上的公钥是否可靠,下载后还需要用其他机制验证.

五、加密和解密

5.1 加密

假定有一个文本文件demo.txt,怎样对它加密呢?

encrypt参数用于加密。

  gpg --recipient [用户ID] --output demo.en.txt --encrypt demo.txt

recipient参数指定接收者的公钥,output参数指定加密后的文件名,encrypt参数指定源文件。运行上面的命令后,demo.en.txt就是已加密的文件,可以把它发给对方。

5.2 解密

对方收到加密文件以后,就用自己的私钥解密。

  gpg --decrypt demo.en.txt --output demo.de.txt

decrypt参数指定需要解密的文件,output参数指定解密后生成的文件。运行上面的命令,demo.de.txt就是解密后的文件。

GPG允许省略decrypt参数。

  gpg demo.en.txt

运行上面的命令以后,解密后的文件内容直接显示在标准输出。

六、签名

6.1 对文件签名

有时,我们不需要加密文件,只需要对文件签名,表示这个文件确实是我本人发出的。sign参数用来签名。

  gpg --sign demo.txt

运行上面的命令后,当前目录下生成demo.txt.gpg文件,这就是签名后的文件。这个文件默认采用二进制储存,如果想生成ASCII码的签名文件,可以使用clearsign参数。

  gpg --clearsign demo.txt

运行上面的命令后 ,当前目录下生成demo.txt.asc文件,后缀名asc表示该文件是ASCII码形式的。

如果想生成单独的签名文件,与文件内容分开存放,可以使用detach-sign参数。

  gpg --detach-sign demo.txt

运行上面的命令后,当前目录下生成一个单独的签名文件demo.txt.sig。该文件是二进制形式的,如果想采用ASCII码形式,要加上armor参数。

  gpg --armor --detach-sign demo.txt

6.2 签名+加密

上一节的参数,都是只签名不加密。如果想同时签名和加密,可以使用下面的命令。

  gpg --local-user [发信者ID] --recipient [接收者ID] --armor --sign --encrypt demo.txt

local-user参数指定用发信者的私钥签名,recipient参数指定用接收者的公钥加密,armor参数表示采用ASCII码形式显示,sign参数表示需要签名,encrypt参数表示指定源文件。

6.3 验证签名

我们收到别人签名后的文件,需要用对方的公钥验证签名是否为真。verify参数用来验证。

  gpg --verify demo.txt.asc demo.txt

举例来说,openvpn网站就提供每一个下载包的gpg签名文件。你可以根据它的说明,验证这些下载包是否为真。

七、参考文档

1. Paul Heinlein, GPG Quick Start

2. Ubuntu help,GnuPrivacyGuardHowto

3. KNL, GnuPG Tutorial

4. Alan Eliasen. GPG Tutorial

5. GnuPG 袖珍 HOWTO (中文版)

6. The GNU Privacy Handbook

(完)

文档信息

2013年7月4日星期四

阮一峰的网络日志

阮一峰的网络日志


RSA算法原理(二)

Posted: 03 Jul 2013 09:07 PM PDT

上一次,我介绍了一些数论知识

有了这些知识,我们就可以看懂RSA算法。这是目前地球上最重要的加密算法。

六、密钥生成的步骤

我们通过一个例子,来理解RSA算法。假设爱丽丝要与鲍勃进行加密通信,她该怎么生成公钥和私钥呢?

第一步,随机选择两个不相等的质数p和q。

爱丽丝选择了61和53。(实际应用中,这两个质数越大,就越难破解。)

第二步,计算p和q的乘积n。

爱丽丝就把61和53相乘。

  n = 61×53 = 3233

n的长度就是密钥长度。3233写成二进制是110010100001,一共有12位,所以这个密钥就是12位。实际应用中,RSA密钥一般是1024位,重要场合则为2048位。

第三步,计算n的欧拉函数φ(n)。

根据公式:

  φ(n) = (p-1)(q-1)

爱丽丝算出φ(3233)等于60×52,即3120。

第四步,随机选择一个整数e,条件是1< e < φ(n),且e与φ(n) 互质。

爱丽丝就在1到3120之间,随机选择了17。(实际应用中,常常选择65537。)

第五步,计算e对于φ(n)的模反元素d。

所谓"模反元素"就是指有一个整数d,可以使得ed被φ(n)除的余数为1。

  ed ≡ 1 (mod φ(n))

这个式子等价于

  ed - 1 = kφ(n)

于是,找到模反元素d,实质上就是对下面这个二元一次方程求解。

  ex + φ(n)y = 1

已知 e=17, φ(n)=3120,

  17x + 3120y = 1

这个方程可以用"扩展欧几里得算法"求解,此处省略具体过程。总之,爱丽丝算出一组整数解为 (x,y)=(2753,-15),即 d=2753。

至此所有计算完成。

第六步,将n和e封装成公钥,n和d封装成私钥。

在爱丽丝的例子中,n=3233,e=17,d=2753,所以公钥就是 (3233,17),私钥就是(3233, 2753)。

实际应用中,公钥和私钥的数据都采用ASN.1格式表达(实例)。

七、RSA算法的可靠性

回顾上面的密钥生成步骤,一共出现六个数字:

  p
  q
  n
  φ(n)
  e
  d

这六个数字之中,公钥用到了两个(n和e),其余四个数字都是不公开的。其中最关键的是d,因为n和d组成了私钥,一旦d泄漏,就等于私钥泄漏。

那么,有无可能在已知n和e的情况下,推导出d?

  (1)ed≡1 (mod φ(n))。只有知道e和φ(n),才能算出d。

  (2)φ(n)=(p-1)(q-1)。只有知道p和q,才能算出φ(n)。

  (3)n=pq。只有将n因数分解,才能算出p和q。

结论:如果n可以被因数分解,d就可以算出,也就意味着私钥被破解。

可是,大整数的因数分解,是一件非常困难的事情。目前,除了暴力破解,还没有发现别的有效方法。维基百科这样写道:

  "对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA算法愈可靠。

  假如有人找到一种快速因数分解的算法,那么RSA的可靠性就会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA密钥才可能被暴力破解。到2008年为止,世界上还没有任何可靠的攻击RSA算法的方式。

  只要密钥长度足够长,用RSA加密的信息实际上是不能被解破的。"

举例来说,你可以对3233进行因数分解(61×53),但是你没法对下面这个整数进行因数分解。

  12301866845301177551304949
  58384962720772853569595334
  79219732245215172640050726
  36575187452021997864693899
  56474942774063845925192557
  32630345373154826850791702
  61221429134616704292143116
  02221240479274737794080665
  351419597459856902143413

它等于这样两个质数的乘积:

  33478071698956898786044169
  84821269081770479498371376
  85689124313889828837938780
  02287614711652531743087737
  814467999489
    ×
  36746043666799590428244633
  79962795263227915816434308
  76426760322838157396665112
  79233373417143396810270092
  798736308917

事实上,这大概是人类已经分解的最大整数(232个十进制位,768个二进制位)。比它更大的因数分解,还没有被报道过,因此目前被破解的最长RSA密钥就是768位。

八、加密和解密

有了公钥和密钥,就能进行加密和解密了。

(1)加密要用公钥 (n,e)

假设鲍勃要向爱丽丝发送加密信息m,他就要用爱丽丝的公钥 (n,e) 对m进行加密。这里需要注意,m必须是整数(字符串可以取ascii值或unicode值),且m必须小于n。

所谓"加密",就是算出下式的c:

  me ≡ c (mod n)

爱丽丝的公钥是 (3233, 17),鲍勃的m假设是65,那么可以算出下面的等式:

  6517 ≡ 2790 (mod 3233)

于是,c等于2790,鲍勃就把2790发给了爱丽丝。

(2)解密要用私钥(n,d)

爱丽丝拿到鲍勃发来的2790以后,就用自己的私钥(3233, 2753) 进行解密。可以证明,下面的等式一定成立:

  cd ≡ m (mod n)

也就是说,c的d次方除以n的余数为m。现在,c等于2790,私钥是(3233, 2753),那么,爱丽丝算出

  27902753 ≡ 65 (mod 3233)

因此,爱丽丝知道了鲍勃加密前的原文就是65。

至此,"加密--解密"的整个过程全部完成。

我们可以看到,如果不知道d,就没有办法从c求出m。而前面已经说过,要知道d就必须分解n,这是极难做到的,所以RSA算法保证了通信安全。

你可能会问,公钥(n,e) 只能加密小于n的整数m,那么如果要加密大于n的整数,该怎么办?有两种解决方法:一种是把长信息分割成若干段短消息,每段分别加密;另一种是先选择一种"对称性加密算法"(比如DES),用这种算法的密钥加密信息,再用RSA公钥加密DES密钥。

九、私钥解密的证明

最后,我们来证明,为什么用私钥解密,一定可以正确地得到m。也就是证明下面这个式子:

  cd ≡ m (mod n)

因为,根据加密规则

  me ≡ c (mod n)

于是,c可以写成下面的形式:

  c = me - kn

将c代入要我们要证明的那个解密规则:

  (me - kn)d ≡ m (mod n)

它等同于求证

  med ≡ m (mod n)

由于

  ed ≡ 1 (mod φ(n))

所以

  ed = hφ(n)+1

将ed代入:

  mhφ(n)+1 ≡ m (mod n)

接下来,分成两种情况证明上面这个式子。

(1)m与n互质。

根据欧拉定理,

  mφ(n) ≡ 1 (mod n)

得到

  (mφ(n))h × m ≡ m (mod n)

原式得到证明。

(2)m与n不是互质关系。

由于n等于质数p和q的乘积,所以m必然等于kp或kq。

以 m = kp为例,考虑到这时k与q必然互质,则根据欧拉定理,下面的式子成立:

  (kp)q-1 ≡ 1 (mod q)

进一步得到

  [(kp)q-1]h(p-1) × kp ≡ kp (mod q)

  (kp)ed ≡ kp (mod q)

将它改写成下面的等式

  (kp)ed = tq + kp

这时t必然能被p整除,即 t=t'p

  (kp)ed = t'pq + kp

因为 m=kp,n=pq,所以

  med ≡ m (mod n)

原式得到证明。

(完)

文档信息