前端登录,这一篇就够了
Category(分类): Browser, Security, Authentication Status: 已更新
原文介绍 Cookie + Session、Token、JWT、SSO 和 OAuth 第三方登录,并配有登录流程图。本文保留这些主线和流程图,但修正“HTTP 每次都重新建立连接”“Cookie + Session 无法避免 CSRF”“Token 放在哪里都安全”“JWT 的 HMAC 使用私钥”等错误,补充 HttpOnly Cookie、刷新令牌、OAuth 2.0 + PKCE、OpenID Connect、Passkey 和现代会话安全。
登录系统要同时解决:
- 认证(Authentication):确认账号对应的主体是谁;
- 会话(Session):在后续请求中识别主体,并管理过期、撤销和登出;
- 授权(Authorization):主体能访问哪些资源,详见前端如何来做权限管理。
一、HTTP 无状态,但不代表每次请求都重新建连接
HTTP 的请求本身通常不携带服务器端会话状态,服务端需要借助 Cookie、Session、Token 或其他机制识别用户。原文把“无状态”解释成每次请求都要重新建立并断开连接,这不准确:HTTP/1.1 Keep-Alive、HTTP/2 多路复用和 HTTP/3/QUIC 都可以复用连接。
无状态指的是协议不会自动替应用保存“这是哪个用户”的业务状态,而不是网络连接一定短命。
二、Cookie + Session 登录
Cookie 是浏览器保存并按域、路径、过期时间和安全属性自动发送的一小段数据。现代浏览器还会考虑 SameSite、第三方 Cookie 策略、分区存储和用户隐私设置。
1. 首次登录流程

- 用户通过 HTTPS 提交账号和密码;
- 服务端校验密码哈希、账号状态、验证码和登录风险;
- 服务端创建一个高熵、不可预测的 Session ID,并把会话数据存到 Redis、数据库或其他共享存储;
- 服务端通过
Set-Cookie把 Session ID 写入浏览器; - 浏览器后续向匹配域名和路径发送请求时自动带上 Cookie;
- 服务端根据 Session ID 查找会话,再执行授权判断。
一个偏安全的 Cookie 响应示例:
Set-Cookie: __Host-session=RANDOM_HIGH_ENTROPY_ID; Path=/; Max-Age=1800; HttpOnly; Secure; SameSite=Lax
__Host- 前缀要求使用 Secure、Path=/ 且不能设置 Domain,可以减少错误的跨子域覆盖。实际属性要结合是否跨站、是否 iframe、是否需要跨子域等业务场景决定。
2. 后续请求

- 浏览器访问受保护资源;
- 浏览器按 Cookie 规则自动发送 Session ID;
- 服务端查找并验证会话是否存在、是否过期、是否被撤销;
- 服务端根据用户、资源、动作和租户执行授权;
- 允许则返回资源,不允许则返回
401或403。
3. Session 存放在哪里?
可以存放在:
- 单机内存:简单,但重启丢失且不适合多实例;
- Redis 等共享缓存:适合多实例和快速过期;
- 数据库:便于持久化、审计和查询,但要关注访问成本;
- 加密/签名 Cookie:把少量会话状态放在客户端,但仍需要密钥轮换、大小控制、撤销策略和隐私评估。
“服务器存放大量 Session 就一定压力过大”也不准确。通过共享存储、过期淘汰、合理 TTL 和横向扩展,Session 仍然是大量生产系统的常见选择。集群不一定要把 Session 复制到每台应用机器,通常使用共享 Session Store 或粘性会话等方案。
4. Session 安全实践
- 登录成功后轮换 Session ID,防止 Session Fixation;
- Session ID 使用密码学安全随机数,不能使用递增 ID、用户 ID 或时间戳;
- 设置空闲超时和绝对过期时间;
- 登出时服务端销毁或撤销会话,同时清除 Cookie;
- 修改密码、提升权限和高风险操作时重新认证或轮换会话;
- 会话存储不保存明文密码;
- 监控异常 IP、设备、并发会话和重放行为,但不要只依赖 IP 作为身份;
- 通过 HTTPS/WSS 保护传输,Cookie 设置
Secure; - 尽量不设置过宽的
Domain,避免低信任子域影响高权限会话。
5. Cookie + Session 与 CSRF
原文说“Session ID 存放在 Cookie 中,所以无法避免 CSRF 攻击”容易误解。Cookie 自动发送确实会带来 CSRF 风险,但可以通过以下方式降低风险:
SameSite=Lax或Strict,按跨站业务需要选择;- 对有副作用的请求使用 CSRF Token 或双重提交 Cookie;
- 校验
Origin/Referer,不能只依赖它们; - 使用合适的 CORS allowlist,但不要把 CORS 当成 CSRF 防护;
- 重要操作进行重新认证或二次确认。
HttpOnly 主要防止 JavaScript 读取 Cookie,不能阻止 XSS 代码借助当前页面发起已认证请求。因此仍然需要 CSP、输出编码、输入校验和安全依赖管理。
三、Token 登录
Token 是服务端发给客户端、用于后续证明会话或授权范围的凭证。它可以是不可读的随机 opaque token,也可以是 JWT。Token 的格式本身不决定安全性。
1. Token 流程

