什么是cookie,token和session?它们之间有什么关系?

关注者
35
被浏览
103,040

9 个回答

相信项目中用JWT Token的应该不在少数,但是发现网上很多文章对 token 的介绍有误,所以对 cookie,session, token 作了一下对比(文中token指jwt token)相信大家看完肯定有收获!

Cookie

1991 年 HTTP 0.9 诞生了,当时只是为了满足大家浏览 web 文档的要求 ,所以只有 GET 请求,浏览完了就走了,两个连接之间是没有任何联系的,这也是 HTTP 为无状态的原因,因为它诞生之初就没有这个需求。

但随着交互式 Web 的兴起(所谓交互式就是你不光可以浏览,还可以登录,发评论,购物等用户操作的行为),单纯地浏览 web 已经无法满足人们的要求,比如随着网上购物的兴起,需要记录用户的购物车记录,就需要有一个机制记录每个连接的关系,这样我们就知道加入购物车的商品到底属于谁了,于是 Cookie 就诞生了。

Cookie,有时也用其复数形式 Cookies。类型为“小型文本文件”,是某些网站为了辨别用户身份,进行 Session 跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息 。

工作机制如下

以加入购物车为例,每次浏览器请求后 server 都会将本次商品 id 存储在 Cookie 中返回给客户端,客户端会将 Cookie 保存在本地,下一次再将上次保存在本地的 Cookie 传给 server 就行了,这样每个 Cookie 都保存着用户的商品 id,购买记录也就不会丢失了

仔细观察上图相信你不难发现随着购物车内的商品越来越多,每次请求的 cookie 也越来越大,这对每个请求来说是一个很大的负担,我只是想将一个商品加入购买车,为何要将历史的商品记录也一起返回给 server ?购物车信息其实已经记录在 server 了,浏览器这样的操作岂不是多此一举?怎么改进呢

Session

仔细考虑下,由于用户的购物车信息都会保存在 Server 中,所以在 Cookie 里只要保存能识别用户身份的信息,知道是谁发起了加入购物车操作即可,这样每次请求后只要在 Cookie 里带上用户的身份信息,请求体里也只要带上本次加入购物车的商品 id,大大减少了 cookie 的体积大小,我们把这种能识别哪个请求由哪个用户发起的机制称为 Session(会话机制),生成的能识别用户身份信息的字符串称为 sessionId,它的工作机制如下

  1. 首先用户登录,server 会为用户生成一个 session,为其分配唯一的 sessionId,这个 sessionId 是与某个用户绑定的,也就是说根据此 sessionid(假设为 abc) 可以查询到它到底是哪个用户,然后将此 sessionid 通过 cookie 传给浏览器
  2. 之后浏览器的每次添加购物车请求中只要在 cookie 里带上 sessionId=abc 这一个键值对即可,server 根据 sessionId 找到它对应的用户后,把传过来的商品 id 保存到 server 中对应用户的购物车即可

可以看到通过这种方式再也不需要在 cookie 里传所有的购物车的商品 id 了,大大减轻了请求的负担!

另外通过上文不难观察出 cookie 是存储在 client 的,而 session 保存在 server,sessionId 需要借助 cookie 的传递才有意义。

session 的痛点

看起来通过 cookie + session 的方式是解决了问题, 但是我们忽略了一个问题,上述情况能正常工作是因为我们假设 server 是单机工作的,但实际在生产上,为了保障高可用,一般服务器至少需要两台机器,通过负载均衡的方式来决定到底请求该打到哪台机器上。

如图示:客户端请求后,由负载均衡器(如 Nginx)来决定到底打到哪台机器

假设登录请求打到了 A 机器,A 机器生成了 session 并在 cookie 里添加 sessionId 返回给了浏览器,那么问题来了:下次添加购物车时如果请求打到了 B 或者 C,由于 session 是在 A 机器生成的,此时的 B,C 是找不到 session 的,那么就会发生无法添加购物车的错误,就得重新登录了,此时请问该怎么办。主要有以下三种方式

1、session 复制

A 生成 session 后复制到 B, C,这样每台机器都有一份 session,无论添加购物车的请求打到哪台机器,由于 session 都能找到,故不会有问题

这种方式虽然可行,但缺点也很明显:

  1. 同一样的一份 session 保存了多份,数据冗余
  2. 如果节点少还好,但如果节点多的话,特别是像阿里,微信这种由于 DAU 上亿,可能需要部署成千上万台机器,这样节点增多复制造成的性能消耗也会很大。

