想象你走进一座名为“奇幻园”的主题乐园。开业第一年,乐园采用储物柜方案:游客在售票处录入信息,员工把资料锁进一排中央储物柜,再发一个带编号的手环,手环号就是柜子号。每个项目入口的检票员看见游客抬手,就跑去柜区核对身份,确认后才放行。这套机制让所有身份信息都存在乐园自己手里,叫做“有状态会话”。后来游客太多,柜区前排起长队,乐园决定升级:不再寄存信息,而是发给游客一张盖着防伪章的“通票”,通票上印着游玩权限和有效期,检票员只需验证印章真伪就能放行——这就是“无状态令牌”。再后来,通票也会过期、也会被伪造,乐园又陆续引入了续期窗口、反窃听与反推搡的安保措施、第三方旅行社联票,以及一页实名认证信息。今天,让我们沿着奇幻园的进化史,把会话管理与 Token 安全的关键知识点一次学透。

岗位关联

会话与 Token 安全是 Web 后端与安全工程师的必备基本功。学完本文,你就能独立设计出抗 XSS、抗 CSRF 的认证系统,为你的 AI 应用和业务数据筑起第一道防线。

目录
  1. 可视化演示
  2. 会话机制基础——储物柜与手环
  3. Token签发验证——盖章通票
  4. JWT结构解析——三页合订通票
  5. 刷新机制设计——续期窗口
  6. 攻击防护体系——反窃听与反推搡
  7. OAuth2授权流程——旅行社联票
  8. OIDC身份层——实名认证
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示JWT签发→验证→刷新全流程,对比Session有状态与JWT无状态两种模型,标注XSS/CSRF攻击面。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Token认证流程 Client→Auth签发JWT→API验证→DB查询,关注XSS/CSRF攻击面 JWT = Header.Payload.Signature HttpOnly Cookie + SameSite Strict 双重防护

会话机制基础——储物柜与手环

奇幻园开业第一年,检票员个个都是“脸盲症”患者——每个项目入口都不记得游客刚买的到底是套票还是单票。于是乐园在中央大厅设置了一排储物柜:游客买票时,售票员把购票信息写在小纸条上锁进柜子,交给他一个印着柜号的手环。每个项目入口的检票员看到手环号,就跑去柜区取出纸条核对,再放行。这个方案正是 Session 会话机制的雏形:机密信息全部锁在服务器端的“柜子”里,浏览器手里只有一个编号。

HTTP 协议天生无状态,服务器处理完一个请求就不记得客户端了。Session 机制就是为解决这个问题而生:服务器为每个登录用户开辟一份会话存储(储物柜),把用户 ID、角色、购物车等状态放进去,再通过 Cookie 把一个随机的会话 ID(手环编号)交给浏览器。浏览器每次请求自动携带这个 ID,服务器据此找到对应的储物柜,恢复用户状态。

会话 ID 的生成绝不能使用自增数字——否则攻击者把 1001 改成 1000 就能冒充他人。业界要求它足够随机、不可预测,通常由哈希函数对用户标识、时间戳和随机盐计算而来。会话的生命周期包含创建、使用、过期与销毁四个阶段:过期时间一到,服务器自动清理柜子,防止存储无限膨胀;用户主动退出时,也应立即销毁会话。

储物柜方案的逻辑很清晰:机密锁在柜里,浏览器只拿编号,泄露一个手环号顶多暴露一次会话。但分布式时代到来后,乐园在隔壁城市开了分园,总园买票的游客到分园却被拒之门外——分园查不到总园的柜子。Session 在集群中扩展困难的痛点,正是 Token 登场的契机。

