一、定义
1、什么是单点登录(SSO)?
官方定义:单点登录是一种身份认证方案,允许用户使用一组凭证(用户名/密码)登录多个相互信任的应用系统,无需在每个系统中重复登录。
通俗理解:就像公司的一卡通系统,刷一次卡就能进入大楼、打开门禁、食堂消费、打印文件,不需要在每个地方都重新验证身份。
二、核心概念与技术原理
1. 信任域与票据传递机制
这是SSO的灵魂。认证中心(IdP) 和各个应用(SP) 必须预先建立信任(通常通过共享密钥或证书)。用户登录后,认证中心颁发的票据就是信任的载体。
-
核心概念:Session (会话)、Ticket (票据,如CAS中的ST)、Token (令牌,如OIDC中的ID Token)。
-
技术原理:应用收到票据后,必须后台连接认证中心验证其有效性,而不是直接相信客户端传来的东西。这是防止伪造的核心。
2. 会话的生命周期与状态管理
SSO中有两种会话:
-
全局会话:用户在认证中心的登录状态。通常由认证中心维护,存储在Redis等缓存中,键是浏览器Cookie中的全局会话ID。
-
本地会话:用户在具体某个应用中的登录状态。由各应用自己维护。
技术要点:单点登出的关键在于,注销全局会话后,需要通知所有建立过本地会话的应用。这通常通过回调通知或应用主动查询会话状态实现。
3. 协议与标准
不同的协议定义了上述流程中"票据"的格式和传递规则:
-
CAS协议:概念清晰,流程如图,适合传统Web系统。
-
OIDC:基于OAuth 2.0的ID Token(一个签名的JWT)作为身份凭证。这是现代微服务架构首选,因为JWT是自包含的,便于在服务间无状态传递。
-
SAML:使用XML格式的断言,非常强大和严谨,是银行、政府等传统企业级集成的常见标准。
4. 安全性设计
-
防重放攻击:票据(如CAS的ST)必须是一次性的,验证后立即失效。
-
防伪造:票据必须包含不可伪造的签名(如JWT签名、SAML断言签名)。
-
安全传输:所有重定向和令牌传递必须使用HTTPS。
5.关键概念详解
| 概念 | 专业解释 | 白话解释 + 场景比喻 |
|---|---|---|
| 身份提供者(IdP) | 负责统一管理用户、进行认证的系统。 | "总服务台/公安局"。它说了算,它说你是谁,你就是谁。 |
| 服务提供者(SP) | 依赖 IdP 认证结果,为用户提供具体业务功能的系统。 | "各个办事窗口(A窗口社保、B窗口公积金)"。它们相信总服务台的认证。 |
| 令牌(Token) | 一种身份凭证的统称,如 JWT。 | "带有防伪芯片的电子身份证"。里面编码了你的信息,并且自带防伪验证机制。 |
| 票据(Ticket) | 特指一种一次性、短效的凭证,用于在系统间临时交换。 | "一次性的排队小票"。你拿着它从总台跑回 A 窗口,用过即废,防止被别人捡去重用。 |
| 重定向(Redirect) | 浏览器从一个地址自动跳转到另一个地址。 | "柜员用手指向总服务台:去那边办"。这是 SSO 流程在浏览器端流转的关键技术。 |
| 会话管理(Session Management) | 维护用户登录状态的技术。SSO 中有全局会话(在认证中心)和本地会话(在各个应用内)。 | 总服务台记住你"已办卡"(全局),A 窗口记住你"正在办社保"(本地)。单点登出就是总台广播:"所有人都注意,这个人走了,请销毁他的本地档案。" |
三、实现SSO方式
在 Java 中实现 SSO,主要可以从 实现原理 和 协议/标准 两个维度来划分。
| 分类维度 | 具体方案/协议 | 核心思想 | 典型技术/实现 | 适用场景 |
|---|---|---|---|---|
| 基于Token | JWT / 自定义Token 无状态令牌: | 无状态令牌:服务端不存储会话,身份信息全部编码在令牌中,由其自身携带的签名保证安全。 | JJWT, java-jwt, Auth0库 | 前后端分离、微服务间API认证、一次性验证 |
| 基于标准协议 | OAuth 2.0 | 授权框架:解决第三方应用在用户授权后安全访问用户资源的问题,原生不是SSO协议。 | Spring Security OAuth2, ScribeJava | 第三方授权登录(如微信、GitHub登录) |
| OIDC (OpenID Connect) | 身份认证层:在OAuth 2.0之上添加了身份层,规定了如何获取用户身份信息,是现代SSO的主流协议。 | Spring Security OIDC, Nimbus JOSE+JWT | 企业级SSO、现代应用统一身份认证 | |
| SAML 2.0 | 安全断言:使用XML格式的"断言"在身份提供者与服务提供者间传递认证信息,复杂但强大。 | Spring Security SAML, OpenSAML | 企业级SSO(尤其在传统、金融领域)、跨安全域联合身份 | |
| 基于集中式Session | CAS(中央认证服务) | 票据交换:引入独立的认证中心,通过签发和验证服务票据来建立信任。 | Apereo CAS, 自研认证中心 | 传统企业内网、多系统(尤其Web)统一登录 |
| Session复制/共享 | 状态共享:将Session存储到所有集群节点或一个集中的缓存(如Redis)中。 | Spring Session + Redis | 集群内应用简单共享登录状态 | |
| 基于代理/网关 | 反向代理统一认证 | 网关拦截:在统一的网关/反向代理层(如Nginx)拦截请求并完成身份验证,再将用户信息注入到下游应用。 | Nginx + Lua, Spring Cloud Gateway | 微服务入口统一认证、遗留系统改造 |
银行如何选择?
银行选择协议的根本原则是:安全 > 标准 > 可控 > 生态 > 性能。
协议的对比:
| 协议 | 核心特点 | 银行选择场景与理由 | 白话比喻 |
|---|---|---|---|
| SAML 2.0 | XML安全断言,极其严谨,协议复杂,配置繁琐。 | 核心交易系统、对接监管报送。理由:1. 安全性极高(XML数字签名,防篡改)。2. 标准化程度最高,满足严格审计。3. 与银行已有的Active Directory(AD) 集成无缝。 | 像"外交照会":格式严谨、盖章齐全、通过专门信使传递,虽然慢,但代表官方,不容置疑。 |
| OIDC | 基于OAuth 2.0的身份层,使用轻量的JWT令牌。 | 新型互联网银行、手机银行、内部管理系统。理由:1. 现代,对移动端友好。2. 协议比SAML简单,易于开发和调试。3. JWT令牌自包含,便于在微服务间传递用户信息。 | 像"电子身份证二维码":用手机亮码,对方一扫就能读出你的姓名、单位,验证真伪也快,适合现代生活场景。 |
| CAS | 简单票据交换,概念清晰,但功能相对单一。 | 简单的内部Web门户。理由:银行较少作为首选,因其在联合身份、属性传递等方面不如SAML/OIDC强大,更常见于高校、传统企业内部。 | 像"游乐园手环":进门验票后给你戴个手环,园内各个项目看到手环就让你玩,但手环里不存你的详细信息。 |
银行真实决策链:
如果对接的系统(如人行征信)强制要求SAML,则用SAML。
如果是银行自主建设的新一代数字平台,优先选择OIDC。
如果你的外包项目接到的是"按行内OIDC规范集成"的任务,那这就是你要用的。
四、SSO核心流程
下图可想象成以下 六步流程:从首次登录到"一次登录,处处通行"