2、session 粘连

这种方式是让每个客户端请求只打到固定的一台机器上,比如浏览器登录请求打到 A 机器后,后续所有的添加购物车请求也都打到 A 机器上,Nginx 的 sticky 模块可以支持这种方式,支持按 ip 或 cookie 粘连等等,如按 ip 粘连方式如下

upstream tomcats {
 ip_hash;
  server 10.1.1.107:88;
  server 10.1.1.132:80;
}

这样的话每个 client 请求到达 Nginx 后,只要它的 ip 不变,根据 ip hash 算出来的值会打到固定的机器上,也就不存在 session 找不到的问题了,当然不难看出这种方式缺点也是很明显,对应的机器挂了怎么办?

3、session 共享

这种方式也是目前各大公司普遍采用的方案,将 session 保存在 redis,memcached 等中间件中,请求到来时,各个机器去这些中间件取一下 session 即可。

缺点其实也不难发现,就是每个请求都要去 redis 取一下 session,多了一次内部连接,消耗了一点性能,另外为了保证 redis 的高可用,必须做集群,当然了对于大公司来说, redis 集群基本都会部署,所以这方案可以说是大公司的首选了。

Token:no session!

通过上文分析我们知道通过在服务端共享 session 的方式可以完成用户的身份定位,但是不难发现也有一个小小的瑕疵:搞个校验机制我还得搭个 redis 集群?大厂确实 redis 用得比较普遍,但对于小厂来说可能它的业务量还未达到用 redis 的程度,所以有没有其他不用 server 存储 session 的用户身份校验机制呢,这就是我们今天要介绍的主角:token。

首先请求方输入自己的用户名,密码,然后 server 据此生成 token,客户端拿到 token 后会保存到本地,之后向 server 请求时在请求头带上此 token 即可。

相信大家看了上图会发现存在两个问题

1、 token 只存储在浏览器中,服务端却没有存储,这样的话我随便搞个 token 传给 server 也行?

答:server 会有一套校验机制,校验这个 token 是否合法。

2、怎么不像 session 那样根据 sessionId 找到 userid 呢,这样的话怎么知道是哪个用户?

答:token 本身携带 uid 信息

第一个问题,如何校验 token 呢?我们可以借鉴 HTTPS 的签名机制来校验。先来看 jwt token 的组成部分

可以看到 token 主要由三部分组成

  1. header:指定了签名算法
  2. payload:可以指定用户 id,过期时间等非敏感数据
  3. Signature: 签名,server 根据 header 知道它该用哪种签名算法,再用密钥根据此签名算法对 head + payload 生成签名,这样一个 token 就生成了。

当 server 收到浏览器传过来的 token 时,它会首先取出 token 中的 header + payload,根据密钥生成签名,然后再与 token 中的签名比对,如果成功则说明签名是合法的,即 token 是合法的。而且你会发现 payload 中存有我们的 userId,所以拿到 token 后直接在 payload 中就可获取 userid,避免了像 session 那样要从 redis 去取的开销

画外音:header, payload 实际上是以 base64 的形式存在的,文中为了描述方便,省去了这一步。

你会发现这种方式确实很妙,只要 server 保证密钥不泄露,那么生成的 token 就是安全的,因为如果伪造 token 的话在签名验证环节是无法通过的,就此即可判定 token 非法。

可以看到通过这种方式有效地避免了 token 必须保存在 server 的弊端,实现了分布式存储,不过需要注意的是,token 一旦由 server 生成,它就是有效的,直到过期,无法让 token 失效,除非在 server 为 token 设立一个黑名单,在校验 token 前先过一遍此黑名单,如果在黑名单里则此 token 失效,但一旦这样做的话,那就意味着黑名单就必须保存在 server,这又回到了 session 的模式,那直接用 session 不香吗。所以一般的做法是当客户端登出要让 token 失效时,直接在本地移除 token 即可,下次登录重新生成 token 就好。

另外需要注意的是 token 一般是放在 header 的 Authorization 自定义头里,不是放在 Cookie 里的,这主要是为了解决跨域不能共享 Cookie 的问题 (下文详述)

Cookie 与 Token 的简单总结

Cookie 有哪些局限性?

1、 Cookie 跨站是不能共享的,这样的话如果你要实现多应用(多系统)的单点登录(SSO),使用 Cookie 来做需要的话就很困难了(要用比较复杂的 trick 来实现,有兴趣的话可以看文末参考链接)