SessionID=Hash(userId∥timestamp∥randomSalt)\text{SessionID} = \text{Hash}(\text{userId} \parallel \text{timestamp} \parallel \text{randomSalt})
通俗地说:会话 ID 就是把“用户标识 + 当前时间 + 随机盐”扔进哈希搅拌机,出来一串无法反推原始信息的随机编号。
  1. 拼接原始信息把用户标识、当前时间戳、随机盐拼成一条字符串,保证不同用户不同时刻生成的 ID 都不重复。
    message=userId∥timestamp∥randomSalt\text{message} = \text{userId} \parallel \text{timestamp} \parallel \text{randomSalt}
  2. 哈希运算对拼接串执行不可逆的 SHA-256 哈希,得到固定长度的摘要。
    digest=SHA-256(message)\text{digest} = \text{SHA-256}(\text{message})
  3. 输出会话 ID把摘要转成 URL 安全的字符串,作为手环编号交给浏览器。
    SessionID=Base64Url(digest)\text{SessionID} = \text{Base64Url}(\text{digest})

Token签发验证——盖章通票

管理层决定取消中央柜区,改发“盖章通票”:售票员把游客信息和游玩权限直接印在票面上,底部盖上一个官方防伪章。每个项目入口的检票员不再跑柜区,而是对照防伪特征快速验章,几十秒就能放行一人。通票本身没有存放任何服务器状态,验证凭据全在票面上——这就是 Token 认证的核心理念。

传统 Session 是“有状态”的:服务器必须记住每个用户。Token 则是“无状态”的:服务器用只有自己知道的密钥对用户信息签名,客户端保存签名后的凭证,后续请求带上它,服务器验签通过就信任信息。这种模式天然适合分布式系统和跨域场景——任何一台服务器只要持有密钥,都能独立完成验证,不需要共享会话存储。

签名算法主要有两类。对称算法(HMAC-SHA256)用同一个密钥签名和验签,性能高,但密钥一旦泄露所有令牌都能被伪造;非对称算法(RSA/ECDSA)用私钥签名、公钥验签,私钥只保存在签发服务器上,适合多方信任的场景。签发流程是:服务端组装用户信息 → 用密钥签名 → 返回令牌;验证流程是:客户端携带令牌 → 服务端验签 → 解析内容 → 放行或拒绝。

盖章通票解决了排队问题,却也带来新矛盾:票面既要印丰富信息,又要让检票员快速识别伪造。怎么设计一张信息完整又防伪可靠的通票?这就催生了三页合订的 JWT。

Sig=HMAC-SHA256(sk, header∥payload)\text{Sig} = \text{HMAC-SHA256}(\text{sk},\ \text{header} \parallel \text{payload})
通俗地说:签名就是用密钥把“前两段内容”封上防伪蜡印,任何人改动内容,蜡印都会破裂,验签随之失败。
  1. 拼接消息把头部和载荷编码后拼成一条消息,作为待签名内容。
    msg=base64(H)∥base64(P)\text{msg} = \text{base64}(H) \parallel \text{base64}(P)
  2. 计算签名用对称密钥对消息执行 HMAC-SHA256,得到固定长度的摘要。
    σ=HMAC(sk, msg)\sigma = \text{HMAC}(\text{sk},\ \text{msg})
  3. 验签比对接收方重新计算签名,并与令牌携带的签名比对,一致才通过。
    σ′=?σ\sigma' \stackrel{?}{=} \sigma

JWT结构解析——三页合订通票

JWT 像一张三页合订的通票。第一页是封面,写明防伪算法(alg)和票据类型(typ);第二页是内容页,印着游客标识、有效期、发行方等声明;第三页是官方验真章——对前两页内容计算出的签名。三页各自用统一的 Base64Url 格式编码后,用句点装订成一体,形成长长的"xxx.yyy.zzz"形态。

JWT(JSON Web Token)的三段结构各司其职。Header 声明签名算法和令牌类型;Payload 承载标准声明,包括 sub(用户唯一标识)、exp(过期时间戳)、iat(签发时间戳)、iss(发行方)、aud(受众);Signature 对前两段的编码串拼接后做签名,是防篡改的最终关卡。

需要特别警惕:Payload 只是 Base64Url 编码,不是加密!任何人都能解码看到内容,所以绝不能放密码、身份证号等敏感信息。哪怕攻击者只改动 Payload 里的一个字符,重新验签时就会因为签名不匹配而失败。Base64Url 与普通 Base64 的区别是把 +、/ 换成 -、_ 并去掉尾部等号,保证令牌能安全放进 URL 和 HTTP Header 中传输。

