专栏首页多云转晴cookie 和 session 原理

cookie 和 session 原理

cookie 和 session

工作流程

说一下 cookie 与 session 搭配的工作流程。

cookie + session 是为了保存用户状态信息的。比如这个用户是否已经登陆,如果登陆了就给这个用户推送一些信息,比如他最近买一些东西、他的购物车、他最近看过的文章或视频等信息。因为 http 是无状态的,所谓的无状态就是说每次请求完成后,不会在客户端和服务器上保存任何的信息。对于客户端和服务器而言,根本就不知道上次请求的信息是什么,甚至不知道本次连接的对端是不是上次连接的那一端。也就是说即使该用户登录了,但 HTTP 本身并不知道是哪个用户登陆了,HTTP 只处理请求与相应。因此如何知道一个用户登录了之后,后端能知道是哪个用户登录了,这是一个问题。

后端只有知道了是哪个用户登陆了,才能比较准确的向该用户推送该用户想了解的内容。因此需要登录验证。

当一个用户使用用户名以及密码登录后,后端可以凭借用户名(用户名必须是唯一的,不然怎么区分是哪个用户,用户名一般是唯一的字符串,比如手机号、邮箱、QQ 号等)去数据库查找该用户的数据,然后将数据返回给前端。

而 cookie 又扮演着一个什么角色呢?

当你登录了之后,比如你登陆了 哔哩哔哩 网站,你在上面看了两个小时的番剧。下一次你又登上哔哩哔哩,可以通过最近浏览找到最近观看的视频,然后点击还没看完的番剧,就可以接着上次的观看记录继续观看番剧了。这是怎么做到的呢?

上面已经说了,HTTP 是无状态的,它只做请求与相应。你观看了这个视频后下次登录还可以“精确”的了解到你看了哪些视频最近甚至看到哪里了。这是因为有 cookie 这样的技术。

虽然你登录了,但是前端每次请求数据时如果不把你的用户信息带上,后端还是没办法知道你是谁。但是如果每次发出请求都把用户信息带上会不会太麻烦,这样每次都要写重复的逻辑。于是 cookie 技术出现了。

cookie 是客户端保存状态的技术,它其实就是一个可以保存字符串数据的微型“数据库”,最大存储一般不能超过 4KB。前面已经说了,如果没有 cookie,前端总是需要把用户信息带上发给服务端这是很麻烦的,而 cookie 技术可以自动把存储在 cookie 发给后端在每次请求时。

当第一次请求一个网站时,网站后台生成一个 cookie 发给前端,前端将 cookie 存储在本地,当下一次(除了第一次,因为第一次前端还没有收到 cookie)前端网络请求时,总是会把 cookie 自动(不要手动操作)发给后端,后端做检验之后发出相应的数据。

上面已经说了,cookie 相当于一个字符串(只能存储字符串)数据库,因此你可以往 cookie 中放入一些信息(这些信息一般是后端有写的操作,而前端什么都不能操作,这是为了做安全考虑,下面会说到)。

cookie机制

前端一般不会去操作 cookie,而只接收后端发来的 cookie。这是为了考虑到安全性。通过上面图片也能看出,前端只存储 cookie 一般不会亲自操作 cookie(当然,也可以操作,但一般是禁止的),而后端一般只操作 cookie 不存储 cookie。

每次请求过来时 cookie 是记录在 request 信息当中的。拿 Node.js 来说,后端操作 cookie 可以通过 request.headers.cookie 获得。使用 response.setHeader("Set-Cookie",[name="xxx";Path=/;]) 的形式来设置 cookie 数据。

const http = require("http");
const fs = require("fs");
const utils = require("util");

const readFile = utils.promisify(fs.readFile);

var server = http.createServer(async (req,res) => {
    if(req.url === "/"){
        // 获取 cookievar cookies = req.headers.cookie;
        console.log(cookies);
        // 设置 cookie
        res.setHeader("Set-Cookie",[`name=ming;HttpOnly;Path=/;Max-Age=${60 * 60 * 24};`,"value=12345678;"]);
        var html = await readFile("./index.html",{encoding:'utf8'});
        res.setHeader("content-type","text/html");
        res.end(html);
    }
});

server.listen(8080,() => {
    console.log("Server is running: http://localhost:8080");
});

上面带有写完后,启动服务,打开浏览器。你会发现服务端的终端会打印 undefined。这是因为首次请求服务器时,前端是没有 cookie 的,首次请求后端会把 cookie 写好然后发给前端。当你再次刷新前端页面时,后端的控制台就会打印出 cookie 字符串。

