17.5 Cookie

Cookie为Web应用程序保存用户相关信息提供了一种有用的方法。例如,当用户访问站点时,可以利用Cookie保存用户首选项或其他信息,这样,当用户下次再访问站点时,应用程序就可以检索以前保存的信息。

其实,Cookie只是小段保存在客户端的数据。如果安装的是XP系统,那么可以看一下“<安装Windows的盘>:\Documents and Settings\<用户名>\Cookies文件夹”,如图17-3所示。

figure_0603_0487

图 17-3 Cookies文件夹

伴随着用户请求和页面在Web服务器和浏览器之间传递,Cookie包含每次用户访问站点时Web应用程序都可以读取的信息。例如,如果在用户请求站点中的页面时,应用程序发送给该用户的不仅仅是一个页面,还有一个包含日期和时间的Cookie,用户的浏览器在获得页面的同时还获得了该Cookie,并将它存储在用户硬盘上的Cookies文件夹。

以后,如果该用户再次请求站点中的页面,当该用户输入URL时,浏览器便会在本地硬盘上查找与该URL关联的Cookie。如果该Cookie存在,浏览器便将该Cookie与页请求一起发送到站点。然后,应用程序便可以确定该用户上次访问站点的日期和时间。也可以使用这些信息向用户显示一条消息,还可以检查到期日期。因此,Cookie常常用来帮助网站存储有关访问者的信息。例如,购物站点上的Web服务器跟踪每位购物者,这样,站点就可以管理购物车和其他的用户特定信息。

最后,还需要说明的是,Cookie与网站关联,而不是与特定的页面关联。因此,无论用户请求站点中的哪一个页面,浏览器和服务器都将交换Cookie信息。用户访问不同站点时,各个站点都可能会向用户的浏览器发送一个Cookie,浏览器会分别存储所有Cookie。

17.5.1 创建Cookie

其实,Cookie通过HttpResponse对象发送到浏览器,该对象公开称为Cookies的集合,可以将HttpResponse对象作为Page类的Response属性来进行访问。需要特别强调的是,要发送给浏览器的所有Cookie都必须添加到此集合中。

在创建Cookie时,需要指定它的Name和Value。每个Cookie必须有一个唯一的名称,以便以后从浏览器读取Cookie时可以识别它。由于Cookie按名称存储,因此用相同的名称命名两个Cookie会导致其中一个Cookie被覆盖。如下面的代码示例创建了一个名为book的Cookie,并将其Value设置为“易学C#”。


Response.Cookies["book"].Value="易学C#";


当然,在创建Cookie时,还可以为Cookie设置到期日期和时间。用户访问编写Cookie的站点时,浏览器将删除过期的Cookie。只要应用程序认为Cookie值有效,就应将Cookie的有效期设置为这一段时间。对于永不过期的Cookie,可将到期日期设置为从现在起50年。如下面的代码名为book的Cookie过期时间设置为3天。


Response.Cookies["book"].Expires=DateTime.Now.AddDays(3d);


如果没有设置Cookie的有效期,仍会创建Cookie,但不会将其存储在用户的硬盘上。而会将Cookie作为用户会话信息的一部分进行维护。当用户关闭浏览器时,Cookie便会被丢弃。这种非永久性Cookie很适合用来保存只需短时间存储的信息,或者保存由于安全原因不应该写入客户端计算机上的磁盘的信息。例如,如果用户在使用一台公用计算机,而你不希望将Cookie写入该计算机的磁盘中,这时就可以使用非永久性Cookie。

注意用户可以随时清除其计算机上的Cookie。即便存储的Cookie距到期日期还有很长时间,但用户还是可以决定删除所有Cookie,清除Cookie中存储的所有设置。

除此之外,还可以通过创建HttpCookie对象的实例来创建Cookie。其中,HttpCookie类提供创建和操作各HTTP Cookie的类型安全方法,可以使用它来获取和设置各Cookie的属性。如下面的代码所示:


HttpCookie myCookie=new HttpCookie("book");

myCookie.Value="易学C#";

myCookie.Expires=DateTime.Now.AddDays(3d);

Response.Cookies.Add(myCookie);


