跳到主要内容

第 13 章 认证与授权:业务的"门禁"

副题:Session vs JWT——凭证怎么生成、怎么验证、怎么吊销

第 12 章把服务端组织好了:分层清晰、链路通畅,鉴权中间件在管道里留好了位置(第 13 章主角)。现在补上这道门禁:**这个请求是谁发的?他有资格吗?**HTTP 是无状态的(第 10 章说过"记不住用户"),但业务必须记住。本章跟着一次登录,把"记住用户"这件事做好,并用决策模型完整走一遍 Session vs JWT 的选择。

本章要建立的认知

  1. 登录的本质是发凭证:凭证怎么生成(无法伪造)、怎么验证(每次请求报上名来)、怎么吊销(退出登录立即失效)——Session 和 JWT 是两种凭证形态,解决的是同一个问题;
  2. 选 Session 还是 JWT,看九步决策,不看"哪个先进"——业务约束(单机?多端?要不要踢人?)决定答案;
  3. 认证(你是谁,401)和授权(你能干什么,403)是两道不同的门——本章都讲,但先分清。

13.1 改一个数字就是另一个人​

故事从一次"身份事故"开始。

接口 POST /v1/login 是个"动作"接口(第 7 章的 12 个接口里,它是为数不多的动作型例外),接受账号密码,返回凭证,401 表示凭证无效。

第一版后端要加登录。小明(兼职前端)图省事,凭证方案选了最简单的:登录成功后,把 userId 直接写进 Cookie:

// 第一版:登录成功就把 userId 写进 Cookie
app.post('/v1/login', (req, res) => {
const user = db.findUser(req.body.username, req.body.password);
if (!user) return res.status(401).json({ code: 'INVALID_CREDENTIALS' });
res.setHeader('Set-Cookie', `userId=${user.id}`); // 明文!谁都能看、谁都能改
res.json({ ok: true });
});
// 之后的请求:读 Cookie 里的 userId 就知道是谁

上线第三天,小明发现了一个让他后背发凉的事:他本来只是调试——在 DevTools 的 Application 面板里看 Cookie 有没有种上,结果看到 userId=2 明晃晃躺在那里。他把它改成 userId=1,刷新,自己就变成了老周——能看到老周的私信内容、能替老周发帖。改一个数字就是另一个人,整个社区没有秘密。

这个事故的根源一句话:凭证是"我是谁"的证明,而这段凭证没有任何防伪手段——它像一张只写名字不盖章的工作证,谁都能自己手写一张。

事故的"发现时刻"也顺带想一下:小明是调试时碰巧看到才发现的——如果没人碰巧看到,这个洞会一直开着:攻击者什么时候发现,只是时间问题,而攻击者发现的概率远比"自己人碰巧看到"高。

凭证这个物件的形态,人类已经换了好几代:令牌(古代——一块刻了字的牌子,验牌认人,服务端认"牌"不认"人",和 Session 的思路一模一样)→ 票据/印章(近代——纸上盖章,章就是防伪,和 JWT 的思路一模一样)→ 数字凭证(现在——Session 是电子令牌,JWT 是电子印章)。人类几千年都在解决同一件事:怎么让"证明我是我"的东西无法伪造——理解这一点,Session 和 JWT 就不再是陌生名词,而是两个古老的思路换了数字形态。

"先让登录能用,防伪以后再说"。但"以后"没有自动到来,事故先到了。凭证是系统里最不能"先凑合"的东西,因为它是信任的唯一来源:前端信任它(带它调接口)、服务端信任它(凭它放行)、用户信任它(凭它免登录)——三方都信一样东西,它伪造了,整个信任链就塌了。

伪造的后果也要分级看清楚(方便下次估风险):最低级——冒充看私信(隐私泄露,一次事故);中级——冒充发帖/评论(污染内容,影响所有读者);最高级——冒充下单/支付(阶段 3 开通后,冒充老周下单就是真金白银的损失)。同一个漏洞,业务越重,代价越大——所以第 4 章把交易后置,不只是砍功能,也是砍风险:阶段 1 的伪造事故赔得起,阶段 3 的赔不起。

