从 SSO 登录到跨系统资源访问:OAuth2 全链路交互详解

全链路交互

角色场景

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

  • 四大角色:

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

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

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

  • 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_id、response_type、scope、state、resource_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_id、resource_id、scope 等原始 OAuth2 协议参数。

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

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

  • 1.5 生成授权码

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

      • 协议参数校验 :再次确认 redirect_uri 与注册白名单匹配,且 response_type 为 code。
      • 持久化授权记录 :在数据库 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

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

  • 2.1 传递Code

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

    • 系统 B 后端接收到前端传来的 Authorization Code 后,立即构建 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_id 及 user_id 强绑定。
      • Code 销毁 :验证通过后,立即从数据库/Redis 中删除该 Code,防止重放攻击。
      • 令牌生成与持久化 :生成正式的 AS_AccessToken ,将其与 user_id、client_id、resource_id 及 scope 绑定并持久化存储。最终,将 Token 通过响应体返回给系统 B 后端。

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

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

  • 3.1 用户身份同步

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

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

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

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

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

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

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

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

  • 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,必须找权威机构验票。

  • 5.1 资源服务器拦截与远程令牌内省

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

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

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


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(指目标资源服务器是谁,比如 systemA 的clientid),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只向内部授权应用开自动授权。

相关推荐
xcl09251 小时前
北京24小时自助健身房系统软件开发实战:从架构到部署全流程指南
java·spring boot·架构
一 乐3 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长3 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn3 小时前
Spring 注解使用详解
java·spring
一条小小yu3 小时前
Spring IoC的理解
java·后端·spring
落魄实习生4 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈4 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
萧瑟余晖4 小时前
Dubbo SPI扩展机制详解
架构·dubbo
Shulex4 小时前
面向跨境电商多渠道消息系统的技术架构:亚马逊站内信合规对接与自动化执行链路设计
运维·架构·自动化
茉莉玫瑰花茶4 小时前
GO [ 方法 ]
开发语言·后端·golang