- 用户提交账号和密码;
- 服务端验证身份并签发短期 Access Token,必要时同时签发 Refresh Token;
- 客户端在后续请求中通过
Authorization: Bearer <token>或 Cookie 携带凭证; - 服务端验证令牌、过期时间、受众、签发者、权限和资源访问范围。

2. Token 的优缺点
优点:
- 适合 API、移动端、微服务和跨服务调用;
- Bearer Token 放在 Authorization 头时,不会像 Cookie 一样自动附加到所有匹配请求;
- JWT 可以在验证签名后本地读取声明,减少部分查询。
限制:
- 可撤销、登出、权限即时生效和刷新轮换仍然需要服务端状态或版本机制;
- Token 泄露后,攻击者可能在有效期内冒充用户;
- 令牌放在
localStorage可被 XSS 读取,放在 Cookie 则要处理 CSRF; - “服务端完全不保存 Token,所以没有压力”不是绝对结论:服务端仍可能保存 Refresh Token、撤销列表、密钥、会话版本、审计和风控状态。
3. 浏览器中的 Token 保存位置
没有一个位置在所有场景都安全:
| 位置 | 优点 | 风险/限制 |
|---|---|---|
| 内存 | 页面刷新后消失,降低持久泄露窗口 | 刷新后需恢复,需配合刷新机制 |
localStorage | 使用简单、刷新后保留 | XSS 可直接读取,不适合高价值长期凭证 |
| HttpOnly Cookie | JavaScript 不能读取,适合服务端会话/BFF | 自动发送,需要 CSRF、SameSite 和域策略 |
| IndexedDB | 可存结构化数据 | 仍受同源脚本访问,不能抵御 XSS |
对浏览器应用,常见的稳妥路线是服务端会话 + HttpOnly Cookie,或 BFF(Backend for Frontend)代浏览器管理 OAuth Token。若业务确实需要浏览器持有 Access Token,应缩短有效期、使用 Refresh Token Rotation、避免放入 URL,并完善 CSP、XSS 防护和撤销策略。
四、JWT 到底是什么
JWT(JSON Web Token)是一个紧凑、URL-safe 的声明传输格式。常见签名 JWT(JWS)由三段组成:
base64url(header).base64url(payload).base64url(signature)
原文中的 playload 应为 payload。JWT 的 Base64URL 编码不是加密,任何拿到 JWT 的人通常都能解码 header 和 payload;签名用于检测篡改,不能用来隐藏敏感信息。
1. Header、Payload、Signature
{
"alg": "HS256",
"typ": "JWT"
}
{
"iss": "https://id.example.com",
"sub": "user-42",
"aud": "api.example.com",
"exp": 1735689600,
"iat": 1735686000,
"nbf": 1735686000,
"jti": "unique-token-id",
"scope": "article:read"
}
iss:签发者;sub:主体标识;aud:受众;exp:过期时间;iat:签发时间;nbf:不早于此时间使用;jti:令牌 ID,可用于重放检测或撤销;- 自定义 claims 要控制大小和敏感性。
2. HMAC 和非对称签名不能混淆
原文说“输入服务器端私钥、unsignedToken,使用 HMAC-SHA256”,这是错误的:
HS256是 HMAC-SHA256,签发和验证使用同一个共享密钥;验证方也拥有生成合法 Token 的能力;RS256、PS256、ES256等非对称算法使用私钥签发、公钥验证;- 服务端必须配置允许的算法集合,不能盲目信任 Token header 中的
alg; - JWT 验证时应检查签名、算法、
iss、aud、时间 claims、权限范围和密钥版本。
3. 服务端验证示意
不要手写生产级 JWT 解析和签名验证。以 Node.js jose 为例:
import { jwtVerify } from 'jose'
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
const verifyAccessToken = async token => {
const result = await jwtVerify(token, secret, {
algorithms: ['HS256'],
issuer: 'https://id.example.com',
audience: 'api.example.com'
})
return result.payload
}
如果使用 RS256/ES256,服务端通过受信任的 JWKS 获取公钥,并处理密钥轮换、缓存、kid 和失败回退。不要把 JWT 直接当作用户权限的永久事实:角色撤销、租户变化和高风险授权仍应查询服务端策略或使用版本号。
4. JWT 常见错误
- 把 Base64URL 当加密;
- 把 payload 中的
role: admin当成无需服务端确认的授权; - 不检查
exp、iss、aud和算法; - 接受
none或非预期算法; - 把 Access Token 放在 URL、日志、Referer 或错误页面;
- 使用一个永不轮换的弱密钥;
- 把 JWT 当成“天然可撤销”的 Session;
- 在浏览器端保存包含密码、身份证号等敏感数据的 claims。
五、SSO 单点登录
SSO(Single Sign-On)通常由一个身份提供商(IdP)为多个业务系统提供认证。它解决的是“多个系统共享登录体验”,不意味着所有系统共享同一个业务权限表。
1. 首次访问业务系统