事故后的处置顺序也顺带走一遍(未来排障流程的雏形):先止血——把明文凭证方案立刻换掉(哪怕先换最朴素的随机串);再定位——确认影响面:日志里有没有异常 userId 的请求?谁可能看过或改过 Cookie?最后根治——用本章的决策流程选正式方案。止血不能等根治:安全事故的第一原则是"先关水龙头,再修水管"——不少团队栽在"想一步到位,结果洞多开了一天"。

还有一个心理细节要承认:当时觉得明文没关系,因为"我在本地跑,没人攻击我"。开发期只有自己一个用户,当然没人改 Cookie。但系统一上线,用户就不是"自己人"了:老周会试、小明会试、路过的任何人都会试——信任模型必须从第一行代码就假设"对方不怀好意"。

13.2 凭证必须防伪:两条思路​

事故之后,防伪成了硬需求。怎么让凭证无法伪造?翻遍方案,其实只有两条路:

思路 A:凭证本身不含信息,服务端记着"凭证↔用户"的对应。凭证是一个随机长字符串(a1b2c3…,没人猜得到),服务器在自己的存储里记一行:凭证 a1b2c3 ↔ 用户 2。请求来了,浏览器报上凭证,服务端查表——查得到,就知道是谁。凭证像餐厅的等位号:号本身没意义,意义在服务员(服务端)手里的登记表。

思路 B:凭证自己带着信息,但加上"盖章"防伪。凭证里直接写 userId=2,但后面附一段签名——签名是用只有服务器知道的密钥算出来的:内容 + 密钥 → 签名。请求来了,服务端用密钥重新算一遍签名,对得上=内容没被改过=可信。凭证像盖了公章的公文:内容是明文,谁都能读,但公章(密钥)只有发证机关有——想伪造,得先偷到公章。

两条路的本质区别一句话:A 把"这个人是谁"记在服务端(查表),B 把"这个人是谁"写在凭证里(验签名)——一个靠存储防伪,一个靠签名防伪。

两条路还有一个共同前提:凭证必须随每个请求自动携带——浏览器自动带 Cookie。没有这个自动携带,每次请求都要手动报身份,系统没法用。所以两条路的工程形态都依赖同一块地基:Cookie 是凭证的运输工具,凭证是 Cookie 里装的东西——这一章就是"砖上盖什么房子"的决策。

凭证除了放 Cookie,还可以放在请求头里:Authorization: Bearer <token>。什么时候用请求头?**没有浏览器的时候。**App 里打开一个页面,没有"浏览器自动帮你存 Cookie、自动帮你带上"这件事——Cookie 是浏览器的专属机制。服务和服务之间调接口,更没有浏览器在中间跑腿。这些场景下,凭证只能由代码手动塞进请求头,靠 Authorization: Bearer 这种格式传递。这就看出 Session 的一个隐性成本:它依赖 Cookie,等于绑定了浏览器。 你想把同样的登录态搬到 App 里,得重新实现一套"存凭证、带凭证"的机制,因为 Cookie 那套自动逻辑在 App 里不存在。而 JWT 本来就设计放在请求头里,浏览器也好、App 也好、服务间调用也好,都是同一套携带方式,**端不端无所谓。**所以 Cookie 是一把双刃剑:对浏览器来说是便利(自动存、自动带、还能控制过期),对 Session 来说却是绑定。阶段 1 只做网页,这个便利免费享用;如果将来要做 App,这个绑定就是额外的适配成本。

两条路的防伪原理:A 靠"猜不到"——随机串的取值空间大到穷举不现实(256 位随机数,取值空间和宇宙原子总数同一数量级);B 靠"算不出"——签名是单向函数(从内容算签名容易,从签名反推内容/伪造签名,只有拿到密钥才行)。一个赌空间,一个赌函数——都是概率上的"不可能",不是绝对意义上的"不可能"。

两种方法还会面对一个共同的敌人:偷取凭证。防伪防的是"伪造"(自己做一张假的),但凭证还可能被"偷"(XSS 脚本偷 Cookie、网络中间人截获)——偷走的真凭证,Session 和 JWT 都拦不住(它们只能靠吊销止损)。所以防伪解决"假证"问题,防盗解决"真证被偷"问题。防盗以后再解决,本章先把"假证"堵死。

13.3 九步决策完整走一遍:Session vs JWT​