除了可以在Cookie中存储一个值之外,也可以在一个Cookie中存储多个名称/值对,这些名称/值对称为子键。如下面的示例代码所示:


Response.Cookies["book"]["Count"]="3";

Response.Cookies["book"]["Name"]="易学C#";

Response.Cookies["book"].Expires=DateTime.Now.AddDays(3d);


或者


HttpCookie myCookie=new HttpCookie("book");

myCookie["Count"]="3";

myCookie["Name"]="易学C#";

myCookie.Expires=DateTime.Now.AddDays(3d);

Response.Cookies.Add(myCookie);


在实际开发中,可能会出于多种原因来使用子键。首先,将相关或类似的信息放在一个Cookie中很方便。此外,由于所有信息都在一个Cookie中,所以诸如有效期之类的Cookie特性就适用于所有信息。反之,如果要为不同类型的信息指定不同的到期日期,就应该把信息存储在单独的Cookie中。

与此同时,带有子键的Cookie还可帮助你限制Cookie文件的大小。一般情况下,Cookie通常限制为4096字节,并且每个站点最多可存储20个Cookie。使用带子键的单个Cookie,使用的Cookie数就不会超过分配给站点的20个的限制。此外,一个Cookie会占用大约50个字符的系统开销(用于保存有效期信息等),再加上其中存储的值的长度,其总和接近4096字节的限制。如果存储五个子键而不是五个单独的Cookie,便可节省单独Cookie的系统开销,节省大约200字节。

17.5.2 控制Cookie的范围

默认情况下,一个站点的全部Cookie都一起存储在客户端上,而且所有Cookie都会随着对该站点发送的任何请求一起发送到服务器。也就是说,一个站点中的每个页面都能获得该站点的所有Cookie。但是,可以通过如下两种方式来设置Cookie的范围:

1)将Cookie的范围限制到服务器上的某个文件夹,这允许你将Cookie限制到站点上的某个应用程序。若要将Cookie限制到服务器上的某个文件夹,请按下面的示例设置Cookie的Path属性:


HttpCookie appCookie=new HttpCookie("book");

appCookie.Value="易学C#";

appCookie.Expires=DateTime.Now.AddDays(3);

appCookie.Path="/Book";

Response.Cookies.Add(appCookie);


这里的路径可以是站点根目录下的物理路径,也可以是虚拟根目录。所产生的效果是Cookie只能用于Book文件夹或虚拟根目录中的页面。例如,如果站点名称为www.comesns.com,则在前面示例中创建的Cookie将只能用于路径为http://www.comesns.com/Book/的页面以及该文件夹下的所有页面,而不能用于其他应用程序中的页面,如http://www.comesns.com/MyBook/中的页面。

需要说明的是,在某些浏览器中,路径是区分大小写的。你无法控制用户如何在其浏览器中键入URL,但如果应用程序依赖于与特定路径相关的Cookie,请确保你创建的所有超链接中的URL与Path属性值的大小写相匹配。

2)将范围设置为某个域,这允许你指定域中的哪些子域可以访问Cookie。默认情况下,Cookie与特定域关联。对于站点www.comesns.com来说,当用户向该站点请求任何页时,你编写的Cookie就会被发送到服务器。当然,这可能不包括带有特定路径值的Cookie。如果该站点具有子域,例如comesns.com、cs.comesns.com等,则可以将Cookie与特定的子域关联。可以通过设置Cookie的Domain属性来执行此操作。如下面的代码所示:


Response.Cookies["book"].Value="易学C#";

Response.Cookies["book"].Expires=DateTime.Now.AddDays(1);

Response.Cookies["book"].Domain="book.comesns.com";


当以此方式设置域时,Cookie将仅可用于指定的子域中的页面。还可以使用Domain属性创建可在多个子域间共享的Cookie。如下面的示例所示:


Response.Cookies["book"].Value="易学C#";

Response.Cookies["book"].Expires=DateTime.Now.AddDays(1);

Response.Cookies["book"].Domain="comesns.com";


这样,Cookie既可用于主域,也可用于book.comesns.com和cs.comesns.com等域。

17.5.3 读取Cookie

浏览器向服务器发出请求时,会随着请求一起发送该服务器的Cookie。因此,可以使用HttpRequest对象来读取Cookie,该对象可用做Page类的Request属性使用。它的读取方式与将Cookie写入HttpResponse对象的方式基本相同。

