架构师实战:Spring Authorization Server 落地企业级“无感” SSO(附设计映射与源码级接口剖析)

在企业级微服务架构中,单点登录(SSO)是不可或缺的基础设施。网上的 OAuth2.0 教程大多停留在"Demo 级别":总是会弹出一个框架自带的简陋登录页面让用户输入账密。

但在真实的生产环境中,我们的业务场景往往是这样的:

用户已经使用账号密码登录了我们的主系统(门户/工作台),进入首页后,点击页面上的"第三方业务系统"图标,要求直接免密、无感地跳转进入子系统。

很多开发者在看完原理图后,一到敲代码的环节就懵了:图上的线条在代码里到底长啥样?到底是前端发请求还是后端发请求?框架底层到底提供了哪些接口?

今天,我们将基于 Spring 生态最新推荐的 Spring Authorization Server (SAS) ,手把手带你落地这套架构。本文最大的特色是:将设计时序图与具体的代码实现进行 1:1 的精准映射,并深度剖析底层的核心接口!

注:本文基于 Spring Boot 3.x 与 Spring Security 6.x 编写。


🏗️ 核心架构与分水岭设计

在动手之前,必须理清本场景中最关键的架构抉择:SP-Initiated(业务系统发起模式)

因为用户登录的入口是主系统(门户 8080),它不是发证机关(IdP 9000),没有资格自己生成 Code。当用户点击"跳转子系统 8081"时,8080 必须引导用户去找 9000"要一张去 8081 的门票"。

这决定了:跳转的第一步,必须是前端执行 window.open,让浏览器带着已有的全局 Cookie,主动去敲 9000 的大门请求静默授权。 ### 角色划分

  • auth-server (IdP 认证中心)9000 端口。全局会话的管理者。
  • portal-client (SP 主系统)8080 端口。用户发起跳转的起点。
  • sub-client (SP 子系统)8081 端口。接收 Code 换票的最终目的地。

🗺️ 架构设计:全链路交互时序图

请牢记这张图,接下来的每一行核心代码,都将严丝合缝地贴合图上的步骤。
子系统 (8081) 认证中心 (9000) 主系统 (8080) 用户浏览器 子系统 (8081) 认证中心 (9000) 主系统 (8080) 用户浏览器 【阶段一:前置条件与身份基石】 此时浏览器已拥有 IdP(9000) 的 Session Cookie 【阶段二:触发无感跳转 (SP-Initiated)】 校验 Cookie 有效,跳过登录页,静默授权 【阶段三:子系统接力与本地开户】 同步用户资料,签发本地业务 JWT 1. 登录主系统 (已完成) 1 2. 点击"进入第三方子系统"按钮 2 3. 前端执行 window.open(IdP授权页) 3 4. 携带 Cookie 访问 /oauth2/authorize 4 5. 302 重定向至子系统回调地址 (携带 code) 5 6. 重定向至 /login/oauth2/code/sub-app 6 7. [后台隐秘通信] 携带 Secret 和 Code 换取 Token 7 8. 返回 Access Token 与 UserInfo 8 9. 302 重定向回子系统前端首页 (携带 JWT) 9 10. Vue/React 渲染,实现无感免密登录! 10


💻 落地实现:代码与设计的 1:1 映射

阶段一:前置条件与身份基石

要让后面的跳转能跑通,认证中心(9000)必须提前认识主系统和子系统。

📍 对应工程:auth-server (9000)

AuthorizationServerConfig 配置类中注册客户端:

java 复制代码
@Bean
public RegisteredClientRepository registeredClientRepository() {
    // 【映射时序图 步骤 1 前置条件】:主系统注册
    RegisteredClient portalClient = RegisteredClient.withId("portal-id")
            .clientId("portal-app")
            .clientSecret("{noop}portal-secret")
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUri("http://127.0.0.1:8080/login/oauth2/code/portal-app")
            // ...省略 scope 等配置
            .build();

    // 【映射时序图 步骤 5 回调目标】:子系统注册
    // 只有在这里注册了,IdP 才会允许给 8081 发放 Code
    RegisteredClient subAppClient = RegisteredClient.withId("sub-id")
            .clientId("sub-app")
            .clientSecret("{noop}sub-secret")
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUri("http://127.0.0.1:8081/login/oauth2/code/sub-app")
            .build();

    return new InMemoryRegisteredClientRepository(portalClient, subAppClient);
}