两种方法使用哪个?这里我们也可以用决策模型来评估:凭什么记住你(目标)→ 在什么条件下做(约束)→ 要解决什么(问题)→ 有哪些路(候选)→ 按什么比(维度)→ 各自几分(打分)→ 选哪条(选择)→ 得到什么、付出什么(收益/代价)→ 什么时候重选(演进)。

① 业务目标:登录后 7 天内记住用户;凭证无法伪造;退出登录立即失效;接口分级(有的要登录、有的不用、有的只有本人能动)。

② 业务约束:阶段 1——单人开发、一台服务器、没有 App(只有浏览器)、登录态 7 天、前后端是两个域名(api 域和前端域)。约束里最关键的一条:只有浏览器一个端,一台服务器。

这一步最能看出约束分析的价值:JWT 的名声远大于它在阶段 1 的价值——市场上铺天盖地的"JWT 教程",没有一条约束(单机、单端、要吊销)是为阶段 1 说话的。决策模型的第一个作用,是挡住"名声最好的方案"——名声是别人的约束养出来的,你的约束要自己看。

③ 技术问题:凭证怎么生成、怎么验证、怎么吊销?

④ 候选:

  • A 明文 userId:现状,已证明会被伪造——直接排除,不用再评;
  • B Session(思路 A 的工程化):随机串凭证 + 服务端存储对应关系;
  • C JWT(思路 B 的工程化):自包含凭证 + 签名防伪。

⑤ 评价维度(从业务约束里长出,不是拍脑袋):实现复杂度(单人开发,复杂是成本)、可控性(能不能立即吊销——7 天登录态意味着凭证活得久,活得久的东西必须能随时作废)、跨端支持(将来会不会有 App/多服务)、安全面(凭证泄露/被伪造的风险面)。四个维度各自对应一条约束,对照着看:复杂度←单人开发(一个人写不完复杂方案);可控性←7 天登录态+退出(活得久的凭证必须能作废);跨端←"将来有没有 App"(现在没有,所以这维度现在打不了高分);安全面←"凭证是信任来源"。

⑥ 逐项打分:

评价维度B(Session)C(JWT)
实现复杂度存表+查表,略简单签名库两行调用,也简单
可控性(吊销/踢人)删记录即生效管不到,要黑名单打折
跨端支持(App/多服务)依赖 Cookie,要适配天然支持,优势在将来
安全面凭证泄露可吊销止损密钥泄露全盘崩溃

四行看下来:B 两胜一平一负,唯一输的那项(跨端支持)在阶段 1 用不上——结论已经很明显,但按流程把⑦ 走完(流程感本身就是决策训练)。

⑦ 方案选择:B(Session)。理由用一句话收:阶段 1 的全部约束(单机、单端、要吊销、要简单)都指向 Session,JWT 的优势(跨端、无状态)在阶段 1 没有用武之地——B 赢在匹配,不在先进。

⑧ 收益:防伪(随机串不可猜)、可控(删记录即踢人)、简单(查表验证)、安全责任清晰(都在服务端)。

⑨ 代价与演进条件:代价是服务端要存凭证——每登录一个用户多一行记录(7 天有效期内占着)。单机时代无所谓;将来两个信号出现,重新评估:一是多台服务器(Session 表要共享——这是 Redis 登场的第二个理由:缓存之外,会话共享也要它);二是多端/多服务(App 或微服务——JWT 的跨端优势开始值钱)。

决策记录:

会话方案=Session;理由:单机单端,要吊销能力;代价:服务端存储;演进条件:集群共享存储(第 16 章 Redis)或多端出现(第 26 章)

这一遍走完,结论是"在阶段 1 的约束下,Session 比 JWT 合适"——约束变了,答案可能变。

13.4 Session 原理:等位号与登记表​

方案定了,看它内部怎么工作——把一次登录到一次请求的完整流程走一遍(图 13-1):

登录(POST /v1/login)
│ ① 校验账号密码(密码哈希比对,见下节)
│ ② 生成随机凭证 session_id(不可猜测的长字符串)
│ ③ 服务端存储:session_id ↔ userId(有效期 7 天)
└ ④ Set-Cookie: session_id=…(浏览器保存)
│
▼ 之后每个请求都自动带上 Cookie
请求(POST /v1/contents)
│ ⑤ 鉴权中间件读 Cookie 里的 session_id(第 12 章留的位置)
│ ⑥ 查服务端存储:找得到且没过期 → 有效,req.userId = 2
│ ⑦ 找不到/过期 → 401,前端跳登录页(第 7 章的约定)
└ ⑧ 有效 → 放行到控制器
图13-1 新 图稿占位
Session 流程
登录→Cookie→服务端会话存储

