架构骚操作:单机搞定 Spring Authorization Server,让你的应用“精神分裂”但极度优雅
有代码洁癖的人可能会说,这种架构不符合微服务的奥义。但工程学讲究的是因地制宜。通过这套配置,我们零成本获得了一个完全符合 OAuth2.0 / OIDC 协议规范的系统。对外,前端页面依然走的是标准的302 跳转 -> 拿 Code -> 后端换 Token的神圣不可侵犯的安全流程。对内,我们省下了一台独立服务器的内存开销和运维成本。等到某天,业务爆发,你需要接入 OA 系统、ERP 系统、小程序
在微服务大行其道的今天,提到 SSO(单点登录)和 OAuth2.0,标准的教科书架构总是教我们:
- 搞一台单独的服务器跑统一身份认证中心(IdP,比如挂在 9000 端口)。
- 搞 N 台服务器跑业务客户端(SP,比如挂在 8080 端口)。
但现实往往很骨感: 老板只批了一台 2 核 4G 的云服务器,或者你的独立开发项目才刚刚起步。为了一个所谓的“标准架构”,强行起两个 Spring Boot 进程?内存直接拉满,简直是杀鸡用牛刀。
那么问题来了:在资源极度有限的情况下,我们能不能把“认证中心(IdP)”和“业务系统(SP)”塞进同一个 8080 端口里,同时还能保留完美的 OAuth2.0 SP-Initiated(业务系统发起)授权码模式?
答案是:绝对可以! 只要你懂怎么让你的应用优雅地“精神分裂”。
🎭 核心思想:一台服务器,三种人格
在 All-in-One 的架构下,我们的 8080 端口需要处理三种截然不同的请求。为了不让它们打架,我们需要利用 Spring Security 的 @Order 注解,为应用切分出三条独立的 SecurityFilterChain(安全过滤器链)。
这就好比在一栋楼里开了三个不同安保级别的门,各司其职:
- 大门 1(IdP 协议端点):专门处理
/oauth2/authorize和/oauth2/token等标准协议。这是发证机关。 - 大门 2(表单登录端点):专门处理
/login。这是查验用户密码的地方。 - 大门 3(SP 业务端点):拦截所有的业务 API
/api/**,以及处理回调/login/oauth2/code/*。这是业务大厅。
虽然它们在同一个进程里,但当 Vue 前端发起登录时,逻辑上依然走的是 SP-Initiated(业务系统发起) 的标准流程:业务大厅(大门 3)将用户踢给发证机关(大门 1),发证机关验明正身(大门 2)后,再把 Code 传回给业务大厅。
{"component":"LlmGeneratedComponent","props":{"height":"750px","prompt":"创建一个名为“All-in-One 单机 SSO 逻辑流转图”的交互式组件。组件旨在直观展示服务器内部的自我调用。\n\n1. 布局结构:\n - 左侧:用户浏览器 (Vue/React)\n - 右侧:一个巨大的虚线框代表“单体服务器 (Port 8080)”。在这个框内部,划分为两块:上方是“大门3: 业务大厅(SP)”,下方是“大门1: 认证中心(IdP)”。\n2. 交互流转(通过点击‘下一步’演示):\n - 步骤1:浏览器发起登录 -> 访问服务器内部的【业务大厅】(/oauth2/authorization/my-app)。\n - 步骤2:业务大厅拦截 -> 给浏览器返回 302 重定向指令。\n - 步骤3:浏览器根据 302 -> 访问同一个服务器的【认证中心】(/oauth2/authorize)。\n - 步骤4:认证中心 -> 返回 302 (携带 code) 给浏览器。\n - 步骤5:浏览器带着 code -> 访问【业务大厅】的回调地址。\n - 步骤6(核心高亮):【业务大厅】在服务器内部,发起一个 **真实的底层 HTTP 请求**,绕出一个明显的弧形箭头,重新打回同一个服务器的【认证中心】去换 Token。用警告色(红色/橙色)高亮这段“自己调自己”的路线。\n3. UI风格:科技感,重点突出“单机内部的 HTTP 闭环”。界面文字使用中文。","id":"im_0d7feffa5dc5ee4d"}}
💻 极客代码:三链合一的终极配置
把以前要写在两个工程里的代码,浓缩到一个大配置类中。注意看 @Order 的精妙运用:
@Configuration
@EnableWebSecurity
public class AllInOneSecurityConfig {
// ==========================================
// 人格 1:我是高贵的“统一认证中心 IdP” (Order 1)
// ==========================================
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
// 没登录的,全给我滚去 /login 页面
http.exceptionHandling(exceptions -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
);
return http.build();
}
// ==========================================
// 人格 2:我是无情的“账号密码检验机” (Order 2)
// ==========================================
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/assets/**").permitAll() // 放行登录页静态资源
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
// ==========================================
// 人格 3:我是卑微的“业务系统 SP” (Order 3)
// ==========================================
@Bean
@Order(3)
public SecurityFilterChain businessClientSecurityFilterChain(HttpSecurity http) throws Exception {
// 划重点:限制拦截范围,别把前两个的活儿抢了!
http.securityMatcher("/api/**", "/login/oauth2/**", "/oauth2/authorization/**")
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
// 配置自定义的 SuccessHandler,用于签发本地 JWT 并重定向回 Vue 前端
.successHandler(new SSOAuthenticationSuccessHandler())
);
return http.build();
}
// 注册客户端:自己注册自己!
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient selfClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("my-internal-app")
.clientSecret("{noop}my-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// 回调地址,指向自己的 8080 端口
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/my-internal-app")
.scope(OidcScopes.OPENID)
.build();
return new InMemoryRegisteredClientRepository(selfClient);
}
}
紧接着,在你的 application.yml 里,配置这个应用作为 OAuth2 客户端去连接它自己:
spring:
security:
oauth2:
client:
registration:
my-internal-app:
client-id: my-internal-app
client-secret: my-secret
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
my-internal-app:
# 魔法降临:Issuer 指向自己的 IP 和端口
issuer-uri: http://127.0.0.1:8080
前端如何触发?
在 Vue/React 前端,你只需要让浏览器直接访问 Spring Security 默认的 SP 触发端点即可开启无感流转:
// 前端直接跳转,拉开单机 SSO 序幕
window.location.href = "http://127.0.0.1:8080/oauth2/authorization/my-internal-app";
💣 高能预警:细数“精神分裂”带来的 3 个深坑
代码写完跑起来,你会发现一套标准的授权码流程居然在单机上跑通了!但在沾沾自喜之前,作为架构师,你必须防范这种“左脚踩右脚上天”架构带来的三个隐患。
坑 1:自己调自己的 HTTP 死锁 (Loopback Starvation)
这是最容易在生产环境翻车的地方。
在标准 OAuth2 流程中,客户端拿到 code 后,必须发起一个 真实的底层 HTTP POST 请求 去找认证中心换 Token。
在 All-in-One 架构中,这意味着 8080 端口发出了一个 HTTP 请求,打向了它自己的 8080 端口!
- 隐患:如果你的并发量突然上来,外面进来的请求把 Tomcat 线程池占满了。这时,处理这些请求的线程又发起了内部 HTTP 请求去换 Token,但 Tomcat 已经没有空闲线程去接收这些内部请求了。死锁诞生,服务直接假死。
- 解法:适当调大
server.tomcat.threads.max,或者在内部换票逻辑中自定义WebClient/RestTemplate的超时时间,避免无限等待。
坑 2:苛刻的 Issuer URI 校验
Spring Authorization Server 有着极度的洁癖。它在校验 Token 时,会严格比对签发者(Issuer)。
如果你在 yml 里配置的 issuer-uri 是 http://127.0.0.1:8080,但你在前端代码里写的是 http://localhost:8080 发起登录,跑到最后一步绝对会报 Issuer mismatch(签发者不匹配)的异常。
- 解法:保证前端发起跳转的域名、回调地址域名、YML 配置域名 三码合一。强烈建议在开发机配置 Hosts 伪造一个真实的域名(如
sso.local.com)进行测试。
坑 3:叠加态的 Session
因为是同一个应用、同一个端口,整个 OAuth2 流转过程中,IdP 人格和 SP 人格共享了同一个 JSESSIONID Cookie。
虽然这在协议层面不会阻断授权码流程,甚至能让鉴权变得极快(因为上下文都在本地内存里),但如果你使用了 Redis Session 共享,或者后续打算剥离 IdP,你会发现 Session 里的数据像毛线球一样纠缠在一起,既有认证中心的授权状态,又有业务系统的上下文。
🎯 总结:先扛起业务,再谈微服务
有代码洁癖的人可能会说,这种架构不符合微服务的奥义。
但工程学讲究的是因地制宜。通过这套配置,我们零成本获得了一个完全符合 OAuth2.0 / OIDC 协议规范的系统。
对外,前端页面依然走的是标准的 302 跳转 -> 拿 Code -> 后端换 Token 的神圣不可侵犯的安全流程。
对内,我们省下了一台独立服务器的内存开销和运维成本。
等到某天,业务爆发,你需要接入 OA 系统、ERP 系统、小程序时,你只需要把那段 @Order(1) 和 @Order(2) 的代码剪切出去,稍微改改 YML,一个独立的高可用认证中心就剥离出来了——前端 Vue 代码甚至连一行都不用改!
这,才是架构演进的最高境界。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)