阶段二:触发无感跳转 (SP-Initiated)

主系统前端发力,利用浏览器自身的机制,打通跨域身份校验。

📍 对应工程:portal-client (8080) 的前端 Vue/React 页面

javascript 复制代码
// 【映射时序图 步骤 2】:绑定到跳转按钮的 click 事件
function handleJumpToSubApp() {
    
    // 构造标准的 OAuth2 授权请求地址
    // ⚠️ 严苛细节:我们要去子系统,所以 client_id 必须是 8081 的标识
    const authUrl = "http://127.0.0.1:9000/oauth2/authorize" + 
                    "?response_type=code" +
                    "&client_id=sub-app" +
                    "&redirect_uri=http://127.0.0.1:8081/login/oauth2/code/sub-app" +
                    "&scope=openid profile";
    
    // 【映射时序图 步骤 3 & 4】:让浏览器自己去请求!
    // window.open 会在新标签页打开。
    // 核心魔法:浏览器访问 9000 端口时,会自动把之前存的 9000 的 Cookie 带上!
    // 9000 后端收到请求,一看有 Cookie 认识你,连登录框都不弹,直接 302 把你扔给 8081。
    window.open(authUrl, "_blank"); 
}

注:时序图中的 Step 4 (静默授权) 和 Step 5 (下发 Code) 全部由 Spring Authorization Server 的底层引擎全自动完成,无需我们手写一行 Controller!


阶段三:子系统接力与本地开户

浏览器被 302 扔到了子系统(8081),子系统开始后台"暗箱操作"。

📍 对应工程:sub-client (8081) 的后端 Spring Boot

首先,在 YML 中配置好自己的身份。
【映射时序图 步骤 6、7、8】 :这三步的高危操作(拦截 Code、发 HTTP 请求换 Token),由于我们引入了 spring-boot-starter-oauth2-client,Spring 底层的 OAuth2LoginAuthenticationFilter全自动搞定!我们只需在 YML 里填好参数:

yaml 复制代码
spring:
  security:
    oauth2:
      client:
        registration:
          sub-app: # 【映射 Step 6】:这里的名字对应回调 URL 的尾巴
            client-id: sub-app
            client-secret: sub-secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          sub-app:
            # 【映射 Step 7】:Spring 会自动向这里发 POST 请求换票
            issuer-uri: http://127.0.0.1:9000

接着,当 Spring 在底层偷偷拉取到用户信息后,我们需要接管最后一棒。

【映射时序图 步骤 9】:自定义登录成功处理器

java 复制代码
@Component
public class SSOAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 
                                        Authentication authentication) throws IOException {
        
        // 【映射 Step 8 结束】:从 Authentication 中提取 9000 传来的身份信息
        OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal();
        String username = oauth2User.getAttribute("preferred_username"); 
        
        // 🌟 业务核心逻辑:如果本地数据库没这个用户,直接根据资料开户
        // userService.syncUserFromIdp(username, oauth2User.getAttributes());
        
        // 签发 8081 子系统自己认的本地 JWT Token
        String localJwtToken = JwtUtils.generateToken(username);
        
        // 【映射 Step 9】:最后一次 302!带着 Token 重定向回 8081 的前端 Vue 页面
        String frontendUrl = "http://localhost:3001/dashboard?token=" + localJwtToken;
        response.sendRedirect(frontendUrl);
    }
}

最后,在前端的 dashboard 页面(【映射时序图 步骤 10】),Vue 组件在 mounted 生命周期中提取 URL 上的 token 存入 localStorage,完美的无感跳转正式闭环!


🔍 进阶剖析:框架底层的"魔法"与核心接口扩展

为什么上面我们只写了几行配置,连 Controller 都不用写,整个流程就能跑通?那是因为 Spring Authorization Server 已经为你开箱即用地准备好了标准的端点(Endpoints)和高度抽象的核心接口。

1. 认证中心默认暴露的标准端点 (Endpoints)

当你启动 9000 端口时,以下接口就已经生效了,前端和子系统后端就是在和这些接口打交道:

  • /oauth2/authorize (授权端点) :我们在前端 window.open 调用的就是它。它接收 response_type=codeclient_idredirect_uri。负责校验用户登录状态(也就是为什么能无感的核心),并签发 Code。
  • /oauth2/token (令牌端点) :子系统 8081 后端静默调用的接口。它接收 grant_type=authorization_codecodeclient_secret,负责核对暗号并发放 Access Token。
  • /oauth2/jwks (JWK 公钥端点):资源服务器用来获取验证 JWT 签名的 RSA 公钥。
  • /userinfo (OIDC 用户信息端点):子系统 8081 拿到 Token 后,调用它来获取当前用户的详细资料(头像、昵称、角色等)。