Session 的核心机制一句话:服务端发一把随机钥匙,自己留一份备案,每次请求对一下备案表。

钥匙(凭证)里没有任何用户信息——它只是一串随机字符,真正的身份存在服务端表里。所以钥匙丢了可以挂失(删表),不像明文 userId 那样一丢就是身份泄露。

完整代码形态:

// 登录:业务层生成钥匙,数据层落库
const sessionId = crypto.randomBytes(32).toString('hex');
sessionRepo.save({ sessionId, userId: 2, expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000 });
res.setHeader('Set-Cookie', 'session_id=' + sessionId);

// 鉴权中间件:查备案表(第 12 章管道的位置)
const row = sessionRepo.find(req.cookies.session_id);
if (!row || row.expiresAt < Date.now()) {
return res.status(401).json({ code: 'UNAUTHORIZED' });
}
req.userId = row.userId; // 挂在 req 上,后续控制器和业务层直接用

认证不是独立系统,就是分层架构里的一个普通功能:业务层编排登录动作,数据层管 session 表,中间件在请求管道里做检查。理解了这个位置,比背任何认证框架都重要。

安全底线一:钥匙怎么造?

用密码学随机数(crypto.randomBytes),不要自己拼。时间戳加用户名加固定后缀这种"伪随机"是可以被猜出来的——攻击者知道生成规则就能预判凭证,等于没有防伪。随机数、哈希、签名,都用成熟库,我们只需要专注于业务逻辑。

安全底线二:密码怎么存?

数据库里绝不明文存密码,也绝不用可逆加密(能被解密出来的都不安全)。正确做法是哈希 + 加盐:

  • 每个用户配一个随机盐
  • 存的是 hash(密码 + 盐)
  • 验证时,把用户输入的密码加上该用户的盐,重新算哈希比对

盐就是一个随机生成的字符串,用密码学安全的随机数生成器(CSPRNG)生成,比如 crypto.randomBytes(16).toString('hex'),每次注册或改密码时现造一个。

盐存在哪? 和哈希值一起明文存在数据库里,盐不需要保密——它的目的不是藏起来,而是让每个人的哈希结果都不一样。即使攻击者拿到了数据库,看到盐也没用,因为他还是要针对每个用户单独重新算一遍,彩虹表(预计算表)依然作废。

为什么加盐?没有盐,攻击者可以预先把常见密码的哈希算好(彩虹表),拿到数据库直接对号入座。每个用户盐不同,同一密码哈希值也不一样,预计算表直接作废,攻击者只能逐个硬算。盐是让"撞库"从秒级变成天级的小设计。

业务后果:忘记密码只能"重置",不能"找回"——因为系统也不知道原密码是什么。这不是缺陷,是安全方案的必然代价。

存储与过期:第一版怎么选?

服务端存 session,第一版直接放内存(一个 Map)或建一张简单表(session_id、user_id、expires_at)就够了。过期完全由服务端控制。登录时写 expires_at,每次请求查表比对——7 天一到,记录作废。注意:过期不是删浏览器的 Cookie,而是让服务端备案表里的那行失效。 所以 Session 的可控性(吊销能力)来自这里。

过期记录会累积,需要清理。两个办法:懒清理(登录时顺手删该用户的旧记录,一行代码,第一版够用)和周期清理(定时任务批量删)。存储的每一行都是成本,过期数据是第一批该清的。

Cookie 跨域小坑

案例前后端分域名(api.example.com 和 www.example.com),浏览器跨域带 Cookie 需要 Set-Cookie 配好 Domain 和 SameSite。配错了的症状是:登录成功,但之后的请求都不带凭证,永远"瞬间掉线"。这是 Session 方案里新手最常踩的坑。

产品体验:7 天与滑动过期

为什么是 7 天不是永久?凭证活得越久,被偷的窗口越大。 安全希望短,体验希望长(老周不想每周输密码),7 天是第 4 章需求清单里定下的平衡点。登录态时长是产品决策,不是技术默认值。