使用 response.setHeader来设置 cookie。上面已经看到 cookie 有一些 ;隔开字符。cookie 的数据以 key=value 的形式表示数据。而有些数据是配置数据,用来配置 cookie。配置项如下:

  • HttpOnly 这个设置上之后,前端没有办法再用脚本(JavaScript)获取(获取都获取不到,更别提操作了) cookie;
  • Max-Age 这是设置 cookie 的过期时间,是个相对时间,即相对于当前设置 cookie 的时间而过期。60 _ 60 _ 24 表示一天(秒为单位)。
  • Path 表示你设置的 cookie 作用的路由区域,如果是 /(默认),表示网站的每个路由都能获取到 cookie;
  • Expires 这个也是设置过期时间,不同于 Max-Age,该设置项是绝对时间,比如:new Date(new Date() + 60 * 60 * 1000 * 24) 表示一天之后过期;
  • name=ming 这是我们设置的数据,你可以看到,Set-Cookie 后面是个数组,数组的每一项是一个 cookie;

上面代码我们设置了两个 cookie,打开浏览器可以看到 cookie 的数据:

set-cookie

上面介绍了怎么使用 cookie,那么 cookie 又如何保存用户状态呢?上面已经说了,每次请求发出(除了第一次),浏览器都会携带 cookie 发给服务端。因此我们可以把用户名(唯一的标识,比如邮箱、手机号)存到 cookie 当中,当请求收到后,后端就可以根据 cookie 当中的用户名来做相应的操作了。

于是一个简单的用户状态就实现了。这就是 cookie 的作用。

需要注意的是,如果不给 cookie 设置 ExpiresMax-Age,那么这个 cookie 就是一个会话 cookie,当浏览器关闭时这个 cookie 就会过期。而如果同时设置了这两个值,Max-Age 的优先级高于 Expires

session

说完了 cookie,那 session 又是干什么的呢?

通过上面对 cookie 的描述我们已经知道了,cookie 是保存在客户端的。如果我们将手机号、邮箱等这样的用户唯一标识保存在客户端的 cookie 中是很危险的。比如,有两个人,小明和小刚,他们都注册过同一个网站,而该网站将用户邮箱作为 cookie 数据来标识用户身份,如果小明想登录小刚的账号是有可能做到的。比如小明知道了小刚的邮箱,然后自己登录后,小明重新设置 cookie (客户端)将自己的邮箱设置成小刚的邮箱,这样做就成功切换成了小刚的账号。这就做到了信息篡改。

显然这是很危险的把重要的信息放在客户端。可以设置 cookie 为 HttpOnly,表示通过脚本获取不到 cookie。但是在浏览器或许获取不到,而在别的环境下可不一定啊。而且把手机号、邮箱这样的用户隐私暴露在外面是很可能会被窃取的。因此 cookie 中不能存放比较敏感的信息。于是引入了 session 的概念。

session 与其说是一种的技术,不如说是一套会话控制方式,因为 session 在不同的语言中的使用方法特点不尽相同。

session 不同于 cookie,他是保存在服务端的。因此相对于 cookie 要安全得多。为了明白 session 的作用是什么,我们还是接着上面的安全以及用户状态接着说。

上面已经提到,把用户敏感信息保存在前端是不太安全的。你的 cookie 一旦被窃取就有可能会泄露一些个人信息。而且 cookie 的存储大小也很小,最大不能超过 4KB,而是用 session 存储数据的话就不要担心这个问题了,在后端你想存多大都行。

考虑到这些方面,于是 session 出现了。session 一般要搭配 cookie 使用,当然也可以不搭配 cookie,比如前端也可以使用 localStoragesessionStorage 做数据存储,但一般 session 一般搭配 cookie 使用。

session 的工作原理如下:

session

通过上图可以看到,往 cookie 中存入的不再是用户信息,而是一个 sid,这个 sid 是使用加密算法生成的字符串,这个 sid 一般是不会改变的,后端不改变,前端也不去改变。后端在拿到 sid 字符串后,就可以获取到存在于服务端 session 当中的数据。

通俗一点说就是这样的:小明第一次访问该网站,这个网站会下发一个 sid 到客户端 cookie 当中(每个浏览器中的 sid 值一般都是不相同的),当小明登录网站后,会触发一些请求,请求就会把 cookie 当中携带的 sid 发到后端,后端根据 sid 找到对应的 session(数据),session 中存入的就是用户的一些操作(比如观看视频记录、购物车信息、用户 id 等等),也即状态。这样就做到了状态保持。

