把 Session Cookie、JWT、OAuth 和 OpenID Connect 当成一回事,网站登录就会迅速变乱。它们并不是同义词。回调反复跳转、Cookie 莫名消失,或者明明拿到了令牌却仍收到 401 时,我会用下面这套模型排查。
🎙️ 发布并录制于: · 更新于 ·
普通网站,我会先把一个不透明的 Session ID 放进 HttpOnly Cookie。Session 存在服务器上,需要时可以立刻撤销。JWT 是一组带签名的声明,适合多个服务在不共享 Session 存储的情况下,校验同一份短期凭据。它不是高级版 Session Cookie。
# 不透明的 Session Cookie:浏览器无法理解其含义
Set-Cookie: __Host-session=s%3A8f1...; Path=/; Secure; HttpOnly; SameSite=Lax
# JWT payload 可读,并未加密
{"sub":"user_42","aud":"api","exp":1784905200}OAuth 做的是委托授权。资源所有者是用户,客户端是你的应用,授权服务器负责询问用户是否同意并签发令牌,资源服务器则是接受令牌的 API。“客户端”不等于浏览器,授权服务器也不一定托管 API。
resource owner: the person
client: your photo-printing site
authorization server: accounts.example
resource server: photos API
scope: permission such as photos.readScope 描述的是客户端可以做什么,不是这个用户可以对所有数据做什么。即使令牌带有 photos.read,也不能读取别人的相册。API 必须同时检查 Scope 和资源归属。
OAuth 不会告诉应用是谁登录了。它只是把某项访问权委托出去。OpenID Connect 才加入身份层,包括 ID Token、UserInfo Endpoint、发现元数据,以及验证身份声明的规则。“使用某某账号登录”应该采用 OIDC,不要靠调用 OAuth 的个人资料 Endpoint 自创一套身份判断。
# Access Token:交给 API;audience 是资源服务器
Authorization: Bearer <access_token>
# ID Token:交给客户端;证明这次登录事件
iss = https://accounts.example
aud = your_client_id
sub = stable-provider-user-id
nonce = value-bound-to-this-loginissuer + subject 这一对值。信任 ID Token 之前,要验证签名、签发者、Audience、过期时间和 Nonce。能解码出其中的 JSON,不等于验证通过。现在实用的流程是:先把浏览器送到授权服务器,拿回一个短期、一次性的 Code,再通过后端通道拿它换令牌。PKCE 会把这次兑换绑定到发起流程的应用实例。应用先生成随机 Verifier,把它的哈希作为 Challenge 发出,换令牌时再提交 Verifier,证明自己确实持有原值。
1. app stores code_verifier + state + nonce
2. /authorize?response_type=code&code_challenge=HASH&code_challenge_method=S256
3. callback?code=ONE_TIME_CODE&state=...
4. POST /token with code + code_verifier
5. validate ID token; create your own app sessionAccess Token 应该短期有效。Refresh Token 能在不要求用户重新登录的情况下换取新令牌,所以价值更高。把它保存在可信后端,或者保护严密的 Cookie 方案里;每次使用都轮换。如果已经轮换过的旧令牌再次出现,就撤销整个令牌家族。
POST /oauth/token
grant_type=refresh_token
refresh_token=<secret>
client_id=<client>
response: access_token + new_refresh_token
保存新的 Refresh Token,并使旧令牌失效CSRF 能成立,是因为浏览器可能把你的网站 Cookie 自动附到由别的网站发起的请求上。SameSite 有帮助,但登录流程还需要一个随机的 state,并把它绑定到浏览器 Session。回调时必须逐字比较,而且只能消费一次。State 不是装饰,也不是返回地址。
OAuthCallbackError: state mismatch
# 重试之前先排查:
1. Was state stored before redirect?
2. Did the same browser/session return?
3. Did a proxy change host or scheme, losing the cookie?
4. Was the callback opened twice or state consumed early?
# 修复 Session、Cookie 或代理问题。绝不要跳过 State 验证。next=https://attacker.example 会制造开放重定向,让攻击者能借你的可信登录域名做钓鱼。只要 JavaScript 能在页面里运行,就能读取 localStorage 中的令牌,包括被入侵的依赖或注入脚本。HttpOnly Cookie 不让 JavaScript 看到凭据,但页面被控制期间,恶意脚本仍可借浏览器发请求。不存在一种存储技巧,能让 XSS 变得可以接受。
# 网站登录不要默认这样做
localStorage.setItem("access_token", token)
# 优先使用 Backend for Frontend Session
browser --HttpOnly session cookie--> your backend
backend --access token--> provider/APIOAuth 回调失败通常很精确,并不神秘。Provider 会严格比较 Redirect URI,Scheme、Host、Port、Path,甚至末尾斜杠都可能必须与登记值完全一致。此外,Authorization Code 有效期短、只能使用一次,并绑定到 Client、Redirect URI 和 PKCE Verifier。
Error 400: redirect_uri_mismatch
# 实际发送: http://localhost:3000/auth/callback
# 登记值: http://localhost:3000/auth/callback/
# 修改登记值或生成的 URI,使两者逐字一致。
{"error":"invalid_grant","error_description":"Bad Request"}
# 常见原因:Code 被重复使用或已过期、code_verifier 错误、
# redirect_uri 错误、时钟偏差,或 Code 签发给了另一个 Client。