ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Sa-Token SSO 单点登录集成常见问题排查手册:从 ticket 失效到多套 SSO 共存

2026/9/14 5:55:15 拓冰建站 浏览量
Sa-Token SSO 单点登录集成常见问题排查手册:从 ticket 失效到多套 SSO 共存 Sa-Token SSO 单点登录集成常见问题排查手册从 ticket 失效到多套 SSO 共存【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token本文基于 Sa-Token 官方文档《SSO 整合常见问题总结》展开覆盖 SSO 模式一/二/三集成中的高频故障场景not handle响应、Ticket 无效、JSON 反序列化失败、Cookie 域配置异常、自动登录设计以及“单项目多套 SSO 共存”等进阶改造问题。读完本篇你可以针对每一类故障建立“现象 → 根因 → 源码级验证 → 解决方案”的完整排查链路并掌握server-url配置简化、SaSsoServerTemplate自定义等官方推荐的扩展手段。一、三种 SSO 模式与本篇问题的适用边界Sa-Token 的 SSO 模块提供三种对接模式详见 模式一文档、模式二文档、模式三文档模式一Client 与 Server 共用同一个 Cookie 域顶级域名通过sa-token.cookie.domain实现 Cookie 共享无需 ticket 交互模式二Client 与 Server 必须连接同一个 RedisClient 通过直连 Redis 校验 Server 下发的 ticket模式三Client 通过HTTP 请求调用 Server 开放接口校验 ticket两者无需共享 Redis适合前后端分离、跨语言场景。本手册的绝大多数问题都发生在“模式选型 配置组合”的交叉点上排查前请先确认自己处于哪种模式。二、Client 端必须用 Alone-Redis 插件访问 Redis 吗答不必须只是推荐。在模式一与模式二下Client 端直连 Redis 是推荐做法而非强制要求。推荐使用 Alone-Redis 插件的原因有两个权限缓存与业务缓存分离SSO 的 ticket 数据、会话数据写入独立的 Redis 实例减少 SSO-Redis 的访问压力避免多端冲突多个 Client 端如果各自使用业务 Redis 存储会话缓存读写容易相互冲突。从源码结构看sa-token-alone-redis、sa-token-alone-redisson等独立 Redis 插件位于 sa-token-plugin 目录下它们的作用是为 SSO 相关数据提供独立的数据访问通道与模式二“Client/Server 共 Redis”的约束互不矛盾。三、访问返回{msg: not handle}怎么办搭建好 sso-server 或 sso-client 后如果访问返回{msg: not handle}说明请求根本没有被 SSO 框架识别。这个响应串在源码中是一个硬编码常量定义在 SaSsoConsts.java#L45-L46/** 表示请求没有得到任何有效处理 {msg: not handle} */ public static final String NOT_HANDLE {\msg\: \not handle\};Client 端的请求分发逻辑位于 SaSsoClientProcessor.dister()#L64-L95请求只会被路由到/sso/login、/sso/logout、/sso/pushC、/sso/logoutCall四个路径全部不匹配时直接返回NOT_HANDLE。Server 端的SaSsoServerProcessor.dister()采用同样的分发机制。排查思路分两层第一层地址写错了。例如统一认证登录地址是http://{host}:{port}/sso/auth而你访问的却是http://{host}:{port}/sso/auth2。地址写错框架不会处理这个请求直接返回{msg: not handle}。所有开放地址可参考 SSO 开放接口文档。第二层地址没错但对应接口没有打开。典型例子sso-server 端的单点注销地址http://{host}:{port}/sso/signoutsso-client 端的注销地址http://{host}:{port}/sso/logout这些地址都需要在配置文件中配置sa-token.sso-server.is-slotrueclient 端为sa-token.sso-client.is-slotrue后才会打开。源码中可以印证这一点SaSsoClientProcessor的ssoLogout()方法开头即有if(cfg.getIsSlo())判断isSlo为 false 时直接落入return SaSsoConsts.NOT_HANDLE分支同理ssoLogoutCall()路由还要求regLogoutCall为 true。补充说明当前源码中 SaSsoClientConfig.java#L86-L88 里 client 端isSlo字段默认初始化为true但 server 端/sso/signout地址的开放仍以sa-token.sso-server.is-slotrue配置为准排查时应逐一核对两端配置。四、一直提示“Ticket 无效”如何排查这是 SSO 集成中最高频的问题。官方文档给出的核心结论是如果是模式二出现此异常概率最大的原因是Client 与 Server 没有连接同一个 Redis。模式二中两者必须连接同一个 Redis 才可以登录成功——因为 Client 是直连 Redis 读取 Server 写入的 ticket 数据。从源码看这条链路在 SaSsoClientProcessor.java#L298-L309 中非常清晰// 两种模式 // isHttptrue模式三使用 http 请求从认证中心校验ticket // isHttpfalse模式二直连 redis 中校验 ticket if(cfg.getIsHttp()) { return _checkTicketByHttp(ticket, currUri); } else { return _checkTicketByRedis(ticket); }模式二走_checkTicketByRedis()内部直接调用SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket, ...)从 Redis 读取并删除ticket见 SaSsoClientProcessor.java#L368-L392。如果 Client 连的 Redis 里没有这条数据checkTicket就会抛出无效 ticket异常错误码CODE_30004见 SaSsoServerTemplate.java#L177-L192。你可能会问我看配置文件明明是同一个啊官方建议排查时不要仅凭肉眼判断分别在 Client 与 Server 启动后调用SaManager.getSaTokenDao().set(name, value, 100000);随便写入一个值看看能不能根据你的预期写进同一个 Redis 里如果能才能证明 Client 与 Server 连接的 Redis 是同一个再进行下一步排查SpringBootApplication public class SaSsoServerApplication { public static void main(String[] args) { SpringApplication.run(SaSsoServerApplication.class, args); System.out.println(\n------ Sa-Token-SSO 统一认证中心启动成功 ); // 分别在 Client 与 Server 启动后调用 set 数据代码看看能否根据预期写入同一个 redis SaManager.getSaTokenDao().set(name, value, 100000); } }如果是模式三则排查是否有重复校验 ticket 的代码一个 ticket 码只能使用一次多次重复使用就会提示 ticket 无效。这一点同样有源码依据——SaSsoServerTemplate.checkTicketParamAndDelete()的注释明确写着“如果此 ticket 是有效的则立即删除”ticket 在第一次校验后即被消费。五、模式二/三报错No serializer found for class ... SysUser报错信息形如Could not write JSON: No serializer found for class com.pj.sso.SysUser and no properties discovered to create BeanSerializer根因一般是因为在 sso-server 端往 session 上写入了某个实体类比如 User而在 sso-client 端没有这个实体类导致反序列化失败。解决方案在 sso-client 也新建这个类而且包名需要与 sso-server 端的一致直接从 sso-server 把实体类复制过来就好了。序列化按全限定类名定位目标类包名不一致时 Client 端会解析失败这正是报错中com.pj.sso.SysUser这类全限定名的来源。六、测试模式一时出现难以理解的现象测试模式一时官方文档总结了三种典型异常现象及对应根因排查时请对号入座现象 1在 sso-client 端点击登录可以成功跳转到 sso-server 端登录后可以跳回 sso-client 端但显示 sso-client 端未登录原因sso-server 后端没有配置sa-token.cookie.domain值。现象 2在 sso-client 端点击登录可以成功跳转到 sso-server 端登录页面刷新一下并没有跳转回 sso-client 端依然提示让你登录可能 1sso-server 后端配置了错误的sa-token.cookie.domain值。可能 2sso-server 后端没有打开sa-token.is-read-cookie值。测试模式二、三时发生这种现象有时候也是因为这个现象 3在 sso-client 端点击登录页面只是闪了一下肉眼没有观察到页面跳转页面也没有显示登录上原因sso-server 的页面存储了不带.的有效 Cookie。为什么会这样常常是因为测试模式二、三后没有清除 Redis 记录或者浏览器记录直接再开始测试模式一模式二、三登录成功后遗留的有效 Cookie 影响了模式一的行为逻辑。解决方案三选一手动清空 Redis 里的所有数据或者手动清空 sso-server 域名下的所有 Cookie换一个新的干净浏览器来测试。经验提示如果本地先后切换过多种模式测试每次切换前建议清理 Redis 与浏览器 Cookie避免跨模式残留状态互相污染。七、模式三配置一堆 xxx-url如何简化可以使用sa-token.sso-client.server-url来简化。配置含义配置 Server 端主机总地址拼接在 authUrl、getDataUrl、signoutUrl、pushUrl 属性前面用以简化各种 url 配置。在开发 SSO 模块时我们需要在 sso-client 配置认证中心的各种地址特别是在模式三下一般代码会变成这样sa-token: sso-client: # SSO-Server端 统一认证地址 auth-url: http://sa-sso-server.com:9000/sso/auth # 单点注销地址 slo-url: http://sa-sso-server.com:9000/sso/signout # SSO-Server端 查询数据地址 get-data-url: http://sa-sso-server.com:9000/sso/getData一堆 xxx-url 配置比较繁琐且含有大量重复字符现在可以将其简化为sa-token: sso-client: server-url: http://sa-sso-server.com:9000只要你配置了server-url地址Sa-Token 就可以自动拼接出其它四个地址例 1使用 server-url 简化你配置的 server-url 值是http://sa-sso-server.com:9000。框架拼接出的 auth-url 值就是http://sa-sso-server.com:9000/sso/auth其它三个 url 配置项同理。例 2使用 server-url auth-url 简化你配置的 server-url 值是http://sa-sso-server.com:9000auth-url 是/sso/auth2。框架拼接出的 auth-url 值就是http://sa-sso-server.com:9000/sso/auth2其它三个 url 配置项同理。例 3auth-url 地址以 http 字符开头你配置的 server-url 值是http://sa-sso-server.com:9000auth-url 是http://my-site.com/sso/auth2。此时框架只以 auth-url 值为准得到的 auth-url 值是http://my-site.com/sso/auth2其它三个 url 配置项同理。源码印证这套拼接逻辑实现在 SaSsoClientConfig.java#L106-L134 的splicingAuthUrl()、splicingGetDataUrl()、splicingSignoutUrl()、splicingPushUrl()四个方法中均委托给SaFoxUtil.spliceTwoUrl(serverUrl, path)完成拼接。同时该配置类给出了四个路径的默认值SaSsoClientConfig.java#L50-L68authUrl/sso/auth、signoutUrl/sso/signout、pushUrl/sso/pushS、getDataUrl/sso/getData——即使不配置 server-url只要 Client 与 Server 部署在同一域名下路径默认值也能直接命中 ApiName.java#L26-L59 中定义的标准路由。八、如何快速分辨一个项目用的是 SSO 模式几接手集成过 Sa-Token SSO 的项目时可以用以下三种方法快速判断模式方法一看代码注释。如果开发这个项目的人没有写清楚注释那就只能靠下面的方法了。方法二根据配置项来分析例如先看配置项sa-token.cookie.domain如果此配置项有值一般是在使用模式一开发否则就是模式二或者模式三再看配置项sa-token.sso-client.is-http如果有值且为 true一般是在使用模式三否则就是模式二。方法三根据约定型配置项sa-token.sso-client.mode的提示来判断。sa-token.sso-client.mode是框架预留的约定型配置项此配置项不对代码逻辑产生任何影响只为系统做一个标记标注此系统用到了 SSO 的哪个模式。源码注释也明确印证了这一点SaSsoClientConfig.java#L35-L38 中mode字段的说明是“指定当前系统集成 SSO 时使用的模式约定型配置项不对代码逻辑产生任何影响”。例如你可以将其配置为sa-token.sso-client.modeclient-2代表当前系统为 sso-client 端使用 SSO 模式二来对接。需要注意这个配置项不是必须的你不写也不会对代码造成任何影响只有在你需要为系统做一个明确的标记时才需要去配置它方便后人阅读代码时快速分析使用的模式。例如我们可以使用以下约定sa-token.sso-client.modeclient-2代表当前系统为 sso-client 端使用 SSO 模式二来对接。sa-token.sso-client.modeclient-2,h5代表当前系统为 sso-client 端使用 SSO 模式二来对接并且是前后端分离模式。sa-token.sso-server.modeserver-123代表当前系统为 sso-server 端同时开放了 SSO 模式一、模式二、模式三。sa-token.sso-server.modeserver-2,client-2代表当前系统既是 sso-server 端又是 sso-client 端使用模式二来对接。等等等等……此配置项可以是任意字符串你也可以自己整理一套合适的表达规则。九、模式二/三第二个 Client 需要点登录按钮才登录上正常吗问SSO 模式二或模式三第一个 client 登录成功之后再访问其它两个 client 不会自动登录需要点一下登录按钮才会登录上答这是正常现象系统 1 登录成功之后系统 2 与系统 3 需要点击登录按钮才会登录成功第一个系统需要点击 [登录] 按钮 → 跳转到登录页 → 输账号密码 → 登录成功第二个系统需要点击 [登录] 按钮 → 登录成功第三个系统需要点击 [登录] 按钮 → 登录成功系统二、三免去重复跳转登录页输入账号密码的步骤追问能否设计成“访问页面即自动登录”可以的。思路很简单只需要给 client 项目加个过滤器拦截所有请求只要检测到未登录就将其重定向至登录页面/** * Sa-Token 配置类 */ Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** 注册 [Sa-Token全局过滤器] */ Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() .addInclude(/**) .addExclude(/sso/*, /favicon.ico) // 这里需要注意排除掉 /sso/* 相关请求不拦截否则就会触发无限重定向 .setAuth(obj - { /* * 这里会被分为两种情况 * 情况1这个请求在当前 client 已经登录此时会顺利进入网站 * 情况2这个请求在当前 client 尚未登录此时会被拦截重定向至当前系统的 /sso/login?back当前地址 * * 情况2会带领着用户继续重定向至 sso-server 认证中心此时又分为两种情况 * 情况2.1此用户在 sso-server 尚未登录此时会停留在登录页面开始输入账号密码进行登录 * 情况2.2此用户在 sso-server 已经登录这证明此用户已经在其它至少一个 sso-client 处完成了登录 * 此时用户会继续重定向回当前 client并携带 ticket 参数完成登录。 */ if(StpUtil.isLogin() false) { String back SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString()); SaHolder.getResponse().redirect(/sso/login?back SaFoxUtil.encodeUrl(back)); SaRouter.back(); } }) ; } }两个细节值得注意/sso/*必须排除拦截否则 SSO 中转请求自身未登录时也会被重定向到/sso/login形成无限重定向这个方案与源码中SaSsoClientProcessor._goServerAuth()的逻辑天然衔接重定向到/sso/login?back...后框架检测到无 ticket 参数会进一步跳转至 sso-server 的/sso/auth若 Server 端会话仍有效则携带一次性 ticket 回跳完成登录见 SaSsoClientProcessor.java#L101-L117。更多登录姿势可以参考 何时引导用户去登录 给出的建议进行设计。十、Client 信息可以做成从数据库读取的吗可以。自定义SaSsoServerTemplate实现类重写getClient与getClients方法即可/** * 重写 SaSsoServerTemplate 部分方法增强功能 */ Component public class CustomSaSsoServerTemplate extends SaSsoServerTemplate { // 获取指定 client 的配置信息 Override public SaSsoClientModel getClient(String client) { if(sso-client1.equals(client)) { SaSsoClientModel scm new SaSsoClientModel(); scm.setAllowUrl(sso-client1); scm.setSecretKey(kQwIOrYvnXmSDkwEiFngrKidMcdrgKor); return scm; } // ... return null; } // 返回所有 client 信息 Override public ListSaSsoClientModel getClients() { // 模拟示例代码真实项目可改为从数据库查询 SaSsoClientModel scm1 new SaSsoClientModel(); scm1.setAllowUrl(sso-client1); scm1.setSecretKey(kQwIOrYvnXmSDkwEiFngrKidMcdrgKor); SaSsoClientModel scm2 new SaSsoClientModel(); scm2.setAllowUrl(sso-client2); scm2.setSecretKey(kQwIOrYvnXmSDkwEiFngrKidMcdrgKor); // ... return Arrays.asList(scm1, scm2); } }其中allowUrl用于限制该 Client 允许接收跳转的地址secretKey是 Client 与 Server 之间 API 调用的签名秘钥。模板类的完整定义见 SaSsoServerTemplate.javaSaSsoClientModel的字段说明见 SaSsoClientModel.java。十一、Client 端没有集成 sa-token-sso 插件或非 Java 语言如何对接问如果 sso-client 端我没有集成 sa-token-sso如何对接需要手动调用 http 请求来对接 sso-server 开放的接口仓库中提供了对应的示例工程 sa-token-demo-sso3-client-nosdk 可以直接参考。问如果 sso-client 端不是 Java 语言可以对接吗可以只不过有点麻烦基本思路和上个问题一致需要手动调用 http 请求来对接 sso-server 开放的接口。所有可调用的接口授权、校验 ticket、查询数据、推送消息等在 SSO-Server 认证中心开放接口文档 中有完整定义。十二、将旧有系统改造为单点登录应该注意什么官方建议不要把其中一个系统改造为 SSO 服务端而是新起一个项目作为 SSO-Server 端所有旧有项目全部作为 Client 端与此对接。这样旧系统之间的耦合保持不变SSO 能力以独立服务形式叠加降低改造风险。十三、怎么在一个项目里同时搭建 sso-server 和 sso-client难点在于解决两边的路由冲突。示例代码// Sa-Token SSO Controller RestController public class SsoController { // 处理 SSO-Server 端所有请求 RequestMapping({/sso/auth, /sso/doLogin, /sso/signout, /sso/pushS}) public Object ssoServerRequest() { return SaSsoServerProcessor.instance.dister(); } // 处理 SSO-Client 端所有请求 RequestMapping({/sso/login, /sso/logout, /sso/logoutCall, /sso/pushC}) public Object ssoClientRequest() { return SaSsoClientProcessor.instance.dister(); } // 配置SSO相关参数 Autowired private void configSsoServer(SaSsoServerTemplate ssoServerTemplate) { // SSO Server 配置代码参考文档前几章 ... } Autowired private void configSsoClient(SaSsoClientTemplate ssoClientTemplate) { // SSO Client 配置代码参考文档前几章 ... } }关键点SaSsoServerProcessor.instance与SaSsoClientProcessor.instance是各自的全局单例源码中定义见 SaSsoClientProcessor.java#L48-L51两者通过互不重叠的路由集合分工——Server 端路由/sso/auth、/sso/doLogin、/sso/signout、/sso/pushSClient 端路由/sso/login、/sso/logout、/sso/logoutCall、/sso/pushC正好与 ApiName.java 中的路由定义一一对应因此同一项目内不会冲突。十四、一个项目里同时搭建两套 sso-server 服务问我一个项目里有两套账号体系都需要单点登录怎么在一个项目里同时搭建两个 sso-server 服务首先推荐不要在一个项目里同时搭建两个 sso-server建议创建两个项目分别搭建各自的 sso-server 服务。如果一定要在一个项目中搭建两套 sso-server 服务参考方案如下第一套还是用前面几章文档给出的示例代码。第二套修改一些参数属性使之与第一套不产生冲突参考代码如下/** * Sa-Token-SSO 第二套 SSO-Server端 Controller */ RestController public class SsoUserServerController { /** * 新建一个 SaSsoServerProcessor 请求处理器 */ public static SaSsoServerProcessor ssoUserServerProcessor new SaSsoServerProcessor(); static { // 自定义一个 getServerConfig SaSsoServerConfig serverConfig new SaSsoServerConfig(); serverConfig.setSecretKey(xxx); // 更多配置 ... // 自定义一个 SaSsoServerTemplate 对象 SaSsoServerTemplate ssoUserTemplate new SaSsoServerTemplate() { Override public SaSsoServerConfig getServerConfig() { return serverConfig; } }; // 使用自定义的 StpLogic 会话对象 ssoUserTemplate.setStpLogic(StpUserUtil.stpLogic); // 让这个SSO请求处理器使用的路由前缀是 /sso-user而不是原先的 /sso ssoUserTemplate.apiName.replacePrefix(/sso-user); // 给这个 SSO 请求处理器使用自定义的 SaSsoTemplate 对象 ssoUserServerProcessor.ssoServerTemplate ssoUserTemplate; } /* * 第二套 sso-server 服务处理所有SSO相关请求 * http://{host}:{port}/sso-user/auth -- 单点登录授权地址 * http://{host}:{port}/sso-user/doLogin -- 账号密码登录接口 * http://{host}:{port}/sso-user/signout -- 单点注销地址isSlotrue时打开 */ RequestMapping(/sso-user/*) public Object ssoUserRequest() { return ssoUserServerProcessor.dister(); } // 自定义 doLogin 方法 */ // 注意点 // 1、第2套 sso-server 对应的 RestApi 登录接口也应该更换为 /sso-user/doLogin而不是原先的 /sso/doLogin // 2、在这里登录函数要使用自定义的 StpUserUtil.login()而不是原先的 StpUtil.login() RequestMapping(/sso-user/doLogin) public Object ssoUserRequest(String name, String pwd) { if(sa.equals(name) 123456.equals(pwd)) { StpUserUtil.login(10001); return SaResult.ok(登录成功).setData(StpUserUtil.getTokenValue()); } return SaResult.error(登录失败); } }这段方案能成立的底层机制是 ApiName.replacePrefix()#L81-L100它会把ApiName中所有以/sso开头的路由批量替换为新前缀示例值/sso-user、/sso-admin同时new SaSsoServerProcessor()创建独立处理器实例不走全局instance单例配合独立的StpLogic会话对象就实现了同一进程内两套账号体系的 SSO 服务物理隔离。十五、排查速查小结现象高概率根因验证/解决手段返回{msg: not handle}路由写错或对应接口未配置开关打开核对开放接口清单核对is-slo、regLogoutCall等开关模式二提示 ticket 无效Client 与 Server 未连同一 Redis用SaManager.getSaTokenDao().set(...)双向写入验证模式三提示 ticket 无效ticket 被重复校验一次性票据检查是否存在重复消费 ticket 的代码JSON 序列化异常Client 端缺少与 Server 相同包名的实体类把实体类原样复制到 Client 端模式一登录后 client 显示未登录sso-server 未配置sa-token.cookie.domain补齐域名配置模式一登录后不跳回 clientcookie.domain配错或未打开sa-token.is-read-cookie逐一核对配置模式一页面一闪而过模式二三测试遗留的无点 Cookie 污染清空 Redis / 清 Cookie / 换浏览器多 url 配置繁琐未使用server-url简化配置sa-token.sso-client.server-url无法判断项目所用模式无注释看cookie.domain、is-http、约定项mode如果以上问题均未覆盖你的场景可以到仓库的 SSO 文档目录 对应文档继续深入或查阅 SSO 开发指南 了解各接口的完整参数与签名规则。【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考