2. 核心接口:实战中必须重写的 Bean

在 Demo 中我们用了各种 InMemory... 的内存实现,但在生产环境中,你需要通过实现以下接口将数据存入 MySQL 或 Redis:

📌 IdP 端(认证中心)核心接口:

  • RegisteredClientRepository :第三方应用管家。生产中必须实现它(或使用自带的 JdbcRegisteredClientRepository),把业务系统的 client_id 和回调地址存到数据库中,实现动态增删应用。
  • OAuth2AuthorizationService:授权状态大管家。负责持久化存储 Code 和 Access Token。如果不替换为 Redis 或 DB 实现,IdP 一旦重启,所有用户发下去的 Token 就全失效了。
  • OAuth2TokenCustomizer:JWT 雕刻刀。如果你想在发给子系统的 Access Token 中塞入用户的部门 ID 或角色权限,就需要实现该接口,动态修改 JWT Payload。

📌 SP 端(业务子系统)核心接口:

  • OAuth2UserService<OAuth2UserRequest, OAuth2User> :用户信息拉取工。这是 SP 端最常被重写的接口! 当底层从 /userinfo 拉回 JSON 后,默认只是转成了 Map。你需要继承 DefaultOAuth2UserService,重写 loadUser() 方法,在这里将其转换为你本地系统的 SysUser 实体类,或者处理接口超时等容错逻辑。

💡 落地避坑指南(架构师 Checklist)

  1. 域名极度敏感(Issuer Mismatch)
    如果 YML 里的 issuer-uri127.0.0.1,那么主系统前端 window.open 的地址也必须是 127.0.0.1,绝不能用 localhost!否则 Spring 验证 Token 签名时必报"签发者不匹配"。建议统一配 Host 域名。
  2. 跨域 Cookie 拦截(SameSite)
    无感 SSO 的灵魂在于浏览器带 Cookie。如果你的 8080 和 9000 不在同一个主域名下,高版本浏览器可能会拦截第三方 Cookie。必须通过 Nginx 将它们代理到同源域名(如 portal.company.comsso.company.com)。
  3. 不要在前端用 AJAX 要 Code
    永远不要用 axios.get 去请求 /oauth2/authorize!CORS 机制不仅会拦截 302 重定向,更无法顺利让浏览器发生页面的实际位移。必须用 window.open<a href>

结语

单点登录的本质,是利用认证中心作为整个架构的"信任锚点"。主系统负责开启全局 Session,子系统负责验证授权码。通过本文的 1:1 映射拆解以及底层源码接口的剖析,希望能帮你彻底扫清从 OAuth2 抽象协议到代码落地的最后一道障碍。

如果这篇文章解答了你心中的疑惑,欢迎点赞收藏,转发给团队的兄弟们一起避坑!

相关推荐
敖正炀1 小时前
Spring 深度内核-核心容器与扩展机制-Spring 循环依赖终极剖析:三级缓存与 AOP 代理的纠缠
spring
超梦dasgg2 小时前
Spring AI 智能航空助手项目实战
java·人工智能·后端·spring·ai编程
敖正炀2 小时前
Spring 深度内核-核心容器与扩展机制-声明式事务的内部 AOP 实现:TransactionInterceptor 全解
spring
counting money3 小时前
Spring框架基础(配置篇)
java·后端·spring
生活真难3 小时前
SpringCloud - 任务调度 - xxl-job
后端·spring·spring cloud
敖正炀4 小时前
Spring 深度内核-核心容器与扩展机制-AOP 进阶:AspectJ 集成、LTW 织入与工程实践
spring
直奔標竿4 小时前
Java开发者AI转型第二十二课!Spring AI 个人知识库实战(一)——架构搭建与核心契约落地
java·人工智能·后端·spring·架构
曹牧6 小时前
Spring WebService 的两种主流实现方式‌
java·后端·spring
直奔標竿6 小时前
Java开发者AI转型第二十三课!Spring AI个人知识库实战(二):异步ETL流水线搭建与避坑指南
java·人工智能·spring boot·后端·spring