6

从我刚开始学程序时这一题就常出现在面试考题里,一直到现在都还是能看见这个问题。

这个问题重要吗?我觉得蛮重要的。因为 Session 所代表的是「状态」,如果没有了状态,一大堆功能都会失效。

对于工程师来说必须去理解什么是 Session,以及如何操作它,而 Cookie 就是这之中很重要的一环。

因此这会是一系列的文章,我称之为 Session 与 Cookie 三部曲,会由浅入深,从不同的面向去看 Session 与 Cookie。

这是系列文的第一篇,想用简单白话的方式通俗地跟大家解释什么是 Session,什么又是 Cookie,目标是希望没有任何技术背景的人也能够看懂。

要向没有技术背景的人讲这种概念性的东西,用一堆专有名词绝对是最差劲的做法。

而最好的做法通常是举一个现实生活中很贴近的例子,藉由这种方式比较能让毫无技术背景的读者们去理解这到底是个什么东西。

因此,我们从经营杂货店开始吧!

小明の杂货店

四十岁的退休以后在家闲得发慌,每一天都过得毫无目标而且浑浑噩噩。

「退休以后不是应该无忧无虑吗?」小明也是这样问自己的,但没办法,他深知自己的个性就是这样,没办法闲下来,一定要做点事情才行。

于是,小明就用了退休金在家里附近的巷口开了间杂货店,并且取名为:「小明の杂货店」,是个毫无创意的名称,但把自己的名字放在招牌上一直是他的梦想。

小明平时人缘还算不错,在倒垃圾时会与旁边的婆婆妈妈闲聊,说着那个谁谁谁的儿子考上了台大,谁谁谁的女儿最近交了个男友,成为左邻右舍八卦网络的一部分。

不只婆婆妈妈,连年轻的那一代也对他感觉不错,八成是因为他很识相地不会硬要跟年轻人尬聊,看到他们都只是简单点个头示意一下,而不是像其他人劈头就把私事全都问了一遍。

因此在开幕那天,杂货店好比 Apple Store 开幕一般(除了没有人特地前一天就跑来排队以外),周遭的邻居们都跑来捧场,把整个店挤得水泄不通,单日营收甚至上百万(台币)。

第一天就能有如此成績,可见人缘是多么重要的一件事。有人缘,有人潮;有人潮,有钱潮。

但开幕毕竟是开幕,通常都是一家商店这辈子的巅峰,除非有跳楼大拍卖(假的那种不算,例如说每天都在大拍卖的)或是周年庆,不然都很难超越了。

随着日历一张张被撕开,店里的生意慢慢恢复正常,还是喜欢传统便利商店的都跑回去便利商店了,而嫌远懒得走这么多路的则选择杂货店消费。

看似步上正轨的杂货店,问题却随着时间慢慢浮上台面。

脸盲症的困扰

小明身为杂货店的店长兼唯一的店员,所有大小事都是他一个人在处理。传统杂货店跟便利商店最大的差别在哪里?在于人情味。

就像是你去菜市场买菜的时候会被说帅哥或美女,或者是去买早餐的时候老板会问你:「一样?」,你只要点个头就行了。这些人与人之间的情感是无论信息怎么发展都无法取代的。

可是小明没有办法,因为他根本记不起来是同一个人。

每一个来店里的人对小明来说都是一个独立的个体,是完全不相干的。你可能会疑惑说:「就算认不出脸,认声音、衣服、气味也都可以吧?」,看来你是太低估小明了。

小明不只认不出脸,他什么都认不出来。我也不知道小明到底哪里出了差错,小明自己也不知道。

但总之就是这样,就算你每天来,每天穿着一样的衣服,用着一样的声音,他都认不出来你是同一个人。

讲一个例子你就知道了,有一次有个顾客结完帐以后把发票忘在柜台,一出店门口才想起来,就立刻跑回去拿。

结果小明完全没认出来是同一个人,还以为这人是想来偷拿发票的,跟他确认过买的品项一致以后才愿意把发票还给他。

对,就是这么夸张,小明每一次结账都是在帮一个全新的人结账。

在生活上或许没什么问题,反正小明无依无靠也没朋友,自己一个人生活惯了,可是在经营杂货店上面就有很大的问题了。

除了会让人觉得很没有人情味以外,最大的问题就是有些顾客的需求他没办法处理。