三页合订的通票确实漂亮,但检票员发现了新问题:通票印着“当日有效”,游客排三小时队后票却过期了。面对“生命周期长怕泄露”与“有效期短体验差”的矛盾,乐园在广场中央开设了续期窗口。

J=B(H) ⋅ B(P) ⋅ σ,B=Base64UrlJ = B(H) \,\cdot\, B(P) \,\cdot\, \sigma,\qquad B = \text{Base64Url}
通俗地说:JWT = 封面页 + 内容页 + 验真章,三页都用 URL 安全的编码串起来,中间用句点作为装订线。
  1. 编码 Header把 JSON 格式的算法声明转成 Base64Url 字符串。
    B(H)=Base64Url({alg,typ})B(H) = \text{Base64Url}(\{\text{alg}, \text{typ}\})
  2. 编码 Payload把用户声明 JSON 转成 Base64Url 字符串。
    B(P)=Base64Url({sub,exp,… })B(P) = \text{Base64Url}(\{\text{sub}, \text{exp}, \dots\})
  3. 生成签名对前两段拼接串执行签名运算。
    σ=Sig(B(H)∥B(P))\sigma = \text{Sig}(B(H) \parallel B(P))
  4. 三段拼接用句点把三段连接为最终令牌。
    J=B(H) ⋅ B(P) ⋅ σJ = B(H) \,\cdot\, B(P) \,\cdot\, \sigma

刷新机制设计——续期窗口

乐园规定通票每两小时失效,可热门项目排队就要三小时。于是乐园开设续期窗口:游客凭一张“续期凭证”就能换到新通票,不必重新买票进园;续期凭证的保质期更长,游客离园时还可以主动作废它。这套双票制完美平衡了安全与体验。

这就是 Access Token 与 Refresh Token 的分工。Access Token 有效期短(15 分钟到 2 小时),即使泄露,攻击者能利用的时间窗口也很小;Refresh Token 有效期长(7 天到 30 天),专门用来换取新的 Access Token。客户端收到 401 响应后,携带 Refresh Token 向授权服务器申请新令牌,用户全程无感知。

刷新机制有两个关键设计。一是令牌轮换:每次刷新时不仅签发新的 Access Token,也签发新的 Refresh Token 并作废旧值,即使旧的刷新令牌被截获也无法重放。二是撤销机制:服务端维护已撤销令牌列表,用户修改密码、退出登录或检测到异常时立即吊销刷新令牌,实现真正的“一键登出”。存储上,Refresh Token 绝不能交给 JavaScript,应放在 HttpOnly Cookie 中,且必须走 HTTPS。

续期窗口让游客畅玩一整天,但安保部门又接到新报案:有人在宣传栏贴带毒海报偷看手环,还有小偷趁游客双手拿满东西时从背后推他一把。XSS 与 CSRF 攻击正式登场。

Tacc<Tref≤Tmax,newAccess=R(oldRefresh)T_{\text{acc}} < T_{\text{ref}} \leq T_{\text{max}},\qquad \text{newAccess} = R(\text{oldRefresh})
通俗地说:访问令牌短命但常用,刷新令牌长命且少用;访问令牌快到期时,用刷新令牌到续期窗口换一张新访问令牌。
  1. 有效期分级访问令牌的有效期远小于刷新令牌,但刷新令牌也有上限。
    Tacc=2 h,Tref=30 dT_{\text{acc}}=2\,\text{h},\qquad T_{\text{ref}}=30\,\text{d}
  2. 刷新调用客户端携带刷新令牌到令牌端点兑换新的访问令牌。
    newAccess=ftoken(refreshToken)\text{newAccess} = f_{\text{token}}(\text{refreshToken})
  3. 旧令失效令牌轮换后,旧的刷新令牌被服务端立即撤销。
    revoke(oldRefreshToken)=True\text{revoke}(\text{oldRefreshToken}) = \text{True}

攻击防护体系——反窃听与反推搡

乐园遇到的坏人分两类。第一类是“窃听贼”:他往官方宣传栏里塞进一张带毒海报,游客一点开,海报里的脚本就开始偷读手环编号,甚至把手环号发给贼——这就是 XSS(跨站脚本攻击)。第二类是“推手”:他躲在储物柜旁,趁游客双手都拿着东西时突然推他一下,让游客“自己”走向危险项目——这就是 CSRF(跨站请求伪造)。