"7 天"有两种算法:固定过期(登录起算 7 天,不管用不用)和滑动过期(每次活跃顺延 7 天)。本书案例中使用滑动过期。因为固定过期会出现"昨天还在用,今天突然要重新登录"的突兀感,老周会以为是 bug。滑动的前提是每次请求都更新 expires_at。登录态的产品体验,藏在过期算法的选择里。

接口安全:错误信息也是攻击面

登录接口返回错误时,"账号不存在"和"密码错误"应该返回同一个错误码(统一 401 INVALID_CREDENTIALS)。分开返回等于告诉攻击者"这个账号存在",注册枚举的入口就开了。接口的错误信息,也是安全边界的一部分。

13.5 JWT 原理:盖了章的公文​

Session 讲完了,为什么还要懂 JWT?两个理由:一是演进条件里写着"将来可能换过去",二是面试和团队讨论里绕不开这个名词。

JWT(JSON Web Token)的结构是三段式,用点号连接:

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIsImV4cCI6MTc1MDAwMDAwMH0.xxxxxx
└───── Header ─────┘ └────── Payload ──────┘ └────── Signature ──────┘
算法类型(HS256) 载荷(userId、过期等) HMAC(前两段, 密钥)
  • Header:声明用的什么算法,比如 HS256。
  • Payload:放业务信息,比如 userId=2、过期时间。注意:Payload 是 Base64 编码,不是加密——谁都能解码看内容。
  • 签名:用服务器密钥对 Header.Payload 算一个指纹。这就是 13.2 说的"盖章"——章盖上了,内容就改不了。

验证时,服务端收到凭证,用同样的密钥重新算一遍签名,和凭证里的签名比对。一致 = 内容没被改过 = 可信;不一致 = 伪造。 整个过程不需要查任何表——信息全在凭证里,自己证明自己。这就是 JWT"无状态"的含义。

代码形态:

app.use((req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
try {
req.userId = jwt.verify(token, SECRET).userId; // 重算签名比对,抛错=伪造或过期
next();
} catch {
res.status(401).json({ code: 'UNAUTHORIZED' });
}
});

对比 Session 版(查表验身份),差别一眼可见:一个查库,一个验签。 其他结构完全一样——都是中间件、都是 401 兜底、都是把 userId 挂到 req 上。机制相通,切换只是换实现。

图13-2 新 图稿占位
JWT 结构
Header.Payload.Signature 三段

JWT 的代价:最大的问题是吊销难。 Session 退出登录,服务端删表里那行,凭证立刻作废。JWT 退出登录,凭证还在用户手机里,在 exp 过期之前,它依然能通过验签——服务端根本不知道这张凭证"应该"失效了。

想让 JWT 提前作废,只有两个办法:一是等它自然过期,二是上黑名单。黑名单就是服务端专门存一张表,记录"哪些凭证已经被吊销了",每次验签前先查一遍。但这样一来,存储回来了,查表也回来了,"无状态"的名存实亡。需要吊销的 JWT,和 Session 的差别只剩下"载荷里多写几个字段"。

所以"JWT 无状态"只在特定场景里才成立:凭证短命、不需要吊销,比如微服务之间的一次性调用。

这正是阶段 1 选 Session 的硬理由。 案例要求 7 天登录态,还要能踢人(老周发现账号异常,立刻下线)。JWT 做不到"立刻",Session 可以。决策记录里写的演进条件,现在有了机制层面的解释。

那案例里什么时候该换 JWT? 两个触发点:

  • 多端出现(以后如果做 App):App 没有浏览器自动带 Cookie 的机制,JWT 放请求头更自然。但也可以做 Cookie 适配,所以是"重新评估",不是"必然换"。
  • 服务间调用(未来拆服务后):服务 A 调服务 B,用短命、不需吊销的 JWT 做一次性凭证,这是 JWT 的舒适区。

密钥是 JWT 的命门。 整个系统的安全性完全押在这一串字符上。密钥泄露,任何人都能伪造合法凭证,认证系统瞬间崩塌。用 JWT 的团队,密钥管理是安全审计的第一项。