典型流程:
- 用户访问
app-a.example,发现没有本地会话; - 业务系统将浏览器重定向到 IdP,并携带明确注册的
redirect_uri、state,OIDC 通常还带nonce; - 用户在 IdP 完成认证和授权;
- IdP 通过浏览器回调传回一次性授权码,而不是把长期 Access Token 放在 URL;
- 业务后端在服务端与 IdP 交换授权码,验证客户端、PKCE、签发者、受众和 nonce;
- 业务系统创建自己的本地会话,并重定向到原始页面。
state 用于防 CSRF 和关联请求,redirect_uri 必须精确匹配注册值,回调必须使用 HTTPS。不要把任意 return_uri 原样反射,避免开放重定向。
2. 继续访问同一业务系统

业务系统使用自己的会话 Cookie 识别用户。IdP 的 Cookie 一般不会被业务系统直接读取,跨域共享 Cookie 也不是 SSO 的正确实现方式。
3. 访问另一个业务系统

- 用户访问
app-b.example,没有app-b本地会话; app-b把浏览器重定向到 IdP;- IdP 发现用户已有 IdP 会话,因此不要求重复输入密码;
- IdP 为
app-b生成新的、短期、一次性的授权码; app-b后端交换授权码并创建自己的本地会话。
每个业务系统仍然要独立执行自己的授权。SSO 只证明身份,不自动授予所有业务权限。
4. 单点退出
原文中“认证中心遍历所有产品并调用退出 API”是一种早期自定义方案,不是所有 SSO 都采用。现代系统可以组合:
- 本地 RP(业务系统)先销毁自己的会话;
- 调用 IdP 的 RP-Initiated Logout 或撤销端点;
- OIDC Back-Channel Logout 或 Front-Channel Logout(如果 IdP 和 RP 支持);
- Refresh Token 撤销和会话版本失效;
- 对高风险系统要求重新登录。
不同应用的本地 Cookie 不会因为删除 IdP Cookie 就自动消失,退出协议和实现能力需要在架构阶段明确。
六、OAuth 第三方登录与 OIDC