XSS 的本质是恶意脚本在受害者浏览器里执行,进而读取 Cookie、LocalStorage 中的令牌。防护三板斧:输入转义(把 <script> 变成无害文本)、开启 CSP(内容安全策略,只允许加载白名单脚本)、为 Cookie 设置 HttpOnly(JS 无法读取)。CSRF 的本质是浏览器自动携带 Cookie 的特性被滥用——恶意站点诱导用户向目标站点发请求,Cookie 被自动带上。对策是在 Cookie 上设置 SameSite 属性、在请求中校验来源、增加 CSRF Token。

SameSite 是浏览器级别的防线,有三种取值:Strict 表示跨站完全不携带 Cookie;Lax 表示顶级导航跨站时携带、子请求不携带;None 表示全部携带但必须搭配 Secure。绝大多数场景推荐 Lax——它允许用户从外部链接正常进入站点,又阻止了跨站表单提交。服务端还应校验 Origin 头,对写操作使用自定义 Header 或 CSRF Token 做第二道验证。

防御升级后,乐园内部稳如泰山。但游客开始抱怨:奇幻园、恐龙园、海洋馆三张门票要分别排队购买。三园管理方一合计,推出了“旅行社联票”——这正是 OAuth2 的授权模型。

Accept  ⟺  Origin=Expected  ∧  SameSite∈{Strict,Lax}\text{Accept} \iff \text{Origin} = \text{Expected} \;\land\; \text{SameSite} \in \{\text{Strict}, \text{Lax}\}
通俗地说:只有当请求来源可信、且浏览器规则允许携带 Cookie 时,才放行这个请求——两道关卡都过了才开门。
  1. 核对来源检查请求头中的 Origin 与站点期望的来源是否一致。
    Origin=https://bank.cn\text{Origin} = \text{https://bank.cn}
  2. 限制携带SameSite 为 Lax/Strict 时,跨站子请求不会自动携带 Cookie。
    SameSite=Lax⇒跨站POST不带Cookie\text{SameSite}=\text{Lax} \Rightarrow \text{跨站POST不带Cookie}
  3. 双重放行来源一致且 Cookie 未被跨站滥用,两个条件同时满足才放行。
    Accept  ⟺  A∧B\text{Accept} \iff A \land B

OAuth2授权流程——旅行社联票

三家乐园不再各自售票,而是委托一家旅行社做统一票务。游客去旅行社办联票,拿到一张“三联通票”,票面标明了能进哪几个园、每园玩几天。三家乐园的检票员看到联票即可放行,但不会把游客的完整资料透露给其他园——只传递必要信息,这就是授权与数据最小化的思想。

OAuth2 中有四个角色:资源所有者(游客)、客户端(第三方应用)、授权服务器(旅行社)、资源服务器(三家园)。最常用的是授权码模式:用户访问客户端 → 客户端引导用户到授权服务器 → 用户同意授权 → 授权服务器返回一个一次性授权码 → 客户端用授权码加上自己的密钥换取访问令牌 → 携带令牌访问资源服务器。

授权码模式的关键设计在于授权码只能使用一次、有效期仅几分钟,并且换取令牌时必须同时出示 client_id 和 client_secret,即使授权码在网络中被截获,攻击者也无法在缺少密钥的情况下兑换令牌。Scope 机制用于声明权限范围,比如“只读”还是“读写”,严格遵循最小权限原则。对于纯前端应用,还要搭配 PKCE 扩展(Code Challenge),用动态生成的校验值防止授权码被其他应用拦截。

联票带来了便利,但三家乐园检票时都发现:联票能证明“买了票”,却证明不了“本人是谁”。游客拿着别人的联票也能入园。实名认证的需求,让 OIDC 应运而生。

