简介:这是一份面向Java Web开发者与系统架构学习者的SSO单点登录实战项目源码,围绕Servlet环境下如何实现一次登录、多系统互信访问这一核心问题展开。资源共80个文件,压缩包约103KB,以xml配置、java源码、jsp页面、properties属性文件及class编译文件为主,另含少量工程配置与依赖描述文件,完整呈现了sso-server、sso-oa、sso-pro三个模块的工程结构。项目通过自定义Filter拦截受保护URL、Session共享认证信息、Token传递与校验等环节,演示了认证中心、Service Ticket换取与服务验证的完整链路,并预留了集成CAS、OAuth2等开源框架的扩展思路。目前已有146人学习,适合希望理解SSO底层机制、动手调试过滤器与登录流程的开发者参考,可据此梳理多系统统一认证的工程组织方式与关键代码位置。
1. 从一次登录超时说起:这套 SSO 单点登录资源到底能解决什么
上周排查一个后台系统的登录问题,运维反馈用户在一个子系统登录后,切到另一个子系统又要求重新输入密码,一天下来光登录就耗掉不少时间。这不是个例,而是多系统各自维护会话的典型症状。这套 SSO 单点登录资源,核心就是解决「一次登录、多系统通行」的问题:用户在一处完成身份认证,其他受信系统直接认可这个身份,不再重复弹登录框。它适合手里有多个 Web 子系统、正在被会话同步和重复认证折磨的后端与全栈开发者,也适合想搞懂 CAS 协议落地细节、准备自己搭一套认证中心的人。资源本身围绕认证中心、票据校验、会话管理这几块展开,不是纯理论文档,而是能跑起来的工程结构。
2. SSO 的认证原理与资源结构拆解
2.1 为什么是 CAS 协议而不是共享 Cookie
多系统单点登录最容易想到的方案是共享 Cookie,把会话写到同一个顶级域名下。这个思路在子系统同域时能跑通,但一旦子系统分散在不同域名,Cookie 就跨不过去,浏览器同源策略会直接拦掉。另一个常见方案是共享 Session,把会话集中存到 Redis,各系统都去读同一个 Session。这个方案能解决跨域,但耦合太重:所有系统必须共用同一套 Session 结构和序列化方式,任何一个系统改了 Session 字段,其他系统都可能翻车。
CAS 协议走的是另一条路:认证中心只负责认证,认证成功后发一张票据,子系统拿票据去认证中心校验,校验通过后自己在本地建立会话。子系统之间不共享会话,只共享「认证中心」这个信任源。这样做的好处是解耦彻底,每个子系统可以有自己的会话实现,认证中心挂了也只影响新登录,不影响已登录用户的本地会话。这套资源选的就是 CAS 思路,认证中心和子系统之间通过票据通信,票据是一次性的,校验完立即失效,避免重放。
2.2 资源目录里各模块的职责
拿到资源后先别急着跑,把目录结构看一遍能省很多事。常见做法是分成认证中心服务端、客户端接入 SDK、示例子系统和配置脚本四块。认证中心服务端负责登录页、票据签发和票据校验接口;客户端 SDK 是给子系统引入的依赖,封装了跳转认证中心、接收票据、校验票据、建立本地会话这几步;示例子系统用来演示接入过程,一般会放两个,方便验证单点效果;配置脚本里是数据库初始化和证书生成相关的命令。
这里有个容易忽略的点:认证中心的票据存储默认走内存,重启就丢。生产环境必须换成 Redis 或数据库,否则认证中心一重启,所有未过期的票据全部失效,用户会被强制重新登录。资源里一般会留一个存储层的接口,改配置就能切换,但默认配置往往是内存,第一次部署时特别容易踩这个坑。
2.3 票据签发与校验的完整链路
把链路走一遍,后面排查问题会快很多。用户访问子系统 A 的受保护页面,子系统 A 发现本地没有会话,于是把用户重定向到认证中心,带上自己的服务地址作为参数。认证中心检查用户是否已有全局会话,没有就展示登录页;用户提交账号密码,认证中心验证通过后建立全局会话,同时生成一张服务票据,重定向回子系统 A 并带上这张票据。子系统 A 拿到票据后,在后台向认证中心发起校验请求,认证中心确认票据有效且对应服务地址匹配,返回用户身份信息。子系统 A 据此建立本地会话,用户正常访问。
用户再访问子系统 B 时,子系统 B 同样发现没有本地会话,重定向到认证中心。这次认证中心发现用户已有全局会话,不再要求登录,直接签发一张针对子系统 B 的服务票据,重定向回去。子系统 B 校验票据后建立本地会话。整个过程用户只输入了一次密码。理解这条链路的关键是分清两种会话:全局会话存在认证中心,本地会话存在各子系统,票据是两者之间的桥梁。
# 认证中心校验票据的请求示例(常见做法) curl -X POST "https://sso-center.example.com/ticket/validate" \ -d "ticket=ST-1234-abcdef" \ -d "service=https://app-a.example.com/callback" # 返回示例:用户名、过期时间、属性信息这段请求里,ticket 是子系统收到的服务票据,service 是子系统自己的回调地址。认证中心会校验票据是否存在、是否过期、绑定的 service 是否与请求中的一致。三个条件有一个不满足就返回失败。service 参数必须和签发时绑定的地址完全一致,包括协议和端口,少一个斜杠都可能校验失败,这是接入时最常见的翻车点之一。
3. 从零跑通认证中心与子系统接入
3.1 认证中心服务端的启动与关键配置
先把认证中心跑起来。资源里一般会提供启动脚本或主类,依赖装好后直接启动即可。启动前要确认三处配置:服务端口、票据存储方式、登录账号来源。端口默认常见是 8080 或 8443,如果和本机其他服务冲突要改。票据存储默认内存,本地验证可以先不动,但要清楚重启会丢票据。登录账号来源可能是写死的测试账号,也可能连数据库,连数据库的话要先执行初始化脚本建表并插入测试用户。
# 初始化数据库(以 MySQL 为例,脚本名以资源实际为准) mysql -u root -p sso_db < init_schema.sql mysql -u root -p sso_db < init_data.sql # 启动认证中心 java -jar sso-server.jar --server.port=8443 \ --sso.ticket.store=redis \ --spring.redis.host=127.0.0.1参数说明:server.port是认证中心监听端口,sso.ticket.store指定票据存储类型,改成 redis 后票据就能跨重启保留。spring.redis.host是 Redis 地址。如果本地没装 Redis,先保持内存模式跑通流程,再换存储。启动后访问认证中心的登录页,能看到登录框就说明服务端起来了。看不到的话先查端口占用和日志里的数据库连接异常,这两类问题占启动失败的大多数。
3.2 子系统接入 SDK 的依赖与过滤器配置
子系统接入的核心是引入客户端 SDK 并配置过滤器。过滤器的作用是拦截受保护请求,检查本地会话,没有就跳认证中心。SDK 一般以依赖形式引入,配置里填认证中心的校验地址、子系统自己的服务地址、以及登录成功后的回调路径。服务地址必须和认证中心那边登记的完全一致,否则票据校验会失败。
<!-- 子系统 pom 中引入客户端依赖(坐标以资源实际为准) --> <dependency> <groupId>com.example.sso</groupId> <artifactId>sso-client</artifactId> <version>1.0.0</version> </dependency># 子系统配置文件 sso.server.url=https://sso-center.example.com sso.client.service=https://app-a.example.com/callback sso.client.logout.url=https://sso-center.example.com/logoutsso.server.url指向认证中心,sso.client.service是子系统自己的回调地址,这个值会作为 service 参数参与票据校验。sso.client.logout.url用于单点登出,用户在一个子系统登出后,认证中心会通知其他子系统清理本地会话。配置完成后启动子系统,访问受保护页面,应该会被重定向到认证中心登录页。如果重定向后报 service 不匹配,回头核对这个地址,协议、域名、端口、路径四段都要对上。
3.3 单点登出的联动处理
单点登出比单点登录更容易被忽略。用户在一个子系统点了登出,如果只清理了本地会话,认证中心的全局会话还在,那么用户再访问其他子系统时会被自动重新登录,看起来就像登出没生效。正确的做法是子系统登出时先重定向到认证中心的登出地址,认证中心清理全局会话后,再通知所有已登录子系统清理各自的本地会话。
通知方式常见有两种:一种是认证中心维护已登录子系统列表,逐个发登出通知;另一种是子系统在下次请求时主动向认证中心确认全局会话是否还在。前者实时性好但实现复杂,后者简单但有延迟。这套资源一般用前者,配置里那个logout.url就是干这个的。测试时开两个子系统分别登录,在其中一个登出,然后刷新另一个,如果还处于登录态,说明登出通知没生效,先查认证中心的已登录子系统列表里有没有另一个子系统的地址。
4. 票据校验失败与跨域问题的排查
4.1 票据校验失败的几类典型现象
票据校验失败是接入阶段最高频的问题,现象都是子系统报校验不通过,但原因分好几种。第一种是票据过期,默认有效期常见是几分钟,如果子系统拿到票据后没有及时校验,或者服务器时间不同步,就会过期。第二种是票据已被使用,CAS 票据是一次性的,同一张票据校验两次,第二次必然失败,这种情况多出现在页面刷新或重复提交。第三种是 service 不匹配,前面提过,地址差一点都过不了。第四种是认证中心重启导致内存里的票据丢失,现象是所有未过期票据同时失效。
排查时先看认证中心返回的具体错误码,不同错误码对应不同原因,比盲目改配置快得多。时间不同步的问题容易被忽略,尤其是认证中心和子系统部署在不同机器上时,建议都开 NTP 同步。票据一次性这个特性在调试时很烦,每次都要重新走登录流程,可以在测试环境临时把票据标记为可重复校验,但生产环境千万别这么干。
4.2 跨域与 Cookie 写入的坑
子系统如果和认证中心不同域,浏览器在重定向回来时可能带不上 Cookie,导致本地会话建立失败。常见原因是 Cookie 的 SameSite 属性设置过严,或者域名作用域没配对。如果子系统是前后端分离的,前端发请求时还要注意带上凭证,否则后端拿不到会话 Cookie。
// 前端请求携带凭证(常见做法) fetch('https://app-a.example.com/api/user', { method: 'GET', credentials: 'include' // 关键:允许跨域携带 Cookie })credentials: 'include'表示请求携带 Cookie,缺了这行,跨域场景下后端收不到会话。同时后端要设置Access-Control-Allow-Credentials: true,并且Access-Control-Allow-Origin不能是通配符,必须是具体域名。这两处配好,跨域会话才能正常建立。如果还是不行,打开浏览器开发者工具看网络请求里 Cookie 有没有发出去,以及响应头里的 CORS 配置,比猜快。
4.3 会话串号与并发登录的处理
会话串号是个隐蔽的坑:用户 A 登录后,用户 B 在同一浏览器登录,结果 A 的会话被覆盖或者两人看到对方的数据。这通常是因为本地会话的 key 设计有问题,比如用了固定 key 而不是和用户绑定。另一个相关问题是并发登录控制,有些系统要求同一账号只能在一个地方登录,后登录的踢掉先登录的。这个逻辑要在认证中心做,登录时检查该账号是否已有全局会话,有就按策略处理。
处理并发登录时要注意,踢掉旧会话不能只清认证中心的全局会话,还要通知旧会话所在的子系统清理本地会话,否则旧会话在本地还有效,直到下次请求认证中心才发现全局会话没了。这套资源一般会提供并发登录的配置开关,默认可能是允许多处登录,需要限制的话在配置里打开,并确认登出通知链路是通的。
5. 生产环境下的票据存储与安全加固
5.1 把票据从内存迁到 Redis
本地跑通后,第一件要改的就是票据存储。内存存储重启即丢,生产环境不可接受。迁到 Redis 需要改两处:认证中心的存储配置,以及 Redis 的连接信息。改完后重启认证中心,登录一次,然后去 Redis 里看有没有对应的票据 key,有就说明迁移成功。Redis 要做持久化,否则 Redis 重启一样丢票据,虽然比认证中心重启影响小,但能避免就避免。
# 查看 Redis 中的票据 key(常见命名前缀) redis-cli keys "sso:ticket:*" # 查看某个票据的过期时间 redis-cli ttl "sso:ticket:ST-1234-abcdef"票据的过期时间要和认证中心配置的有效期一致,Redis 的 ttl 到点自动删除,省去手动清理。如果发现 ttl 是 -1,说明写入时没设过期时间,票据会一直留着,既占内存又有安全风险,要回去检查存储层的写入逻辑。
5.2 票据有效期与时钟同步的参数取舍
票据有效期设多长是个取舍。设太短,用户网络慢一点票据就过期了,体验差;设太长,票据被截获后的风险窗口就大。常见做法是服务票据有效期设几分钟,全局会话有效期设几小时,两者分开控制。全局会话过期后用户需要重新登录,服务票据过期只影响单次校验,用户重新走一次跳转即可,通常无感。
时钟同步前面提过,这里再强调一次:认证中心和所有子系统的时间差要控制在票据有效期的十分之一以内,否则会出现票据刚签发就过期,或者已过期的票据还能校验通过。部署时统一走 NTP,别依赖机器本地时间。
5.3 登录凭证传输与票据防重放
登录页必须走 HTTPS,否则账号密码在传输过程中就是明文。票据本身也要防重放,CAS 的一次性票据机制已经能挡住大部分重放,但要注意校验接口本身也要走 HTTPS,并且校验请求最好带签名或走内网,避免票据在校验途中被截获。如果子系统和认证中心之间走公网,建议加一层双向认证或者至少校验来源 IP。
另外,票据里不要放敏感信息,比如密码、身份证号。票据只是身份凭证,具体用户信息应该在校验通过后由认证中心返回,或者子系统拿用户标识去查。票据被截获最多导致一次未授权访问,如果里面还带了敏感数据,损失就大了。
6. 用压测和日志验证 SSO 的稳定性
6.1 用脚本模拟多用户并发登录
功能跑通不代表稳定,得压一压。写个脚本模拟多用户并发登录和票据校验,观察认证中心的响应时间和错误率。重点看两个指标:登录接口的 P99 延迟,以及票据校验接口在并发下的失败率。失败率如果随并发上升,多半是票据存储扛不住,比如内存存储在高并发下锁竞争严重,换 Redis 后通常能缓解。
import requests import concurrent.futures def login_and_validate(user): session = requests.Session() # 登录获取全局会话 session.post("https://sso-center.example.com/login", data={"username": user, "password": "test123"}) # 访问子系统触发票据校验 resp = session.get("https://app-a.example.com/protected") return resp.status_code users = [f"user{i}" for i in range(100)] with concurrent.futures.ThreadPoolExecutor(max_workers=20) as pool: results = list(pool.map(login_and_validate, users)) print(f"成功: {results.count(200)}, 失败: {len(results) - results.count(200)}")这段脚本用线程池模拟 100 个用户、20 并发,每个用户走一遍登录和校验。max_workers控制并发度,调大能压出更高负载。跑完看失败数,如果失败集中在某几个用户,可能是账号问题;如果随机分布,多半是服务端瓶颈。压测时记得用测试账号,别拿生产账号跑。
6.2 从日志里定位票据生命周期问题
认证中心的日志要开详细级别,把票据签发、校验、过期、登出都记下来,带上票据 ID 和时间戳。出问题时按票据 ID 搜一遍,就能看到这张票据从生到死的完整轨迹。常见异常轨迹有两种:一种是签发后迟迟没有校验记录,说明子系统没发起校验,查子系统的过滤器配置;另一种是校验失败但票据没过期,查 service 匹配和票据是否已被使用。
日志里还要关注全局会话的创建和销毁,以及登出通知的发送记录。如果用户反馈登出后还能访问,先看登出通知有没有发出去,再看子系统有没有收到并处理。日志级别别一直开 debug,生产环境用 info 级别记录关键事件即可,debug 只在排查时临时开。
6.3 我踩过的那个坑
有次上线后用户反馈,登录后过一段时间操作就跳登录页,但重新登录又正常。查了半天发现是全局会话的有效期设得比本地会话短,本地会话还在,全局会话已经过期,用户操作时子系统去认证中心确认全局会话,发现没了就跳登录。把两个有效期对齐,全局会话略长于本地会话,问题就没了。从那以后我每次配 SSO 都强制走一遍有效期对齐检查,先确认全局会话和本地会话的过期时间关系,再上线。希望帮到你。
本文还有配套的精品资源,点击获取