有些人逛杂货店喜欢慢慢挑慢慢选嘛,然后有些物品可能又很重,或者是在结账的时候才突然想起来还要买什么,这时候就会把东西先放在收银台那里,自己跑回去拿其他品项。

我前面已经提过了,小明认人的能力是零,当客人拿新的物品回去收银台的时候,小明已经认不出他来了。

因此他不知道收银台上面那些物品是谁的,客人也很难跟小明证明说:「对,这些是我刚刚想买的」。

这个使用者体验简直差到不行,因此店里的生意每况愈下,只有那种果断型顾客会来消费(一进杂货店就往自己的目标走,拿完之后立刻结账的那种)。

小明当然注意到了这个状况,也知道不能再这样下去了,继续这样的话大概不用两个月店就会倒了。于是小明左思右想,快思慢想,东想西想,终于想到了一个解决方法。

方法虽老旧但有用

前面有提到过小明最大的问题是「每个客人都是新的客人」,他没办法认出他们是同一个客人,所以自然也无法记住他们的「状态」,而这个才是最大的问题。

山不转路转,路不转头转,既然小明自己没办法记住状态,写张纸条不就得了吗?

当你在收银台结账的时候写一张纸条给你,上面写着:「五香乖乖x1、义美鲜奶茶x1」,然后你就可以回去挑其他你想要的东西,当你再回来收银台的时候把这张纸条给小明,小明就知道这些东西是你的。

或者你是个常客,每次来都买一样的东西,小明就在结账时写给你一张纸条,把你常买的东西全都写上去,这样下次结账时你只要带那一张纸条过来,小明就知道你常买什么了!

你有看过那种凄美爱情电影吗?男女主角其中一方得了罕见疾病,每天都会彻底失忆一次,另一方就会在家里帮他写满便条纸,透过那些便条纸,主角才能知道自己是谁、对方是谁,以及自己到底发生了什么事。

对,你可以把小明想象成就是失忆的那个,而便条纸就是给客人的纸条。既然自己记不住,就让这些纸条代劳,把状态放在上面。

虽然说客人要把纸条留着其实蛮不方便的,但前面说过小明人缘其实不错,因此常客都会看在他的面子上把纸条带着,让这个机制得以继续运作。而小明店里的生意也因此好转一点点。

对,只有一点点而已,因为随身携带一张纸条实在是太麻烦了,所以也没多少人会这样做。

再继续往下讲之前,我们先进入中场休息。

中场休息

让我们先从比喻回到网络世界里,HTTP 是无状态的,所以每一个 Request 都是不相关的,就像是对小明来说每一位客人都是新的客人一样,他根本不知道谁是谁。

既然你没办法把他们关联,就代表状态这件事情也不存在。

把左边换成顾客,右边换成小明也依然成立。多一个得是我多打了,但我懒得修。

那怎么办呢?在故事里我们用纸条来解决这件事情,小明会在结账时写下纸条并递给客人,客人下次只要再带着纸条过来,小明就知道发生什么事了。

小纸条功不可没

小明最大的问题就是他自己没办法记忆「状态」,因此需要倚靠一个机制来帮他管理「状态」,而这个机制我们就叫做 Session。

原本对小明来说,每一个客人都是新的客人,彼此之间毫无关联,所以也没有任何状态可言。

但有了纸条以后,两个在小明眼中完全不同的客人被关联了起来,小明就可以知道:「原来这个新的客人是以前那个来买木材的客人!」

所以 Session 是什么?就是一种让 Request 变成 Stateful 的机制。以小明的例子来说,Session 就是一种让客人之间能互相关联起来的机制。

小明靠纸条来实作 Session 机制,那在网络世界中可以靠什么呢?举一个最简单的例子,网址!

让我们假设有个购物网站的网址是:market.tw,当你把苹果加入购物车的时候,你其实是送一个 Request 给服务器,然后服务器会把你导到 market.tw?

item1=apple,接着你再把火山硅肺病加入购物车,网址就会变成:market.tw?item1=apple&item2=pneumonoultramicroscopicsilicovolcanoconiosis

最后你按下结账,服务器就靠着你地址栏上的信息来判断你的状态是什么,在这个例子中就等同于看你的购物车里面有什么。

简单来说呢,地址栏上的信息就是小明故事中的纸条,是储存状态的地方。而上述例子 Client 与 Server透过地址栏上的状态来实作 Session 机制。

好,中场休息差不多到这边要结束了。这一段是想先拉回网络的部分,从原本故事中的比喻切回真实世界网络的运作模式,以及先让大家理解 Session 到底是个什么东西。