OAuth 2.0 是授权委托协议:它回答“客户端能否代表资源所有者访问某些资源”。如果应用要确认用户身份,应使用在 OAuth 之上的 OpenID Connect(OIDC),验证 id_token 的签名和 claims。
不要把“OAuth Access Token 一定就是用户登录凭证”写成通用结论;不同提供商的 Token、用户信息接口、scope、过期和撤销机制不同。
1. 现代 Web 应用推荐:Authorization Code + PKCE
浏览器/客户端 IdP 业务后端
| | |
|-- code_challenge ------->| |
|<-- 登录/同意页面 ---------| |
|<-- authorization code ----| |
|-------------------------->|-- code + verifier ->|
| |<-- tokens ----------|
|<-- 业务会话/安全结果 ------| |
流程:
- 客户端生成高熵
code_verifier; - 使用 S256 计算
code_challenge; - 跳转 IdP 的授权端点,携带
client_id、redirect_uri、response_type=code、scope、state、code_challenge; - 用户在 IdP 登录并同意授权;
- IdP 回调一次性授权码;
- 客户端或后端提交授权码和
code_verifier到 Token Endpoint; - 服务端验证
state、PKCE、redirect URI、client 身份和 Token claims; - 对浏览器应用,优先建立 HttpOnly 本地会话,而不是把长期 Token 暴露给 JavaScript。
浏览器端生成 PKCE 的示意:
const toBase64Url = bytes => {
let binary = ''
for (const byte of bytes) binary += String.fromCharCode(byte)
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
}
const verifierBytes = crypto.getRandomValues(new Uint8Array(32))
const codeVerifier = toBase64Url(verifierBytes)
const digest = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(codeVerifier)
)
const codeChallenge = toBase64Url(new Uint8Array(digest))
代码示例展示协议步骤,真实项目应使用经过维护的 OAuth/OIDC 客户端库,并正确处理回调、错误、超时、重试、浏览器返回和多标签页。
2. 不推荐的隐式模式
早期 SPA 常见的 Implicit Flow 会把 Access Token 放在前端重定向结果中,容易通过浏览器历史、Referer、日志和脚本环境泄露。OAuth 2.0 Security Best Current Practice 推荐 Authorization Code,并使用 PKCE;不要因为旧文章仍列出 response_type=token 就在新项目中采用。
3. 以微信等提供商为例

