(一)初识HTTP消息头
做过Socket编程的人都知道,当我们设计一个通信协议时,“消息头/消息体”的分割方式是很常用的,消息头告诉对方这个消息是干什么的,消息体告诉对方怎么干。HTTP传输的消息也是这样规定的,每一个HTTP包都分为HTTP头和HTTP体两部分,后者是可选的,而前者是必须的。每当我们打开一个网页,在上面点击右键,选择“查看源文件”,这时看到的HTML代码就是HTTP的消息体,那么消息头又在哪呢?IE浏览器不让我们看到这部分,但我们可以通过截取数据包等方法看到它。
下面就来看一个简单的例子:
首先制作一个非常简单的网页,它的内容只有一行:
hello world
把它放到WEB服务器上,比如IIS,然后用IE浏览器请求这个页面( http://localhost:8080/simple.htm),当我们请求这个页面时,浏览器实际做了以下四项工作:
1 解析我们输入的地址,从中分解出协议名、主机名、端口、对象路径等部分,对于我们的这个地址,解析得到的结果如下:
协议名:http
主机名:localhost
端口:8080
对象路径:/simple.htm
2 把以上部分结合本机自己的信息,封装成一个HTTP请求数据包
3 使用TCP协议连接到主机的指定端口(localhost, 8080),并发送已封装好的数据包
4 等待服务器返回数据,并解析返回数据,最后显示出来
由截取到的数据包我们不难发现浏览器生成的HTTP数据包的内容如下:
GET /simple.htm HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
Host: localhost:8080
Connection: Keep-Alive
消息的第一行“GET”表示我们所使用的HTTP动作,其他可能的还有“POST”等,GET的消息没有消息体,而POST消息是有消息体的,消息体的内容就是要POST的数据。后面/simple.htm就是我们要请求的对象,之后 HTTP1.1表示使用的是 HTTP1.1协议。
第二行表示我们所用的浏览器能接受的Content-type,三四两行则是语言和编码信息,第五行显示出本机的相关系信息,包括浏览器类型、操作系统信息等,很多网站可以显示出你所使用的浏览器和操作系统版本,就是因为可以从这里获取到这些信息。
第六行表示我们所请求的主机和端口,第七行表示使用 Keep- Alive方式,即数据传递完并不立即关闭连接。
服务器接收到这样的数据包以后会根据其内容做相应的处理,例如查找有没有“/simple.htm”这个对象,如果有,根据服务器的设置来决定如何处理,如果是HTM,则不需要什么复杂的处理,直接返回其内容即可。但在直接返回之前,还需要加上HTTP消息头。
服务器发回的完整HTTP消息如下:
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.1
X-Powered-By: ASP.NET
Date: Fri, 03 Mar 2006 06:34:03 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Fri, 03 Mar 2006 06:33:18 GMT
ETag: "5ca4f75b8c3ec61:9ee"
Content-Length: 37
hello world
消息头第一行“HTTP/1.1”也是表示所使用的协议,后面的“200 OK”是HTTP返回代码,200就表示操作成功,还有其他常见的如404表示对象未找到,500表示服务器错误,403表示不能浏览目录等等。
第二行表示这个服务器使用的WEB服务器软件,这里是IIS 5.1。第三行是ASP.Net的一个附加提示,没什么实际用处。第四行是处理此请求的时间。第五行就是所返回的消息的content-type,浏览器会根据它来决定如何处理消息体里面的内容,例如这里是text/html,那么浏览器就会启用HTML解析器来处理它,如果是image/jpeg,那么就会使用JPEG的解码器来处理。
消息头最后一行“Content-Length”表示消息体的长度,从空行以后的内容算起,以字节为单位,浏览器接收到它所指定的字节数的内容以后就会认为这个消息已经被完整接收了。
理解HTTP消息头 (二)
常见的HTTP返回码
1 403 Access Forbidden
HTTP/1.1 403 Access Forbidden
Server: Microsoft-IIS/5.1
Date: Mon, 06 Mar 2006 08:57:39 GMT
Connection: close
Content-Type: text/html
Content-Length: 172
Directory Listing Denied
This Virtual Directory does not allow contents to be listed.2 404 Object not found
HTTP/1.1 404 Not Found
Date: Mon, 06 Mar 2006 09:03:14 GMT
Server: Apache/2.0.55 (Unix) PHP/5.0.5
Content-Length: 291
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1
Not Found
The requested URL /notexist was not found on this server.
Apache/2.0.55 (Unix) PHP/5.0.5 Server at localhost Port 8080
3 401 Access Denied
HTTP/1.1 401 Access Denied
Server: Microsoft-IIS/5.1
Date: Mon, 06 Mar 2006 09:15:55 GMT
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
Connection: close
Content-Length: 3964
Content-Type: text/html
……
此时浏览器上给出的提示如下图,让我们输入用户名和密码:
当我们在输入了用户名和密码以后,服务器与客户端会再进行两次对话。首先客户端向服务器索取一个公钥,服务器端会返回一个公钥,二者都用BASE64编码,相应的消息如下(编码部分已经做了处理):
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
Host: 192.168.0.55:8080
Connection: Keep-Alive
Authorization: Negotiate ABCDEFG……
Server: Microsoft-IIS/5.1
Date: Mon, 06 Mar 2006 09:20:53 GMT
WWW-Authenticate: Negotiate HIJKLMN……
Content-Length: 3715
Content-Type: text/html
……
客户端拿到公钥之后使用公钥对用户名和密码进行加密码,然后把加密以后的结果重新发给服务器:
GET / HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
Host: 192.168.0.55:8080
Connection: Keep-Alive
Authorization: Negotiate OPQRST……
4 302 Object Moved
先来看一下Transfer的例子:
例如ASP文件1.asp只有一行
<% Server.Transfer "1.htm" %>
HTML文件1.htm也只有一行:
this is 1.htm
如果我们从浏览器里请求1.asp,发送的请求是:
GET /1.asp HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
Host: localhost:8080
Connection: Keep-Alive
Cookie: ASPSESSIONIDACCTRTTT=PKKDJOPBAKMAMBNANIPIFDAP
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.1
Date: Mon, 06 Mar 2006 12:52:44 GMT
X-Powered-By: ASP.NET
Content-Length: 20
Content-Type: text/html
Cache-control: private
this is 1.htm
不难看出,通过Server.Transfer语句服务器端已经做了页面重定向,而客户端对此一无所知,表面上看上去得到的就是1.asp的结果。
如果把1.asp的内容改为:
<% Response.Redirect "1.htm" %>
再次请求1.asp,发送的请求没有变化,得到的回应却变成了:
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.1
Date: Mon, 06 Mar 2006 12:55:57 GMT
X-Powered-By: ASP.NET
Location: 1.htm
Content-Length: 121
Content-Type: text/html
Cache-control: private
GET /1.htm HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
Host: localhost:8080
Connection: Keep-Alive
If-Modified-Since: Thu, 02 Mar 2006 06:50:13 GMT
If-None-Match: "b224758ec53dc61:9f0"
Cookie: ASPSESSIONIDACCTRTTT=PKKDJOPBAKMAMBNANIPIFDAP
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.1
X-Powered-By: ASP.NET
Date: Mon, 06 Mar 2006 12:55:57 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 06 Mar 2006 12:52:32 GMT
ETag: "76d85bd51c41c61:9f0"
Content-Length: 20
this is 1.htm
很明显,两种重定向方式虽然看上去结果很像,但在实现原理上有很大的不同。
5 500 Internal Server Error
<% if %>
显然这个程序并不完整,于是得到的结果为:
HTTP/1.1 500 Internal Server Error
Server: Microsoft-IIS/5.1
Date: Mon, 06 Mar 2006 12:58:55 GMT
X-Powered-By: ASP.NET
Content-Length: 4301
Content-Type: text/html
Expires: Mon, 06 Mar 2006 12:58:55 GMT
Set-Cookie: ASPSESSIONIDACCTRTTT=ALKDJOPBPPKNPCNOEPCNOOPD; path=/
Cache-control: private
……
理解HTTP消息头 (三)
(三) 客户端发送的内容
这一次主要来观察HTTP消息头中客户端的请求,从中找到一些有意思的内容。
写两个简单的网页:
a.htm:
to page b
b.htm:
haha
内容很简单,就是网页A中有一个到B的链接。把它们放到IIS上,并访问网页A,从中再点击到B的链接,于是看到了B页的“haha”。那么这两次请求有什么不同吗?观察它们所发送的HTTP消息头,最明显的区别就是访问B页时比访问A页时多了一行:
Referer: http://localhost/a.htm
这一行就表示,用户要访问的B页是从A页链接过来的。
服务器端要想取得这个值也是很容易的,以ASP为例,只需要写一句
<% =Request.ServerVariables("HTTP_REFERER") %>
就可以了。
一些网站通过HTTP_REFERER来做安全验证,判断用户是不是从允许的页面链接来的,而不是直接从浏览器上打URL或从其他页面链接过来,这样可以从一定程度上防止网页被做非法使用。但从上述原理来看,想要骗过服务器也并不困难,只要手工构造输入的HTTP消息头就可以了,其他常用的手段还有通过HOSTS文件伪造域名等。
除了超链接以外,还有其他几种方式会导致HTTP_REFERER信息被发送,如:
内联框架:
框架集:
表单提交:
SCRIPT引用:
CSS引用:
XML数据岛:
script转向:
script开新窗口:
META转向:
引入图片:
COOKIE是大家都非常熟悉的了,通过它可以在客户端保存用户状态,即使用户关闭浏览器也能继续保存。那么客户端与服务器端是如何交换COOKIE信息的呢?没错,也是通过HTTP消息头。
首先写一个简单的ASP网页:
<%
Dim i
i = Request.Cookies("key")
Response.Write i
Response.Cookies("key") = "haha"
Response.Cookies("key").Expires = #2007-1-1#
%>
第一次访问此网页时,屏幕上一片白,第二次访问时,则会显示出“haha”。通过阅读程序不难发现,屏幕上显示的内容实际上是COOKIE的内容,而第一次访问时还没有设置COOKIE的值,所以不会有显示,第二次显示的是第一次设置的值。那么对应的HTTP消息头应该是什么样的呢?
第一次请求时没什么不同,略过
第一次返回时消息内容多了下面这一行:
Set-Cookie: key=haha; expires=Sun, 31-Dec-2006 16:00:00 GMT; path=/
很明显,key=haha表示键名为“key”的COOKIE的值为“haha”,后面是这则COOKIE的过期时间,因为我用的中文操作系统的时区是东八区,2007年1月1日0点对应的GMT时间就是2006年12月31日16点。
第二次再访问此网页时,发送的内容多了如下一行:
Cookie: key=haha
它的内容就是刚才设的COOKIE的内容。可见,客户端在从服务器端得到COOKIE值以后就保存在硬盘上,再次访问时就会把它发送到服务器。发送时并没有发送过期时间,因为服务器对过期时间并不关心,当COOKIE过期后浏览器就不会再发送它了。
如果使用IE6.0浏览器并且禁用COOKIE功能,可以发现服务器端的set-cookie还是有的,但客户端并不会接受它,也不会发送它。有些网站,特别是在线投票网站通过记录COOKIE防止用户重复投票,破解很简单,只要用IE6浏览器并禁用COOKIE就可以了。也有的网站通过COOKIE值为某值来判断用户是否合法,这种判断也非常容易通过手工构造HTTP消息头来欺骗,当然用HOSTS的方式也是可以欺骗的。
HTTP协议本身是无状态的,服务器和客户端都不保证用户访问期间连接会一直保持,事实上保持连接是 HTTP1.1才有的新内容,当客户端发送的消息头中有“Connection: Keep- Alive”时表示客户端浏览器支持保持连接的工作方式,但这个连接也会在一段时间没有请求后自动断开,以节省服务器资源。为了在服务器端维持用户状态,SESSION就被发明出来了,现在各主流的动态网页制做工具都支持SESSION,但支持的方式不完全相同,以下皆以ASP为例。
当用户请求一个ASP网页时,在返回的HTTP消息头中会有一行:
Set-Cookie: ASPSESSIONIDCSQCRTBS=KOIPGIMBCOCBFMOBENDCAKDP; path=/
服务器通过COOKIE的方式告诉客户端你的SESSIONID是多少,在这里是“KOIPGIMBCOCBFMOBENDCAKDP”,并且服务器上保留了和此SESSIONID相关的数据,当同一用户再次发送请求时,还会把这个COOKIE再发送回去,服务器端根据此ID找到此用户的数据,也就实现了服务器端用户状态的保存。所以我们用ASP编程时可以使用“session("name")=user”这样的方式保存用户信息。注意此COOKIE内容里并没有过期时间,这表示这是一个当关闭浏览器时立即过期的COOKIE,它不会被保存到硬盘上。这种工作方式比单纯用COOKIE的方式要安全很多,因为在客户端并没有什么能让我们修改和欺骗的值,唯一的信息就是SESSIONID,而这个ID在浏览器关闭时会立即失效,除非别人能在你浏览网站期间或关闭浏览器后很短时间内知道此ID的值,才能做一些欺骗活动。因为服务器端判断SESSION过期的方式并不是断开连接或关闭浏览器,而是通过用户手工结束SESSION或等待超时,当用户关闭浏览器后的一段时间里SESSION还没有超时,所以这时如果知道了刚才的SESSIONID,还是可以欺骗的。因此最安全的办法还是在离开网站之前手工结束SESSION,很多网站都提供“Logout”功能,它会通过设置SESSION中的值为已退出状态或让SESSION立即过期从而起到安全的目的。
SESSION和COOKIE的方式各有优缺点。SESSION的优点是比较安全,不容易被欺骗,缺点是过期时间短,如果用过在超过过期时间里没有向服务器发送任何信息,就会被认为超过过期了;COOKIE则相反,根据服务器端设置的超时时间,可以长时间保留信息,即使关机再开机也可能保留状态,而安全性自然大打折扣。很多网站都提供两种验证方式相结合,如果用户临时用这台电脑访问此访问则需要输入用户名和密码,不保存COOKIE;如果用户使用的是自己的个人电脑,则可以让网站在自己硬盘上保留COOKIE,以后访问时就不需要重新输入用户名和密码了。
浏览器访问服务器常用的方式有GET和POST两种,GET方式只发送HTTP消息头,没有消息体,也就是除了要GET的基本信息之外不向服务器提供其他信息,网页表单(FROM)的默认提交方式就是用GET方式,它会把所有向服务器提交的信息都作为URL后面的参数,如a.asp?a=1&b=2这样的方式。而当要提交的数据量很大,或者所提交内容不希望别人直接看到时,应该使用POST方式。POST方式提交的数据是作为HTTP消息体存在的,例如,写一个网页表单:
访问此网页,并在表单中填入一个“haha”,然后提交,可以看到此次提交所发送的信息如下:
POST /form.asp HTTP/1.1
Accept: */*
Referer: http://localhost:8080/form.asp
Accept-Language: zh-cn
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; InfoPath.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
Host: localhost:8080
Content-Length: 10
Connection: Keep-Alive
Cache-Control: no-cache
Cookie: key=haha; ASPSESSIONIDCSQCRTBS=LOIPGIMBLMNOGCOBOMPJBOKP
前面关键字从“GET”变为了“POST”,Content-Type变成了“application/x-www-form-urlencoded”,后面内容并无大变化,只是多了一行:Content-Length: 10,表示提交的内容的长度。空行后面是消息体,内容就是表单中所填的内容。注意此时发送的内容只是“Name=Value”的形式,表单上其他的信息不会被发送,所以想直接从服务器端取得list box中所有的list item是办不到的,除非在提交前用一段script把所有的item内容都连在一起放到一个隐含表单域中。
如果是用表单上传文件,情况就要复杂一些了,首先是表单声明中要加上一句话:enctype='multipart/form-data',表示这个表单将提交多段数据,并用HTML:input type=file来声明一个文件提交域。
表单内容如下:
我们为text1输入文字:hehe,为file1选择文件haha.txt,其内容为“ABCDEFG”,然后提交此表单。提交的完全信息为:
POST /form.asp HTTP/1.1
Accept: */*
Referer: http://localhost:8080/form.asp
Accept-Language: zh-cn
Content-Type: multipart/form-data; boundary=---------------------------7d62bf2f9066c
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; InfoPath.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
Host: localhost:8080
Content-Length: 337
Connection: Keep-Alive
Cache-Control: no-cache
Cookie: key=haha; ASPSESSIONIDCSQCRTBS=LOIPGIMBLMNOGCOBOMPJBOKP
Content-Disposition: form-data; name="text1"
-----------------------------7d62bf2f9066c
Content-Disposition: form-data; name="file1"; filename="H:\Documents and Settings\Administrator\桌面\haha.txt"
Content-Type: text/plain
-----------------------------7d62bf2f9066c--
如果我们想要自己写一个上传文件组件来接收HTML表单传送的文件数据,那么最核心的任务就是解析此数据包,从中取得需要的信息。
理解HTTP消息头 (四)
服务器返回的消息
服务器返回的HTTP消息也分为消息头和消息体两部分。前面连载的第二篇里已经介绍了返回消息中常见返回代码的含义。对于非正常的返回代码的处理比较简单,只要照着要求去做就好了,而对于正常的返回代码(200),其处理方式就多种多样了。
1 Content-Type
text/html HTML文本
image/jpeg JPG图片
image/gif GIF图片
application/xml XML文档
audio/x-mpegurl MP3文件列表,如果安装了Winamp,则可以直接把它当面M3U文件来打开
更多的内容类型可以在注册表“HKCR\MIME\Database\Content Type”下看到
对于IE6浏览器来说,如果Content-Type中的类型和实际的消息体类型不一致,那么它会根据内容中的类型来分析实际应该是什么类型,对于JPG、GIF等常用图片格式都可以正确的识别出来,而不管Content-Type中写的是什么。
如果Content-Type中指定的是浏览器可以直接打开的类型,那么浏览器就会直接打开其内容显示出来,如果是被关联到其它应用程序的类型,这时就要查找注册表中关于这种类型的注册情况,如果是允许直接打开而不需要询问的,就会直接调出这个关联的应用程序来打开这个文件,但如果是不允许直接打开的,就会询问是否打开。对于没有关联到任何应用程序的类型,IE浏览器不知道它该如何打开,此时IE6就会把它当成XML来尝试打开。
2 Content-Disposition
Response.AddHeader("Content-Disposition: attachment");
请求此页面是得到的结果如:
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.1
Date: Thu, 23 Mar 2006 07:54:53 GMT
Content-Disposition: attachment
Cache-Control: private
Content-Type: text/html; charset=utf-8
……
也就是说,通过AddHeader函数可以为HTTP消息头加入我们自定义的内容。使用这种方法可以强制让浏览器提示下载文件,即使这个文件是我们已知的类型,基于是HTML网页。如果想要让用户下载时提示一个默认的文件名,只需要在前面一句话后加上“filename=文件名”即可。例如:
Response.AddHeader("Content-Disposition: attachment; filename=mypage.htm");
3 Content-Type与Content-Disposition
如果把Content-Type和Content-Disposition结合在一起使用会怎么样呢?
打开一个网页时,浏览器会首先看是否有Content-Disposition: attachment这一项,如果有,无论Content-Type的值是什么,都会提示文件下载。
如果指定了filename,就会提示默认的文件名为此文件名。注意到在IE6中除了“保存”按扭外还有“打开”按扭,此时打开文件的类型是由在filename中指定的文件扩展名决定的,例如让filename=mypic.jpg,浏览器就会查找默认的图片查看器来打开此文件。
如果没有指定filename,那么浏览器就根据Content-Type中的类型来决定文件的类型,例如Content-Type类型为image/gif,那么就会去查找默认的看GIF图片的工具,并且设置此文件的名字为所请求的网页的主名(不带扩展名)加上对应于此文件类弄扩展名,例如请求的mypage.aspx,就会自动变成mypage.gif。如果并没有指定Content-Type值,那么就默认它为“text/html”,并且保存的文件名就是所请求的网页文件名。
但如果没有指定Content-Disposition,那么就和前面关于Content-Type中所讨论的情况是一样的了。
4 Cache
返回消息中的Cache用于指定网页缓存。我们经常可以看到这样的情况,打开一个网页时速度不快,但再次打开时就会快很多,原因是浏览器已经对此页面进行了缓存,那么在同一浏览器窗口中再次打开此页时不会重新从服务器端获取。网页的缓存是由HTTP消息头中的“Cache-control”来控制的,常见的取值有private、no-cache、max-age、must-revalidate等,默认为private。其作用根据不同的重新浏览方式分为以下几种情况:
(1) 打开新窗口
如果指定cache-control的值为private、no-cache、must-revalidate,那么打开新窗口访问时都会重新访问服务器。而如果指定了max-age值,那么在此值内的时间里就不会重新访问服务器,例如:
Cache-control: max-age=5
表示当访问此网页后的5秒内再次访问不会去服务器
(2) 在地址栏回车
如果值为private或must-revalidate(和网上说的不一样),则只有第一次访问时会访问服务器,以后就不再访问。如果值为no-cache,那么每次都会访问。如果值为max-age,则在过期之前不会重复访问。
(3) 按后退按扭
如果值为private、must-revalidate、max-age,则不会重访问,而如果为no-cache,则每次都重复访问
(4) 按刷新按扭
无论为何值,都会重复访问
当指定Cache-control值为“no-cache”时,访问此页面不会在Internet临时文章夹留下页面备份。
另外,通过指定“Expires”值也会影响到缓存。例如,指定Expires值为一个早已过去的时间,那么访问此网时若重复在地址栏按回车,那么每次都会重复访问:
Expires: Fri, 31 Dec 1999 16:00:00 GMT
在ASP中,可以通过Response对象的Expires、ExpiresAbsolute属性控制Expires值;通过Response对象的CacheControl属性控制Cache-control的值,例如:
Response.ExpiresAbsolute = #2000-1-1# ' 指定绝对的过期时间,这个时间用的是服务器当地时间,会被自动转换为GMT时间
Response.Expires = 20 ' 指定相对的过期时间,以分钟为单位,表示从当前时间起过多少分钟过期。
Response.CacheControl = "no-cache"
Expires值是可以通过在Internet临时文件夹中查看临时文件的属性看到的,如: