从购物车到第三方登录,全面梳理认证授权的核心机制
早期 Web 基于 HTTP 协议,而 HTTP 是无状态的,服务器无法记住用户上一次做了什么。这导致一个很实际的问题:用户在电商网站把商品加入购物车,刷新页面后购物车就空了。为了解决这个痛点,网景公司的工程师 Lou Montulli 在 1994 年发明了 Cookie。有了它,服务器可以将购物车数据交给浏览器"记住",下次访问时再带回给服务器,从而实现了状态保持。
Cookie 的本质是服务器委托浏览器存储的一小段数据(通常不超过 4KB)。这个机制分两步:
Set-Cookie: key=value; ...,浏览器收到后会将该数据存储到本地,并明确记录该 Cookie 属于哪个域名。Cookie: key=value,将数据无脑发回服务器。这个过程完全由浏览器自动完成,无需 JavaScript 干预,是 HTTP 状态管理的基石。
sessionId。用户登录后,服务器生成一个唯一的会话标识符,通过 Set-Cookie 传给浏览器。后续请求带上此标识符,服务器就能查到对应的登录用户,维持登录状态。Cookie 如果不加限制,极易成为攻击的跳板,因此诞生了多个重要的属性:
跨站脚本攻击(XSS)是通过注入恶意脚本窃取用户的 Cookie。如果 Cookie 标记了 HttpOnly,浏览器将禁止 JavaScript(document.cookie)读取该 Cookie,它仅用于 HTTP 请求传输,从而大幅降低 XSS 窃取会话的风险。
跨站请求伪造(CSRF/XSRF)是利用用户已登录的身份,在第三方网站向目标站点发送恶意请求(如转账、改密)。因为请求会自动附带目标域名的 Cookie,攻击者能"借用"用户的身份。
传统的防御手段之一是检查 Referer请求头,判断请求来源是否合法,但 Referer 可能被隐藏或篡改。更可靠的方式是结合 CSRF Token(服务器下发随机令牌,提交时必须带上)和 SameSite 属性:
SameSite=Strict:仅当请求来自同站时才发送 Cookie,防御最强,但可能影响从外部链接打开时的登录状态。SameSite=Lax:允许在顶级导航的 GET 请求中发送 Cookie(如点击链接),阻止在 POST 表单提交、iframe 等场景发送,平衡了安全与体验,已成为现代浏览器的默认行为。SameSite=None:必须同时设置 Secure,允许跨站发送,适用于第三方嵌入式场景。标记 Secure的 Cookie 仅在 HTTPS 加密连接中传输,防止中间人嗅探。如今生产环境都应开启 HTTPS 并为敏感 Cookie 加上此标记。
Authorization 头专门用于携带认证凭证,告诉服务器"我是谁"或"我被授权访问什么"。它与 Cookie 最大的区别在于:Authorization 必须由客户端代码主动构造并附加,不会像 Cookie 那样由浏览器自动携带,因而天然对 CSRF 免疫(但更需防范 XSS 窃取令牌)。
最朴素的 HTTP 认证方式,格式为:
Authorization: Basic <base64(username:password)>
客户端将用户名和密码用冒号拼接,进行 Base64 编码后放入请求头。
注意:Base64 只是编码,不是加密!在 HTTP 明文传输下,密码极易泄露。因此 Basic 认证必须在 HTTPS 下使用,否则毫无安全性可言。它通常用于简单 API 调用、内部系统等场景。
格式为:
Authorization: Bearer <token>
Bearer 意为"持有者",任何人只要拿到这个令牌就能代表用户。由于它通常不依赖 Cookie,天然防 CSRF,但必须严防令牌泄漏。Bearer Token 最常见的来源就是 OAuth2 协议。
以掘金使用 GitHub 登录为例。掘金说:"用户,请让 GitHub 告诉我们你的基本信息。" 这个过程其实是用户授权 GitHub 将一部分数据访问权限交给掘金。掘金拿到权限后,从 GitHub 获取用户信息,再在自己的系统中创建或绑定一个账号,最后以该账号完成登录。
这里容易混淆的是:第三方登录的"第三方"是 GitHub,而授权的"第三方"是掘金。从用户视角看,我用 GitHub 账号登录了掘金;从协议视角看,我授权掘金访问我的 GitHub 信息。
为什么不能直接把 Token 给浏览器?因为浏览器环境不可控,直接给 Token 容易泄漏。OAuth2 通过授权码(Authorization Code)配合后端密钥来保证安全。
1. 发起授权请求
用户点击"使用 GitHub 登录",掘金前端引导浏览器跳转到 GitHub 授权页面(https://github.com/login/oauth/authorize),携带参数:
client_id:GitHub 事先分配给掘金的公开标识。redirect_uri:授权后跳转回来的掘金回调地址。scope:请求的权限范围(如读取用户资料)。2. 用户同意授权
GitHub 授权页面会展示掘金的图标和应用名称(这个图标和名称是掘金在 GitHub 后台注册应用时填写的,由 GitHub 服务器调取,并非掘金前端传递),并询问用户是否授权。用户点击"同意"。
3. 获取授权码(Authorization Code)
GitHub 将浏览器重定向回掘金指定的 redirect_uri,并在 URL 上附带一个临时授权码 code。这个 code有效期很短(通常几分钟),且只能使用一次。它仅仅代表"用户同意了",但还不是真正访问数据的钥匙。
4. 换取访问令牌(Access Token)
掘金的后端服务器拿到浏览器传回的 code后,立即向 GitHub 的令牌端点(https://github.com/login/oauth/access_token)发送 POST 请求,参数包括:
client_idclient_secret:GitHub 分配给掘金的严格保密的密钥,绝不会暴露到前端。code:刚才的授权码。因为 client_secret的存在,GitHub 能确认请求确实来自掘金服务器,而不是某个冒充的客户端。这一步全程在服务器之间进行,且必须使用 HTTPS。
5. 颁发令牌
GitHub 验证通过后,向掘金服务器返回 access_token(可能还有 refresh_token)。授权过程至此结束。
6. 获取用户信息并完成登录
掘金服务器拿着 access_token,用 Authorization: Bearer <access_token>请求 GitHub 的用户信息接口(https://api.github.com/user)。拿到 GitHub 用户 ID、昵称等信息后,在自己的数据库中查找或创建一个内部账号(通常会以 GitHub ID 或邮件为关联标识),然后生成自己平台的会话(如通过 Set-Cookie下发 sessionId),最后将登录成功的信息返回给浏览器。整个第三方登录流程完成。
关键安全点:授权码在前端出现,但极其短暂且需配合后端密钥才能换令牌,这使得直接暴露给浏览器或中间人攻击的令牌风险大大降低。
微信的 OAuth2 流程在原理上与 GitHub 一致,但有一些产品上的特殊设计:
Access Token 一般有效期较短(几小时到几天),一旦过期就需要重新登录,体验很差。Refresh Token 解决了这个问题。
Refresh Token 是与 Access Token 一起颁发的、有效期更长(几周甚至几个月)的凭证。流程如下:
这样,即便 Access Token 意外泄漏,其影响窗口也很短;而 Refresh Token 仅在后端使用,且可被服务器主动吊销,兼顾了安全与体验。
HttpOnly、SameSite、Secure等属性防范 XSS 和 CSRF。只有深入理解这两大机制的工作原理与安全模型,才能在设计 Web 应用时做出正确的架构决策,为用户提供既便捷又安全的体验。