这里有几点需要注意,一是一般 sid 的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串;二是如果用户访问网站时就有 sid,则说明以前已经为此客户端创建过 session,后端就会通过 sid 把对应的 session 检索出来,如果没有找到,则重新创建一个 session。三是客户端的 sid 应与后端的 sessionID 值相同。

在 Node.js 中可以简单的创建一个 session,当然,一般使用加密或者使用 npm 模块,比如 express-session

下面是一个简单的 session 实现,利用 js 对象存储 sid,当然这里使用了时间戳 + 随机数的形式创建的 sid,一般不会这样做,而是生成一个加密的字符串,比如用上面的字符串再加一些随机的字符串,然后使用 MD5 加密,这种处理方式成为 MD5加盐

首先是引入模块:

const http = require("http");
const fs = require("fs");
const utils = require("util");

// SESSION 对象的格式是这样的:/*
    {
        uewewkf^^xbsj*: { 用户的一些数据 },
        xgkajsjkanmc_#: { 用户的一些数据 },
        ....
    }
*/const SESSION = {};     // 保存 session 数据的地方const readFile = utils.promisify(fs.readFile);

接下来就是处理路由:

var server = http.createServer(async (req,res) => {
    var sid;    // sid// 每次请求都拿到 cookievar cookie = req.headers.cookie;
    if(cookie){
        // 如果 cookie 存在,就从中拿到 sid// 当然这里只有一个存储 sid 的 cookie// 因此操作比较简单,如果有多个 cookie 还要做额外的处理
        sid = cookie.split("=")[1];
        // 通过 sid 拿到 session 数据// 里面保存着用户的一些信息var session = SESSION[sid];
        if(!session){
            // 这种情况可能是 session 里保存的数据已经过期
            SESSION[sid] = {};
        }
    }else{
        // 没有 cookie(可能是客户端删除了 cookie 或者第一次访问)// 生成 sid 创建 session,然后返回给客户端
        sid = String(+newDate()) + Math.random();
        SESSION[sid] = {};      // 设置 cookie
        res.setHeader("Set-Cookie",[`sid=${sid};HttpOnly;Path=/;Max-Age=${60 * 60 * 24};`]);
    }

    if(req.url === "/" && req.method === "GET"){        // 访问首页,发送 HTML 页面var html = await readFile("./index.html",{encoding:'utf8'});
        res.setHeader("content-type","text/html");
        res.end(html);
    }
    if(req.url === "/api/count" && req.method === "GET"){
        // 当访问这个 get 接口时,就会获取 sidvar sid = req.headers.cookie.split("=")[1];
        // 通过 sid 获得 count 数据,没有的话就等于 0// 有的话就加一,然后返给前端数据
        SESSION[sid].count = SESSION[sid].count >= 0 ? SESSION[sid].count += 1 : 0;
        res.setHeader("content-type","application/json");
        res.end(JSON.stringify({
            count: SESSION[sid].count
        }));
    }
});

server.listen(8080,() => {
    console.log("Server is running: http://localhost:8080");
});

然后是前端代码,前端代码只有一个 Ajax 请求:

<!-- index.html --><h1>The count equal: <span></span></h1><script>
    const span = document.querySelector("span");
    // 发送请求,然后将请求的数据渲染到 span 标签上
    fetch("/api/count")
    .then(d => d.json())
    .then(data => span.innerHTML = data.count)
    .catch(err => console.log(err));
</script>

运行上面的代码,第一次数值是 0,当每次刷新时会发现每次都加 1(如果把 cookie 删除了就又会从零开始)。通过上面代码可以看到,其实 session 并不算上是一种复杂的技术,因为 session 完全可以有很多种设计,而不是 cookie,cookie 是 http 的一部分。

在 express 中,可以使用 cookie-parserexpress-session 两个模块处理 cookie 和 session。

const cookieParser = require("cookie-parser");
const session = require("express-session");
const SECRET = "hello world!!!";        // 加密用的字符串
app.use(cookieParser(SECRET));      // 使用中间件处理 cookie
app.use(session({       // 处理 session// session 存储的仓库(redis数据库中)
    store: new RedisStore({ client: redisClient }),
    secret: SECRET,
    resave: false,
    // name 就是 cookie 的键名
    name: "_sid",
    cookie: {   // 这个配置的 cookie 会生成一个 sid 的特殊cookie// 设置后期时间
        maxAge: 24 * 60 * 60 * 1000,
    }
}));

