












平时写前端 / 全栈,服务端鉴权我一直是"会用但说不清"。这篇是我把它从头啃明白的笔记,基于公开标准(OAuth 2.0 / RFC 6749、JWT / RFC 7519),配一个能跑的最小 demo。如果你也被 JWT、OAuth、token 刷新绕晕过,希望它对你有用。
你有没有想过:
这篇就从最基础的概念,把这几个问题一路讲清楚。(不熟的名词,文末有「名词解释」可随时查。)
两个总被搞混的词,先掰开:
一句话:先"认证"确认你是你,再"授权"决定你能碰哪些东西。
实现登录态,有两条主流思路:
| Session-Cookie | Token (JWT) | |
|---|---|---|
| 状态存哪 | 服务端存 session,客户端只存 sessionId | 服务端不存,token 自己带信息 |
| 水平扩展 | 多台机器要共享 session(如 Redis) | 无状态,天然好扩展 |
| 跨域 / 多端 | Cookie 跨域麻烦 | 带 header 即可,App / 小程序友好 |
| 注销 | 删 session 即时失效 | 签发后难即时失效(要额外机制) |
怎么选?
OAuth 2.0(RFC 6749)是业界标准。最常用的两种 grant:
① 用户授权 —— Authorization Code 用户在授权页点"同意",应用拿到授权码,再换成 token。 典型场景:"用 GitHub / 微信账号登录某网站"。
② 服务间调用 —— Client Credentials
没有用户参与,一个服务用自己的 client_id + client_secret 换 token,去调它有权限的接口。
典型场景:后端服务之间互调。(这俩凭证怎么来、怎么验,见第 5 节。)
JWT 在这里是什么角色?
JWT(RFC 7519)是 token 的一种自包含格式:本身就带着身份(sub)和权限(scope),外加签名防篡改。服务端不用查库,验签 + 读 claims 就知道"你是谁、能干啥"。
简单记:OAuth 解决"怎么安全地把 token 发给你",JWT 解决"这个 token 长什么样、怎么自证"。
这是 JWT 最关键、却最容易被略过的一点。既然 claims 是明文、谁都能解码看到——那攻击者把自己的 scope 改成 admin,不就越权了?
答案藏在 JWT 的第三段:签名 (Signature)。一个 JWT 长这样,三段用 . 隔开:
header . payload . signature
eyJ... . eyJ... . KkvtzZ...
验证真伪的过程(验签):
header.payload 重新算一遍签名。为什么伪造不了? 攻击者能看、甚至能改 payload(比如把 scope 改成 admin),但他没有密钥,算不出对应的合法签名。服务端用真密钥一验,对不上,当场识破。
记住这句:JWT 的安全不靠"藏住内容"(内容是明文的),而靠"签名"——没有密钥,就伪造不出能通过验证的 token。
两种签名方式(知道即可):
secret,签发和验证用同一把钥匙。适合自己签、自己验(单体应用)。下面的 demo 就是这种。那公钥从哪来?—— JWKS + 公钥轮换
用 RS256 时,签发方把公钥挂在一个标准端点(JWKS,常见 /.well-known/jwks.json):里面是一组公钥,每个带一个 kid(key id);JWT 的 header 也带 kid,验证方按它取对应公钥来验签。验证方会缓存公钥;而签发方会定期轮换密钥,所以缓存有过期时间——一旦过期、或遇到没见过的 kid,就重新拉一次 JWKS 再验。妙在:公钥能公开分发,验证方不用预存任何密钥、签发方能独立换钥,这也是它撑得起 SSO / 多服务的原因。
第 3 节的 Client Credentials(服务间调用)里,服务靠 client_id + client_secret 换 token。这俩哪来的、怎么验、怎么管,是真实工程里绕不开的一环。
client_id ≈ 用户名:公开,标识"你是哪个服务"。client_secret ≈ 密码:机密,证明"你真的是这个服务"。区别只是:它给机器 / 服务用,不是给人用。
在授权服务器 / 平台**「注册应用」时分配**的:
client_id 通常是个唯一串(如 UUID);client_secret 是一段高熵随机串,常只在创建时显示一次,平台自己存的是它的 hash(和用户密码一个道理)。服务带着 id + secret 来换 token,授权服务器:
Authorization: Basic base64(id:secret))。client_id 查出记录。401 invalid_client。看出来了吗:和用户登录是同一套 —— 查身份、比对凭证 hash、发 token。
光看概念容易飘,我写了个最小可跑的服务(完整代码 👉 GitHub · jwt-oauth-demo),五个接口刚好覆盖上面所有概念:
| 接口 | 作用 |
|---|---|
POST /login | 账号密码换 access token + refresh token |
GET /profile | 受保护接口,要验 access token |
GET /admin | 受保护 且 要 admin scope(演示授权 ≠ 认证) |
POST /refresh | 用 refresh token 换新的 access token(无感续期 + 轮转) |
POST /token | 服务间 client_credentials 换 token(凭证怎么来 → 见第 5 节) |
跑起来后,有几个现象值得亲自验证(把 auth.js 里 ACCESS_TTL 改成 '30s' 更明显):
/profile → 正常返回;不带 token → 401。/profile 报 401 → 调 /refresh 拿到新 token → 又能访问了(这就是"登录一次后长期不用重登"的原理)。/admin(她有 admin scope),换 bob 就 403(他没有)—— 这就是"认证通过 ≠ 有权限"。/profile → 401,亲眼看到第 4 节说的"篡改后验签失败"。想看 claims 长啥样:把
/login拿到的 token 第二段解码即可——node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1],'base64url')))" "你的token"。你会看到sub、scope、exp明明白白(也就亲眼验证了"JWT 是明文")。
把 demo 跑通后,这几点是真正要记住的:
鉴权这套东西,拆开看其实就几层:认证(你是谁)→ 授权(你能干啥)→ 会话维持(用 access / refresh token 让你不用反复登录);而签名保证这张牌不会被伪造,密钥管理保证发牌的"母钥匙"不泄露。OAuth 负责安全发牌,JWT 负责让牌自证。
一张速查表收尾:
| 场景 / 问题 | 推荐做法 |
|---|---|
| 传统单体网站、要即时注销 | Session-Cookie |
| 多端 / App / 小程序、跨域 | JWT(access) + 服务端 refresh 白名单 |
| 让用户用第三方账号登录 | OAuth2 Authorization Code |
| 后端服务之间互调 | OAuth2 Client Credentials |
| access token 有效期 | 短(如 15 分钟) |
| refresh token | 长(如 7 天)+ 存白名单可撤销 + 轮转 |
| 防伪造 | 靠签名验证;跨方验证用 RS256(公钥验) |
| client_secret / 密码 | 注册分配、hash 存、密钥管理读、HTTPS 传、可轮换 |
| 前端(SPA/App)拿 token | 用 PKCE,不要放 client_secret |
| 敏感信息 | 绝不放进 JWT(能被直接解码) |
刚接触鉴权,这些词容易卡,放这儿随时查:
header.payload.signature 三段组成。sub、scope、exp。profile、admin。kid 选对 key,并随密钥轮换更新。这是我"补服务端短板"系列的第一篇(讲机制)。第二篇 👉 《鉴权怎么落地?—— 场景选型与避坑》 讲真要做时怎么选方案、避哪些坑。完整代码与后续都在我的 GitHub:Aimee1608/backend-notes。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。