画外音: 所谓单点登录,是指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

但如果用 token 来实现 SSO 会非常简单,如下

只要在 header 中的 authorize 字段(或其他自定义)加上 token 即可完成所有跨域站点的认证。

2、 在移动端原生请求是没有 cookie 之说的,而 sessionid 依赖于 cookie,sessionid 就不能用 cookie 来传了,如果用 token 的话,由于它是随着 header 的 authoriize 传过来的,也就不存在此问题,换句话说token 天生支持移动平台,可扩展性好

综上所述,token 具有存储实现简单,扩展性好这些特点。

token 有哪些缺点

那有人就问了,既然 token 这么好,那为什么各个大公司几乎都采用共享 session 的方式呢,可能很多人是第一次听到 token,token 不香吗? token 有以下两点劣势:

1、 token 太长了

token 是 header, payload 编码后的样式,所以一般要比 sessionId 长很多,很有可能超出 cookie 的大小限制(cookie 一般有大小限制的,如 4kb),如果你在 token 中存储的信息越长,那么 token 本身也会越长,这样的话由于你每次请求都会带上 token,对请求来是个不小的负担

2、 不太安全

网上很多文章说 token 更安全,其实不然,细心的你可能发现了,我们说 token 是存在浏览器的,再细问,存在浏览器的哪里?既然它太长放在 cookie 里可能导致 cookie 超限,那就只好放在 local storage 里,这样会造成安全隐患,因为 local storage 这类的本地存储是可以被 JS 直接读取的,另外由上文也提到,token 一旦生成无法让其失效,必须等到其过期才行,这样的话如果服务端检测到了一个安全威胁,也无法使相关的 token 失效。

所以 token 更适合一次性的命令认证,设置一个比较短的有效期

误解: Cookie 相比 token 更不安全,比如 CSRF 攻击

首先我们需要解释下 CSRF 攻击是怎么回事

攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过(cookie 里带来 sessionId 等身份认证的信息),所以被访问的网站会认为是真正的用户操作而去运行。

比如用户登录了某银行网站(假设为 examplebank.com/,并且转账地址为 examplebank.com/withdra),登录后 cookie 里会包含登录用户的 sessionid,攻击者可以在另一个网站上放置如下代码

<img src="examplebank.com/withdra">

那么如果正常的用户误点了上面这张图片,由于相同域名的请求会自动带上 cookie,而 cookie 里带有正常登录用户的 sessionid,类似上面这样的转账操作在 server 就会成功,会造成极大的安全风险

CSRF 攻击的根本原因在于对于同样域名的每个请求来说,它的 cookie 都会被自动带上,这个是浏览器的机制决定的,所以很多人据此认定 cookie 不安全。

使用 token 确实避免了CSRF 的问题,但正如上文所述,由于 token 保存在 local storage,它会被 JS 读取,从存储角度来看也不安全(实际上防护 CSRF 攻击的正确方式是用 CSRF token)

所以不管是 cookie 还是 token,从存储角度来看其实都不安全,都有暴露的风险,我们所说的安全更多的是强调传输中的安全,可以用 HTTPS 协议来传输, 这样的话请求头都能被加密,也就保证了传输中的安全。

其实我们把 cookie 和 token 比较本身就不合理,一个是存储方式,一个是验证方式,正确的比较应该是 session vs token。

总结

session 和 token 本质上是没有区别的,都是对用户身份的认证机制,只是他们实现的校验机制不一样而已(一个保存在 server,通过在 redis 等中间件获取来校验,一个保存在 client,通过签名校验的方式来校验),多数场景上使用 session 会更合理,但如果在单点登录,一次性命令认证上使用 token 会更合适,最好在不同的业务场景中合理选型,才能达到事半功倍的效果。

以上内容分享自华为云社区《Session/Cookie/Token 还傻傻分不清?》,作者: 龙哥手记。


点击关注,第一时间了解华为云新鲜技术~

《一文搞懂cookie、token和seesion以及在nodejs中的实战》

前言:无状态的 HTTP

众所周知,HTTP 协议是无状态的。但是随着 web 应用的发展,越来越多的场景需要标识用户身份。例如:单点登陆、购物车等等。

而 cookie、session 与 token,就是为了实现带有状态的“会话控制”。曾经我也傻傻搞不清他们的区别,只知道他们是为了解决 http 协议无状态的技术方案。

这篇文章,阐述他们的概念、用途和区别,配合代码和场景加深理解

