C#网络编程

Cookie

作者:陈广 日期:2018-11-28


Cookie 是饼干、甜品的意思,中文翻译为曲奇。网络早期早大的问题之一是如何管理状态。前面我们已经说过 HTTP 协议是无状态的,一旦客户端和服务器数据交换完毕,就会断开连接,再次请求,会重新连接。这就是说,服务器无法知道两个请求是否来自同一浏览器。当一个浏览器向服务器发送多个请求以进行一系列相关联的操作时,处理非常麻烦,需要在页面中使用表单或通过 URL 进行参数传递。这两个解决方案都是手动操作,容易出错。网景公司当时一名员工 Lou Montulli 在 1994 年将 Cookie 的概念应用于网络通信,用来维持用户和跟踪用户信息。假设每次有新用户请求时,服务器给它颁发一个身份证,下次访问时,必须带上身份证,这样服务器就会知道是谁来访问了,这就是 cookie 原理。cookie 是和 session 配合使用的,cookie 用于客户端,session 用于服务器端。

Cookie 是如何工作的

为了让大家理解 Cookie 是如何工作的,我们来做一个小实验。

创建页面

index.html

新建一个 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

接下来新建一个名为 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

新建一个名为 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

最后新建一个名为 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所示:

图 1: 查看 header 中的 cookie

我们发现,在请求 header 中,我们添加的 cookie 项user=20181126已经存在,继续点击链接跳转至第三及第四个页面,查看 header,cookie依旧存在。这说明了我们只需在一个页面中设置了 cookie,之后的一系列请求中,都会自动带上这个 cookie。

接下来关闭浏览器再重新打开,在不点击【设置 Cookie】按钮的情况下点击跳转链接,我们可以发现,header 中不再带有 Cookie 项。这说明,关闭浏览器后 cookie 失效了。

试想,在没有 cookie 的情况下,如果每个页面都需要向服务器发送user=20181126,那么每个页面都要在表单或 URL 中加入参数,这会是多么麻烦的一件事。现在只需一次设置,在不关闭浏览器的情况下,后继所有请求都会自动带上这个值,这给我们编写程序带来了很大的便利。

服务器端的 Cookie 操作

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 页面并传递给浏览器。

_ViewImports.cshtml

在项目根目录下新建 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 文件夹下的所有视图文件中共享。也就是说,在此处导入后,其它所有视图都不再需要添加导入命名空间语句。第一条语句导入等下视图中使用到的命名空间,第二条语句导入标签助手。

Index.cshtml

在 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

Second.cshtml

在 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()

Three.cshtml

在 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】启动程序,打开开发者工具,查看响应标头,如下图所示:

图 2: 根页面响应 header 中的 cookie

可以看到,服务器在返回 Index 页面时,还通过响应标头传递了三个 Cookie。所使用的是Set-Cookie header。

接下来点击【跳转至第二个页面】链接,结果如下图所示:

图 3: 第二个页面

在请求第一个页面时,服务器设置了浏览器的 cookie,在之后的一系列请求中,浏览器都会在请求 header 中带上这个 cookie。这一点可以在开发者工具中的【请求标头】中查看到。ASP.NET Core 在收到此请求后,读取出 cookie 的值并放置在视图中返回给浏览器显示。

接下来点击【跳转至第三个页面】链接,结果与第二个页面类似。只是进一步验证了 cookie 会伴随之后的一系列操作。

写到这里,我就在想,这到底是讲 HTTP 协议还是讲 ASP.NET Core。画风有些不对啊!算是借 HTTP 的名义,带大家入 ASP.NET Core 的门吧。

流程分析

这一系列操作下来,对于初学者来说,很容易头晕。画张图让大家理清下思路吧。

图 4: 程序运行时序图

Cookie 的作用

Cookie 的最主要作用是用来做用户认证,还可以用于保存用户的一些其他信息。Cookie 也可用于互联网精准广告定向技术,比如用户浏览了某些商品,就可以用 Cookie 将其记录下来,对网民所有的上网行为进行个性化深度分析;按广告主需求锁定目标受众,进行一对一传播,提供多通道投放,按照效果收费。

Cookie 的属性

Cookie 有几个重要属性:

  • Expires:Expires 的值是一个时间,代表过期时间。过了这个时间,该 Cookie 就失效了。如果不指定 Expirers,表示关闭浏览器/页面的时候,此 Cookie 就应该被浏览器删除了。
  • Path:表示 Cookie 所属的路径,ASP.NET 默认为/,就是根目录。假设同一个服务器上的目录如下:/test//text/cd/test/dd。现一个 Cookie1 的 path 为/test/,Cookie2 的 path 为/text/cd,那么 text 下的所有页面都可以访问到 Cookie1,而/test/dd的子页面不能访问 Cookie2。这是因为 Cookie 只能让其 path 路径下的页面访问。
  • HttpOnly:这个是关于安全方面的属性,将一个 Cookie 设置为 HttpOnly 后,通过 JavaScript 脚本将无法读取到 Cookie 信息,这能有效地防止黑客用 XSS 发起攻击。一般来说,跟登录相关的 Cookie 必须设置为 HttpOnly。

设置 Cookie 属性示例

示例很简单,只需将上例稍作更改即可。打开 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 已经失效。如下图所示:

图 5: Cookie 的过期
;

© 2018 - IOT小分队文章发布系统 v0.3