配置好后,就可以在其他的路由中使用这两个模块了。在 req.cookies 中可以获取到 cookie,使用 req.signedCookies 可以获得加密的 cookie;使用 res.cookie(key,value) 的方式可以设置 cookie,使用下面的方式可以设置加密 cookie:

function router(req,res){
    res.cookie("age",18,{
        signed: "Hello !!!"
    });
}

以上都是 cookie-parser 的功能,而 express-session 可以直接使用 req.session 获取到 session,使用 req.sessionID 获取到 sid。你可以直接将数据存储到 session 上,比如:req.session.count = 1。这样就表示 session 上存入了 count 这个数据。

需要注意的是,cookie-parser 中的 cookie 配置与原生 Node 上的配置有些不同,比如 max-age 的配置,Node 中是以秒为单位,而 cookieParser 中以毫秒为单位,配置参数可以看下表。

key

默认值

说明

path

/

指定 cookie 影响到的路径

expires

没有值

指定 cookie 过期时间,这个时间应是绝对时间。不具体指定时,表示会话期间(session) cookie 不会过期,session 时间到了 cookie 就会失效

maxAge

没有值

这也是给 cookie 设置过期时间,它表示的是在 cookie 失效之前需要经过的秒数。秒数为 0 或 -1 将会使 cookie 直接过期。因此,maxAge 是相对值

secure

false

当为 true 时表示 cookie 只有在请求使用 SSL 和 HTTPS 协议的时候才会被发送到服务器。

httpOnly

false

当为 true 时表示 cookie 不能使用 JavaScript 经由 Document.cookie 属性、XMLHttpRequest 和 Request APIs 进行访问,以防范跨站脚本攻击(XSS)

通过上面也可以看出 session 是保存在内存中的一种数据结构(这里是 js 当中的对象,大致上就是数据结构中的哈希表)。因此这种 session 有一些缺点,比如当服务宕机或者重启时 session 数据会丢失,因为内存数据会被释放掉;还有一个问题是如果用户过多,session 会很耗费 Node 服务器的运行内存,这样服务器性能会不好,甚至会让服务器挂掉。因此用一个变量来存储 session 是不太友好的。

使用 Redis 存储 session

因此需要一个专门存储 session 的东西比较好。于是 Redis 数据库就出现了,Redis 数据库的性能非常高,与一般的数据库不同,Redis 数据库是将数据存储在内存中(而不是硬盘中),因为 Redis 是脱离于 Node 服务器的,Redis 与 Node 是两个运行程序,Redis 的运行不会影响 Node 服务器。想要存入 session 数据,是用 Redis 进行存储,把原本 Node 中使用变量存储的数据搬移到了 Redis 中,这样可以大大缓解 Node 的服务器压力。

这里演示一下在 express 框架中如何使用 Redis 存储 session 数据。

使用 Redis 之前需要先下载 Redis 数据库。首先来到 GitHub 上,下载 Windows 版本的话可以到这个网址下载:

windows redis 下载[1]

然后一步步安装,安装页面上有一个添加环境变量,最好勾上,这样就不需要手动添加了。

安装完成后,打开终端运行 redis-cli,如果成功进入命令行里,说明安装完成。

接下来就是使用 Node.js 操作 Redis。需要下载三个模块:

yarn add redis connect-redis express-session

redis 是用 Node 操作 Redis 数据库的模块,connect-redis 模块是中间件,可以很方便的将 session 存储到 Redis 中,因此我们还需要下载 express-session 方便操作 session。

下载好后就是配置中间件。首先建立一个 redisDB.js 文件,这个文件负责初始化 redis 数据库:

const redis = require("redis");

let redisClient = redis.createClient({
    host: "127.0.0.1",
    port: 6379
});     // 启动 redis 服务

redisClient.on("error",function(err){
    console.error("Error: ",err);
});

redisClient.on("connect",function(){
    console.log("Redis service is connected");
});

module.exports = redisClient;

然后是配置中间件:

const path = require("path");
const express = require("express");
const session = require("express-session");
const cookieParser = require("cookie-parser");
const { SECRET } = require("./config");
const RedisStore = require('connect-redis')(session);
app.use(cookieParser(SECRET));
app.use(session({
    // session 存储的仓库(redis数据库中)
    store: new RedisStore({ client: redisClient }),
    secret: SECRET,
    resave: false,
    // name 就是 cookie 的键名
    name: "_sid",
    cookie: {
        // 设置后期时间
        maxAge: 24 * 60 * 60 * 1000,
    }
}));

这些配置好后,就可以验证一下 session 存入的数据是不是在 Redis 中。这里需要注意的是,express-session 中间件应该在 cookie-parser 中间件下面。

完成配置之后,每次写入的 session 就会存入到 Redis 中。当然,这时候眼见为实才好判断。首先打开 CMD,运行 redis-cli 就会进入 redis 操作界面。

redis-cli

然后输入 keys * 就会查到 session 有关的数据。

redis-session

或者在 Node 程序中写入几个变量到 session 中:

function router(req,res){
    req.session.name = "ming";
    req.session.age = 18;
    res.send({
        msg: "session set up successfully!"
    });
}
app.use("/",router);

打开浏览器刷新页面,然后断开服务器,如果不使用 Redis,按理说断开之后 session 中的数据就会丢失,但如果连接了 Redis 后就会发现 session 中的数据还保存着。

至于 session 中的数据什么时候过期就看你设定的 expires 或者 maxAge 的值是多少了,时间到了 session 就会过期,Redis 就会自动清除数据。

关于 Redis 数据库的操作可以参考官网 API 文档。

Redis API 文档[2] Redis 中文文档[3]

最后

session 相比于 cookie 有点更多一些,但是 session 也不是完美的。比如耗费内存、如果不使用 Redis,session 将很难被应用。因此使用 session 还要多学习关于数据库的内容。使用 session + cookie 的方式时需要注意的是,如果用户禁用了 cookie 可能会导致 web 程序没有办法正常运行,有些网站禁用了 cookie 后会导致登录不上去,因为 sid 保存不到 cookie 里了,后端就无法得到用户登录状态信息。在 chrome 中可以进入 设置 --> 高级 --> 隐私设置和安全 --> 网站设置 --> Cookie 和网站数据 --> 允许网址读取和保存数据 将 cookie 功能关掉(当然不推荐这么做)。

当然除了 session + cookie 管理用户状态之外,还有一种技术也很流行,就是 token 技术,而 token 通常又与 JWT 技术有关,使用 token 可以代替 session 。关于 token 技术下次再更新吧!

参考资料

[1]

windows redis 下载: https://github.com/microsoftarchive/redis/releases

[2]

Redis API 文档: https://redis.io/commands

[3]

Redis 中文文档: http://www.redis.cn/commands.html

本文分享自微信公众号 - Neptune丶(Neptune_mh_0110),作者:多云转晴丶

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 数据结构——二叉堆

    首先需要了解三个函数。这三个函数可以通过索引检索出父节点,也可以通过父节点的索引检索出子节点。例如下面一个最小二叉堆,可用数组的表示:

    多云转晴
  • 前端常见的跨域方式

    同源策略 是浏览器中一个重要的安全策略。当两个 URL 的协议、端口和主机都一直时,浏览器认为这两个 URL 是同源的。

    多云转晴
  • 排序算法(一)

    数组排序算法是一个经典的算法问题,这类排序算法非常多,比如我们熟知的冒泡排序、插入排序、快速排序等算法。这篇文章主要说一下五种排序算法:

    多云转晴
  • 【WebView的cookie机制 】轻松搞定WebView cookie同步问题

    在进行APP+H5混合开发的时候,一些功能是用native方法实现的,如登陆,一些功能是用H5实现的。所以往往需要将在native方法登陆的状态同步到H5中避免...

    CrazyCodeBoy
  • ASP.NET Core2.1 你不得不了解的GDPR(Cookie处理)

    ...然后就碰到问题了... 我发现..cookie竟然存不进去了..(怨念+100)

    GuZhenYin
  • 设定指定的页面才可以访问Cookie

    设置cookie下的cookie.Domain="rupeng.com",,则该域名下的所有网页均可以访问,设定的cookie

    静心物语313
  • csrf攻击原理和防御

    上面的代码意思大概是,如果有username的存在,那么说明用户已经登录成功,否则通过判断url中是否有login参数,如果有就进行登录,并写cookie。该代...

    theanarkh
  • 浅谈iOS中的WKWebView添加cookie

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

    用户1451823
  • requests库入门-16-Session和Cookie

    作者 | Anthony_tester ,300w+访问量博主,Oracle测试开发工程师。

    测试开发社区
  • 破解之美:利用ECB加密缺陷突破cookie加密

    密码学诞生于古人类社会,例如古希腊斯巴达的密码棒、欧洲中世纪时期的密码栏、阿伯提发明的密码盘等。古代的密码学是一门加密隐蔽的学科,而现代密码学定义为加密与解密,...

    FB客服

扫码关注云+社区

领取腾讯云代金券