Cookie

认识 Cookie

cookie 是以 K-V 形式,存储在浏览器中一种数据。它可以在服务端设置,也可以在浏览器端用 js 代码设置。它拥有 maxAge、domain、path 等属性,借助这些属性,可以实现父子域名之间的数据传递。

设置 Cookie

虽然 cookie 是 K-V 形式存储的,但是在设置 cookie 的值的时候,是直接给定形如key1=value1; key2=value2的字符串。它在服务器/浏览器端均可以设置:

  • 浏览器端:通过 js 代码来设置,例如 document.cookie = "firstName=dongyuanxin; path=/
  • 服务器端:通过给 Http Response Headers 中的Set-Cookie字段赋值,来设置 cookie。客户端接收到Set-Cookie字段后,将其存储在浏览器中。

在服务端,以 koajs 为例,设置 key 为id,value 为xxoo521.com的 cookie。代码如下:

const Koa = require("koa");
const Router = require("koa-router");
const serve = require("koa-static");

const port = 3333;

const app = new Koa();
const router = new Router();

router.get("/api", async (ctx, next) => {
    const cookie = ctx.cookies.get("id");
    if (!cookie) {
        // 设置 id = xxoo521.com
        ctx.cookies.set("id", "xxoo521.com");
    }
    ctx.response.body = "原文地址:xxoo521.com";
});

app.use(serve("."))
    .use(router.routes())
    .listen(port, () => {
        console.log("listen port:", port);
    });

代码调试

在启动上面代码,并且请求/api接口后。在 Chrome Dev Tools 中,能看到服务器返回的 Headers 的信息,如下图:



按照协议,浏览器应该成功保存了 cookies 的值。此时,找到Application => Storage => Cookies => 当前域名,即可验证 cookie 是否设置成功,如下图:



整体流程

以用户购买商品为例,整体流程如下:

  • 监测到浏览器客户端没有标识用户的 cookie,跳转到登陆界面
  • 用户账号密码登陆,后端验证,成功后,在Set-cookie中设置标识用户的 cookie
  • 登陆成功,保存用户标识的 cookie
  • 购买商品,自动携带用户身份的 cookie,后端验证无误后,购买成功

总结

由此可见,单纯的使用 cookie,需要将用户的身份信息保存在客户端,并不安全。除此之外,cookie 还有大小限制,以及只能使用字符串类型作为 value 值。

Session

认识 Session

Session 机制准确来说,也是通过 K-V 数据格式来保存状态。其中:

  • Key:也称 SessionID,保存在客户端浏览器。
  • Value:也称“Session”,保存在服务端。

可以看到,客户端只需要存储 SessionID。具体映射的数据结构放在了服务端,因此跳出了仅仅浏览器 cookie 只可以存储 string 类型的限制。

而客户端存储 SessionID,还是需要借助 cookie 来实现。

整体流程



假设/login接口登陆成功后,服务器可以生成 sessionId 和 session。其中,session 中保存了过期时间,一些冗余信息等。代码如下:

router.get("/login", async (ctx, next) => {
    const { user, pwd } = querystring.parse(ctx.request.search.slice(1));
    // mock数据,模拟一下登陆过程
    if (user === "test" && pwd === "123456") {
        // 生成客户端存储的sessinId
        const sessionId = crypto
            .createHash("md5")
            .update(user + pwd)
            .digest("hex");
        // 生成服务端存储的session
        const session = {
            expire: Date.now() + 1000 * 60 * 60 * 24, // 过期时间
            info: {
                // 保存的信息
                name: user
            }
        };
        sessions.set(sessionId, session);
        ctx.cookies.set("sessionId", sessionId);
        ctx.response.body = "登陆成功";
    } else {
        ctx.response.body = "登陆失败";
        ctx.response.status = 401;
    }
});

然后客户端在 cookies 中携带 sessionId,访问/userInfo接口,获得用户信息。服务端检查 sessionId 合法性,以及是否过期。代码如下:

router.get("/userInfo", async (ctx, next) => {
    const sessionId = ctx.cookies.get("sessionId");
    const session = sessions.get(sessionId);
    // 如果sessionId不存在
    if (!session) {
        ctx.response.body = "无法识别身份";
        ctx.response.status = 401;
        return;
    }
    // session过期
    if (session.expire <= Date.now()) {
        ctx.response.body = "session过期,请重新登陆";
        ctx.response.status = 401;
        return;
    }

    ctx.response.body = session.info;
});

