单点登录(SSO):从原理到实战,彻底搞懂多系统统一登录方案

227 阅读18分钟

单点登录(SSO):从原理到实战,彻底搞懂多系统统一登录方案

在企业级应用或多系统架构中,“重复登录” 是用户体验的一大痛点 —— 例如,用户登录电商平台后,访问关联的会员中心、订单系统、支付系统时,还需再次输入账号密码,不仅繁琐,还增加了密码泄露风险。而单点登录(Single Sign-On,简称 SSO) 作为解决这一问题的核心方案,能实现 “一次登录,多系统互通”,大幅提升用户体验与系统安全性。本文将从 SSO 的核心原理出发,拆解主流实现方案,结合实战案例演示落地过程,并分析安全风险与防护措施,帮你彻底掌握 SSO 的设计与应用。

一、为什么需要 SSO?从传统登录痛点说起

在未引入 SSO 的多系统架构中,登录流程存在三大核心问题,这也是 SSO 诞生的初衷:

1. 传统登录的三大痛点

  • 用户体验差:用户需记忆多个系统的账号密码(若系统间账号不互通),或重复输入同一套账号密码(若账号互通但未做统一登录)。例如,某企业的 OA 系统、CRM 系统、财务系统账号相同,但用户登录 OA 后,访问 CRM 仍需再次登录。
  • 系统管理复杂:每个系统需单独维护登录模块(如账号校验、密码加密、会话管理),重复开发且难以统一升级(如密码策略从 “6 位” 改为 “8 位含特殊字符”,需所有系统同步修改)。
  • 安全性风险高:用户为简化记忆,可能在多个系统使用相同密码,一旦某系统密码泄露,所有关联系统都会面临风险;同时,多系统分散的会话管理,也增加了 “会话劫持” 的攻击面。

2. SSO 的核心价值

SSO 的本质是 “在多个独立系统间,共享用户的认证状态”,核心价值体现在三点:

  • 用户体验优化:一次登录后,无需重复验证即可访问所有信任的系统(如登录微信后,可直接使用朋友圈、公众号、微信支付等功能);
  • 开发与维护效率提升:统一的认证中心负责账号校验、会话管理,各业务系统无需重复开发登录模块,只需集成 SSO 客户端即可;
  • 安全性增强:集中管理认证流程(如多因素认证、异常登录检测),避免分散系统的安全漏洞;同时,统一的会话销毁机制(如用户退出登录,所有关联系统同步登出),降低会话劫持风险。

二、SSO 的核心原理:如何实现 “一次登录,多系统互通”?

要理解 SSO,需先明确其核心组件与通用流程 —— 无论采用哪种实现方案,SSO 的底层逻辑都围绕 “中央认证 + 跨系统会话共享” 展开。

1. SSO 的核心组件

SSO 架构通常包含三个关键角色,各角色职责明确:

组件名称核心职责示例
身份提供商(IdP,Identity Provider)中央认证服务器,负责用户身份校验(账号密码验证、短信验证等)、生成认证凭证(如 Token)、管理全局会话微信开放平台(提供微信登录)、企业内部的 SSO 认证中心
服务提供商(SP,Service Provider)各业务系统(如 OA、CRM、电商订单系统),依赖 IdP 完成认证,无需存储用户密码,仅需验证 IdP 颁发的凭证企业 OA 系统、电商平台的订单系统、第三方应用(如用微信登录的小游戏)
用户(User)终端使用者,通过 IdP 完成一次登录后,可访问所有信任的 SP 系统企业员工、电商平台用户

2. SSO 的通用认证流程