想象一下,你第一次去一个大型政务中心,需要在总服务台办一张"通用访客卡"(这就是SSO)。
第一步:走向第一个办事窗口(访问应用A)
你走进大厅,直接去A窗口办社保。柜员(应用A)说:"请出示您的访客卡。"你没有卡。
第二步:被指引到总服务台(重定向到认证中心)
柜员说:"请先去那边总服务台办卡。"并给你一张指引条,上面写着"办完卡请回A窗口"。
第三步:在总服务台验证身份(集中认证)
你到总服务台(认证中心)。工作人员要求你出示身份证+人脸识别(双因素认证)。验证通过后,他在你的浏览器里设置了一个你看不见的"已认证"标记(HttpOnly Cookie),并给你一张只能用于A窗口的临时小票(Service Ticket)。
第四步:返回A窗口办业务(验证票据,建立本地会话)
你拿着小票回到A窗口。柜员用内部电话联系总服务台:"我收到一张小票XXX,是真的吗?"总台确认后,柜员为你建立一份A窗口专属的档案(本地Session),开始办理社保。
第五步:去B窗口办公积金(触发SSO)
办完社保,你直接去B窗口办公积金。B窗口柜员同样说:"请出示访客卡。"你没有B窗口的卡。
第六步:无感登录B窗口(SSO精髓)
你再次被指引到总服务台。总台工作人员看到你浏览器里"已认证"的标记,瞬间明白:"这人刚才办过卡了,不用再验身份证了。"于是直接给你一张专门用于B窗口的新小票。你拿着新小票回到B窗口,B柜员同样电话核实后,为你建立B窗口专属档案。全程无需第二次出示身份证。
流程核心:关键在于第五、六步------认证中心通过浏览器Cookie发现你已登录,自动跳过登录页面,为新应用颁发独立票据。
五、协议对应的代码流程(请指正)
一、CAS(Central Authentication Service)
1、核心思想
CAS 是一种 票据(Ticket)驱动 的 SSO 协议。用户首次访问任一服务(Service)时,被重定向到 CAS Server 登录;登录成功后,CAS Server 返回 Service Ticket (ST),服务端用 ST 向 CAS Server 验证用户身份。
java
CAS 不依赖 JWT,通常使用 HTTP Session 存储登录状态,但可集成 Redis 实现分布式 Session
2、 用户从 A → B 的 SSO 流程(代码视角)
- 用户访问 A 系统(未登录) → 被重定向到 CAS Server 的 /login。
- 用户在 CAS Server 输入账号密码认证。
- CAS Server 生成 TGT(Ticket Granting Ticket) 存入服务端 Session(或 Redis),并重定向回 A 系统,附带 ST(Service Ticket)。
- A 系统后台用 ST 向 CAS Server 的 /serviceValidate 发起校验。
- 校验成功后,A 系统创建本地 Session(HttpSession 或 Redis),用户登录成功。
- 用户访问 B 系统(未登录)→ B 重定向到 CAS Server。
- CAS Server 检查 TGT 存在(已登录)→ 直接颁发新 ST 给 B。
- B 后台校验 ST 成功 → 创建本地 Session → SSO 完成。
java
# application.yml
cas:
server-url-prefix: https://cas.example.com/cas
server-login-url: ${cas.server-url-prefix}/login
client-host-url: https://a.example.com
java
// CasSecurityConfig.java
@Configuration
@EnableWebSecurity
public class CasSecurityConfig {
@Value("${cas.server-url-prefix}")
private String casServerUrlPrefix;
@Value("${cas.client-host-url}")
private String clientHostUrl;
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties sp = new ServiceProperties();
sp.setService(clientHostUrl + "/login/cas"); // CAS回调地址
sp.setSendRenew(false);
return sp;
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(casTicketValidator()); // 校验ST
provider.setUserDetailsService(userDetailsService());
provider.getKey(); // 设置唯一key
return provider;
}
@Bean
public TicketValidator casTicketValidator() {
return new Cas30ServiceTicketValidator(casServerUrlPrefix);
}
}
🧠 是否使用 HttpSession / Redis?
- 默认使用 HttpSession:CAS Client 在验证 ST 成功后,会将 CasAuthenticationToken 存入 HttpSession。
- 若需集群共享 Session → 改用 Redis:
Redis 配置(Spring Session + Redis)
java
# application.yml
spring:
session:
store-type: redis
redis:
host: localhost
port: 6379
timeout: 2000ms
java
// 启用 Redis Session
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class HttpSessionConfig {}
此时所有 HttpSession.setAttribute() 实际写入 Redis,Key 格式:spring:session:sessions:
无需自定义 Redis 类,Spring Session 自动代理 Session 操作。
| 类型 | 类名 | 作用 |
|---|---|---|
| 架构类 | RedisIndexedSessionRepository | Spring Session 提供,负责 Session 的 CRUD 到 Redis |
| 架构类 | RedisOperationsSessionRepository | 底层操作 RedisTemplate |
| 业务类 | 无 | 业务代码只需用 HttpSession,不直接操作 Redis |
- CAS 协议 不使用 JWT。
二、OAuth 2.0(Authorization Code Flow,用于 SSO 场景)
注:纯 OAuth 2.0 是授权协议,但常被"误用"做 SSO(如 GitHub 登录)。严格 SSO 应用 OIDC。
🧭 用户流程(A → 登录 → B)
- 用户访问 A → 重定向到 Authorization Server(如 Auth0、Keycloak)的 /oauth/authorize。
- 用户登录并授权。
- Auth Server 重定向回 A,附带 code。
- A 后台用 code + client_id + client_secret 换取 access_token。
- A 用 access_token 获取用户信息(如 /userinfo)→ 创建本地 Session。
- 用户访问 B → B 重定向到 Auth Server。
- Auth Server 发现用户已登录(通过 Cookie)→ 直接返回 code 给 B。
- B 换 token → 获取用户信息 → 创建本地 Session → SSO 成功。
java
# application.yml
spring:
security:
oauth2:
client:
registration:
my-auth-server:
client-id: a-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/my-auth-server"
scope: openid,profile,email
provider:
my-auth-server:
issuer-uri: https://auth.example.com/realms/myrealm
java
// 不需要额外配置类,Spring Boot AutoConfig 自动生效
// 登录后可通过 SecurityContextHolder.getContext().getAuthentication() 获取 OAuth2User
🧠 是否使用 HttpSession / Redis?
- 默认使用 HttpSession 存储 OAuth2AuthorizedClient 和 OAuth2AuthenticationToken。
- 集群部署 → 启用 Redis Session(同 CAS 配置):
java
@EnableRedisHttpSession
public class HttpSessionConfig {}
📌 Redis 使用说明
- 同 CAS,无需自定义 Redis 类。
- 所有 Session 数据自动序列化到 Redis。
📌 JWT 使用?
- OAuth 2.0 本身不强制 JWT,但多数 Auth Server(如 Keycloak)返回的 access_token 是 JWT。
- A/B 系统通常不解析 JWT,仅将其作为 opaque token 传给资源服务器。
- 若需解析 JWT(如自建 Auth Server),可配置:
java
// 解析 UserInfo 响应中的 JWT(非必须)
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/.../jwks").build();
}
但标准 OAuth 2.0 SSO 流程中,客户端不处理 JWT 内容,只存 token。
三、OpenID Connect(OIDC)
🧭 用户流程(A → 登录 → B)
- 用户访问 A → 重定向到 OP(OpenID Provider)的 /authorize,请求 scope=openid。
- 用户登录。
- OP 返回 code + id_token(可选)到 A。
- A 用 code 换取 access_token + id_token(JWT)。
- A 验证并解析 id_token → 获取用户身份 → 创建本地 Session。
- 用户访问 B → B 重定向到 OP。
- OP 发现已登录 → 返回 code(静默登录)。
- B 换 token + id_token → 验证 → 创建 Session → SSO 成功。
java
# application.yml
spring:
security:
oauth2:
client:
registration:
oidc-client:
client-id: a-client
client-secret: secret
scope: openid,profile,email
provider:
oidc-client:
issuer-uri: https://auth.example.com/realms/myrealm
Spring Security 自动处理 id_token 验证(签名、过期、audience)。
🧠 HttpSession / Redis?
-
同 OAuth 2.0,默认用 HttpSession。
-
集群 → @EnableRedisHttpSession。
📌 JWT 使用(核心!)
-
id_token 是 JWT,包含
{ "sub": "user123", "iss": "https://auth.example.com", "aud": "a-client", "exp": 1700000000, "email": "user@example.com" } -
Spring Security 自动验证 JWT 签名(通过 JWK Set)
java
OidcUser user = (OidcUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String email = user.getEmail();
Jwt idToken = user.getIdToken(); // 可获取原始 JWT
🔐 JWT 验证配置(自动)
- issuer-uri 会自动加载 .well-known/openid-configuration → 获取 jwks_uri。
- 无需手动配置 JWT Decoder。
四、SAML 2.0
🧭 用户流程(A → 登录 → B)
- 用户访问 A → A 生成 SAML AuthnRequest → 重定向到 IdP(如 Okta、ADFS)。
- 用户在 IdP 登录。
- IdP 生成 SAML Response(含用户属性)→ POST 回 A 的 ACS(Assertion Consumer Service)。
- A 验证 SAML 签名、有效期 → 创建本地 Session。
- 用户访问 B → B 生成 AuthnRequest → 重定向到 IdP。
- IdP 发现已登录 → 直接 POST SAML Response 给 B。
- B 验证 → 创建 Session → SSO 成功。
关键配置(Spring Security SAML 2.0)
使用 spring-security-saml2-service-provider
java
# application.yml
spring:
security:
saml2:
relyingparty:
registration:
my-idp:
identityprovider:
entity-id: https://idp.example.com/idp/shibboleth
sso-url: https://idp.example.com/idp/profile/SAML2/Redirect/SSO
verification:
credentials:
- certificate-location: "classpath:idp.crt"
assertingparty:
entity-id: https://a.example.com/saml2/service-provider-metadata
java
// SAML2LoginConfigurer 自动配置
@Configuration
@EnableWebSecurity
public class SamlSecurityConfig {
// 无需额外 Bean,AutoConfig 生效
}
🧠 HttpSession / Redis?
- 默认使用 HttpSession 存储 Saml2AuthenticatedPrincipal。
- 集群 → @EnableRedisHttpSession。
📌 JWT 使用?
- SAML 不使用 JWT。SAML Token 是 XML 格式。
- 所有断言(Assertion)为 Base64 编码的 XML。
🔐 证书说明
- idp.crt:IdP 的公钥证书,用于验证 SAML Response 签名。
- SP(A/B 系统)需提供自己的 metadata(含公钥)给 IdP。
总结
| 协议 | 是否用 HttpSession | 是否支持 Redis Session | 是否用 JWT | Token 类型 |
|---|---|---|---|---|
| CAS | ✔️ | ✔️(Spring Session) | ❌ | ST(字符串) |
| OAuth 2.0 | ✔️ | ✔️ | ⚠️(access_token 可能是 JWT,但客户端不解析) | Opaque / JWT |
| OIDC | ✔️ | ✔️ | ✔️(id_token 必为 JWT) | JWT |
| SAML | ✔️ | ✔️ | ❌ | XML(Base64) |