从 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_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

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

  • 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_iduser_id 强绑定。
      • Code 销毁 :验证通过后,立即从数据库/Redis 中删除该 Code,防止重放攻击。
      • 令牌生成与持久化 :生成正式的 AS_AccessToken ,将其与 user_idclient_idresource_idscope 绑定并持久化存储。最终,将 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 是否包含获取用户信息所需的权限(如 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 的数据。

  • 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_idclient_secret,以向授权服务器证明自己是合法的资源服务器,从而获准查询令牌详情。
  • 5.2 鉴权与数据交付

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

      • 元数据解析 :解析响应体,确认令牌状态为 active: true,识别出请求方为 System B ,授权主体为 Zhangsan ,且被授予的权限范围包含 user.readuser.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(指目标资源服务器是谁,比如 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只向内部授权应用开自动授权。

相关推荐
奋进的芋圆14 分钟前
Spring Boot 实现三模安全登录:微信扫码 + 手机号验证码 + 邮箱验证码
spring boot·redis·微信
怪兽源码24 分钟前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
恒悦sunsite30 分钟前
Redis之配置只读账号
java·redis·bootstrap
梦里小白龙36 分钟前
java 通过Minio上传文件
java·开发语言
人道领域37 分钟前
javaWeb从入门到进阶(SpringBoot事务管理及AOP)
java·数据库·mysql
csdn_aspnet1 小时前
ASP.NET Core 中的依赖注入
后端·asp.net·di·.net core
sheji52611 小时前
JSP基于信息安全的读书网站79f9s--程序+源码+数据库+调试部署+开发环境
java·开发语言·数据库·算法
毕设源码-邱学长1 小时前
【开题答辩全过程】以 基于Java Web的电子商务网站的用户行为分析与个性化推荐系统为例,包含答辩的问题和答案
java·开发语言
Jing_jing_X1 小时前
CPU 架构:x86、x64、ARM 到底是什么?为什么程序不能通用?
arm开发·架构·cpu
摇滚侠1 小时前
Java项目教程《尚庭公寓》java项目从开发到部署,技术储备,MybatisPlus、MybatisX
java·开发语言