SSO 与 OAuth 2.0 实战
认证授权是每个产品的基础,但 OAuth/OIDC/JWT/Session 的关系很多人没理清。本文讲清现代 Web 认证:Session vs JWT、OAuth 2.0 授权流程、OIDC(认证层)、SSO 实现、token 存储与安全,让你不再被这套概念绕晕。
本文要点
- 主题聚焦:React、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
做过五个以上项目的认证系统后,我发现一个规律:90% 的开发者能把"用 Google 登录"跑通,但其中不到 10% 能讲清楚 OAuth 和 OIDC 的区别、JWT 为什么不能直接拿来"登出"、access token 放 localStorage 到底错在哪。这套概念之所以绕,不是因为难,而是因为名字太多、层次叠在一起、每个名字背后都拖一段历史包袱。
本文把现代 Web 认证里最绕的几对概念——认证 vs 授权、Session vs JWT、OAuth vs OIDC、单点登录 SSO——按层次拆开讲。代码以 Next.js / React 场景为主,但思路适用于任何 Web 栈。
一、先把"认证"和"授权"分清楚
很多人混着用,但它们是两个完全不同的问题:
- 认证(Authentication,缩写 AuthN):回答"你是谁"。账号密码、人脸识别、指纹、短信验证码——都是认证。系统验证你的身份声明是否成立。
- 授权(Authorization,缩写 AuthZ):回答"你能干什么"。你已经登录了(身份已知),系统判断你有没有权限读这个文件、调这个接口、进这个后台。
认证:你是张三吗? → 出示身份证
授权:张三能删这个文件吗? → 查权限表
先认证,后授权,顺序固定。HTTP 401 是"没认证或认证失效"(请先登录),403 是"认证了但没权限"(你登录了,但不能删别人的文章)。
这个区分不是哲学游戏——OAuth 2.0 当年就是设计来只做授权、不做认证的,而太多人把它当"登录协议"用,踩了一堆坑。这点我们在第三节展开。
二、Session vs JWT:有状态和无状态的取舍
认证做完,系统得记住"这个请求是张三发的"。主流两条路:Session 和 JWT。选哪个,基本决定了你整套架构的复杂度。
Session:服务端记状态,客户端只带 ID
流程:
1. 用户 POST /login (账号密码)
2. 服务端验证通过 → 在服务端存储里写一条 { sessionId: "abc", userId: 42, ... }
3. 响应 Set-Cookie: sessionId=abc; HttpOnly; Secure; SameSite=Lax
4. 后续请求浏览器自动带 cookie,服务端用 sessionId 查回用户
服务端存储可以是内存(单机)、Redis(分布式标配)、数据库(慢,不推荐)。关键点:状态在服务端,客户端只拿一把钥匙。
优点是撤销极快——服务端把 session 删了,下一秒这个用户就被踢下线。改密码、强制下线、封号,都好做。缺点是水平扩展要共享 session 存储:多台服务器必须共用 Redis,否则请求打到 A 机器查不到 B 机器创建的 session。
JWT:无状态,token 自包含
JWT 是一段签名过的字符串,长得像 xxxxx.yyyyy.zzzzz,三段分别是 Header、Payload、Signature。Payload 里直接塞 userId、过期时间、角色,服务端用密钥验签后信任内容,不需要查任何存储。
1. 用户 POST /login
2. 服务端验证通过 → 用密钥签名生成 JWT,payload 含 { userId: 42, exp: ... }
3. 把 JWT 返回客户端(放 cookie 或 localStorage)
4. 后续请求带 JWT,服务端验签 → 信任 payload,完成认证
优点是无状态、可水平扩展。任何一台机器只要密钥相同就能验签,微服务、Serverless 场景天然契合。缺点是撤销难——JWT 一旦签发,在过期前一直有效,服务端"删 session"那一招失效了。这是 JWT 最大的代价。
怎么选
我的经验法则:
- 单体 / 后台管理系统:优先 Session。撤销需求多(改权限立即生效、踢人),有 Redis 就够了。
- 微服务 / API 网关 / Serverless:JWT 更顺手。但一定要配合短期 access token + 长期 refresh token(见第六节),别让一个 token 用一年。
- 需要立即撤销 JWT 的场景:维护一个黑名单或 token 版本号,验签时多查一次 Redis——但这一查,JWT 的"无状态"红利就少了一半。所以真有强撤销需求,老实回去用 Session。
三、OAuth 2.0:它是"授权框架",不是"登录协议"
OAuth 2.0 解决的核心问题是:第三方授权。
举个例子:你用一个第三方图片打印服务,需要读你 Google 相册里的照片。你不应该把 Google 账号密码告诉这个第三方。OAuth 2.0 让你在 Google 自己的页面授权这个第三方读你的相册,第三方拿到的是一个有限权限的 access token,而不是你的密码。
用户 ──> 第三方应用 ──> "我要访问你的 Google 相册"
↓
跳转到 Google 授权页(用户在 Google 登录)
↓
用户点"同意" → Google 给第三方一个 access token
↓
第三方拿 token 调 Google API 读相册
整个过程里第三方应用从来不碰用户的 Google 密码。这就是 OAuth 2.0 的精髓。
四种流程,记住授权码流程就够了
RFC 6749 定义了四种授权流程(Grant Type),对应不同场景:
- 授权码流程(Authorization Code)——最常用,Web/移动端标配,下面细讲。
- 隐式流程(Implicit)——早期给纯前端 SPA 用,现已废弃,被 PKCE 替代。
- 密码流程(Password Credentials)——用户直接把密码给客户端换 token。仅限高度可信的官方 App,不推荐。
- 客户端凭证流程(Client Credentials)——机器对机器(M2M),后端服务调后端服务,没有"用户"参与。
授权码流程:为什么是"两步换 token"
步骤 1:浏览器跳到授权服务器,用户登录并同意授权
→ 授权服务器回跳你的回调 URL,带一个 authorization code
(code 是短期的、一次性的,即便泄露也基本无害)
步骤 2:你的后端拿 code + client_secret 去换 access token
(这步是后端对授权服务器,密钥不经过浏览器)
为什么绕这一步不直接给 token?因为浏览器是不可信通道。直接把 token 放在 URL 里回跳,会被浏览器历史、Referer、日志截获。code 是个一次性短码,即便被偷,也只能换一次,而且要配 client_secret 才能换——client_secret 只在后端,前端永远拿不到。
SPA / 移动端用 PKCE
纯前端 SPA 没有后端存 client_secret(前端代码谁都能看),怎么办?用 PKCE(Proof Key for Code Exchange)。客户端生成一个 code_verifier,算出 code_challenge 一起发出去;换 token 时再带 code_verifier,授权服务器验证哈希匹配。用动态密钥替代静态密钥,前端不用存任何秘密。2026 年的今天,所有公开客户端(SPA、移动 App)都必须用 PKCE,OAuth 2.1 已经把这件事写死。
关键认知:OAuth 2.0 不是认证
OAuth 2.0 给你的是 access token,它代表"这个应用可以代表用户做某些事",但不直接告诉你"用户是谁"。你以为拿到 token 就知道用户身份了——其实那只是个副作用。直接拿 OAuth 当登录用,会出各种诡异 bug(同一用户在不同 client 上身份不一致、登出状态不同步、无法可靠拿邮箱)。要做"登录",需要下一层的 OIDC。
四、OIDC:在 OAuth 之上加一层"认证"
OIDC(OpenID Connect)是 OAuth 2.0 的认证扩展。它在授权流程之上,加发一个 ID Token。
ID Token 本身是一个 JWT,里面直接包含用户身份信息:sub(用户唯一 ID)、email、name、iss(签发者)、aud(受众)、exp(过期)。客户端拿到 ID Token 后,验签 + 校验 iss/aud/exp,就能可靠地知道"这个用户是谁"——这就是"用 Google 登录"的真正机制。
OAuth 2.0 授权码流程 + OIDC:
scope=openid profile email ← 关键:加 openid
↓
回调拿 code → 后端换 token,这次拿到三样:
- access_token (调 API 用,授权)
- id_token (JWT,认证 —— 用户是谁)
- refresh_token (续期用)
所以"用 Google 登录"的准确描述是:走 OIDC 协议,基于 OAuth 2.0 授权码流程 + PKCE,客户端从 ID Token 里取出用户身份。Google、GitHub、Microsoft 的"登录"按钮背后跑的都是 OIDC。
实操三条铁律:
- scope 里一定要带
openid——不带就是普通 OAuth,拿不到 ID Token。 - 客户端必须验签 ID Token,并校验
iss(确实是 Google 签的)、aud(确实是发给你的 client_id)、exp(没过期)。别信服务端返的 JSON 就直接用——ID Token 的安全性来自签名,不是来自传输层。 - OIDC 走完后,你自己系统该怎么管 session 还是照样管(签你自己的 JWT 或 session),不要把 Google 的 access token 当你系统的长期凭证。Google 的 token 是给你调 Google API 用的,不是给你自己系统用的。
五、SSO:一次登录,多系统通用怎么落地
SSO(Single Sign-On,单点登录) 是一种体验诉求:用户在一个地方登录,所有相关系统都认识他。比如你登录了 Google,Gmail、YouTube、Drive 都不用再登。
SSO 不是某个具体协议,而是一种架构模式。落地思路是中央认证服务 + token 传递:
┌──────────────────┐
│ 认证中心 (IdP) │ ← 用户在这里登录
│ 发 ticket/token │
└────────┬─────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ 系统 A │ │ 系统 B │ │ 系统 C │
└────────┘ └────────┘ └────────┘
主流有两种实现:
1. CAS 模式(传统企业):用户访问系统 A,没登录 → 跳到认证中心登录 → 认证中心发一个 ticket 回系统 A → 系统 A 用 ticket 去认证中心换用户信息。系统 B 来时,发现认证中心已有全局 session,直接发 ticket,不用再输密码。SAML 协议是这类的代表,大企业内部 SSO 常用。
2. OIDC 模式(现代互联网):把 OIDC Provider(Identity Provider)当中央认证服务。各业务系统都作为 OIDC 的 client,登录都走 OIDC。你在 OIDC Provider 登录一次,所有 client 共享这次登录状态(因为都指向同一个 Provider 的 session)。Auth0、Authing、Keycloak、Logto 都走这条路。
落地建议:
- 小团队 / 新项目:别自己撸 SSO,直接接 Auth0 / Logto / Authing / 自建 Keycloak。SSO 看起来简单,边界 case(登出同步、token 刷新、多域名 cookie 共享)多到让你怀疑人生。
- 自建 SSO 必须解决的三个问题:
- 跨域 cookie:不同主域的系统不能共享 cookie,得靠跳转 + URL 参数传 ticket,或前端 iframe 传 token。
- 统一登出(SLO,Single Logout):用户在 A 登出,所有系统都要失效。CAS 有专门协议,OIDC 有 RP-initiated Logout。
- session 续期一致:别让 A 系统 token 还有效、B 系统已经过期,体验断裂。
六、Token 存哪、怎么用才安全
这是最高频踩坑区。先把 token 分两种:
- access token:短期(15 分钟 ~ 1 小时),用于调 API。即便泄露,窗口期短。
- refresh token:长期(几天 ~ 几十天),只在 access token 过期时用来换新的,不参与业务请求。泄露风险高,要重点保护。
这种"短 access + 长 refresh"的设计是工程上的折中:用 refresh 续期换取 access 的高频使用,既保证安全(短 access 即便泄露危害有限),又保证体验(用户不用反复登录)。
存哪里?httpOnly cookie 最安全
✗ localStorage / sessionStorage —— XSS 一打就偷,直接 game over
✗ 普通 cookie(没 HttpOnly) —— JS 可读,XSS 同样能偷
✓ httpOnly + Secure + SameSite cookie —— JS 读不到,自动带,这是标配
绝对不要把 access token 放 localStorage。任何一个第三方 npm 包里的 XSS,都能把你的 token 偷走,然后冒充用户调任意 API。这不是理论风险——npm 供应链攻击每年都有真实案例。
httpOnly cookie 的代价是天然防不了 CSRF(浏览器会自动带 cookie)。补救:SameSite=Lax/Strict(默认不带跨站 cookie,挡掉大部分 CSRF)+ 后端校验 Origin/Referer + 关键操作(转账、删账号)要求二次确认或独立的 CSRF token。
refresh token 更要小心
refresh token 寿命长,一旦泄露等于长期钥匙。额外措施:
- 只存 httpOnly cookie,绝不放 localStorage。
- rotation:每次用 refresh token 换新 access token 时,同时换发一个新的 refresh token,旧的失效。这样一旦被偷,合法用户和攻击者只有一个能用,下一次另一个就会暴露。
- 绑定设备指纹 / IP 范围(高安全场景)。
七、踩过的坑:JWT 撤销、token 泄露、redirect 校验
坑 1:以为 JWT 能"登出"。JWT 无状态,服务端没有"删除"按钮。你前端把 token 删了只是"自己忘了",token 本身在过期前仍然有效——如果被偷过,攻击者照样能用。真正的"登出"需要服务端黑名单(回到有状态)或等过期。所以改密码、踢人、账号被封这类场景,JWT 是天生弱者——必须配合黑名单 + 短 exp,或干脆用 Session。
坑 2:redirect_uri 不校验。OAuth 流程里,授权服务器要回跳到 redirect_uri。如果这个参数不做严格白名单校验,攻击者可以注册一个相似域名钓鱼,把 code 骗到自己的服务器,再换你的 token。修复:redirect_uri 必须精确匹配白名单(不是前缀、不是子域名通配),授权服务器侧强校验。
坑 3:开放重定向。你的回调处理完 code 后,如果根据 query 参数跳转(?next=/home)又不校验,就成了开放重定向漏洞——攻击者发 ?next=https://evil.com,用户登录后被甩去钓鱼站。修复:next 只允许相对路径,或严格白名单。
坑 4:state 参数漏了 → CSRF。授权码流程开始时,客户端生成随机 state 跟着跳转,回调时校验同一个 state。漏了它,攻击者可以伪造回调让你绑上别人的账号(账号绑定场景尤其严重)。永远带 state,永远校验。
坑 5:把 access token 当 session 用。Google 给你的 access token 是给你调 Google API 的,不是拿来当你自己系统的 session 凭证。OIDC 走完后,你应该用 ID Token 里的用户信息签发你自己的 session/JWT,这才是你系统的登录态。混着用会出现"Google token 过期了,我的系统就以为用户登出了"这种诡异 bug。
小结
- 认证(AuthN)回答"你是谁",授权(AuthZ)回答"你能干什么"。401 是没认证,403 是没权限。OAuth 2.0 当年只设计做授权,不是登录。
- Session 有状态好撤销,JWT 无状态好扩展——撤销难是 JWT 的硬伤。短 access + 长 refresh 是工程上的折中,别让一个 token 用一年。
- OAuth 2.0 是授权框架,OIDC 在它之上加认证层(ID Token)才是真正的"用 Google 登录"。授权码流程 + PKCE 是 2026 年 Web/移动端的唯一推荐姿势,隐式流程已废弃。
- SSO 不是协议,是架构——中央认证服务 + token 传递。新项目别自己撸,接 Auth0 / Logto / Keycloak。
- token 存 httpOnly cookie,不要 localStorage;refresh token 必须 rotation;
redirect_uri/state/next三个参数不校验就是三个洞。
一句话收尾:认证授权这件事,概念分清层次、token 存对地方、参数严格校验——这三件事做对了,90% 的安全坑都不会踩。 剩下的 10%,是你自己造的轮子里的 bug——所以,能接成熟方案就别自己写。
Related