Session 与 JWT 认证:登录会话机制的详细对比
Category(分类): Browser, Security, Authentication Status: 已更新
Session 和 JWT 经常被放在一起比较,但它们并不是完全同一层面的概念:Session 主要描述“登录状态由谁保存和管理”,JWT 主要描述“令牌使用什么格式传递声明”。本文从请求流程、数据存储、扩展性、安全性、注销、过期和实际选型等方面,对两种常见方案进行完整对比。
一、先给结论
如果只想快速做选择,可以先记住下面几句话:
- 传统网站、SSR 应用、管理后台:优先考虑服务端 Session + HttpOnly Cookie。
- 浏览器应用:如果没有必须让浏览器直接调用第三方 API 的需求,BFF 或服务端 Session 往往更容易做好安全控制。
- 移动端、开放 API、微服务之间的调用:短期 Access Token 比较常见,JWT 可以作为 Access Token 的一种格式。
- 需要立即注销、立即封禁、权限立即生效:服务端 Session 更直接;JWT 需要额外设计撤销或版本机制。
- JWT 不是“更高级的 Session”,也不是只要使用 JWT 就自动安全。
- Session 不等于 Cookie,JWT 也不等于 localStorage:存储位置和令牌格式是不同维度的问题。
最核心的区别可以概括为:
服务端 Session:浏览器保存一个会话标识,服务端保存会话状态
JWT:浏览器保存一个自包含的签名令牌,服务端验证令牌即可得到部分状态
这里的“无状态”是相对的。JWT 服务端仍然需要保存签名密钥,也可能查询用户、权限、租户和风控数据;它只是不必为每一个 Access Token 保存一条登录会话记录。
二、先把几个容易混淆的概念分开
1. HTTP 无状态
HTTP 请求本身不会自动记住“上一个请求是哪一个用户”。服务端需要通过 Cookie、Session ID、Token、OAuth/OIDC 或其他机制,识别请求对应的主体。
HTTP 无状态描述的是请求上下文不会自动替应用保存,不代表每次请求都必须重新建立 TCP 连接。HTTP/1.1 Keep-Alive、HTTP/2 多路复用和 HTTP/3/QUIC 都可以复用连接。
2. Cookie
Cookie 是浏览器提供的存储和传输机制。服务端通过 Set-Cookie 写入 Cookie,浏览器在域名、路径、过期时间、Secure、SameSite 等条件允许时,自动在后续请求中添加 Cookie 请求头。
Cookie 中可以保存:
- 服务端 Session ID;
- Opaque Token;
- JWT;
- 主题、语言等普通偏好设置。
Cookie 本身不代表一定是 Session,也不代表一定安全。
3. Session
Session 是一次登录会话及其服务端状态的抽象。常见的服务端 Session 方案中,浏览器只保存一个随机的、不可预测的 Session ID,用户、过期时间、撤销状态等信息存储在 Redis、数据库或其他 Session Store 中。
严格来说,Session 也可以把少量状态加密或签名后放到 Cookie 中,这种方案的服务端状态较少,不能简单地说“所有 Session 都必须查数据库”。不过本文重点讨论最常见的服务端 Session。
4. Token
Token 是“令牌”的泛称,是后续请求中用来证明身份或权限的一段凭证。Token 可以是:
- 只包含随机字符的 Opaque Token;
- JWT;
- OAuth 2.0 Access Token;
- 某种自定义的签名字符串。
Token 不一定是 JWT。
5. JWT
JWT(JSON Web Token)是一种紧凑、URL-safe 的声明传输格式。最常见的签名 JWT(JWS)包含 Header、Payload 和 Signature 三部分:
base64url(header).base64url(payload).base64url(signature)
JWT 的 Payload 通常可以被解码阅读,签名用于发现篡改,不等于加密。因此不能把密码、身份证号、银行卡号等敏感数据直接放到 Payload 中。
三、两种方案的共同目标
无论使用 Session 还是 JWT,登录系统都要解决以下问题:
- 用户提交账号和密码,服务端验证身份;
- 服务端创建或签发登录凭证;
- 浏览器或客户端保存凭证;
- 后续请求携带凭证;
- 服务端验证凭证并识别用户;
- 服务端根据用户、资源、操作和业务上下文执行授权;
- 处理过期、注销、改密、封禁、权限变化和异常登录。
认证只回答“你是谁”,授权还要回答“你能做什么”。无论 Token 中写了什么,最终的资源权限都必须由服务端执行,不能只依赖前端隐藏按钮或路由守卫。
四、服务端 Session 的工作原理
1. 登录流程
典型流程如下:
浏览器 服务端 Session Store
| | |
| -- 用户名、密码 ------------> | |
| | -- 校验密码和账号状态 --------> |
| | -- 创建 Session --------------> |
| <--------- Set-Cookie ------- | |
具体步骤:
- 用户通过 HTTPS 提交用户名和密码;
- 服务端校验密码哈希、账号状态、验证码和风险策略;
- 服务端生成高熵、不可预测的随机 Session ID;
- 服务端将 Session ID 对应的会话记录写入 Redis、数据库等存储;
- 服务端通过
Set-Cookie把 Session ID 返回给浏览器; - 浏览器保存 Cookie。
一个典型的响应头如下:
Set-Cookie: __Host-session=RANDOM_SESSION_ID; Path=/; Max-Age=1800; HttpOnly; Secure; SameSite=Lax
2. 后续访问流程
浏览器 服务端 Session Store
| | |
| -- Cookie: session=... ----> | |
| | -- 查询并验证 Session --------> |
| | <------- 用户和过期信息 -------- |
| <--------- 页面或 API ------- | |
服务端收到请求后通常会:
- 从 Cookie 中取出 Session ID;
- 对 Session ID 进行格式和长度检查;
- 查询对应的 Session 记录;
- 判断会话是否存在、是否过期、是否被撤销;
- 关联用户并检查用户是否仍然启用;
- 加载必要的权限和租户信息;
- 执行资源授权,最后返回页面或 API 响应。
3. Session 表可以保存什么
一个简化的 Session 表可能如下:
| 字段 | 含义 |
|---|---|
token_hash | Session ID 的哈希值,避免数据库泄露时直接暴露可用凭证 |
user_id | 会话所属用户 |
created_at | 会话创建时间 |
last_seen_at | 最近使用时间,可用于空闲超时 |
expires_at | 绝对过期时间 |
revoked_at | 撤销时间,或使用单独的状态字段 |
user_agent | 可选的设备信息,便于会话管理和审计 |
ip | 可选的登录来源信息,不应单独作为身份判断依据 |
示意数据:
sessions
-----------------------------------------------------------------
token_hash user_id created_at expires_at
sha256(...) 42 2026-03-01 10:00:00 2026-03-31 10:00:00
安全实现通常不把原始 Session ID 明文保存到数据库,而是保存哈希:
const sessionId = randomBytes(32).toString('base64url')
const sessionHash = sha256(sessionId)
// 浏览器拿到 sessionId,服务端存储 sessionHash
浏览器持有原始值,服务端根据请求中的原始值计算哈希后查询数据库。即使数据库被读出,攻击者也不能直接拿哈希值当作 Cookie 使用。
4. Session 的过期方式
Session 的过期通常有几种策略:
- 绝对过期:从登录时刻起固定有效一段时间,例如 30 天;
- 空闲过期:用户一段时间没有操作后失效,例如 30 分钟;
- 滑动过期:用户每次访问都延长过期时间,但不能超过绝对上限;
- 手动撤销:用户退出登录、修改密码、被管理员封禁时立即删除或标记失效。
要注意:Cookie 的过期时间只是浏览器端的保存策略,Session Store 中的 expires_at 才是服务端真正的判断依据。即使 Cookie 还存在,如果服务端的 Session 行已经被删除,用户仍然会被视为未登录。
5. Session 的优点
撤销简单
退出登录时删除当前 Session 记录即可。管理员封禁用户时,可以删除该用户的所有 Session。修改密码时,也可以让所有旧会话失效。
DELETE FROM sessions WHERE token_hash = ?;
-- 让某个用户的所有登录设备下线
DELETE FROM sessions WHERE user_id = ?;
权限变化容易及时生效
服务端每次请求都可以从 Session Store 或权限系统读取最新状态。用户角色被降级后,不需要等待旧 Token 自然过期。
客户端凭证更容易保护
浏览器通常只保存一个没有业务含义的随机 ID。将它放入 HttpOnly Cookie 后,普通 JavaScript 不能直接读取原始值。
适合传统 Web 和 SSR
服务端渲染页面时,可以直接从请求 Cookie 恢复用户会话。模板、页面数据和接口使用同一套认证逻辑,整体模型比较直接。
6. Session 的缺点
需要服务端存储
每个活跃会话都要占用 Redis、数据库或其他存储空间,请求通常也需要一次读取。生产环境不能依赖单机进程内存,否则重启会丢失会话,多实例之间也无法共享登录状态。
需要解决多实例部署
负载均衡后的任意应用实例都必须能够访问同一个 Session Store。常用方案是 Redis 或数据库,而不是把会话只保存在某一台应用服务器的内存中。
需要处理存储故障和性能
Session Store 变慢或不可用会直接影响登录验证。因此需要考虑连接池、超时、缓存、过期淘汰、监控和故障降级策略。
Cookie 自动发送带来 CSRF 风险
如果认证凭证放在 Cookie 中,浏览器可能自动把它带到跨站请求。需要结合 SameSite、CSRF Token、Origin/Referer 校验和正确的 CORS 策略防御 CSRF。
五、JWT 的工作原理
1. JWT 的结构
一个常见的签名 JWT 如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiJ1c2VyLTQyIiwiYXVkIjoiYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxODkzNDU2MDAwfQ
.
SIGNATURE
解码后大致是:
{
"alg": "HS256",
"typ": "JWT"
}
{
"iss": "https://id.example.com",
"sub": "user-42",
"aud": "api.example.com",
"exp": 1893456000,
"iat": 1893452400,
"scope": "article:read"
}
常见声明包括:
iss:签发者;sub:主体,一般表示用户或客户端;aud:受众,表示这个令牌发给哪个服务使用;exp:过期时间;iat:签发时间;nbf:在此时间之前不能使用;jti:令牌唯一标识,可用于重放检测或撤销;scope或自定义权限声明:令牌被允许做什么。
2. JWT 登录和访问流程
客户端 认证服务 业务 API
| | |
| -- 用户凭证 -------------> | |
| | -- 签发 JWT ----------------> |
| <--------- JWT ------------ | |
| | |
| -- Authorization: Bearer JWT ----------------------------> |
| | |-- 验证签名、exp、aud
| <--------------------------- API 响应 --------------------- |
具体步骤:
- 用户提交账号和密码;
- 认证服务验证身份;
- 认证服务把用户标识、受众、过期时间和必要的权限范围写入 JWT;
- 认证服务使用密钥签名;
- 客户端在后续请求中携带 JWT;
- 业务 API 使用密钥或公钥验证签名;
- 业务 API 检查
exp、iss、aud、nbf、scope 等声明; - 验证通过后,使用
sub等声明识别主体并执行授权。
令牌可以放在:
Authorization: Bearer ACCESS_TOKEN
也可以放入 HttpOnly Cookie:
Cookie: access_token=JWT_VALUE
JWT 是否放在 Cookie 中,与 JWT 是否“无状态”是两个不同问题。放在 Cookie 中仍然可以是无状态 JWT,但需要额外处理 CSRF。
3. JWT 为什么被称为无状态
传统服务端 Session 的验证方式是:
const session = await sessionStore.get(hash(sessionId))
if (!session || session.expiresAt <= Date.now()) {
return unauthorized()
}
return findUser(session.userId)
服务端必须查询一条“这个令牌对应哪个用户、是否有效”的记录。
JWT 的验证方式则类似:
const claims = verifySignature(jwt, verificationKey)
if (claims.exp <= now()) {
return unauthorized()
}
return claims.sub
只要签名密钥在服务端,服务端就可以验证 JWT 的完整性,并从声明中读取主体和有效期,不需要为每个 Access Token 维护一条 Session 记录。因此称为“无状态”。
这里有三个重要边界:
- 无状态不是没有任何服务端状态:服务端仍要保存签名密钥,可能还要保存用户、权限、租户和风控数据;
- 无状态不是客户端可信:客户端可以解码 Payload,但不能在没有密钥的情况下生成有效签名;
- 无状态不是永远无法撤销:可以通过短过期时间、用户会话版本、撤销列表或在线校验实现撤销,只是这些机制会重新引入服务端状态或查询。
4. JWT 的优点
服务间验证方便
多个 API 服务可以使用同一组可信公钥验证 JWT,不必每次都访问中心 Session Store。认证服务负责签发,业务服务负责验证。
尤其是非对称签名算法中,认证服务持有私钥,业务服务只持有公钥。业务服务能够验证令牌,但不能签发新的合法令牌。
适合 API、移动端和跨服务场景
非浏览器客户端可以主动把 JWT 放在 Authorization 请求头中,不依赖浏览器 Cookie 规则。不同服务、不同技术栈之间也更容易约定标准化声明。
令牌中可以携带有限的上下文
服务端验证签名后可以直接读取 sub、aud、scope 等声明,减少把“主体是谁”作为一次单独查询的需要。
横向扩展时不必复制 Access Token 状态
应用实例可以共享验证密钥或公钥,不需要把每条 Access Token 会话复制到每个实例的本地内存中。
5. JWT 的缺点
难以立即撤销
服务端只验证签名和过期时间时,已经签发的 JWT 在 exp 到来前通常都有效。用户点击退出登录时,删除浏览器里的 JWT 只能阻止当前浏览器继续发送,无法让已经被窃取的 JWT 立即失效。
权限可能过期
如果 JWT 中写入了角色或 scope,用户权限变化后,旧 JWT 里的声明不会自动变化。在旧令牌过期前,服务端如果完全信任这些声明,就可能继续使用旧权限。
令牌泄露后影响较大
JWT 本质上通常是 Bearer Token:谁拿到谁就可以使用。JWT Payload 可以阅读,不能因为“做了签名”就把它当作隐私容器。
令牌尺寸可能较大
Session ID 通常只是几十个随机字符,而 JWT 可能包含较多声明。把它放在每个请求的 Header 或 Cookie 中,会增加请求体积;放进 Cookie 时还受到浏览器 Cookie 大小限制。
密钥轮换和受众管理更复杂
多服务环境需要管理密钥发布、轮换、kid、JWKS 缓存、算法白名单、iss 和 aud。如果所有服务共享一个 HMAC 密钥,任意一个服务泄露密钥都可能伪造令牌。
六、Session 与 JWT 的详细对比
| 对比维度 | 服务端 Session | JWT |
|---|---|---|
| 本质 | 服务端保存会话状态,客户端保存会话标识 | 客户端保存包含声明的签名令牌 |
| 服务端是否保存每个登录状态 | 通常需要 | 纯 Access Token 验证模式通常不需要 |
| 客户端保存内容 | 随机、无业务含义的 Session ID | 用户标识、过期时间、受众等声明及签名 |
| 服务端如何识别用户 | 查询 Session ID 对应的用户 | 验证签名后读取 sub 等声明 |
| 过期判断 | 读取 Session 的 expires_at | 检查 JWT 的 exp |
| 注销当前设备 | 删除或撤销 Session 记录 | 删除本地 Token;要立即失效需额外撤销机制 |
| 管理员让用户全部下线 | 删除该用户的所有 Session | 需要撤销列表、会话版本或等待 Token 过期 |
| 权限变化生效速度 | 可以较快生效,取决于服务端缓存策略 | 旧 JWT 中的声明可能继续有效 |
| 多实例扩展 | 需要共享 Session Store 或其他同步方案 | 各实例共享公钥/密钥即可验证 Access Token |
| 存储压力 | 活跃会话数越多,服务端存储越大 | Access Token 不保存时,服务端存储压力较小 |
| 认证查询 | 通常需要 Session Store 查询 | 可本地完成签名和时间验证,但授权仍可能查库 |
| 浏览器常见传输 | HttpOnly Cookie | Authorization Header 或 HttpOnly Cookie |
| Cookie 自动发送 | 常见,需要处理 CSRF | 若放 Cookie,同样需要处理 CSRF |
| 放入 localStorage | 一般不需要 | 常见但会暴露给同源 XSS |
| 泄露后的风险 | 直到 Session 被撤销或过期 | 直到 JWT 过期或被额外撤销 |
| 审计和设备管理 | 直接记录每个 Session,比较方便 | 需要额外记录 Refresh Token 或登录事件 |
| 实现复杂度 | 模型直观,生产上需要设计存储和扩展 | 表面简单,但密钥、撤销、刷新、声明和轮换更复杂 |
| 更适合 | 传统网站、SSR、后台、BFF | API、移动端、OAuth/OIDC、微服务调用 |
七、安全性对比:不要只看“服务端有没有存储”
1. Session ID 和 JWT 都是凭证
无论是下面哪一种:
Cookie: session=RANDOM_ID
还是:
Authorization: Bearer eyJ...
只要攻击者能够拿到并发送,服务端就可能把攻击者当成真实用户。两者都应当:
- 只通过 HTTPS 传输;
- 使用密码学安全的随机值或安全的签名算法;
- 设置合理的过期时间;
- 避免写入 URL、Referer、日志和错误信息;
- 对登录、登出、改密和权限变化进行审计;
- 对高风险操作要求重新认证或 MFA。
2. Cookie、HttpOnly 和 CSRF
如果凭证放在 Cookie 中,浏览器会根据 Cookie 规则自动发送。推荐使用:
Set-Cookie: __Host-session=...; Path=/; HttpOnly; Secure; SameSite=Lax
HttpOnly 可以阻止 JavaScript 直接读取 Cookie,但不能阻止 XSS 代码借助当前页面发起已经认证的请求。因此还需要做好 XSS 防护。
Cookie 自动发送可能产生 CSRF 风险,应该结合:
SameSite=Lax或SameSite=Strict;- 有副作用请求的 CSRF Token;
Origin/Referer校验;- 精确的 CORS allowlist;
- 重要操作重新认证。
3. Authorization Header 和 XSS
如果 JWT 放在 Authorization Header 中,浏览器不会像 Cookie 一样在所有匹配请求中自动附加,CSRF 风险通常更低。但前端必须自己读取和发送 Token。
如果 Token 放到 localStorage,同源 XSS 代码可以直接读取它并发送到攻击者服务器。sessionStorage 只缩短了跨标签页和持久化范围,不能解决 XSS 读取问题。
这不是说 JWT 一定不能放在浏览器,而是要根据架构选择:
- 浏览器只访问自己的后端:优先 BFF 或 HttpOnly Session Cookie;
- 必须由 SPA 直接调用 API:使用短期 Access Token,并设计安全的 Refresh Token 方案;
- 不要把长期、高权限的凭证长期放在可被 JavaScript 读取的存储中。
4. 算法和声明校验
JWT 验证不能只做“能否解码”:
- 固定允许的算法集合;
- 验证签名;
- 检查
exp、nbf、iat; - 检查
iss和aud; - 检查 scope、权限范围和租户边界;
- 处理时钟偏差,但不要允许过大的容忍窗口;
- 做密钥轮换和公钥缓存;
- 不接受非预期的
none或算法降级。
5. Session Fixation
Session 方案需要防止 Session Fixation:登录前后不能继续使用攻击者预先知道的 Session ID。正确做法是登录成功后轮换 Session ID,并将旧的匿名会话与新会话分离。
JWT 通常由登录成功后新签发的令牌代表新会话,也要避免接受来源不明、算法不符合预期或绑定关系不清晰的 Token。
八、注销、续期与权限变化
1. Session 的注销
Session 注销通常是:
- 从请求中取出 Session ID;
- 在服务端删除或标记对应记录为 revoked;
- 清除浏览器 Cookie。
即使攻击者之前复制了 Session ID,只要服务端删除记录,后续请求也会失败。
2. JWT 的注销
JWT 注销至少有两个动作:
- 清除客户端保存的 JWT;
- 处理服务端仍然有效的 JWT。
如果不做第二步,已经被盗的 JWT 仍可以在过期前使用。常见处理方式包括:
- 让 Access Token 很短时间过期;
- 保存并撤销 Refresh Token;
- 维护
jti黑名单; - 为用户保存
sessionVersion,JWT 中带版本号,请求时比较版本; - 对高风险接口做在线 Introspection;
- 发生改密、封禁时拒绝旧的
iat或旧会话版本。
这些方法越强调“立即失效”,越需要服务端状态,JWT 的纯无状态特征也就越弱。
3. Refresh Token 不等于普通 JWT
生产系统经常使用:
短期 Access Token + 较长期 Refresh Token
Access Token 用于访问 API,过期时间较短;Refresh Token 只用于换取新的 Access Token,通常需要:
- 存储在更安全的位置;
- 服务端记录哈希或会话信息;
- 每次刷新后轮换;
- 检测重用行为;
- 退出登录和改密时撤销;
- 按设备或客户端管理。
因此,“使用 JWT”并不意味着系统完全不需要数据库。很多系统只是让 Access Token 自包含,而把长期会话、刷新和撤销状态保留在服务端。
九、扩展性和性能:JWT 不一定天然更快
1. Session 的扩展
单机开发时,Session 可以放在进程内存中,但生产环境多实例部署通常需要共享存储:
请求 -> 任意应用实例 -> Redis/数据库 Session Store
常见优化包括:
- 使用 Redis 等低延迟共享存储;
- 设置过期时间和自动淘汰;
- 只在需要时查询完整用户信息;
- 对稳定的非敏感数据做短时缓存;
- 监控存储延迟和连接池;
- 不把 Session 绑定到单台应用实例。
2. JWT 的扩展
JWT 的业务服务可以本地验证签名:
认证服务 -> 私钥签发 JWT
业务服务 -> 使用公钥验证 JWT
这能减少中心 Session Store 的读取,但带来新的运维问题:
- 公钥如何分发和缓存;
- 密钥如何轮换;
- 多个服务如何限制
aud和 scope; - 旧密钥保留多久;
- 如何处理 Token 泄露和立即撤销;
- JWT 过大导致请求头膨胀。
3. 查询次数不是唯一指标
Session 多一次 Session Store 查询,不代表一定更慢;Redis 查询可能比复杂的业务数据库查询更快。JWT 少一次会话查询,也不代表整个请求不查数据库,因为服务端可能仍需要:
- 查询用户是否被封禁;
- 查询资源所属租户;
- 查询最新角色和权限;
- 读取订单、文章或文件数据;
- 记录审计和风控事件。
真正应该根据实际流量、延迟、可用性和撤销需求做压测,而不是简单认为“JWT 无状态,所以一定快”。
十、不同业务场景应该怎么选
场景一:传统服务端渲染网站
推荐:
服务端 Session + HttpOnly Cookie
原因:
- SSR 页面可以直接识别用户;
- 登录、登出和权限变化容易管理;
- Cookie 不需要暴露给页面 JavaScript;
- 与服务端路由、中间件和模板结合自然。
场景二:后台管理系统
通常也优先选择服务端 Session。后台操作涉及删除、导出、改密、用户管理等高风险动作,立即撤销和权限即时生效比“少查一次 Session Store”更重要。
如果使用 JWT,应缩短 Access Token 生命周期,并为管理员操作增加重新认证、MFA、设备管理和撤销能力。
场景三:SPA + 自有 API
不要先入为主地把 JWT 放进 localStorage。可以按下面顺序考虑:
- 使用 BFF,由后端保存和转发上游 Token;
- 使用 HttpOnly Cookie + 服务端 Session;
- 确实需要前端直接调用 API 时,再使用短期 Access Token,并完善 Refresh Token Rotation、CSP 和 XSS 防护。
场景四:移动端 App
移动端通常不能完全依赖浏览器 Cookie 自动管理,会使用 OAuth 2.0/OIDC 的 Access Token 和 Refresh Token。具体保存应使用系统安全存储,例如 Keychain 或 Android Keystore,而不是普通文本文件。
JWT 是否作为 Access Token,要看资源服务器是否需要本地验证、权限变化是否频繁,以及是否已有标准身份平台。
场景五:微服务
可以采用短期 JWT Access Token,让网关或各个服务使用公钥验证。但每个服务仍必须:
- 校验签发者和受众;
- 只接受自己需要的 scope;
- 独立执行资源授权;
- 不把网关已经认证当作全部授权结论;
- 设计密钥轮换和故障策略。
也可以让网关把外部认证转换为内部 Session 或内部身份上下文,具体取决于组织边界和服务治理方式。
场景六:需要多设备会话管理
如果需要展示“我的登录设备”、单独踢出某台设备、查看登录时间和 IP,服务端 Session 或服务端保存 Refresh Token 的方案更直观。
纯 JWT Access Token 很难仅凭令牌本身完成设备级管理,因为服务端没有一条对应的可操作会话记录。
十一、当前项目属于哪种方式
当前项目使用的是服务端 Session + HttpOnly Cookie,不是 JWT:
浏览器 Cookie: archive_session
服务端数据库: .data/auth.sqlite
服务端表: sessions
登录成功后:
- 服务端生成一个随机 Token;
- 浏览器保存原始 Token;
- 服务端将 Token 的 SHA-256 哈希写入
sessions.token_hash; - 同时保存
user_id、created_at和expires_at; - 后续请求自动携带
archive_sessionCookie; - 服务端重新计算哈希并查询
sessions表; - 查询到有效会话后,再关联
users表识别用户。
当前会话的有效期是从创建时刻开始计算的固定 30 天。访问网页不会自动把它变成新的 30 天,也就是当前不是滑动续期模式。
退出登录时,服务端删除对应的 sessions 记录,同时清除 Cookie。即使浏览器还保留旧 Cookie,服务端也不会再认可它。
十二、常见误区
误区一:Cookie 就是 Session
不准确。Cookie 是浏览器的保存和传输机制,Session 是服务端对登录会话的管理方式。常见组合是“Session ID 放在 Cookie 中”。
误区二:Token 就是 JWT
不准确。Token 是泛称,Opaque Token、OAuth Access Token 和 JWT 都可以被称为 Token。
误区三:JWT 加密了 Payload
不准确。普通签名 JWT 的 Payload 通常可被解码。签名用于防篡改,不用于保密。需要保密时应该使用合适的加密方案,而不是把数据放进普通 JWT。
误区四:JWT 无状态就不能退出登录
不准确。可以删除客户端令牌,也可以通过短期 Access Token、Refresh Token 撤销、黑名单或会话版本实现退出和失效,只是立即撤销需要额外服务端状态。
误区五:JWT 一定比 Session 更适合前端
不准确。浏览器应用最关心的是 XSS、CSRF、撤销、权限变化、渲染模式和部署拓扑。很多 Web 应用使用 HttpOnly Cookie + 服务端 Session 更简单、风险边界也更清晰。
误区六:JWT 中的 role: admin 就能决定权限
不准确。JWT 声明只能作为认证上下文或权限提示的一部分。服务端仍需要检查用户状态、租户边界、资源归属和当前授权策略。
误区七:Session 存数据库一定会拖垮服务器
不准确。合理的 TTL、索引、Redis、连接池和水平扩展可以支撑大量 Session。是否适合要通过实际压测判断。
误区八:HttpOnly 可以防住 XSS
不准确。HttpOnly 可以防止脚本直接读取 Cookie,但 XSS 仍可能借助当前页面发起已认证的请求。输入处理、输出编码、CSP 和依赖安全仍然不可缺少。
十三、最终决策清单
选择认证方案时,可以依次回答这些问题:
- 这是传统网站、SSR、SPA、移动端还是开放 API?
- 是否需要多个服务直接验证同一份凭证?
- 是否要求管理员立即让用户下线?
- 用户改密、封禁、角色变化后,旧凭证能否继续有效几分钟?
- 是否需要设备级会话列表和单设备注销?
- 浏览器是否必须直接调用第三方 API?
- 能否可靠实现 CSRF、XSS、CSP、密钥轮换和 Token 刷新?
- 是否有 Redis、数据库或统一身份平台来维护必要状态?
- 是否明确了
iss、aud、scope、租户和资源授权边界? - 是否做过真实流量下的性能、故障和撤销测试?
一个简单的选择思路是:
浏览器自有网站 / SSR / 后台
-> HttpOnly Cookie + 服务端 Session
SPA / 移动端 / 开放 API
-> 短期 Access Token + 安全的 Refresh Token 方案
多个系统统一身份
-> OAuth 2.0 / OpenID Connect + 各业务系统自己的本地会话
十四、总结
Session 和 JWT 的核心区别不是“一个用 Cookie、一个不用 Cookie”,而是服务端是否为每个登录凭证维护可查询、可撤销的会话状态。
- 服务端 Session:客户端保存随机 ID,服务端保存会话详情;撤销和权限变化容易,适合网站和后台,但需要共享存储和 CSRF 防护。
- JWT:客户端保存带签名的声明,服务端可以本地验证;适合 API、移动端和服务间调用,但立即撤销、权限更新、密钥轮换和刷新机制更复杂。
- Cookie 和 Authorization Header:是凭证传输方式,不决定凭证究竟是 Session ID 还是 JWT。
- JWT 的无状态:指服务端不必为每个 Access Token 保存登录记录,不代表系统完全没有服务端状态。
- 安全的关键:HTTPS、凭证保护、合理过期、撤销策略、服务端授权、密钥管理和完整的风控设计,而不是简单选择某个名词。
在大多数面向浏览器的自有网站中,先把 HttpOnly Cookie、服务端 Session、CSRF 和服务端授权做好,往往比为了“无状态”而引入复杂 JWT 体系更稳妥。