不同平台字段和接口地址不同,但常见流程仍是:
- 在开放平台注册应用,获得
client_id/appid和客户端配置; - 用户点击第三方登录,跳转提供商授权页面;
- 用户同意后回调一次性
code; - 服务端使用 code、客户端凭证(如果该客户端类型需要)和 redirect URI 换取 Token;
- 服务端通过提供商用户信息接口获取稳定的第三方主体 ID;
- 将第三方主体绑定到本站账号;
- 创建本站 Session 或本站 Token,并设置安全 Cookie;
- 后续业务权限由本站系统判断。
client_secret 不能放到浏览器。提供商具体是否需要 secret、scope 和用户信息接口,要以官方文档为准;不要把某个平台 2020 年的流程图当成所有 OAuth 提供商的统一协议。
七、密码和登录接口的安全底线
密码处理
- 只通过 HTTPS 提交密码;
- 服务端使用 Argon2id、bcrypt 或 scrypt 等专用密码哈希,设置合适成本和唯一 salt;
- 永不保存明文密码、可逆加密密码或把密码放进 JWT;
- 登录失败提示不要泄露账号是否存在;
- 对登录、验证码、密码重置和 MFA 接口限速;
- 密码重置链接使用短期、一次性、不可预测令牌;
- 密码修改、邮箱变更和支付操作可以要求重新认证。
登录接口
POST /api/login HTTP/1.1
Content-Type: application/json
{"email":"ada@example.com","password":"..."}
成功后可以返回:
HTTP/1.1 204 No Content
Set-Cookie: __Host-session=...; Path=/; Max-Age=1800; HttpOnly; Secure; SameSite=Lax
不要把 Session ID、Access Token 或密码放在 URL 查询参数中。错误响应、日志、分析脚本和第三方监控也不应记录完整凭证。
认证和授权状态码
401:没有有效认证,需要登录或刷新认证;403:已经识别主体,但不允许执行操作;- 对象不存在和无权限时是否统一返回
404,应根据防止资源枚举的策略决定; - 前端收到
401不应无休止地刷新 Token,避免多个标签页形成刷新风暴。
八、Passkey 和多因素认证
密码不是唯一登录方式。Passkey/WebAuthn 使用公钥凭证和设备验证器,服务端保存公钥而不是私钥,能够抵抗许多钓鱼攻击。MFA、风险登录、设备绑定和恢复码可以降低密码泄露后的风险,但恢复流程本身也必须安全。
前端只负责调用浏览器 API 和展示状态,注册、断言验证、挑战随机数、Origin/RP ID 和凭证绑定仍由服务端完成。
九、常见错误
错误 1:把 Token 放到 localStorage 就“没有 CSRF”
它可能减少 Cookie 自动发送带来的 CSRF 面,但 XSS 可以读取并外传 Token。存储位置是风险权衡,不是绝对安全结论。
错误 2:认为 JWT 不需要服务端状态
JWT 的签名验证可以不查询会话,但撤销、Refresh Token、权限版本、密钥轮换、异常检测和审计都需要服务端能力。
错误 3:把 OAuth 当作认证协议
OAuth 的核心是授权委托,身份认证使用 OIDC 或提供商明确的身份接口,并验证 issuer、audience、nonce、签名和时间 claims。
错误 4:把第三方 Token 当本站 Session
第三方 Access Token 的受众和权限属于第三方资源服务器,不能直接当作本站登录态。应绑定第三方主体并创建本站会话。
错误 5:把客户端角色当作授权依据
前端可以隐藏按钮,但用户可以直接调用 API。服务端必须检查主体、动作、资源归属和租户边界。
错误 6:只删除浏览器 Cookie 就算退出
如果服务端 Session 仍有效、Refresh Token 未撤销或其他设备仍在线,单纯清空本地 Cookie 不能完成完整登出。退出策略应覆盖本地会话、IdP 会话、刷新令牌和设备会话。
十、方案选择
| 场景 | 常见方案 | 重点 |
|---|---|---|
| 同域 Web 应用 | 服务端 Session + HttpOnly Cookie | CSRF、会话轮换、过期和撤销 |
| 浏览器前后端分离 | BFF + HttpOnly Cookie,或短期 Access Token | 避免长期 Token 暴露,处理 CORS |
| 移动端/桌面端 | Authorization Code + PKCE | 安全回调、刷新令牌轮换 |
| 企业多系统 | OIDC SSO | 每个业务系统独立会话和授权 |
| 第三方登录 | OAuth/OIDC 官方流程 | state、nonce、PKCE、精确 redirect URI |
| 高价值操作 | Session/Token + MFA/Passkey | 重新认证、审计和风控 |
总结
- HTTP 无状态不等于每次请求都重新建立连接;
- Cookie + Session 仍是浏览器 Web 应用的可靠方案,集群可用共享 Session Store;
- Cookie 自动发送需要 CSRF 防护,HttpOnly 不能消除 XSS;
- Token 不等于 JWT,JWT 的 payload 不是加密内容,HS256 共享密钥和 RS/ES 非对称密钥不能混淆;
- JWT 验证必须检查签名、算法、issuer、audience、过期时间和业务授权;
- SSO 共享的是认证体验,每个业务系统仍要有自己的会话和授权;
- OAuth 是授权委托,登录身份场景优先使用 OIDC,浏览器应用使用 Authorization Code + PKCE;
- Passkey/WebAuthn 和 MFA 可以提升高价值账号的抗钓鱼能力;
- 凭证存储、撤销、轮换、登出和审计要结合威胁模型设计。
参考资料
- MDN:Session management
- MDN:Secure cookie configuration
- MDN:Using HTTP cookies
- OWASP:Session Management Cheat Sheet
- OWASP:Authentication Cheat Sheet
- OWASP:JSON Web Token Cheat Sheet
- RFC 7519:JSON Web Token
- RFC 7636:PKCE
- RFC 9700:OAuth 2.0 Security Best Current Practice
- OpenID Connect Core 1.0
- MDN:Web Authentication API