一个常见误解要澄清: JWT 和 Session 不是"有没有加密"的区别。JWT 的 Payload 是 Base64 明文,谁都能解码看;Session 的凭证也只是随机串,本身不带信息。两者都不靠"藏"来防伪——JWT 靠签名防篡改,Session 靠查表验身份。认为"JWT 是加密的所以更安全",是初学者最常踩的坑。它们是两种不同的取舍,没有谁天生更高级。

但无论底层怎么换,接口契约不变。 凭证无效时返回 401,前端跳登录页——这个约定不随方案改变。换 Session 或换 JWT,前端代码一行不用改。这就是第 7 章"接口是承诺"的日常回报:底下可以重构,上面的约定保持稳定。

最后一条纪律:Payload 别放敏感信息。 userId 没问题,但密码、手机号、身份证号绝不能进去。JWT 是自包含的,任何拿到凭证的人——哪怕不是伪造,只是偷看请求——都能直接解码读出 Payload 里的明文。JWT 的"自包含"是便利,也是信息泄露面。 写进凭证的内容,默认等于公之于众。

13.6 授权:401 与 403 是两道不同的门​

门禁装了,但"进门"和"进哪个房间"是两回事。认证(authentication)回答"你是谁",没凭证就 401;授权(authorization)回答"你能干什么",没权限就 403。

我们把第 7 章留下的接口权限标注表,实现一下:

接口认证要求授权要求
POST /v1/login无无
GET /v1/contents无(公开)无
POST /v1/contents要登录无(登录即可发)
POST /v1/orders要登录无
DELETE /v1/contents/{id}要登录只有作者本人

认证在中间件。 authMiddleware 验证凭证(Session 查表或 JWT 验签),有效就把 userId 挂到 req 上,无效直接返回 401。前端收到 401 就跳登录页——这是当时定好的契约。

哪些接口过这道关?照着表挂,不要全挂再豁免。 常见坑是 app.use(authMiddleware) 全局一挂了事,结果登录接口自己也过不了鉴权——用户永远登不进去。正确做法是:公开接口(内容列表)不挂;需要登录的接口逐个挂,或者把登录、注册这类"无认证入口"显式放在中间件之前。

无认证入口要列清单维护: 登录、注册、密码重置。漏挂一个在鉴权后面,新用户就永远 401;多挂一个在外面,公开接口就裸奔。

授权在业务层。 中间件只认识"你是谁"(req.userId),不认识"这条内容是谁的"。所以 DELETE /v1/contents/{id} 的"只有作者能删",要在业务层实现:先查 content 表的 author_id,跟 req.userId 比对,不是本人就返回 403。

401 和 403 千万别用反。

  • 401:凭证没带或无效 → "你没进门" → 前端跳登录页
  • 403:凭证有效但权限不足 → "你进门了,但这个房间不能进" → 前端提示无权限

用反了,前端行为全错:该跳登录页的时候提示"无权限",该提示"无权限"的时候把人踢去登录。

前端怎么处理?

Session 模式下,浏览器自动存 Cookie、自动带 Cookie,前端几乎不用管凭证携带。只需要做一件事:统一拦截 401。 用请求拦截器(框架或 HTTP 库都支持),所有响应先过一遍,遇到 401 就跳登录页,业务代码不用每个接口都写一遍。

这是"横切关注点"思想的前端版——和后端中间件是孪生兄弟:日志、鉴权、错误处理这些横切的事,前后端各自抽成一层,机制一模一样。

授权模型将来会变复杂。 案例现在只有"公开/登录即可/只有作者"三种,是最简单的模型。阶段 3 上线管理员功能后,会引入角色(管理员/普通用户),从"逐人判断"变成"按角色判断"(RBAC)。到时候权限表加一列"角色"就行——权限模型是跟着业务长出来的,不是一开始就设计出来的。

最后预告:权限表是最好的测试用例清单。 每个接口三问:没登录调用(应 401)?登录了没权限调用(应 403)?有权限正常调用(应 200)?一张表,一行接口,三行断言。

13.7 OAuth 一句话 + 代价与演进​

OAuth 一句话定位: "用微信登录"是 OAuth 的活。它解决的是"第三方应用能不能读取我的身份"——你把身份授权给微信,微信发一个授权凭证。但 OAuth 和"登录后怎么记住你"是两个层次:OAuth 只管"怎么拿到身份",Session 或 JWT 才管"拿到身份后怎么保持会话"。将来接入第三方登录,组合是"OAuth 拿身份 + 自己的 Session 保持会话"。OAuth 不会替代本章的凭证方案,只是多一个身份来源。