在尝试获取Cookie的值之前,应确保该Cookie存在;如果该Cookie不存在,将会收到NullReferenceException异常。在页面中显示Cookie的内容前,应该先调用HtmlEncode方法对Cookie的内容进行编码。这样可以确保恶意用户没有向Cookie中添加可执行脚本。如下面的代码所示:


if(Request.Cookies["book"]!=null)

{

Response.Write(Server.HtmlEncode

((Rquest.Cookies["book"].Value));

}

if(Request.Cookies["book"]!=null)

{

HttpCookie aCookie=Request.Cookies["book"];

Response.Write(Server.HtmlEncode(aCookie.Value));

}


或者这样来读取Cookie中的子键值:


Server.HtmlEncode(Request.Cookies["book"]["Name"]);


因为Cookie中的子键被类型化为NameValueCollection类型的集合。因此,获取单个子键的另一种方法就是获取子键集合,然后再按名称提取子键值。如下面的示例所示:


if(Request.Cookies["book"]!=null)

{

System.Collections.Specialized.NameValueCollection

UserInfoCookieCollection;

UserInfoCookieCollection=Request.Cookies["book"].Values;

Response.Write(

Server.HtmlEncode(UserInfoCookieCollection["Count"]));

Response.Write(

Server.HtmlEncode(UserInfoCookieCollection["Name"]));

}


有时,可能需要读取可供页面使用的所有Cookie。若要读取可供页面使用的所有Cookie的名称和值,可以使用如下代码依次通过Cookies集合:


StringBuilder str=new StringBuilder();

HttpCookie aCookie;

for(int i=0;i<Request.Cookies.Count;i++)

{

aCookie=Request.Cookies[i];

str.Append("Cookie name="

+Server.HtmlEncode(aCookie.Name)+"<br/>");

str.Append("Cookie value="

+Server.HtmlEncode(aCookie.Value)+"<br/>");

}

Response.Write(str.ToString());


在上面的示例中,我们发现如果Cookie有子键,则会以一个名称/值字符串来显示子键。

其实,在实际应用中,可以读取Cookie的HasKeys属性来确定Cookie是否有子键,如果有,则可以读取子键集合以获取各个子键名称和值。可以通过索引值直接从Values集合中读取子键值,相应的子键名称可在Values集合的AllKeys成员中获得,该成员将返回一个字符串数组。还可以使用Values集合的Keys成员。但是,首次访问AllKeys属性时,该属性会被缓存。相比之下,每次访问Keys属性时,该属性都生成一个数组。因此在同一页请求的上下文内,在随后访问时,AllKeys属性要快得多。

下面的示例演示对前一示例的修改。该示例使用HasKeys属性来测试是否存在子键,如果检测到子键,便从Values集合获取子键:


StringBuilder str=new StringBuilder();

HttpCookie aCookie;

for(int i=0;i<Request.Cookies.Count;i++)

{

aCookie=Request.Cookies[i];

str.Append("Name="+aCookie.Name+"<br/>");

if(aCookie.HasKeys)

{

for(int j=0;j<aCookie.Values.Count;j++)

{

str.Append("Subkey name="

+Server.HtmlEncode(aCookie.Values.AllKeys[j])

+"<br/>");

str.Append("Subkey value="

+Server.HtmlEncode(aCookie.Values[j])+"<br/>");

}

}

else

{

str.Append("Value="

+Server.HtmlEncode(aCookie.Value)+"<br/>");

}

}

Response.Write(str.ToString());


当然,同样可以将子键作为NameValueCollection对象提取。如下面的示例所示:


StringBuilder str=new StringBuilder();

HttpCookie aCookie;

for(int i=0;i<Request.Cookies.Count;i++)

{

aCookie=Request.Cookies[i];

str.Append("Name="+aCookie.Name+"<br/>");

if(aCookie.HasKeys)

{

System.Collections.Specialized.NameValueCollection

CookieValues=aCookie.Values;

string[]CookieValueNames=CookieValues.AllKeys;

for(int j=0;j<CookieValues.Count;j++)

{

str.Append("Subkey name="

+Server.HtmlEncode(CookieValueNames[j])

+"<br/>");

str.Append("Subkey value="

+Server.HtmlEncode(CookieValues[j])+"<br/>");

}

}

else

{

str.Append("Value="

+Server.HtmlEncode(aCookie.Value)+"<br/>");

}

}

Response.Write(str.ToString());


17.5.4 修改Cookie

其实,Cookie是不能直接修改的,修改Cookie的过程涉及创建一个具有新值的新Cookie,然后将其发送到浏览器来覆盖客户端上的旧版本Cookie。如下面的示例代码所示:


if(Request.Cookies["book"]==null)

{

Response.Cookies["book"].Value="易学C#";

Response.Cookies["book"].Expires=DateTime.Now.AddDays(3);

}

else

{

Response.Cookies["book"].Value="ASP.NET4程序设计";

Response.Cookies["book"].Expires=DateTime.Now.AddDays(1);

}

Response.Write(Request.Cookies["book"].Value);


17.5.5 删除Cookie

由于Cookie在用户的计算机中,因此无法将其直接移除。但是,可以让浏览器来删除Cookie。该方法是创建一个与要删除的Cookie同名的新Cookie,并将该Cookie的到期日期设置为早于当前日期的某个日期。当浏览器检查Cookie的到期日期时,浏览器便会丢弃这个现已过期的Cookie。下面的代码示例演示删除应用程序中所有可用Cookie的一种方法:


HttpCookie aCookie;

string cookieName;

int count=Request.Cookies.Count;

for(int i=0;i<count;i++)

{

cookieName=Request.Cookies[i].Name;

aCookie=new HttpCookie(cookieName);

aCookie.Expires=DateTime.Now.AddDays(-1);

Response.Cookies.Add(aCookie);

}


如果要删除单个子键,可以操作Cookie的Values集合,该集合用于保存子键。首先,需要通过从Cookies对象中获取Cookie来重新创建Cookie。然后,就可以调用Values集合的Remove方法,将要删除的子键的名称传递给Remove方法。接着,将Cookie添加到Cookies集合,这样Cookie便会以修改后的格式发送回浏览器。如下面的代码所示:


HttpCookie aCookie=Request.Cookies["book"];

aCookie.Values.Remove("Name");

aCookie.Expires=DateTime.Now.AddDays(1);

Response.Cookies.Add(aCookie);


17.5.6 Cookie的优点与局限性

Cookie可以用于在客户端上存储少量经常更改的信息,这些信息与请求一起发送到服务器。因此,使用Cookie具有如下优点:

1)可配置到期规则。Cookie可以在浏览器会话结束时到期,或者可以在客户端计算机上无限期存在,这取决于客户端的到期规则。

