拓冰建站拓冰建站
首页 / 资讯中心 / 正文

单点登录(SSO)原理与三种主流实现方式详解

在工作当中只要你负责过两个以上的内部系统就一定遇到过这种场景登录了A系统再打开B系统还得重新输一遍账号密码如果业务系统有七八个每天光登录就能把人搞得心烦意乱。而单点登录SSOSingle Sign-On解决的就是这个问题——一次登录全网通行。简单说你只需要在认证中心登录一次其他所有互相信任的系统就都自动完成身份识别了。这篇文章我会从实际业务场景出发把SSO的原理讲清楚然后重点拆解目前最主流的三种实现方式传统Cookie-Session共享、CAS中央认证、基于Token的认证JWT/OAuth2。每种方案的真实优缺点、适合什么样的业务规模、落地时有哪些坑我都会结合自己做过的一些项目经验来说。不管你是刚接触这个概念的开发新人还是正在做技术选型的架构师这篇文章应该都能给你一些参考。1. 单点登录到底在解决什么问题1.1 多系统时代的“登录困局”我先举个自己遇到的真实例子。之前给一家公司做内部信息化改造他们有OA、CRM、项目管理系统、HR系统、财务系统加起来六个。每个系统都是不同时期采购的账号体系完全独立有的用员工工号登录有的必须用邮箱密码规则也各不相同。这种情况下员工的体验有多差我统计过保守估计每个员工每天平均要登录系统4到6次每次登录平均耗时20秒。一百个员工的公司一天光花在登录上的时间就是两三个小时。而且密码多了安全习惯就崩了——我见过有人把所有系统密码都设成同一个还有人直接拿便利贴贴在显示器上。这还不是最可怕的。最可怕的是IT部门的管理成本。员工离职的时候IT管理员要跑到六个系统里去分别注销账号漏掉一个这个离职员工的账号就一直挂在系统里没人管这是很大的安全隐患。甚至有些系统里存着客户资料、财务数据账号一旦泄露整个公司的数据都可能出问题。1.2 SSO的核心逻辑一个认证中心多系统共享SSO的核心思路其实特别朴素既然每个系统都要验证用户身份那干脆把“验证身份”这件事抽出来做成一个统一的服务。这个服务就是所谓的认证中心Authentication Center。用户第一次访问任意一个业务系统时系统发现你没登录就跳转到认证中心。认证中心核对完账号密码生成一个凭证Ticket或者Token然后带着这个凭证把你送回业务系统。业务系统验证凭证有效就认为你身份OK建立本地会话Session。之后你再访问B系统B系统同样发现你没登录带你跳转到认证中心。但认证中心一看你之前的登录状态还在啊于是不让你重新输密码直接发一个新的凭证给你然后把你送回B系统。整个过程对用户来说是透明的体感上就像“只登录了一次”。整个过程的关键在于认证中心必须是一个独立的、所有系统都信任的服务。它承担了存储用户、校验密码、生成和验证凭证的全部职责而各个业务系统不再自己保存密码、不再自己做登录校验只认认证中心发出来的凭证。1.3 SSO体系里的三个核心角色理解SSO一定要先搞清楚三个角色用户User要访问业务系统的自然人。用户关注的是“我能不能少输几次密码”这是SSO体验提升的直接感受方。认证中心SSO Server / IdP负责用户身份认证的权威服务它掌握着用户凭证的签发权。所有业务系统都无条件信任它。在行业术语里它也叫Identity Provider身份提供方。业务系统SPService Provider用户要访问的应用本身。业务系统不直接验证账号密码而是通过验证SSO Server签发的凭证来确认用户身份。如果你刚接触这个概念可以把这个体系理解成机场安检认证中心就好比安检口你过了安检会拿到一张登机牌票据/Token之后去任何一个登机口业务系统地勤只看你的登机牌不会让你重新过安检。而各个登机口之间互不干扰它们都只认安检口发的那张登机牌。2. 三种主流实现方式详解2.1 方式一传统Cookie-Session共享这是最早期、最“朴素”的实现方式在单体应用时代特别常见。他的思路是这样既然多个系统都要共享登录状态那就把Session会话状态从各个应用里抽出来放到一个共享的存储服务里比如Redis。业务系统之间通过共享同一个Cookie来标识用户。具体流程用户访问系统A系统A发现没有登录跳转到统一登录页。用户输入账号密码认证逻辑验证通过。认证流程把Session信息写入共享Redis同时把一个会话IDSessionId通过Cookie写入用户浏览器。此时用户再访问系统B浏览器会自动把携带SessionId的Cookie发过去。系统B收到请求后拿着Cookie里的SessionId去共享Redis里查查到就认为是已登录用户。这个方案看起来没什么问题但它有一个致命前提所有业务系统必须在同一个顶级域名下。因为Cookie是按域名隔离的如果系统A是oa.example.com系统B是crm.example.com你只要把Cookie的域属性设置成.example.com浏览器就会在两个系统间自动携带同一个Cookie。真正的痛点跨域就废了如果系统B是crm.another-domain.com或者直接是另一个完全独立的域名这套方案就彻底失效。浏览器出于安全策略不会允许你读取其他域名下的Cookie。跨域很难受如果业务系统分布在不同域名或者有移动端App场景根本没法带Web Cookie这套方案就没法用了。共享存储是单一故障点Redis挂了所有系统的登录状态全部失效。而且随着系统规模变大Redis的并发压力会越来越大。适用场景这套方案最适合几个同域名下的内部系统比如同一个公司的OA、HR、财务系统部署在同一批域名下系统数量不超过7-8个不需要对接外部系统。它实现简单改造量小很多老系统改造成本最低的方式就是它。但如果你要做的是面向互联网的SaaS平台、多域名业务系统、有移动端App、需要给第三方系统开放登录那这个方案最好别碰后面两种才是更合适的选择。2.2 方式二CAS中央认证服务CASCentral Authentication Service是耶鲁大学开源的一个SSO框架后来Apereo基金会一直在维护。它在教育界和政府信息化系统里用得非常多是一套非常经典、成熟的标准协议。CAS的核心概念是“票据”Ticket整个流程围绕票据的签发和校验来展开。它相比Cookie共享方案最大的优势是不依赖Cookie跨域名也能用。具体流程以最常说的CAS协议流程为例用户访问受保护的业务系统A系统A发现请求里没有服务票据Service Ticket就把用户重定向到CAS Server的登录地址同时带上一个参数告诉CAS Server“验证成功后请回跳到这个地址”。这个参数就是service比如https://cas.example.com/login?servicehttps://system-a.example.com/cas/callback。CAS Server检查用户是否已经登录通过CAS Server自己域名下的Cookie TGT来判断。如果没登录就展示登录页。用户输入账号密码CAS Server验证通过后生成一个一次性票据STService Ticket然后通过重定向把用户带回业务系统A的service地址并附上ticket参数。业务系统A在后台通过服务端通信拿着这个ST去CAS Server校验这一步是服务器到服务器浏览器不知道。CAS Server确认ST有效ST是一次性的用完作废5分钟内有效返回用户身份信息比如用户名系统A接收信息创建本地会话然后放行用户的访问请求。当用户再访问系统B时流程一样的跳转到CAS ServerCAS Server发现用户有TGT也就是全局登录状态有效就不再让用户输密码直接生成一个新的ST重定向给系统B系统B再校验ST完成登录。核心机制TGT与STCAS协议里有两个核心票据理解他们你就理解了CASTGTTicket Granting Ticket这是用户在CAS Server上全局登录成功的凭证它存放在用户浏览器的Cookie里CAS Server域名下生命周期通常和Session一致。只要TGT有效用户就不会被要求重复登录。STService TicketTGT是“总票”而ST是“分票”。用户每访问一个业务系统CAS Server都会用TGT换取一张新的ST这张ST只对这一个系统有效只能使用一次而且有时效性。ST这种一次性的设计就是为了防止重放攻击。真实项目里的配置要点CAS本身不是一个可以拿过来就用的开箱产品它是一个框架。实际落地时你需要部署CAS Server官方发行包是基于Java构建的需要跑在Tomcat之类的容器里。你可以自己在上面定制登录页、密码校验逻辑一般企业会去对接自己的用户库比如LDAP统一用户目录、AD域或者自定义的用户表。各系统接入CAS ClientJava生态用cas-client的依赖包现在已经合并到Spring Security CAS模块里配置一下CAS Server地址和本系统的service地址即可。其他语言也有对应的客户端实现比如Python可以用python-cas。这套方案的好处是标准、可控、跨域没问题适合中大型企业内部系统集成或者需要对接大量旧系统的场景。但它的缺点也很明显对接成本偏高每个系统都要改造代码来支持重定向和票据校验而且CAS Server本身性能有上限虽然可以做集群部署但维护不太轻松。2.3 方式三基于Token的认证JWT/OAuth2这是目前互联网主流的方案也是我觉得最适合现代化应用架构的一种方式。它的核心思路是认证中心签发出一个结构化、可自校验的Token通常是JWT业务系统拿到Token后不需要再跑去认证中心查询直接本地解析Token就能拿到用户身份。JWT的结构JWTJSON Web Token本质上是一串经过签名或加密的字符串分为三段Header声明用了什么算法比如HS256对称签名或者RS256非对称签名。Payload这里存用户相关的信息比如uid、username、exp过期时间、iss签发者等。Signature对Header和Payload做的签名保证内容没被篡改。我用个简单例子说明一下JWT的校验原理。比如用密钥mysecret签名的JWT系统A收到请求的Token后不会去查询数据库而是用同样的密钥重新计算签名对比是否一致。一致就说明Token确实是认证中心签发的、内容没被篡改过。// 伪代码示例展示JWT生成和校验的思路 // 签发 String token Jwts.builder() .setSubject(user123) .claim(name, 张三) .setIssuer(sso-server) .setExpiration(new Date(System.currentTimeMillis() 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, mysecret) .compact(); // 校验 JwsClaims claims Jwts.parser() .setSigningKey(mysecret) .parseClaimsJws(token); String userId claims.getBody().getSubject();结合OAuth2的SSO实现如果你们公司有自己的开放平台或者需要给第三方应用做授权登录那么“SSO OAuth2”这种组合会是你最常听到的。OAuth2本身不是SSO它解决的是“授权”问题但它的授权码模式天然适合做SSO的场景。以大家最熟悉的“使用企业微信扫码登录第三方系统”为例整个流程长这样用户在第三方系统点击“使用企业微信登录”系统A带上appId和redirectUri把用户重定向到企业微信的授权页面。企业微信显示一个二维码或者直接用企业微信扫码确认。用户扫码并确认授权后企业微信服务器带着一个授权码code重定向回第三方系统。第三方系统用code后端去企业微信换access_token这一步必须后端做不能暴露给浏览器。第三方系统再用access_token调用企业微信的接口获取用户身份信息然后建立自己的本地会话。这个流程里企业微信扮演认证中心的角色IdP第三方系统是SP用户不需要在企业微信之外的地方第二次输密码这就是SSO的效果。JWT方案的真实优点无状态水平扩展友好业务系统不存SessionToken自包含用户信息任何一台后端服务器都能独立校验Token。这对微服务架构、Kubernetes部署、弹性扩容的场景特别友好。跨域、跨端、跨协议Token放在请求头Authorization Header里不依赖Cookie天然支持Web、App、小程序、甚至第三方系统不受域名限制。生态成熟OAuth2、OIDCOpenID Connect就是基于Token这套体系构建的标准协议各种语言的库都很完善对接起来非常快。需要注意的坑Token的失效问题JWT签发之后在过期之前只要签名正确就永远有效。你要踢人下线、“封号”的时候单独靠JWT根本做不到必须配合“Token黑名单”把失效Token存Redis或者把Token有效期缩短加刷新机制。Payload别放敏感信息JWT的Payload默认只是Base64编码不是加密的谁都能看得到。千万不要把密码、手机号、身份证号码这些敏感信息直接塞进Payload。我见过有团队把用户明文密码放进去这等于裸奔。密钥管理要严肃如果用HS256对称签名密钥一旦泄露所有Token都能被伪造。正规做法是使用RS256非对称签名私钥保存在认证中心业务系统只保存公钥用于验签。3. 从原理到落地实际选型与配置要点3.1 怎么选择适合自己业务的方案很多刚接触SSO的人会问我“老师到底用哪种方案好”我的回答永远是没有最好的只有最适合你的。为了帮你决策我给这三个方案画了一张对照表维度Cookie-Session共享CAS中央认证基于TokenJWT/OAuth2跨域支持差仅限同域名好不依赖Cookie好天然支持移动端/App支持差麻烦一般需额外改造好Token放Header即可实现难度低中高中生态/标准无标准私有化有标准协议但偏旧标准协议业界主流无状态/水平扩展差依赖共享存储一般CAS Server需保持状态好业务系统无状态适用的系统规模小规模几个内部系统中大规模老系统多各种规模尤其适合现代架构安全性中等高票据一次性高注意密钥管理和失效机制我做过的项目中凡是传统老企业有大量遗留系统、统一域名、Linux服务器都在内网CAS这套方案很稳但凡公司稍微互联网化一点或者要接App、要对接第三方生态我一定会优先选JWT/OAuth2。Cookie共享方式现在基本只出现在“最小改动量”的遗留系统改造里。3.2 认证中心的搭建步骤基于Spring Security OAuth2 JWT示例下面这套是我落地过的一个Java单体项目方案非常简单干净适合很多中小型公司直接抄作业。技术栈是Spring Boot 2.7 Spring Security JWT Redis。核心思路是认证中心独立部署签发JWT业务系统通过Spring Security的OncePerRequestFilter拦截请求校验JWT。第一步认证中心登录接口RestController RequestMapping(/auth) public class AuthController { Autowired private AuthenticationManager authenticationManager; GetMapping(/login) public Result login(RequestParam String username, RequestParam String password) { // 1. 调用Spring Security的用户校验逻辑 Authentication authentication authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password) ); // 2. 认证通过生成JWT UserDetail user (UserDetail) authentication.getPrincipal(); String jwt JwtUtils.createToken(user.getId(), user.getUsername()); // 3. 存入Redis用于统一注销/踢人 redisTemplate.opsForValue().set(sso:token: user.getId(), jwt, 30, TimeUnit.MINUTES); return Result.success(jwt); } }第二步业务系统校验JWT的过滤器这一步是关键。每个接入SSO的业务系统只需要一个过滤器解析Authorization头里的Token调公钥验证签名然后解析出用户信息放入SecurityContext放行。如果Token无效返回401让前端跳转登录页。Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private SsoProperties ssoProperties; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { // 使用RS256验签公钥由认证中心下发 Claims claims JwtUtils.parseToken(token, ssoProperties.getPublicKey()); String userId claims.getSubject(); // 这里可以再做一次Redis/TTL校验看用户是否被注销 UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken(userId, null, new ArrayList()); SecurityContextHolder.getContext().setAuthentication(auth); } catch (Exception e) { // Token无效不设置认证信息由后续权限控制拦截 SecurityContextHolder.clearContext(); } } chain.doFilter(request, response); } }第三步认证中心下发公钥给业务系统JWT如果用RS256认证中心生成一对公私钥公钥放在一个配置接口上比如/auth/public-key业务系统启动时初始化拉取一次之后验签直接用。这样就算业务系统被攻击也无法伪造Token因为伪造需要的是私钥而私钥永远只留在认证中心。3.3 结合企业微信、飞书等平台做免登最近几年做内部系统越来越多客户要求对接企业微信、钉钉、飞书这类办公平台做免登。这里我给你一个通用思路以企业微信为例注册应用获取corpId、agentId、secret这是企业微信管理后台里申请的应用凭证相当于你在企业微信这个SSO系统的“门禁卡”。前端跳转授权用户在自研系统的登录页点击“企业微信登录”前端把用户重定向到企业微信的授权链接带上redirect_uri授权后回调地址和state防CSRF用。回调换取用户身份企业微信授权后回调到自研系统后端后端拿到code再调用企业微信的接口通过code换取access_token再用access_token获取员工的userid、姓名、部门等用户信息。自研系统建立会话拿到用户信息后在自己的用户表里找到对应账号这时候可以自动创建账号或者绑定账号然后签发自己的JWT完成SSO。这个流程里企业微信就是那个“认证中心”你的系统只是“接入方”。这种模式的好处是用户不需要再记一套企业内部的账号密码员工入职、离职也直接在企微后台统一管理。3.4 统一用户源的重要性我发现一个特别常见的坑很多公司在做SSO的时候只做了“单点登录”的外壳结果发现用户信息源是乱的。客户有六个系统每个系统都有自己的用户表有的字段是工号有的是手机号有的是邮箱。SSO做完了用户确实只需要登录一次但“用户身份”没有一个权威的ID来对应这个人在系统A里的工号是0001在系统B里却是zhang3等真正打通权限、做用户关联的时候就抓瞎了。所以在做任何SSO之前第一件事一定是先把用户主数据拉通。通常做法是建一个统一的用户中心或者用LDAP目录服务把所有系统的用户映射到统一的主键上最好是用工号或员工ID作为唯一标识。LDAP在传统企业内部系统里用得特别多把统一认证和SSO一起做会是更完整的方案。4. 常见问题与排查技巧实录4.1 跨域与Cookie丢失问题症状用户从系统A跳转到系统B后B系统还是没有登录状态。排查思路先确认两个系统是否为同域名。Cookie共享和CAS的传统实现都极其依赖域名的正确配置。如果两个系统是a.example.com和b.example.com需要把Cookie的Domain设成.example.com如果CAS重定向后浏览器没带TGTCookie优先检查CAS Server域名和访问域名的关系。检查是否开了Secure属性。如果CAS Server启用了HTTPSCookie的Secure属性要求只能在HTTPS下传输。如果你测试的时候用HTTP访问Cookie就发不出去页面会出现“无限循环跳转登录”的经典症状。检查SameSite属性。Chrome 80之后的版本默认Cookie的SameSiteLax这会限制跨站请求携带Cookie。如果认证中心在cas.example.com业务系统在system-a.com浏览器很有可能因为SameSite策略拒绝传递Cookie。实际解决方式是调整Cookie策略或者直接切换到Token方案推荐。4.2 Token过期与会话不同步症状用户用着用着突然被踢下线有时提示Token失效有时业务系统之间状态不一致。排查思路JWT的exp是“绝对过期时间”只要超过就失效。如果用户长时间停留页面页面里的Token早过期了他不刷新可能都没发现一旦他发起请求后端就会校验失败。解决思路是引入“刷新令牌Refresh Token”和“滑动过期”机制——用户在活跃期间系统自动续期。多系统场景下各系统的时钟可能存在几秒钟的偏差如果Token的exp设置得非常短比如5分钟有一个系统时钟快30秒就可能出现“认证中心签发的Token在系统B上提前过期”的情况。排查时注意检查各服务器时间是否同步NTP同步。如果做了“踢人下线”功能前端可以正常“下线”但服务器端的JWT还在有效期内。必须在每次请求时加一层Redis黑名单校验请求Token的同时去Redis查一下这个Token是否在“已注销”列表里。4.3 安全漏洞与防护细节症状SSO做完测试却发现几个非常低级但致命的漏洞。排查思路重放攻击CAS的ST是一次性的天然能防重放但如果你自己实现Token必须考虑Token泄露后被人重放的风险。解决方式是缩短Token有效期、加入jtiJWT唯一ID并且服务器端对jti做一次性校验。OAuth2回调state参数丢失或未校验state参数用来防CSRF很多开发直接忽略掉这会导致登录过程的“会话固定攻击”。务必生成随机state并存Session回调时比对。JWT签名算法混淆攻击有些JWT库在验签时会自动跟随Header算法比如把RS256改成HS256攻击者可以用公钥当密钥来伪造Token。解决办法是验签时强制指定算法不要自动推断。SSO流程中的开放重定向漏洞调用service参数或OAuth2的redirect_uri时如果不校验白名单攻击者可以构造一个恶意的回调地址诱导用户登录后跳转到钓鱼网站。所有回调地址必须做严格白名单校验。4.4 从日志里快速定位问题我建议所有做SSO的系统在日志规范上从第一天就统一加两个字段userId和sessionId或tokenId。这样一旦用户反馈“我明明登录了A系统但B系统还是让我登录”你可以通过日志链路从头追到尾快速定位是在CAS Server那里没生成TGT还是在哪个业务系统的service校验环节出了问题。没有这两个字段排查SSO问题基本等同于大海捞针。5. 踩坑心得与后续扩展建议做SSO这几年我最大的体会是SSO的难点从来不在技术而在“取舍”。每个方案都有它存在的理由但一旦业务发展变化比如从纯PC端拓展到移动端、从几个内部系统变成对外SaaS开放平台原先的方案可能很快就会成为瓶颈。如果让我给一个建议我会说新项目、新型公司从第一天就大胆上“JWT OAuth2/OIDC Redis黑名单”这套组合。它没有传统CAS那么重的历史包袱也不受Cookie和域名限制未来无论是接企业微信还是给第三方做开放授权都游刃有余。如果你们的系统里已经有大量老系统、改造成本有限那CAS反而可能是更稳妥的“最小改造量”方案。最后分享一个小技巧无论用哪种方案一定要把“认证中心”的登录页当作最核心的服务来对待。它的高可用、性能、安全防御投入都应该是普通业务系统的数倍。因为SSO一旦挂了意味着所有接入系统全部不可用这个故障半径是非常可怕的。如果你正准备做公司内部的SSO强烈建议先把上面的“用户主数据拉通”这件事做扎实再考虑协议选型。账号都没对齐就谈单点登录后面你会被各种奇奇怪怪的同名账号、数据同步问题搞到怀疑人生。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门