在接下来的故事里面,小明会碰到更多更多的问题,他能迎刃而解吗?让我们继续看下去。

到底谁会随身携带纸条?

前面已经有提过了,尽管小明靠着这个纸条的机制留住了一些常客,但是新客人呢?有多少人会愿意为了再来这间店而特地留下具有状态的纸条?

基本上没有,因为这样子太麻烦了!

有天小明在快要入眠时,忽有一庞然大物拔山倒树而来,盖一灵感也。他想到了一个绝妙的 idea:「不会有人随身携带纸条,但总会随身携带手机吧!」

于是流程就变成这样子:

  • 客人来店里消费,小明结账时请他拿出手机,并在手机里面留了一些信息
  • 客人第二次来店里,小明看看手机里有没有之前自己留下的信息

先不用管到底小明把信息放在手机的哪里,这不是重点;重点是手机里的信息取代了以前的纸条,客人不用刻意再带一个没有用的纸条了,只需要把本来就会随身携带的手机拿出来就好,跟以前相比方便许多。

好,接下来我们终于要讲到标题的第二个东西了:Cookie。Cookie 是什么?Cookie 就是故事里面存在手机的信息。

想要知道真正使用 Cookie 的流程,你只要把上面的客人用「浏览器」来取代,小明用「服务器」来取代,就是答案了:

  • 浏览器发送一个 Request 给 Server,Server 叫浏览器设置 Cookie,浏览器便把这些数据存在 Cookie 里面。
  • 浏览器带着 Cookie 一起发 Request 给 Server,Server 根据 Cookie 的内容决定状态。

这次没有买火山硅肺病了

虽然在现实生活中不是每个人都会随身携带手机,但是每个浏览器都会把 Cookie 一并带上去,也会按照 Server 的指令来储存 Cookie。

你可以把 Cookie 称作是一个机制,Server 可以利用 Set-Cookie 这个语法让浏览器储存一些内容,而这些内容会在浏览器发送 Request 时一并送上来。

而浏览器里储存的那些内容也叫做 Cookie,就是我们故事中所提的小纸条或者是存在手机里的信息。

前面有提过 Session 机制可以只靠地址栏操作,跟 Cookie 可以一点关系都没有。

但在实际应用上,Session 之所以常常跟 Cookie 绑在一起,就是因为靠 Cookie 来操作 Session 机制的话非常方便。

或者应该这样说,Cookie 本来就是为了操作 Session 而生的。藉由标准化的规范,制定了一个专门用来让浏览器与 Server 交换数据的机制。

如果用故事来比喻,就好比政府制定说每个人随身一定要携带手机,然后手机里面一定要存小明留下来的状态。

这边再来做个简单的总结:

Session 是什么?就是一种让 Request 变成 Stateful 的机制。以小明的例子来说,Session 就是一种让客人之间能互相关联起来的机制。在故事里面我们用了纸条跟手机里的信息来比喻,有多种方式可以达成 Session。

在网络世界中,也有很多种方式可以来操作 Session,前面介绍过第一种是地址栏,第二种就是靠 Cookie。而 Cookie 就是存在浏览器里的一些信息。

讲到这边,差不多就把 Session 与 Cookie 的定义与介绍讲完了,但故事还没完呢,我们还有最后一个问题要来解决。

咖啡寄杯的烦恼

虽然店里生意还可以,但小明无时无刻不想着怎么样发大财赚大钱,让店里的生意变得更好。

他观察到最近好多便利商店开始卖起了咖啡,而且时不时就买一送一或是第二件半价,并且贴心地提供了寄杯的服务。

寄杯就是指说你今天先喝一杯,剩下那杯我帮你记着,你下次来的时候跟我讲我再给你。

如果不提供这种服务,那买一送一就一定要两个人才能喝了(或是你立刻喝两杯),根本就是排挤像小明这样的边缘人。秉持着将心比心的原则,小明当然是希望提供寄杯服务的。

那该怎么寄呢?照之前那样不就得了吗?原本客人的手机里面会存着消费习惯之类的东西,现在多存一个还有几杯咖啡不就行了?

例如说客人买两杯只喝一杯,就在上面写着:coffee=1,代表还剩一杯咖啡,下次来的时候只要出示这个信息,就再给他一杯。

听起来十分合理,而且小明也这样做了,店里的生意变得更好,买咖啡的人愈来愈多,靠着咖啡就让单月营收翻了两倍。