2)不需要任何服务器资源。Cookie存储在客户端并在发送后由服务器读取。因此,它不需要任何服务器资源。

3)简单性。Cookie是一种基于文本的轻量结构,包含简单的键值对。

4)数据持久性。虽然客户端计算机上Cookie的持续时间取决于客户端上的Cookie过期处理和用户干预,Cookie通常是客户端上持续时间最长的数据保留形式。

当然,除了上面这些优点之外,Cookie也存在着一些局限性:

1)大小受到限制。尽管在当今新的浏览器和客户端设备版本中,支持8192字节的Cookie大小已愈发常见。但是,大多数浏览器对Cookie的大小还是只有4096字节的限制。

2)用户配置为禁用。如果用户禁用了浏览器或客户端设备接收Cookie的能力,则限制了这一功能。

3)潜在的安全风险。Cookie可能会被篡改。用户可能会操纵其计算机上的Cookie,这意味着会对安全性造成潜在风险或者导致依赖于Cookie的应用程序失败。另外,虽然Cookie只能被将它们发送到客户端的域访问,但是黑客们稍微作一些手脚,就可以从用户计算机上的其他域访问Cookie。当然,可以采用手动加密和解密Cookie,但这需要额外的编码,并且加密和解密需要耗费一定的时间而影响应用程序的性能。

实际上,建议千万不要在Cookie中存储敏感信息,如用户名、密码、信用卡号等。不要在Cookie中放置任何不应由用户掌握的内容,也不要放置可能被其他窃取Cookie的人控制的内容。