T=E(code, client_id, client_secret, redirect_uri)T = E(\text{code},\ \text{client\_id},\ \text{client\_secret},\ \text{redirect\_uri})
通俗地说:客户端拿一次性授权码到令牌窗口,出示自己的“户口本”(client_secret),核对无误后领走访问令牌。
  1. 携带授权码客户端把一次性授权码发给授权服务器的令牌端点。
    code→POST /token\text{code} \rightarrow \text{POST}\ /\text{token}
  2. 客户端认证同时提交 client_id 和 client_secret,证明自己是被信任的第三方应用。
    (id,secret)⇒确认客户端身份(\text{id}, \text{secret}) \Rightarrow \text{确认客户端身份}
  3. 校验回调地址redirect_uri 必须与申请时登记的一致,防止授权码被重定向到攻击者站点。
    redirect_uri=?registered\text{redirect\_uri} \stackrel{?}{=} \text{registered}
  4. 兑换令牌校验全部通过后返回访问令牌与刷新令牌。
    E⇒{access_token,refresh_token}E \Rightarrow \{\text{access\_token}, \text{refresh\_token}\}

OIDC身份层——实名认证

旅行社在联票上增加了一页“实名认证信息”:姓名、证件号、照片、签发日期,由官方认证机构盖章。三家乐园只需用官方公布的验章信息核验,就能确认游客身份真实。联票从“入园凭证”升级成了“数字身份证”。

OIDC(OpenID Connect)在 OAuth2 之上增加了身份认证层。OAuth2 只回答“客户端能否访问资源”,OIDC 回答“用户是谁”。其核心产物是 ID Token——一个由授权服务器签发的 JWT,包含标准声明:sub(用户唯一标识)、iss(签发者)、aud(受众)、exp(过期时间)、iat(签发时间)。客户端拿到 ID Token 后,就能安全地确认当前用户的身份。

验证 ID Token 需要经过四道关卡:用授权服务器公布的 JWKS 公钥验签、校验 iss 是否为期望的签发者、校验 aud 是否包含自己的 client_id、校验 exp 是否未过期。OIDC 还提供了发现文档(/.well-known/openid-configuration),客户端访问一个固定 URL 就能拿到授权端点、令牌端点、JWKS 地址等全部配置,实现零配置接入。基于这套标准,企业可以搭建 SSO 单点登录——员工在门户登录一次,就能访问所有接入系统,无需重复输密码。

从储物柜手环、盖章通票、三页通票、续期窗口,到旅行社联票与实名认证,奇幻园的进化史就是 Web 会话安全的发展史。现在,你可以把这些经验带回自己的系统设计中了。

OK  ⟺  verify_sig∧(iss=issuer)∧(aud=client_id)∧(exp>now)\text{OK} \iff \text{verify\_sig} \land (\text{iss} = \text{issuer}) \land (\text{aud} = \text{client\_id}) \land (\text{exp} > \text{now})
通俗地说:ID 令牌要过四道关——印章真、发行者对得上、受众是自己、没过期,四关全过才算可信身份。
  1. 验签用授权服务器发布的 JWKS 公钥验证 ID Token 的签名是否真实。
    verify_sig(IDToken)=True\text{verify\_sig}(\text{IDToken}) = \text{True}
  2. 校验发行者与受众iss 必须与授权服务器一致,aud 必须包含自己的 client_id,防止令牌被冒用。
    iss=issuer  ∧  aud=client_id\text{iss} = \text{issuer} \;\land\; \text{aud} = \text{client\_id}
  3. 校验过期时间exp 时间戳必须晚于当前时间,过期令牌直接拒绝。
    exp>now\text{exp} > \text{now}

AI 应用场景

企业级单点登录(SSO)门户

阶段:架构设计 / 统一身份认证

解决的问题:传统方式下每套系统独立登录,员工需要记多套密码,AI 平台(如内部 LLM 网关、模型训练平台)与统一身份体系难以打通。通过 OIDC + SSO,员工在门户登录一次即可访问所有 AI 工具,权限由 ID Token 中的角色声明统一控制。

具体案例:某公司用 Keycloak 做统一认证中心,内部 AI 门户接入 OIDC。员工登录一次,就能访问 JupyterHub、模型注册中心和向量数据库控制台。GPU 集群的调度界面根据 ID Token 里的角色字段判断谁能提交训练任务,安全且高效。