打开 chrome dev tools,我们可以看到,http 请求自动携带了 cookie 中的 sessionId。并且通过后端检验,拿到了用户信息。



总结:比较 cookie 与 session

session 传输数据少,数据结构灵活:相较于 cookie 来说,session 存储在服务端,客户端仅保留换取 session 的用户凭证。因此传输数据量小,速度快。

session 更安全:检验、生成、验证都是在服务端按照指定规则完成,而 cookie 可能被客户端通过 js 代码篡改。

session 的不足:服务器是有状态的。多台后端服务器无法共享 session。解决方法是,专门准备一台 session 服务器,关于 session 的所有操作都交给它来调用。而服务器之间的调用,可以走内网 ip,走 RPC 调用(不走 http)。

Token

为什么需要 Token?

这也是我刚接触 token 时候的疑惑,因为 session 对比 cookie 来说,解决了很多问题,所以感觉上 session 已经很完美了。

但对于 session 来说,服务器是有状态的。这个事情就很麻烦,尤其是在分布式部署服务的时候,需要共享服务器之间的状态。总不能让用户不停重复登陆吧?虽然专门准备一个服务器用来处理状态是可行的,但是能不能让服务器变成无状态的,还不能像单纯 cookie 那么蹩脚?

token 就解决了这个问题。它将状态保存在客户端,并且借助加密算法进行验证保证安全性。

整体流程



如上图所示,整体流程总结如下:

  • 用户尝试登陆
  • 登陆成功后,后端依靠加密算法,将凭证生成 token,返回给客户端
  • 客户端保存 token,每次发送请求时,携带 token
  • 后端再接收到带有 token 的请求时,验证 token 的有效性

在整个流程中,比较重要的是:生成 token、验证 token 的过程。这里设计一种简单的技术实现

  • 生成:token 的组成为:${user}.${HS256(user, secret)}。其中,secret 是加密需要的密钥,保存在服务端,不能泄漏。HS256 是加密算法,使用 RS256、HS512 也可以。
  • 验证:将请求中携带的 token 按照.分开,得到payloadsig。用服务器密钥对payload进行加密,将加密结果和sig比较,如果相同,那么通过验证。

值得一提的是,这里无需对sig进行解密。

代码实现

借助crypto实现 HS256 算法加密:

/**
 * @param {string} content
 * @param {string} key
 * @return {string}
 */
function HS256(content, key = "jnajdnf9328u4") {
    const hmac = crypto.createHmac("sha256", key);
    hmac.update(content);
    return hmac.digest("hex");
}

用户登陆成功后,将用户名作为payload,生成对应的sig,拼接为 token,返回给客户端:

router.get("/login", async (ctx, next) => {
    const { user, pwd } = querystring.parse(ctx.request.search.slice(1));
    // mock数据,模拟一下登陆过程
    if (user === "test" && pwd === "123456") {
        ctx.response.body = {
            token: `${user}.${HS256(user)}`
        };
    } else {
        ctx.response.body = "登陆失败";
        ctx.response.status = 401;
    }
});

客户端再请求需要用户身份的 api 的时候,应该携带 token,服务端对 token 合法性进行检验:

router.get("/userInfo", async (ctx, next) => {
    const { token } = querystring.parse(ctx.request.search.slice(1));
    if (typeof token !== "string") {
        ctx.response.body = "请携带token";
        ctx.response.status = 401;
        return;
    }

    const [payload, sig] = token.split(".");
    if (!payload || !sig) {
        ctx.response.body = "token格式不合法";
        ctx.response.status = 401;
        return;
    }

    if (HS256(payload) !== sig) {
        ctx.response.body = "签名不合法";
        ctx.response.status = 401;
        return;
    }

    ctx.response.body = "用户信息是巴拉巴拉";
});

请求成功后,服务端返回数据,下图所示:



总结:token 真香

token 的优点多多:

  • 服务器变成无状态了,实现分布式 web 应用授权
  • 可以进行跨域授权,不再局限父子域名
  • token 设计绝对了它本身可以携带更多不敏感数据,例如最常用的 JWT
  • 安全性更高,密钥保存在服务器。若密钥被窃取,可以统一重新下发密钥。

当然,token 增加了服务器压力(毕竟要加密)。但还是真香

参考链接


专注前端与算法的系列干货分享,欢迎关注「心谭博客」。

[Doge] 这是个有趣的域名:xxoo521.com