以 “企业员工登录 OA 系统后,访问 CRM 系统” 为例,SSO 的通用流程如下(适用于大多数实现方案):

  1. 首次登录:用户访问 SP1(OA 系统)
    • 用户访问 OA 系统,OA 检测到用户未登录,自动重定向到 IdP(企业 SSO 认证中心);
    • 用户在 IdP 输入账号密码,IdP 验证通过后,生成 “全局认证凭证”(如 Token、SessionId),并创建全局会话(记录用户登录状态);
    • IdP 将 “全局认证凭证” 通过重定向传递给 OA 系统,同时在用户浏览器写入 IdP 域下的 Cookie(存储全局会话 ID,用于后续验证);
    • OA 系统验证 “全局认证凭证” 有效性(向 IdP 发起校验请求),验证通过后,创建 OA 系统的 “局部会话”(用户在 OA 内的登录状态),并跳转回 OA 的目标页面;
  1. 免登录访问 SP2(CRM 系统)
    • 用户访问 CRM 系统,CRM 检测到用户未登录,重定向到 IdP;
    • IdP 检测到用户浏览器已存在 “全局会话 Cookie”,验证全局会话有效(用户已登录),无需再次输入账号密码;
    • IdP 直接生成 “针对 CRM 的认证凭证”,重定向传递给 CRM;
    • CRM 验证凭证有效性后,创建 “局部会话”,跳转回 CRM 的目标页面,用户实现免登录访问。
  1. 退出登录
    • 用户在任一 SP 系统(如 OA)点击 “退出登录”,OA 销毁自身的 “局部会话”,并向 IdP 发起 “全局登出请求”;
    • IdP 销毁 “全局会话”,并通知所有已登录的 SP 系统(如 CRM)销毁各自的 “局部会话”;
    • 用户再次访问任一 SP 系统时,需重新通过 IdP 登录。

三、SSO 的主流实现方案:原理、优缺点与适用场景

SSO 的实现方案因 “凭证类型”“跨域处理方式” 不同而分为多种,以下是企业级应用中最常用的 4 种方案,需根据业务场景(如是否跨域、是否对接第三方、安全性要求)选择:

1. 方案 1:基于 Cookie+Session(传统 SSO)

原理
  • 核心逻辑:IdP 创建全局会话(Session),用户登录后,IdP 在自身域名下写入 “全局 SessionId Cookie”;各 SP 系统通过 “跨域请求”(如 iframe、后端转发)向 IdP 验证 Cookie 有效性,若有效则创建 SP 的局部会话。
  • 关键步骤
    1. 用户登录 IdP 后,IdP 生成全局 Session(存储在服务器),并在浏览器写入domain=idp.com的 Cookie(值为 SessionId);
    1. 用户访问 SP(如sp1.com),SP 通过后端接口向 IdP 发起请求(携带用户浏览器的 IdP Cookie);
    1. IdP 验证 Cookie 中的 SessionId,若有效则返回 “用户已登录” 的结果,并附带用户信息;
    1. SP 创建局部 Session,用户免登录访问。
优缺点
优点缺点
实现简单(依赖传统 Session 机制)跨域限制严格:Cookie 仅能在 IdP 域名下生效,若 SP 与 IdP 不在同一主域(如idp.com和sp1.cn),Cookie 无法携带,需额外处理(如 URL 参数传递 SessionId,安全性低)
服务器端管理会话,易于控制登出扩展性差:全局 Session 存储在 IdP 服务器,高并发下需考虑 Session 集群(如 Redis 共享 Session),增加复杂度
适用场景
  • 对安全性要求一般,且系统规模较小(如 10 个以内 SP)。

2. 方案 2:基于 JWT(轻量级 SSO)

原理

JWT(JSON Web Token)是一种 “无状态” 的认证凭证,由 IdP 生成并签名,包含用户身份信息(如用户 ID、角色),SP 无需向 IdP 请求校验,可直接通过签名验证凭证有效性。核心流程:

  1. 登录阶段:用户在 IdP 输入账号密码,验证通过后,IdP 用私钥生成 JWT(包含 Header、Payload、Signature 三部分),返回给前端;
  1. 存储阶段:前端将 JWT 存储在localStorage或HttpOnly Cookie中;
  1. 访问 SP 阶段:用户访问 SP 时,前端在请求头(如Authorization: Bearer )携带 JWT,SP 用 IdP 的公钥验证 JWT 签名(确认未被篡改),解析 Payload 中的用户信息,创建局部会话,实现免登录。