移动端 AI 应用的 Token 自动刷新

阶段:客户端开发 / 网络层优化

解决的问题:移动端网络不稳定,Access Token 过期会导致请求频繁失败,用户被迫反复登录,体验极差。利用“短期 Access Token + 长期 Refresh Token + 令牌轮换”机制,客户端在拦截器中捕获 401,自动换新令牌并重放请求。

具体案例:一款 AI 聊天 App 使用 15 分钟的有效访问令牌和 7 天的刷新令牌。用户在弱网环境下聊天,令牌过期后应用自动用刷新令牌换取新令牌,同时作废旧刷新令牌,全程无感知,且泄露风险始终被压缩在 15 分钟窗口内。

第三方 AI 能力开放平台

阶段:API 网关 / 开放平台

解决的问题:对外提供 AI 接口(如翻译、OCR、语音识别)时,需要让合作方安全调用,又不能暴露主账号密钥。通过 OAuth2 授权码模式,每个合作方获得独立的 client_id/secret,Scope 限制其只能调用特定模型接口,配额独立计费。

具体案例:某云厂商的开放平台采用 OAuth2 授权码模式,合作方申请应用凭证后,通过授权码换取自己的 Access Token。Scope 声明中写明“只能调用通用OCR接口”,即使 Token 泄露,攻击者也无法调用其他高价值模型。

Python 演示

python56 行
import base64import hashlibimport hmacimport jsonimport time SECRET_KEY = b"your-secret-key"  def b64url_encode(raw: bytes) -> str:    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")  def b64url_decode(data: str) -> bytes:    padding = "=" * (-len(data) % 4)    return base64.urlsafe_b64decode(data + padding)  def sign_message(message: str, secret: bytes) -> str:    digest = hmac.new(secret, message.encode("utf-8"), hashlib.sha256).digest()    return b64url_encode(digest)  def create_jwt(user_id: str, expire_minutes: int = 30) -> str:    header = {"alg": "HS256", "typ": "JWT"}    now = int(time.time())    payload = {        "sub": user_id,        "iat": now,        "exp": now + expire_minutes * 60,        "iss": "example-app"    }    header_b64 = b64url_encode(json.dumps(header).encode("utf-8"))    payload_b64 = b64url_encode(json.dumps(payload).encode("utf-8"))    message = header_b64 + "." + payload_b64    signature = sign_message(message, SECRET_KEY)    return message + "." + signature  def verify_jwt(token: str) -> dict:    header_b64, payload_b64, sig = token.split(".")    message = header_b64 + "." + payload_b64    expected_sig = sign_message(message, SECRET_KEY)    if sig != expected_sig:        raise ValueError("签名验证失败:令牌被篡改")    payload = json.loads(b64url_decode(payload_b64))    if payload["exp"] < int(time.time()):        raise ValueError("令牌已过期")    return payload  if __name__ == "__main__":    token = create_jwt("user_1001", expire_minutes=1)    print("签发JWT:", token)    decoded = verify_jwt(token)    print("验证通过,载荷:", decoded)
目的:用 Python 标准库实现一个最小化的 JWT 签发与验证工具,演示 HMAC-SHA256 签名的完整流程,可用于理解 JWT 三段结构与验签原理。
  1. b64url_encode(raw: bytes) -> str把字节做 Base64Url 编码并去掉尾部等号,这是 JWT 的标准编码方式,保证令牌在 URL 中安全传输。
  2. sign_message(message, secret)用 HMAC-SHA256 对“前两段拼接串”计算签名,对应正文中“官方防伪章”的生成。
  3. payload = {"sub": ..., "exp": ..., "iss": ...}构造 Payload 标准声明,sub 是用户唯一标识,exp 控制过期时间。
  4. signature = sign_message(message, SECRET_KEY)签名只覆盖前两段,Payload 任何改动都会导致验签失败。
  5. if sig != expected_sig:验签时重新计算签名并比对,这是 Token 防篡改的核心检查。
  6. if payload["exp"] < int(time.time()):检查过期时间,过期令牌直接拒绝,对应正文中“通票过期失效”。

常见误区

