那些刚刚进行Web编程的开发人员,总是难以理解维护应用程序状态的问题。万维网最基础的协议HTTP协议是一个无状态协议,也就是说,从Web服务器端的角度来看,每一个访问请求都是一个新的用户。HTTP协议不提供任何方法来决定两个访问请求是否是同一个用户发出的。
然而,维护状态对任何Web应用程序来说都是非常重要的。购物车是一个经典的例子。如果希望在多个页面请求之间关联一个购物车和某个用户,就需要一些维护状态的方法。
这一系列关注三种包含在ASP.NET 2.0 Framework中的、在多个页面请求间关联数据和特定的用户的方法。在第一部分,你将了解到如何创建和处理浏览器cookie。浏览器cookie用于关联一些文本到网站的每一个用户。
接着,你将了解如何使用会话状态(session state)。会话状态用于关联任意类型的对象到任何用户。例如,可以存储购物车对象到会话状态中。
你将了解到如何使用不依赖cookie的会话状态,这样,即使浏览器禁用了cookie,你也能使用会话状态。你还将了解到如何通过开启进程外会话状态,从而让会话状态更健壮(robust)。
最后,我们介绍ASP.NET 2.0 Framework引入的一个新特性:profile对象。profile对象提供一个创建强类型和持久化方式的会话状态的方法。
你将了解到定义用户配置文件的不同方法,以及如何在一个组件内使用profile对象。最后,你将了解如何实现一个自定义profile提供程序。
1.1 使用浏览器cookie
cookie是由第一个版本的Netscape浏览器引入。Netscape的开发人员发明了cookie来解决当时折磨互联网的一个问题,那就是没办法赚钱,因为没办法创建一个购物车。
注解:可以从http://home.netscape.com/newsref/std/cookie_spec.html 阅读Netscape的原始cookie定义。
先讲讲cookie是如何工作的。当Web服务器端创建一个cookie时,一个附加的HTTP首部在浏览器显示页面时被发送到浏览器。HTTP首部类似如下形式:
Set-Cookie: message=Hello
Set-Cookie首部使得浏览器创建一个名为message的cookie,包含值Hello。
在浏览器创建cookie后,它从相同的应用程序请求页面时,都将像下面这样发送这个HTTP首部:
Cookie:message=Hello
cookie头包含所有的Web服务器端设置的cookie。每次浏览器向Web服务器端请求页面时,cookie都会发送回服务器端。
注意,一个cookie就是一段文本。cookie只能用于存储字符串值。
实际上,我们可以存储两种类型的cookie:会话cookie和持久化cookie。一个会话cookie只存在于内存中。当用户关闭浏览器时,会话cookie就永远消失了。
而持久化cookie可以存在几个月甚至几年。当创建一个持久化cookie时,cookie被浏览器长久存在用户的电脑上。以IE为例,cookie以一组文本文件的形式保存在下面的文件夹:
\Documents and Settings\[user]\Cookies
另一方面,对于Mozilla Firefox浏览器则将cookie保存在下面的文件:
\Documents and Settings\[user]\Application Data\Mozilla\Firefox\Profiles\➥[random folder
name]\Cookies.txt
因为不同的浏览器将cookie存于不同的位置,cookie是浏览器独立的。使用IE浏览器请求页面并创建的cookie,不存在于Firefox或Opera浏览器中。
此外,应注意IE和Firefox浏览器都以明文形式存储cookie。我们不应该将诸如社会保险号,或信用卡号码之类的敏感信息存于cookie中。
注解 cookie这个名称从哪里来?根据Netscape最初的cookie文档,cookie并不是因为什么特别的原因被选中的。然而,这个名称最有可能继承自UNIX世界,类似“magic cookie”,在程序间传递的一种不透明的令牌。
1.1.1 cookie的安全性限制
cookie会涉及安全方面的问题。当创建一个持久化cookie时,我们会修改访问者电脑上的文件。有些人就是一天到晚梦想着对你的电脑做些坏事。为了避免电脑遭到袭击,浏览器对cookie有一些强制的安全限制。
首先,所有的cookie是域名独立的。Amazon网站设置的cookie,巴诺书店网站访问不到。浏览器创建一个cookie时,会记录关联到cookie的域名,不会将其发送到另一个域名。
注解 一个包含在Web页面的图片可能来自和页面不同的另一个域名。当浏览器请求一个图片时,cookie可能被另一个域名设置。诸如DoubleClick这样的公司,就利用这样的后门在多个不同的页面间跟踪并显示广告统计信息。这种类型的cookie就称为第三方cookie。
另一个浏览器存储cookie的重要限制是其大小的限制。一个域名存储的cookie总大小不能超过4 096byte。这个大小限制,包含所有的cookie名称和值在内。
注解:IE5.0以上版本支持一个名叫userData行为的功能。userData行为允许存储远大于cookie的数据(对局域网10 240KB,对因特网站点1 024KB)。要了解更多关于userData行为的信息,请访问微软MSDN网站(msdn.microsoft.com)。
最后,大多数浏览器都限制可以被设置的cookie数量,一个域名不超过20个cookie(不包括IE浏览器)。超过这个数目,旧的cookie会被自动删除。
注解:美国白宫管理和财政办公室阻止所有的联邦网站创建持久化cookie,除非“强制需要”。见http://www.whitehouse.gov/omb/memoranda/m00-13.html。
美国国家安全应急中心网站(www.nsa.gov)最近就遇到了创建持久化cookie的问题。他们在收到来自隐私倡议的阻止警告后停止了使用持久化cookie。
个人认为这样的cookie偏执策略有点疯狂,不过我们还是要注意它。
因为所有关联到cookie的安全性方面的因素,所有的现代浏览器给用户提供了禁用cookie的选项。这意味着,除非在构建局域网应用程序时我们控制每个人的浏览器,其他时候的默认情况下,不应该企图依赖cookie。我们应尽量只在存储非关键信息时使用cookie。
据说,ASP.NET Framework的许多部分依赖cookie。例如,Web部件、表单验证、会话状态和匿名用户配置文件默认情况下都依赖cookie。如果你使用这些功能中的任何一个,就必须使用cookie。
并且,许多网站也依赖cookie。Yahoo!和MSDN网站的许多网页如果没有开启cookie就访问不了。换句话说,需要访问者开启cookie才能访问我们的网站,并不完全是不合理的要求。
1.1.2 创建cookie
可以通过给Response.Cookies集合添加cookie来创建新的cookie。Response.Cookies集合包含所有Web服务器端发送到Web浏览器的cookie。
例如,代码清单1-1所示的页面将创建一个新的名叫Message的cookie。该页面包含一个输入Message这个cookie的值的表单(见图1-1)。
图1-1 创建cookie
代码清单1-1 SetCookie.aspx
<
%@ Page
Language
="C#"
%
>
<!
DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<
script
runat
="server"
>
protected void btnAdd_Click(object sender, EventArgs e)
{
Response.Cookies["message"].Value = txtCookieValue.Text;
}
</
script
>
<
html
xmlns
="http://www.w3.org/1999/xhtml"
>
<
head
id
="Head1"
runat
="server"
>
<
title
>
Set Cookie
</
title
>
</
head
>
<
body
>
<
form
id
="form1"
runat
="server"
>
<
div
>
<
asp:Label
id
="lblCookieValue"
Text
="Cookie Value:"
AssociatedControlID
="txtCookieValue"
Runat
="server"
/>
<
asp:TextBox
id
="txtCookieValue"
Runat
="server"
/>
<
asp:Button
id
="btnAdd"
Text
="Add Value"
OnClick
="btnAdd_Click"
Runat
="server"
/>
</
div
>
</
form
>
</
body
>
</
html
>
注意,cookie的名称是大小写敏感的。设置一个名叫message的cookie和设置一个名叫Message的cookie是不同的。
如果希望修改代码清单1-1所示的页面创建的cookie,可以打开页面,并给Message这个cookie输入一个新的值。当Web服务器端将它发送到浏览器时,修改的cookie值就会被设置到浏览器。
代码清单1-1所示的页面创建的是一个会话cookie(session cookie)。当关闭Web浏览器时,该cookie就消失了。如果希望创建持久化cookie(persistent cookie),则需要为cookie指定一个过期时间。
代码清单1-2所示的页面创建了一个持久化cookie。
代码清单1-2 SetPersistentCookie.aspx
<
%@ Page
Language
="C#"
%
>
<!
DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<
script
runat
="server"
>
void Page_Load()
{
// Get current value of cookie
int counter = 0;
if (Request.Cookies["counter"] != null)
counter = Int32.Parse(Request.Cookies["counter"].Value);
// Increment counter
counter++;
// Add persistent cookie to browser
Response.Cookies["counter"].Value = counter.ToString();
Response.Cookies["counter"].Expires = DateTime.Now.AddYears(2);
// Display value of counter cookie
lblCounter.Text = counter.ToString();
}
</
script
>
<
html
xmlns
="http://www.w3.org/1999/xhtml"
>
<
head
id
="Head1"
runat
="server"
>
<
title
>
Set Persistent Cookie
</
title
>
</
head
>
<
body
>
<
form
id
="form1"
runat
="server"
>
<
div
>
You have visited this page
<
asp:Label
id
="lblCounter"
Runat
="server"
/>
times!
</
div
>
</
form
>
</
body
>
</
html
>
代码清单1-2所示的页面记录页面被请求的次数。一个名叫counter的持久化cookie被用来记录页面被请求的次数。注意,counter的过期时间被设为2年以后。当为某个cookie设置了过期时间后,cookie就被保存为持久化cookie了。
1.1.3 读取cookie
可以使用Response.Cookies集合创建和修改cookie,也可以使用Request.Cookies集合读取cookie值。
例如,代码清单1-3所示的页面读取message cookie的值。
代码清单1-3 GetCookie.aspx
<
%@ Page
Language
="C#"
%
>
<!
DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<
script
runat
="server"
>
void Page_Load()
{
if (Request.Cookies["message"] != null)
lblCookieValue.Text = Request.Cookies["message"].Value;
}
</
script
>
<
html
xmlns
="http://www.w3.org/1999/xhtml"
>
<
head
id
="Head1"
runat
="server"
>
<
title
>
Get Cookie
</
title
>
</
head
>
<
body
>
<
form
id
="form1"
runat
="server"
>
<
div
>
The value of the message cookie is:
<
asp:Label
id
="lblCookieValue"
Runat
="server"
/>
</
div
>
</
form
>
</
body
>
</
html
>
在代码清单1-3中,IsNothing()函数用来在读取cookie值之前检查cookie是否存在。如果不包含该检查,就有可能获得一个空引用异常。同样,不要忘记cookie的名称是大小写敏感的。
代码清单1-4所示的页面列出包含在Request.Cookies集合中的所有cookie(见图1-2)。
图1-2 显示所有cookie列表
代码清单1-4 GetAllCookies.aspx
<
%@ Page
Language
="C#"
%
>
<!
DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<
script
runat
="server"
>
void Page_Load()
{
ArrayList colCookies = new ArrayList();
for (int i = 0; i
<
Request.Cookies.Count
; i++)
colCookies.Add(Request.Cookies[i]);
grdCookies.DataSource
= colCookies;
grdCookies.DataBind();
}
</script
>
<
html
xmlns
="http://www.w3.org/1999/xhtml"
>
<
head
id
="Head1"
runat
="server"
>
<
title
>
Get All Cookies
</
title
>
</
head
>
<
body
>
<
form
id
="form1"
runat
="server"
>
<
div
>
<
asp:GridView
id
="grdCookies"
Runat
="server"
/>
</
div
>
</
form
>
</
body
>
</
html
>
注意,从Request.Cookies集合迭代获得的有意义的信息只有HasKey,Name和Value属性。其他列都显示了不正确的信息。例如,Expires列总是显示最小日期。浏览器不与页面请求交互这些额外的属性,所以也就获取不了这些属性的值。
使用Request.Cookies集合时,非常重要的一点是,理解For...Each循环返回的值和For...Next循环返回的值不同。如果用For...Each循环迭代Request.Cookies集合,返回的是cookies的名称。如果用For...Next循环迭代该集合,返回的是HeepCookie类(下一节介绍)的示例。
1.1.4 设置cookie属性
HttpCookie类代表cookie。当创建或读取一个cookie时,可以使用该类的下面这些属性:
q Domain——用于设置关联到cookie的域名,默认值是当前域名;
q Expires——用于通过给定一个过期时间创建一个持久化cookie;
q HasKeys——用于指定该cookie是否是一个多值cookie(见本章稍后的3.1.6节);
q HttpOnly——用于避免cookie被JavaScript访问;
q Name——用户指定cookie的名称;
q Path——用于指定关联到cookie的路径。默认值为/;
q Secure——用于指定cookie需要通过安全Socket层(SSL)连接传递;
q Value——允许读/写cookie的值;
q Values——当使用多值cookie时,用于读/写特定的值(见本章稍后的3.1.6节)。
这些属性中的一部分需要更多的解释。例如,你可能发现Domain属性有些奇怪,因为你修改不了关联到cookie的domain。
Domain属性对于组织子域名时会非常有用。如果需要设置cookie可以被Sales.MyCompany.com,Managers.MyCompany.com和Support.MyCompany.com访问,则需要设置Domain属性值为.MyCompany.com(注意开头的部分),而不能使用该属性关联cookie到一个完全不同的域名。
HttpOnly属性用户设置一个cookie是否可以通过JavaScript访问。该属性只对IE6(SP1)级以上版本有效。引入该属性是为了防止跨站点脚本攻击。
Path属性用于限定cookie到一个特定的路径。例如,如果在相同的域名部署多个应用程序,而不希望应用程序共享相同的cookie,则需要设置Path属性避免一个应用程序读取另一个应用程序的cookie。
Path属性听起来很有用。不幸的是,你不应该使用它。IE对于路径是大小写敏感的。如果用户在地址栏输入一个不同大小写的路径,则cookie不会被发送。换句话说,下面两个路径并不匹配:
http://localhost/original/GetAllCookies.aspx
http://localhost/ORIGINAL/GetAllCookies.aspx
1.1.5 删除cookie
删除一个cookie的方法并不直观。要删除一个存在的cookie,必须设置其过期时间为一个过去的时间。
代码清单1-5所示的页面演示了如何删除一个单值cookie。页面包含一个用于输入cookie名称的表单。当提交表单时,指定名称的cookie就被删除了。
代码清单1-5 DeleteCookie.aspx
<
%@ Page
Language
="C#"
%
>
<!
DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<
script
runat
="server"
>
protected void btnDelete_Click(object sender, EventArgs e)
{
Response.Cookies[txtCookieName.Text].Expires = DateTime.Now.AddDays(-1);
}
</
script
>
<
html
xmlns
="http://www.w3.org/1999/xhtml"
>
<
head
id
="Head1"
runat
="server"
>
<
title
>
Delete Cookie
</
title
>
</
head
>
<
body
>
<
form
id
="form1"
runat
="server"
>
<
div
>
<
asp:Label
id
="lblCookieName"
Text
="Cookie Name:"
AssociatedControlID
="txtCookieName"
Runat
="server"
/>
<
asp:TextBox
id
="txtCookieName"
Runat
="server"
/>
<
asp:Button
id
="btnDelete"
Text
="Delete Cookie"
OnClick
="btnDelete_Click"
Runat
="server"
/>
</
div
>
</
form
>
</
body
>
</
html
>
当需要删除cookie时设置的特定的日期只要是一个过去的值,无所谓实际的值是多少。在代码清单1-5中,过期时间被设为一天前。
代码清单1-6所示的页面从浏览器删除当前域名(和路径)下的所有cookie。
代码清单1-6 DeleteAllCookies.aspx
<
%@ Page
Language
="C#"
%
>
<!
DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<
script
runat
="server"
>
void Page_Load()
{
string[] cookies = Request.Cookies.AllKeys;
foreach (string cookie in cookies)
{
BulletedList1.Items.Add("Deleting " + cookie);
Response.Cookies[cookie].Expires = DateTime.Now.AddDays(-1);
}
}
</
script
>
<
html
xmlns
="http://www.w3.org/1999/xhtml"
>
<
head
id
="Head1"
runat
="server"
>
<
title
>
Delete All Cookies
</
title
>
</
head
>
<
body
>
<
form
id
="form1"
runat
="server"
>
<
div
>
<
h1
>
Delete All Cookies
</
h1
>
<
asp:BulletedList
id
="BulletedList1"
EnableViewState
="false"
Runat
="server"
/>
</
div
>
</
form
>
</
body
>
</
html
>
代码清单1-6所示的页面从Request.Cookies集合中循环遍历并删除所有的cookie。
1.1.6 使用多值cookie
根据cookie规范,对单个域名,浏览器不能存储超过20个cookie。可以通过创建多值cookie来超越该限制。多值cookie是一个包含子键的单一cookie。可以根据需要创建任意数量的子键。
例如,代码清单1-7所示的页面创建了一个名叫preferences的多值cookie。preferences cookie用于存储first name(名)、last name(姓)和favorite color(喜爱的色彩)(见图1-3)。
图1-3 创建多值cookie
代码清单1-7 SetCookieValues.aspx
<
%@ Page
Language
="C#"
%
>
<!
DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<
script
runat
="server"
>
void btnSubmit_Click(Object s, EventArgs e)
{
Response.Cookies["preferences"]["firstName"] = txtFirstName.Text;
Response.Cookies["preferences"]["lastName"] = txtLastName.Text;
Response.Cookies["preferences"]["favoriteColor"] = txtFavoriteColor.Text;
Response.Cookies["preferences"].Expires = DateTime.MaxValue;
}
</
script
>
<
html
xmlns
="http://www.w3.org/1999/xhtml"
>
<
head
id
="Head1"
runat
="server"
>
<
title
>
Set Cookie Values
</
title
>
</
head
>
<
body
>
<
form
id
="form1"
runat
="server"
>
<
div
>
<
asp:Label
id
="lblFirstName"
Text
="First Name:"
AssociatedControlID
="txtFirstName"
Runat
="server"
/>
<
br
/>
<
asp:TextBox
id
="txtFirstName"
Runat
="server"
/>
<
br
/><
br
/>
<
asp:Label
id
="lblLastName"
Text
="Last Name:"
AssociatedControlID
="txtFirstName"
Runat
="server"
/>
<
br
/>
<
asp:TextBox
id
="txtLastName"
Runat
="server"
/>
<
br
/><
br
/>
<
asp:Label
id
="lblFavoriteColor"
Text
="Favorite Color:"
AssociatedControlID
="txtFavoriteColor"
Runat
="server"
/>
<
br
/>
<
asp:TextBox
id
="txtFavoriteColor"
Runat
="server"
/>
<
br
/><
br
/>
<
asp:Button
id
="btnSubmit"
Text
="Submit"
OnClick
="btnSubmit_Click"
Runat
="server"
/>
</
div
>
</
form
>
</
body
>
</
html
>
当提交代码清单1-7所示的页面时,下面的HTTP首部被发送到浏览器:
Set-Cookie: preferences=firstName=Steve&lastName=Walther&favoriteColor=green;
expires=Fri, 31-Dec-9999 23:59:59 GMT; path=/
代码清单1-8所示的页面从preferences cookie读取值。
代码清单1-8 GetCookieValues.aspx
<
%@ Page
Language
="C#"
%
>
<!
DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<
script
runat
="server"
>
void Page_Load()
{
if (Request.Cookies["preferences"] != null)
{
lblFirstName.Text = Request.Cookies["preferences"]["firstName"];
lblLastName.Text = Request.Cookies["preferences"]["lastName"];
lblFavoriteColor.Text = Request.Cookies["preferences"]["favoriteColor"];
}
}
</
script
>
<
html
xmlns
="http://www.w3.org/1999/xhtml"
>
<
head
id
="Head1"
runat
="server"
>
<
title
>
Get Cookie Values
</
title
>
</
head
>
<
body
>
<
form
id
="form1"
runat
="server"
>
<
div
>
First Name:
<
asp:Label
id
="lblFirstName"
Runat
="server"
/>
<
br
/>
Last Name:
<
asp:Label
id
="lblLastName"
Runat
="server"
/>
<
br
/>
Favorite Color:
<
asp:Label
id
="lblFavoriteColor"
Runat
="server"
/>
</
div
>
</
form
>
</
body
>
</
html
>
可以使用HttpCookie.HasKey属性判断一个cookie是一个普通cookie还是一个多值cookie。