【java中的SSO】

一、定义

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)
相关推荐
代码不停2 小时前
JVM基础知识
java·jvm·java-ee
白露与泡影2 小时前
60亿消息表如何分库分表?
java·开发语言·面试
纵有疾風起2 小时前
【C++—STL】哈希表底层封装与unorderedset/unorderedmap模拟实现
开发语言·数据结构·c++·stl·哈希算法·散列表
FAFU_kyp2 小时前
银行技术岗位招聘面试题准备
java·spring boot·spring
zhangyifang_0092 小时前
Spring中的BeanDefinition
java·后端·spring
不会代码的小猴2 小时前
C++的第十四天笔记
java·开发语言
Heavydrink2 小时前
Java项目部署云服务器详细教程
java·服务器·开发语言
milanyangbo2 小时前
深入解析 Disruptor:从RingBuffer到缓存行填充的底层魔法
java·数据库·后端·架构
yaoxin5211232 小时前
266. Java 集合 - ArrayList vs LinkedList 内存使用深度剖析
java·开发语言