JWT 结构解析(以eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJBRE1JTiIsImV4cCI6MTY4MDAwMDAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c为例):
  • Header(头部) :指定算法(如 HS256)和类型,Base64 编码后的值为eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9;
  • Payload(载荷) :存储用户信息(如userId:1、role:ADMIN)和过期时间(exp),Base64 编码后的值为eyJ1c2VySWQiOjEsInJvbGUiOiJBRE1JTiIsImV4cCI6MTY4MDAwMDAwMH0;
  • Signature(签名) :用 Header 指定的算法(如 HS256),结合 IdP 的私钥对 “Header.Base64 + Payload.Base64” 签名,确保 JWT 未被篡改,值为SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c。
优缺点
优点缺点
无状态:SP 无需向 IdP 校验,减少网络请求,适合分布式系统无法主动销毁:JWT 一旦生成,在过期前始终有效,若用户需 “强制登出”,需额外维护 “黑名单”(如 Redis 存储失效的 JWT)
跨域友好:JWT 通过请求头携带,不受 Cookie 域名限制payload 不宜过大:JWT 会随每次请求传递,过大导致请求体积增加,影响性能
实现简单:无需维护服务器端 Session,适合中小规模系统签名验证需公钥 / 私钥管理:若私钥泄露,攻击者可伪造 JWT,需妥善保管密钥
适用场景
  • 跨域场景(如 SP 与 IdP 域名不同);
  • 轻量级系统(如移动端 APP、小程序、第三方 API 服务);
  • 对实时登出要求不高的场景(如内容阅读类应用)。