什么时候引入? 阶段 2 用户增长期,老周的朋友不愿意再注册一套账号,喊着"用微信直接进"——这个诉求逼出第三方登录。但结构不变:仍是 OAuth 拿身份,已有 Session 保持会话。

本章代价清单

Session 的代价:

  • 服务端要存凭证,每登录一人占一行,7 天内有效
  • 多台服务器时必须共享凭证表——这是第 16 章 Redis 登场的第二个理由(第一个是缓存)
  • 凭证放在浏览器 Cookie 里,有被偷的风险面。偷走的真凭证只能吊销止损(第 19 章展开),所以登录态别留太长。7 天是产品权衡的结果,不是技术默认值

JWT 的代价:

  • 吊销难、密钥是命门。阶段 1 不选它,但这笔账要记着

共同代价:

  • 凭证失效要处理——过期、被吊销、多设备互踢。Session 模式下这些都是服务端表里的一行操作,这也是它适合小团队的原因

多设备互踢的产品细节: 一个用户可以在多台设备登录,Session 表里就有多行,每台设备一个凭证。"退出登录"只删当前设备那行;"改密码后踢掉所有设备"则删该用户的所有行。踢单设备还是全踢,是产品决策——登录页里"您的账号在其他设备登录"的提示,背后就是这两行删除的差别。

演进回顾: 明文 userId(事故)→ Session(被防伪逼出)→ 集群共享/多端。每一次都是"原方案无法承受"逼出来的,和前面章节同一个节奏。

全章收成一条线——凭证的完整生命周期:

  1. 生成(登录:随机串或签名)
  2. 携带(Cookie 或请求头,浏览器自动带)
  3. 验证(鉴权中间件:查表或验签)
  4. 失效(过期、退出删除、改密码全踢)

13.8 本章对应表​

业务诉求技术选择为什么代价/取舍
登录后 7 天记住用户会话凭证(Session)HTTP 无状态需补丁(第 10 章"第一块砖"兑现)服务端要存凭证,凭证有被偷风险
凭证防伪随机串 + 服务端存储(Session)/ 签名(JWT)明文凭证已证明可伪造(13.1 事故)靠存储或靠密钥,各有命门
密码安全存储哈希 + 加盐明文/可逆加密都是灾难(第 19 章)忘记密码只能重置不能找回
退出登录立即失效Session 吊销(删记录)7 天登录态必须能随时作废JWT 要黑名单才做得到(无状态打折)
接口分级(谁能用)认证中间件(401)+ 业务层授权(403)第 7 章权限标注表兑现;分层各司其职(第 12 章)权限表要维护,401/403 用反前端全错
第三方登录OAuth(一句话定位)不自己造身份授权轮子引入外部依赖,两个层次别混
Session vs JWT九步决策(★完整实战)约束决定答案:单机单端要吊销选 Session演进条件:集群共享/多端再评估

HTTP 无状态,需要 Session 保持登录态;凭证用随机串防伪(Session 存服务端 / JWT 用签名),密码哈希加盐存储。退出登录需即时吊销,Session 天然支持而 JWT 需黑名单。接口分级通过中间件认证(401)与业务层授权(403)实现,第三方登录用 OAuth。

本章小结​

  • 登录的本质是发凭证:生成(防伪)、验证(报上名来)、吊销(随时作废)——Session 和 JWT 是两种凭证形态,解决同一个问题;
  • 选 Session 与先进无关,它是决策模型在阶段 1 约束下的答案;
  • 认证(401)和授权(403)是两道门:中间件管进门,业务层管进房间;
  • 密码存哈希、Cookie 里的凭证会被偷——留给第 19 章安全章修补。

下一章​

门禁装好了:谁是谁(认证)、谁能干什么(授权)都清楚了。请求终于可以一路走到业务层,干真正的活了——但业务干的活,全都围着一样东西转:数据。

内容存哪张表、怎么查得快、事务怎么保证"要么都成功要么都回滚"。第 14 章,数据层:业务的记忆——身份确认了,接下来看业务怎么记住它自己。