误区我一直以为 Session 就是把数据存在 Cookie 里,因为登录后浏览器里能看到 sessionid,就认为 Cookie 就是 Session。

正解Session 是服务器端的存储状态,Cookie 只是浏览器携带 SessionID 的运输工具。好比 Session 是储物柜,Cookie 是手环——手环上只写编号,真正的物品锁在柜子里。手环丢了可以补办,柜子里的数据还在;反过来把柜子拆了,手环就成了一串废塑料。

为何常见:日常调试中开发者最先接触的是 Cookie 里那个 sessionid 字段,把运输载具当成了存储本身。

误区很多教程说 JWT 要放 localStorage,因为 Cookie 会被 CSRF 利用,所以 localStorage 更安全。

正解localStorage 的短板是任何同源脚本都能读,一旦发生 XSS,令牌直接被偷走;而 HttpOnly Cookie 恰恰能防住 XSS 读取。真正的做法是按风险取舍:优先用 HttpOnly Cookie 加 SameSite,或使用短寿命令牌加内存存储。安全不是单点选择,而是分层防御。

为何常见:网上说法以讹传讹,把 CSRF 与 XSS 的防护对象彻底搞混,导致开发者选错存储位置。

误区把刷新令牌有效期设成一年甚至永不过期,用户体验最好,反正 Access Token 短就行。

正解刷新令牌是“长期通行证”,一旦泄露等于把账号永久交给攻击者。正确做法是设定合理有效期(如 7–30 天)、每次刷新轮换旧令牌、服务端实现撤销机制、检测异常立即吊销。便利与安全要平衡,不能无限拉长有效期。

为何常见:产品经理追求零登录体验,开发者在压力下不断放宽有效期,忽视了长令牌泄露的放大风险。

误区网站上了 HTTPS 后数据都加密了,Token 存储和传输细节都不用担心。

正解HTTPS 只保护传输过程,不保护存储端。Token 放在 localStorage 里遇到 XSS 一样被读走;日志、Referer、浏览器历史都可能泄露 URL 中的令牌。安全必须纵深防御:传输用 HTTPS、存储用 HttpOnly、必要时加动态校验与指纹绑定。

为何常见:把网络层安全等同于整体安全,忽略了应用层存储、日志等大量攻击面。

误区后端已经用了 JWT 认证,为什么还要 CSRF Token?JWT 不就能防 CSRF 吗?

正解JWT 解决“你是谁”的认证问题,CSRF Token 解决“这个请求是否用户主动发出”。只要 JWT 放在 Cookie 里被浏览器自动携带,跨站表单就能借用它发起恶意请求,CSRF 依然成立。所以即使有 JWT,也应配合 SameSite 或 CSRF Token 做双重防护。

为何常见:把认证与防伪这两个不同层级的机制混为一谈,以为认证通过就自然防御了所有攻击。

面试题

Q1. Session 与 Token 最核心的区别是什么?

单选题 · easy

  • A.Session服务端存状态,Token带签名← 正确答案
  • B.Session走Cookie,Token只能走请求头
  • C.Session绝对安全,Token永不安全
  • D.Session可注销,Token不可注销

答案:A。Session 把状态存在服务器端储物柜,Token 把签名后的凭证交给客户端携带。B 错误,Token 也能放 Cookie;C、D 都过于绝对,Token 在服务端记录撤销列表时也能注销。

Q2. JWT 中哪一段被篡改后验签必然失败?

单选题 · easy

  • A.Header和Payload内容被改动← 正确答案
  • B.Signature字段时间戳失效
  • C.只有Payload过期时间异常
  • D.Base64Url的填充字符被修改

答案:A。Signature 是对 Header 和 Payload 拼接串的签名,所以 Header 或 Payload 任一改动都会导致签名不匹配。B、D 描述的对象不在验签范围内;C 只是 Payload 改动的一种特例,不全面。

Q3. 设置 HttpOnly 属性的主要目的是什么?

单选题 · mid

  • A.禁止JS脚本读取Cookie内容← 正确答案
  • B.阻止Cookie在浏览器持久保存
  • C.强制Cookie走HTTPS加密传输
  • D.让Cookie可以跨站点自动发送