实战代码示例(Spring Boot 实现 JWT SSO)
(1)IdP 端:生成 JWT
// 1. 引入JWT依赖(Maven)
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
// 2. JWT工具类
@Component
public class JwtUtil {
    // 私钥(生产环境需放在配置中心,避免硬编码)
    @Value("${jwt.secret}")
    private String secret;
    // 过期时间(2小时)
    @Value("${jwt.expire}")
    private long expire;
    // 生成JWT
    public String generateToken(Long userId, String role) {
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + expire * 1000);
        return Jwts.builder()
                .setHeaderParam("typ", "JWT") // Header部分
                .setSubject(userId.toString()) // Payload:用户ID
                .claim("role", role) // Payload:自定义字段(角色)
                .setIssuedAt(now) // 签发时间
                .setExpiration(expireDate) // 过期时间
                .signWith(SignatureAlgorithm.HS256, secret) // 签名(用私钥)
                .compact();
    }
    // 解析JWT,获取用户ID
    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
        return Long.parseLong(claims.getSubject());
    }
    // 验证JWT有效性(是否过期、签名是否正确)
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            // 过期、签名错误等均返回false
            return false;
        }
    }
}
// 3. 登录接口(IdP端)
@RestController
@RequestMapping("/idp")
public class LoginController {
    @Autowired
    private UserService userService;
    @Autowired
    private JwtUtil jwtUtil;
    @PostMapping("/login")
    public Result login(@RequestBody LoginDTO loginDTO) {
        // 1. 校验账号密码(实际场景需加密校验,如BCrypt)
        User user = userService.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword());
        if (user == null) {
            return Result.fail("账号或密码错误");
        }
        // 2. 生成JWT
        String token = jwtUtil.generateToken(user.getId(), user.getRole());
        // 3. 返回JWT(前端存储到localStorage或Cookie)
        return Result.success("登录成功").put("token", token);
    }
}
(2)SP 端:验证 JWT(以 OA 系统为例)
// 1. JWT拦截器:验证请求头中的JWT
@Component
public class JwtInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtUtil jwtUtil;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取请求头中的JWT
        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            response.setStatus(401);
            response.getWriter().write("未登录或Token无效");
            return false;
        }
        token = token.substring(7); // 去掉"Bearer "前缀
        // 2. 验证JWT有效性
        if (!jwtUtil.validateToken(token)) {
            response.setStatus(401);
            response.getWriter().write("Token已过期或无效");
            return false;
        }
        // 3. 解析用户信息,存入请求属性(后续接口可获取)
        Long userId = jwtUtil.getUserIdFromToken(token);
        request.setAttribute("userId", userId);
        return true;
    }
}
// 2. 配置拦截器(拦截OA系统的所有接口,排除登录页)
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private JwtInterceptor jwtInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/oa/**") // 拦截OA系统所有接口
                .excludePathPatterns("/oa/loginPage"); // 排除登录页(未登录时跳转)
    }
}
// 3. OA系统业务接口(需登录才能访问)
@RestController
@RequestMapping("/oa")
public class OaController {
    @GetMapping("/user/info")
    public Result getUserInfo(HttpServletRequest request) {
        // 从请求属性中获取用户ID(JWT拦截器已解析)
        Long userId = (Long) request.getAttribute("userId");
        // 查询用户信息(实际场景需从数据库或缓存获取)
        UserInfoDTO userInfo = userService.getUserInfoById(userId);
        return Result.success(userInfo);
    }
}

3. 方案 3:基于 OAuth2.0(第三方登录 SSO)

原理

OAuth2.0 是一种 “授权框架”,并非专为 SSO 设计,但常被用于 “第三方登录” 场景(如用微信、QQ、GitHub 登录第三方应用),本质是通过 “授权码” 实现 SP 与第三方 IdP 的认证互通。核心角色除了 IdP(如微信开放平台)、SP(如第三方小游戏),还增加了 “资源所有者”(用户)和 “资源服务器”(如微信的用户信息接口)。

以 “用微信登录小游戏” 为例,OAuth2.0 的授权码模式(最常用、最安全)流程如下:

  1. 发起授权请求:用户点击小游戏的 “微信登录”,小游戏(SP)重定向到微信开放平台(IdP)的授权页面,携带client_id(SP 在 IdP 的唯一标识)、redirect_uri(授权后回调地址)、response_type=code(请求授权码);
  1. 用户授权:用户在微信授权页面确认授权,IdP 生成 “授权码(code)”,并重定向到 SP 的redirect_uri,携带 code;
  1. 获取访问令牌:SP 后端用code+client_id+client_secret(SP 在 IdP 的密钥)向 IdP 发起请求,获取 “访问令牌(access_token)”;
  1. 获取用户信息:SP 用access_token向微信的资源服务器请求用户信息(如昵称、头像);
  1. 创建局部会话:SP 验证用户信息后,创建局部会话,用户免登录访问小游戏。
优缺点
优点缺点
支持第三方登录:无需用户注册新账号,直接用现有账号(如微信、QQ)登录流程复杂:需理解授权码、access_token、refresh_token 等概念,开发成本高
安全性高:授权码仅短期有效,且通过后端交换 access_token,避免令牌泄露依赖第三方 IdP:若 IdP 服务不可用(如微信开放平台故障),SP 的登录功能会受影响
灵活度高:支持多种授权模式(如授权码、密码、客户端凭证),适配不同场景权限控制复杂:需管理 access_token 的权限范围(如仅获取用户昵称,不获取手机号)
适用场景
  • 第三方登录场景(如电商平台支持微信 / QQ 登录、论坛支持 GitHub 登录);
  • 跨组织的 SSO(如企业 A 的系统需接入企业 B 的认证体系)。

4. 方案 4:基于 CAS(企业级高安全 SSO)

原理

CAS(Central Authentication Service)是专为 SSO 设计的企业级框架,基于 “票据(Ticket)” 机制,支持单点登录与单点登出,安全性高、功能完善(如多因素认证、权限管理)。核心流程与 JWT 类似,但引入了 “TGT(Ticket Granting Ticket,票据授予票据)” 和 “ST(Service Ticket,服务票据)”:

  1. 登录获取 TGT:用户登录 CAS Server(IdP),验证通过后,CAS Server 生成 TGT(存储在服务器,对应用户全局会话),并在浏览器写入 CAS 域下的 Cookie(存储 TGT 标识);
  1. 访问 SP 获取 ST:用户访问 SP,SP 重定向到 CAS Server,CAS Server 检测到 TGT 有效,生成 ST(短期有效,仅用于当前 SP),重定向回 SP 并携带 ST;
  1. 验证 ST 并登录:SP 用 ST 向 CAS Server 发起校验请求,验证通过后,CAS Server 返回用户信息,SP 创建局部会话,用户免登录访问。
优缺点
优点缺点
安全性极高:支持 HTTPS、票据加密、防 CSRF/XSS,适合企业级核心系统部署复杂:需搭建 CAS Server 集群,维护成本高
支持单点登出:用户退出任一 SP,CAS Server 通知所有 SP 销毁会话性能依赖 CAS Server:高并发下需优化 CAS Server(如 Redis 缓存 TGT)
功能完善:内置多因素认证、用户管理、日志审计等功能重量级:对小规模系统而言,过于复杂
适用场景
  • 企业级核心系统(如金融、政务、医疗系统),对安全性要求极高;
  • 系统规模大(如 50 个以上 SP),需统一管理登录与登出。

四、SSO 的安全风险与防护措施

SSO 虽提升了用户体验,但也集中了认证入口 —— 一旦 IdP 被攻击或凭证泄露,所有关联 SP 都会面临风险。需针对性解决以下安全问题:

1. 风险 1:凭证泄露(如 JWT 被窃取、Cookie 被劫持)

风险场景
  • JWT 存储在localStorage时,易被 XSS 攻击窃取(如恶意脚本通过document.localStorage.getItem("token")获取 JWT);
  • Cookie 未设置HttpOnly属性时,同样可能被 XSS 攻击窃取;
  • 未使用 HTTPS 时,凭证(如 JWT、Cookie)在传输过程中可能被中间人劫持。
防护措施
  • 存储安全
    • JWT 优先存储在HttpOnly + Secure + SameSite的 Cookie 中(HttpOnly禁止 JS 访问,避免 XSS;Secure仅通过 HTTPS 传输;SameSite=Strict防止 CSRF);
    • 若需存储在localStorage,需配合 XSS 防护(如输入过滤、CSP 策略)。
  • 传输安全:所有 SSO 相关请求(登录、凭证传递)必须使用 HTTPS,避免中间人攻击。
  • 凭证短期有效:缩短 JWT/access_token 的过期时间(如 1 小时),同时提供 “刷新令牌(refresh_token)”(长期有效,存储在HttpOnly Cookie),过期后用 refresh_token 获取新凭证,减少泄露风险。

2. 风险 2:CSRF 攻击(跨站请求伪造)

风险场景
  • 攻击者诱导已登录 SSO 的用户访问恶意网站,恶意网站向 SP 或 IdP 发起 “登出”“修改密码” 等请求(利用用户浏览器中有效的 SSO Cookie),例如:
    • 恶意网站嵌入,用户访问时自动发起登出请求,导致用户被强制登出。
防护措施
  • CSRF 令牌:IdP 和 SP 的关键接口(如登出、修改密码)需验证 CSRF 令牌(前端从页面获取令牌,请求时携带,后端校验);
  • SameSite Cookie:设置 Cookie 的SameSite=Strict或SameSite=Lax,限制 Cookie 仅在同域或信任的跨域请求中携带,阻止恶意网站的请求携带 Cookie;
  • 验证 Referer/Origin:后端校验请求的Referer或Origin头,仅允许信任的域名(如 SP 的域名)发起请求。

3. 风险 3:凭证伪造(如伪造 JWT、伪造 CAS 票据)

风险场景
  • 攻击者获取 JWT 的结构后,伪造 Payload(如修改用户角色为 ADMIN),若未正确验证签名,会导致权限越权;
  • 攻击者伪造 CAS 的 ST,向 SP 发起请求,若 SP 未向 CAS Server 校验 ST 有效性,会导致虚假登录。
防护措施
  • 严格签名验证:JWT 必须用非对称加密(如 RSA),SP 用 IdP 的公钥验证签名,避免私钥泄露;CAS 的 ST 必须由 SP 后端向 CAS Server 校验,不可前端直接验证;
  • 凭证唯一性与时效性:ST、授权码等短期凭证必须唯一(如 UUID),且过期时间短(如 5 分钟),使用后立即失效,避免重复使用;
  • 异常检测:IdP 监控异常请求(如同一 IP 短时间多次请求登录、异地登录),触发验证码或临时冻结账号。

五、SSO 落地的关键注意事项

1. 跨域问题处理

  • Cookie 跨域:若 SP 与 IdP 不在同一主域(如idp.comsp.cn),Cookie 无法共享,此时建议用 JWT(请求头携带)或 CAS(票据传递),避免依赖 Cookie;
  • 接口跨域:SP 前端向 IdP 发起请求时(如获取用户信息),需 IdP 配置 CORS(跨域资源共享),允许 SP 的域名访问(如Access-Control-Allow-Origin: sp.com)。

2. 会话管理策略

  • 全局会话与局部会话同步:用户登录后,IdP 维护全局会话,各 SP 维护局部会话;全局会话过期(如 2 小时)后,所有局部会话需同步过期,避免 “全局登出但局部会话仍有效”;
  • 强制登出实现:对需实时登出的场景(如管理员禁用账号),JWT 方案需维护 “JWT 黑名单”(Redis 存储失效的 JWT,过期时间与 JWT 一致),SP 验证 JWT 时先检查是否在黑名单中;CAS 方案直接销毁 TGT,所有 SP 的 ST 会失效。

3. 高可用与性能优化

  • IdP 高可用:IdP 是 SSO 的核心,需部署集群(如 CAS Server 集群、JWT IdP 集群),避免单点故障;
  • 缓存优化:IdP 的全局会话(如 TGT、JWT 黑名单)、SP 的用户信息建议用 Redis 缓存,减少数据库访问,提升性能;
  • 限流防护:IdP 的登录接口、凭证校验接口需限流(如每秒 1000 次请求),避免被恶意请求压垮。

六、SSO 选型建议:不同场景如何选择方案?

业务场景推荐方案核心原因
企业内部系统,同主域、小规模Cookie+Session实现简单,无需额外依赖,适合内部信任环境
跨域系统,轻量级需求JWT跨域友好,无状态,部署成本低
第三方登录(如微信 / QQ 登录)OAuth2.0支持第三方授权,用户体验好,安全性高
企业级核心系统,高安全需求CAS支持单点登出、多因素认证,适合大规模部署

总结

单点登录(SSO)的核心是 “统一认证入口,共享登录状态”,不同实现方案(Cookie+Session、JWT、OAuth2.0、CAS)各有优劣,需根据业务场景(跨域、安全性、是否第三方登录)选择。落地时需重点关注安全风险(凭证泄露、CSRF、伪造),通过 HTTPS、HttpOnly Cookie、签名验证、黑名单等措施防护,同时保证 IdP 高可用与性能优化。

SSO 不仅是技术方案,更是用户体验与系统安全的平衡艺术 —— 好的 SSO 设计能让用户 “无感登录”,让开发者 “少重复开发”,让系统 “更安全可控”,这也是其在企业级应用中广泛普及的核心原因。