一切看似非常顺利,直到小明月底对帐的时候:不对啊,为什么买咖啡的数量只有 55 杯,卖出去的却有 66 杯?

一向很相信人的小明,在那一瞬间见识到了人心的险恶之处。没错,有人自己偷改信息,例如说把 coffee=1 加个几划改成 coffee=7,就获得了额外六杯的免费咖啡。

这些奥步让小明狠狠一夜之间变成了大人,绝望的小明把悲愤转化成力量,只花了三个晚上就想到了两个解决方法。

第一个方法最简单,就是只要把存在客人手机上的信息加密就好了。

例如说原本是 coffee=1,经过小明自制的特殊加密算法之后,会变成 ED85B89167A84B631C10B046B5FB7FC0 这串只有小明知道怎么解开的密文。

这样一来,除非客人可以破解这段密码,否则信息就不可能被窜改。但有一个小缺点,那就是当小明想存的信息愈来愈多之后,这一串字也会愈来愈长,就会在客人的手机里面占更大的容量。

这个容量是有上限的,客人不会把整台手机都给你存这些信息,所以这点要特别注意。

这个方法解决问题的思路是这样的:「既然存在手机上的信息会被窜改,那我让他不能改就好」。

而第二个方法解决问题的思路是这样的:「既然存在手机上的信息会被窜改,那我把信息存在我这边不就好了吗?」

与其把那些消费习惯或是寄杯数量存在客人的手机里,不如把这些东西记在我的笔记本里面,并且用一种方式把这两个信息对应起来,这样就不怕数据会被改动了。

举例来说,小明可以在笔记本写下客人的身份证字号跟相关资讯,例如说:「A111111111 coffee=1」,接着小明只在客人的手机里面存「A111111111」。

下次客人再来消费的时候,就透过身份证字号去笔记本里面查,就知道客人到底还剩几杯咖啡了。

由于小明的笔记本每天下班都会锁在保险箱里面,因此不用害怕被偷或是被改,可以假设它一定是准确的。

而这样子的方式不把主要信息存在客人那里,而是存在自己这里,所以也不会有被窜改的风险。

可是有个问题,如果有人把身份证字号改成其他人的怎么办?那不就破功了吗?就可以伪造其他人的身份。

这个简单,不如不要用身份证字号,用一个 16 位数的英数字混合乱码好了,例如说:A59Uhe7I94J330mN,这样就很难被猜到了吧!

于是流程会变成这样:

客人那编只需要报 ID 即可,其他资讯都在小明那里

跟之前一样,他们都是透过一张纸条或者是手机里的信息来沟通,但唯一的差别是客人跟小明之间只透过 A59Uhe7I94J330mN 这个存在手机里的 ID 来验证身份,其他相关资讯都写在小明的笔记本里面。

这种验证的方法就像是我曾经去过的网咖。因为会员打咖比较便宜嘛,一小 60 变成一小 36,不办白不办,就办了一张会员卡。店员特别说明认卡不认人,一定要出示卡片才行。

我只要去打咖的时候出示这张会员卡,店员就知道我曾经消费过多少钱,也知道我喜欢点的餐点,所有的信息都是存在他们的系统里面,而我的身份就是透过这张会员卡来表示。

寄杯的例子中,会员卡就是 A59Uhe7I94J330mN 这个 ID,网咖的电脑系统就是小明的笔记本。

小明最后决定用第二种方法,也就是这种靠 ID 认人的方式来管理客人的状态。

从此之后就没有客人能够窜改信息了,而寄杯服务也运行的十分顺利,真是皆大欢喜,可喜可贺。

至于后来变得生意太好,让小明开了分店以后碰到的那些问题,就又是另外一段故事了。

储存状态的方式

小明的故事说完了,该来把上面这一段变成网络的实际案例了。其实在网络世界中问题也是一样的。

前面已经提到过我们会把状态存在 Cookie 里面,让 Request 之间能够变得有关联。

假设我们今天要来做一个会员系统,那我要怎么知道这个 Request 代表的是哪一个会员?

最直觉的方式就是登入以后把会员帐号存在 Cookie 里面嘛,这样不就知道是谁了吗?

可是会碰到的问题就跟寄杯的故事一样,Cookie 里的东西是可以被窜改的,如果我改成了别人的会员帐号,我就可以伪造他的身份登入了!

解决方法跟上面寄杯的解法一样:

第一个解法就是把 Cookie 里面的内容给加密,这样就无法被窜改了。这种方式就称之为 Cookie-based session,意思就是你把所有的 Session 状态都存在 Cookie 里面。

所以不要把「用 Cookie 来操作 Session 机制」跟「Cookie-based session」搞混了,两者是不一样的。

至于缺点的话前面有提到,Cookie 的大小是有限制的,超过大小的话浏览器就不帮你存了。

因此当你想存的信息越来越多,Cookie 当然也越来越大,就有可能超过这个限制。

或者是哪天你的加密方式以及密钥被黑客破解,那黑客一样可以伪造任何人的身份。

第二个解法就是透过一个 ID 来辨识身份,这个 ID 称之为 Session Identifier,简称 Session ID。

Server 只在 Cookie 里面存一个 Session ID,其余的状态都存在 Server 那边,我习惯把 Server 那边的数据称为 Session Data:

其实就是小明笔记本的翻版而已

Session ID 的产生方式跟前面说的一样,通常会是一个无法猜测的随机数。

你可能会想说:「很难猜是一回事,但机率不是 0 啊!」,对,的确是有机率能够猜到,但是那个机率太低太低了(例如说几千亿分之一之类的)。

而且 Server 在你乱猜猜错几次之后就有可能把你 ban 掉不让你继续猜,所以没什么问题。

不过这边要特别注意的一点是 Session ID 基本上是种认证不认人的方式,也就是说一旦你的 Session ID 被偷走,别人就可以伪造你的身份来登录了。而这个 Session ID 通常都是保存在 Cookie 之中。

这就是为什么有些网站发生骇客入侵的情形之后你会突然被注销,因为黑客可能偷到一批 Session ID,这时候服务器就会把所有 Session 数据全部清空。

以故事来比喻就是把笔记本丢掉,买一本新的,这样被偷走的那些 Session ID 就没用了,而 Server 找不到你的 Session ID,自然就无法登入,因此把你给注销了。

网站发生问题时客服会要你先把 Cookie 清掉也是类似的道理,因为 Cookie 跟状态有关,有时候可能程序有一些 Bug,把你导到了错误的状态,把 Cookie 清空等于把状态清空,重新再开始,就有可能变得正常。

总结

其实我原本以为我很懂 Cookie 跟 Session,但越研究越发现好像不是这么一回事,只是我自我感觉良好而已。

但把该看的数据都看完一遍之后,再让自己沉淀个几天,大致上就能完全理解整个脉络的发展。

Session 是什么?就是一种让 Request 变成 Stateful 的机制。以小明的例子来说,Session 就是一种让客人之间能互相关联起来的机制。

在故事里面我们用了纸条跟手机里的信息来比喻,有多种方式可以达成 Session。

在网络世界中,也有很多种方式可以来实作 Session,前面介绍过第一种是地址栏,第二种就是靠 Cookie,而 Cookie 就是存在浏览器里的一些信息。

常见的错误认知是一定要有 Cookie 才能实作 Session,这是错误的。

有了 Session 之后,会碰到数据被窜改的问题,这时候有两种解决方式:

  • 一个是 Cookie-based session,意思是你照旧把状态存在 Cookie,但是加密以后再存。
  • 另一个方法是把状态存在 Server 端,靠一个 Session ID 来辨识,这个状态你可以存成档案,可以存在內存里,也可以存在数据库,只是实作方式的不同而已,但原理都是一样的。

而这个状态储存的地方在口语上也会被称之为「Session」,例如说:「帮我把 user id 存在 Session 里」,或者是「注销记得把 Session 清空」之类的。

所以在实际用法中,我认为 Session 之所以不好理解是因为太多地方用到同一个词,但却是在指代不同的东西(可是又很类似)。跟 API 有点像,太多地方都用到这个词了。

作者:胡立
出处:https://medium.com/@hulitw/se...

关注 民工哥技术之路 微信公众号对话框回复关键字:1024 可以获取一份最新整理的技术干货:包括系统运维、数据库、redis、MogoDB、电子书、Java基础课程、Java实战项目、架构师综合教程、架构师实战项目、大数据、Docker容器、ELK Stack、机器学习、BAT面试精讲视频等。


民工哥
26.4k 声望56.7k 粉丝

10多年IT职场老司机的经验分享,坚持自学一路从技术小白成长为互联网企业信息技术部门的负责人。2019/2020/2021年度 思否Top Writer