答案:A。HttpOnly 是给 Cookie 加一道“防读锁”,让 JavaScript 无法读取内容,专门防 XSS 窃取会话。B 是过期时间的作用,C 是 Secure 属性的作用,D 是 SameSite=None 的行为。

Q4. CSRF 攻击成功的关键前提是什么?

单选题 · mid

  • A.浏览器自动携带目标站点的Cookie← 正确答案
  • B.攻击者成功窃取了用户明文密码
  • C.用户浏览器彻底禁用了JavaScript
  • D.目标站点使用Token而非Session

答案:A。CSRF 利用的是浏览器自动携带 Cookie 的特性,目标站点识别成“用户本人”发出的请求。B 是窃取凭证而非伪造请求;C 禁用 JS 反而降低攻击面;D 中 Token 认证同样可能被 CSRF 利用。

Q5. SameSite=Lax 模式下 Cookie 何时会被携带?

单选题 · mid

  • A.顶级导航跨站请求时才会携带← 正确答案
  • B.所有跨站子请求都会自动携带
  • C.任何请求场景都不会携带它
  • D.只有HTTPS安全请求才会携带

答案:A。Lax 是推荐模式:用户点击外部链接进入网站的顶级导航会带 Cookie,而跨站 iframe、AJAX 等子请求不带。B 是 None 的行为;C 是 Strict 的行为;D 与 SameSite 无关,是 Secure 的职责。

Q6. OAuth2 授权码模式下授权码的有效期通常是多久?

单选题 · hard

  • A.几分钟内失效且只能使用一次← 正确答案
  • B.可以重复使用直到自己过期
  • C.与访问令牌的有效期保持相同
  • D.永不过期直到用户主动注销

答案:A。授权码是一次性票据,几分钟内有效,换到令牌后立即作废。这样即使授权码在网络中被截获,攻击者也来不及兑换。B、D 会放大泄露风险;C 混淆了授权码与访问令牌的生命周期。

Q7. 以下哪些属于 JWT Payload 的标准声明?

多选题 · mid

  • A.sub 声明用户的唯一标识← 正确答案
  • B.exp 声明令牌的过期时间← 正确答案
  • C.alg 声明签名算法类型
  • D.aud 声明令牌的受众← 正确答案

答案:ABD。sub、exp、aud 都是 Payload 的标准声明,分别表示主题、过期时间和受众;alg 属于 Header 的声明,描述签名算法,不在 Payload 中。

Q8. 以下哪些措施可以有效防护 XSS 攻击?

多选题 · easy

  • A.对用户输入进行HTML转义← 正确答案
  • B.启用CSP内容安全策略← 正确答案
  • C.为Cookie设置HttpOnly← 正确答案
  • D.用POST替代GET请求方式

答案:ABC。输入转义让恶意脚本失效,CSP 限制脚本加载来源,HttpOnly 让脚本读不到 Cookie,三者都是 XSS 的经典防线。D 是防止敏感数据泄露的通行做法,与 XSS 没有直接关系。

Q9. 以下哪些是刷新令牌的推荐实践?

多选题 · mid

  • A.每次刷新后轮换旧刷新令牌← 正确答案
  • B.为刷新令牌设置合理有效期← 正确答案
  • C.只在HTTPS通道中传输令牌← 正确答案
  • D.把刷新令牌放在URL参数里

答案:ABC。轮换、限时、HTTPS 传输都是刷新令牌的安全基线。D 是典型反模式:URL 会出现在历史记录、日志和 Referer 中,刷新令牌放在 URL 里极易泄露。

Q10. OIDC 相比 OAuth2 核心新增的能力是什么?

单选题 · hard

  • A.签发可验证的身份认证ID令牌← 正确答案
  • B.提供资源服务器的访问权限
  • C.支持多种授权码模式变体
  • D.自动托管用户的账号密码

答案:A。OIDC 在 OAuth2 授权基础上增加身份认证层,核心是签发标准化的 ID Token,让客户端安全确认“用户是谁”。B 是 OAuth2 本来就有的能力;C 是协议流程细节;D 完全不是 OIDC 的职责。