作者:陈广 日期:2018-11-28
Cookie 是饼干、甜品的意思,中文翻译为曲奇。网络早期早大的问题之一是如何管理状态。前面我们已经说过 HTTP 协议是无状态的,一旦客户端和服务器数据交换完毕,就会断开连接,再次请求,会重新连接。这就是说,服务器无法知道两个请求是否来自同一浏览器。当一个浏览器向服务器发送多个请求以进行一系列相关联的操作时,处理非常麻烦,需要在页面中使用表单或通过 URL 进行参数传递。这两个解决方案都是手动操作,容易出错。网景公司当时一名员工 Lou Montulli 在 1994 年将 Cookie 的概念应用于网络通信,用来维持用户和跟踪用户信息。假设每次有新用户请求时,服务器给它颁发一个身份证,下次访问时,必须带上身份证,这样服务器就会知道是谁来访问了,这就是 cookie 原理。cookie 是和 session 配合使用的,cookie 用于客户端,session 用于服务器端。
为了让大家理解 Cookie 是如何工作的,我们来做一个小实验。
新建一个 Web 文件夹,右键菜单上选择【Open with Code】打开。新建一个名为 index.html 的文件,输入代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Cookie 示例</title>
</head>
<body>
这是第一个页面<br>
<button id="Btn" onclick="btnSetCookie()">设置 Cookie</button><br>
<a href="second.html">跳转至第二个页面</a>
</body>
<script>
function btnSetCookie() {
document.cookie = "uerName=20181126";
}
</script>
</html>
此页面有一个按钮,在按钮事件中,我们设置了 cookie。JavaScript 代码 document.cookie = "user=20181126";
表示添加一个 cookie,键为user
,值为20181126
。
页面中还有一个链接,它可以跳转至 second.html。
接下来新建一个名为 second.html 的文件,输入代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
这是第二个页面<br>
<a href="three.html">跳转至第三个页面</a>
</body>
</html>
此页面有一个跳转至 three.html 的链接。
新建一个名为 three.html 的文件,输入代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
这是第三个页面<br>
<a href="four.html">跳转至第四个页面</a>
</body>
</html>
第三个页面和第二个页面类似,只是链接会跳转至第四个页面。
最后新建一个名为 four.html 的文件,输入代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
这是第四个页面
</body>
</html>
按下【Ctrl + F5】快捷键运行程序(必须先安装 IIS Express 扩展,参考《HTTP 协议》这篇文章)。单击【设置 Cookie】按钮以添加一个 Cookie。按下【F12】打开开发者工具,选中【网络】和【标头】选项卡。然后单击【跳转至第二个页面】链接。观察开发者工具,如图1所示:
我们发现,在请求 header 中,我们添加的 cookie 项user=20181126
已经存在,继续点击链接跳转至第三及第四个页面,查看 header,cookie依旧存在。这说明了我们只需在一个页面中设置了 cookie,之后的一系列请求中,都会自动带上这个 cookie。
接下来关闭浏览器再重新打开,在不点击【设置 Cookie】按钮的情况下点击跳转链接,我们可以发现,header 中不再带有 Cookie 项。这说明,关闭浏览器后 cookie 失效了。
试想,在没有 cookie 的情况下,如果每个页面都需要向服务器发送user=20181126
,那么每个页面都要在表单或 URL 中加入参数,这会是多么麻烦的一件事。现在只需一次设置,在不关闭浏览器的情况下,后继所有请求都会自动带上这个值,这给我们编写程序带来了很大的便利。
Cookie 是一小段文本信息,伴随着用户请求和页面在浏览器和 Web 服务器之间传递。Cookie 是一种 HTTP Header,以键值对的形式组成,比如user=20181126
。两个 Cookie 之间用分号隔开,比如user=20181126;pwd=abcdefg
。
浏览器将 Cookie 通过 HTTP 请求中的 Header 发送给 Web 服务器,如:Cookie:user=20181126
。Web 服务器通过 HTTP 响应中的 Header 把 Cookie 发送给浏览器。如:Set-Cookie: user=20181126
。
我们使用 ASP.NET Core MVC 来做一个例子,看看 ASP.NET Core 是如何处理 Cookie 的。这次的程序比第一个程序更为复杂了,也顺便演示了 MVC 程序的简单编写方法。
新建一个 Cookie 文件夹,在右键菜单中选择【Open with Code】打开此文件夹。按下【Ctrl + ~】打开终端,输入命令 dotnet new empty
创建一个新的空白项目。打开 Properties 文件夹下的 launchSettings.json 文件,将sslPort
项的值更改为0
以关闭 HTTPS。将applicationUrl
项的值更改如下:
"applicationUrl": "http://localhost:5000",
接下来在Startup
类中添加 MVC 及路由服务,更改 Startup.cs 代码如下:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace Cookie
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvcWithDefaultRoute();
}
}
}
接下来添加控制器。在项目根目录中添加一个 Controllers 文件夹,并在其中新建一个 HomeController.cs 文件,输入代码如下:
using Microsoft.AspNetCore.Mvc;
namespace Cookie.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
//添加三个 cookie
HttpContext.Response.Cookies.Append("Item1", "value1");
HttpContext.Response.Cookies.Append("Item2", "value2");
HttpContext.Response.Cookies.Append("Item3", "value3");
return View();
}
public IActionResult Second()
{ //读取客户端传来的 Cookie 并存入变量 cookies 中
var cookies = HttpContext.Request.Cookies;
return View(cookies); //返回默认视图 Second.cshtml,并将 cookies 作为参数传递给视图
}
public IActionResult Three()
{ //读取客户端传来的 Cookie 并存入变量 cookies 中
var cookies = HttpContext.Request.Cookies;
return View(cookies); //返回默认视图 Three.cshtml,并将 cookies 作为参数传递给视图
}
}
}
此控制器对应了三个视图,Index
action 方法返回根视图,它使用HttpContext.Response.Cookies.Append
方法添加了三个 cookie,这三个方法会通过 HTTP 响应的 header 传递给浏览器。
第二个 action 方法Second
对应着 Second.cshtml 视图,它读取浏览器通过请求 header 传递过来的 cookie集合,并直接返回给 Second 视图以显示。HttpContext.Request.Cookies
表示 cookie 集合,它由KeyValuePair
键值对组成。
第三个 action 方法Three
对应着 Three.cshtml 视图,处理过程和Second
方法基本一样。
第一个示例使用的是 HTML 页面,后缀名为 .html。这次变为视图,后缀名为 .cshtml。视图允许加入 C# 语句,最终会被转换成 HTML 页面并传递给浏览器。
在项目根目录下新建 Views 文件夹,在 vscode 左侧【Explorer】(资源管理器)中的 Views 文件夹的右键菜单中选择【Open in Terminal】打开终端窗口(这样打开会使命令行自动跳至 Views 文件夹下),输入以下命令:
dotnet new viewimports
此命令会在 Views 文件夹下新建一个 _ViewImports.cshtml,在此文件中输入如下代码:
@using System.Collections.Generic
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
_ViewImports.cshtml 文件为视图导入文件,在此文件中导入的命名空间可以 Views 文件夹下的所有视图文件中共享。也就是说,在此处导入后,其它所有视图都不再需要添加导入命名空间语句。第一条语句导入等下视图中使用到的命名空间,第二条语句导入标签助手。
在 Views 文件夹下新建 Home 文件夹。然后在 Home 文件夹下新建 Index.cshtml 文件,并输入如下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<a asp-action="Second">跳转至第二个页面</a>
</body>
</html>
此视图为根视图,里面只存在一个跳转至第二个页面的链接。它所对应的 action 方法为Index
。
在 Views/Home 文件夹下新建 Second.cshtml 文件,输入如下代码:
@model Microsoft.AspNetCore.Http.IRequestCookieCollection
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h2>这是第二个页面</h2>
服务器读取的 Cookie 列表:<br>
@foreach (var c in Model)
{
<h3>@c.Key = @c.Value</h3>
}
<a asp-action="Three">跳转到第三个页面</a>
</body>
</html>
此视图为第二个页面所对应的视图文件,第一句@model
语句表明此视图将接受由 action 方法传递过来的IRequestCookieCollection
对象,并在后面通过Model
对象调用。action 方法的传递方式可通过查看 HomeController.cs 文件下的Second()
方法的返回语句:
return View(cookies);
此视图使用了 Razor 语句foreach
遍历所有 Cookie 并显示出来。视图的最后是一个跳转至第三个页面的链接。asp-action
属性是标签助手,指示此链接所对就的 action 方法为Three()
。
在 Views/Home 文件夹下新建 Three.cshtml 文件,输入如下代码:
@model Microsoft.AspNetCore.Http.IRequestCookieCollection
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h2>这是第三个页面</h2>
服务器读取的 Cookie 列表:<br>
@foreach (var c in Model)
{
<h3>@c.Key = @c.Value</h3>
}
</body>
</html>
此视图为第三个页面,与 Second.cshtml 文件基本类似,此处不再赘述。
选中【Debug】➤【Start Without Debugging】启动程序,打开开发者工具,查看响应标头,如下图所示:
可以看到,服务器在返回 Index 页面时,还通过响应标头传递了三个 Cookie。所使用的是Set-Cookie
header。
接下来点击【跳转至第二个页面】链接,结果如下图所示:
在请求第一个页面时,服务器设置了浏览器的 cookie,在之后的一系列请求中,浏览器都会在请求 header 中带上这个 cookie。这一点可以在开发者工具中的【请求标头】中查看到。ASP.NET Core 在收到此请求后,读取出 cookie 的值并放置在视图中返回给浏览器显示。
接下来点击【跳转至第三个页面】链接,结果与第二个页面类似。只是进一步验证了 cookie 会伴随之后的一系列操作。
写到这里,我就在想,这到底是讲 HTTP 协议还是讲 ASP.NET Core。画风有些不对啊!算是借 HTTP 的名义,带大家入 ASP.NET Core 的门吧。
这一系列操作下来,对于初学者来说,很容易头晕。画张图让大家理清下思路吧。
Cookie 的最主要作用是用来做用户认证,还可以用于保存用户的一些其他信息。Cookie 也可用于互联网精准广告定向技术,比如用户浏览了某些商品,就可以用 Cookie 将其记录下来,对网民所有的上网行为进行个性化深度分析;按广告主需求锁定目标受众,进行一对一传播,提供多通道投放,按照效果收费。
Cookie 有几个重要属性:
/
,就是根目录。假设同一个服务器上的目录如下:/test/
、/text/cd
、/test/dd
。现一个 Cookie1 的 path 为/test/
,Cookie2 的 path 为/text/cd
,那么 text 下的所有页面都可以访问到 Cookie1,而/test/dd
的子页面不能访问 Cookie2。这是因为 Cookie 只能让其 path 路径下的页面访问。示例很简单,只需将上例稍作更改即可。打开 HomeController.cs 文件,更改代码如下:
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
namespace Cookie.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
//添加三个 cookie
HttpContext.Response.Cookies.Append("Item1", "value1", new CookieOptions
{
Expires = DateTime.Now.AddSeconds(10), //过期时间为当前时间往后推10秒
HttpOnly = true
});
HttpContext.Response.Cookies.Append("Item2", "value2");
HttpContext.Response.Cookies.Append("Item3", "value3");
return View();
}
public IActionResult Second()
{ //读取客户端传来的 Cookie 并存入变量 cookies 中
var cookies = HttpContext.Request.Cookies;
return View(cookies); //返回默认视图 Second.cshtml,并将 cookies 作为参数传递给视图
}
public IActionResult Three()
{ //读取客户端传来的 Cookie 并存入变量 cookies 中
var cookies = HttpContext.Request.Cookies;
return View(cookies); //返回默认视图 Three.cshtml,并将 cookies 作为参数传递给视图
}
}
}
注意,添加了两个命名空间,并更改了Index
action 方法。将第一个 cookie 的过期时间设定为10秒过期,并将HttpOnly
属性设置为true
。
运行程序,在浏览器中打开开发者工具。查看响应标头,关于 Cookie 的内容如下:
Set-Cookie: Item1=value1; expires=Wed, 28 Nov 2018 02:49:50 GMT; path=/; samesite=lax; httponly
Set-Cookie: Item2=value2; path=/
Set-Cookie: Item3=value3; path=/
可以看到,第一个 cookie 的相应属性已经设置完毕。确保已经打开页面 10 秒后,我们再点击【跳转至第二个页面】链接,此时可以发现,Item1 已不再显示,表示第一个 cookie 已经失效。如下图所示: