0
点赞
收藏
分享

微信扫一扫

单点登录SSO又双叒叕登录失效了?这5步架构设计让你一次登录全网通行!

作为一名后端开发,经历过太多SSO单点登录的"惨案":

  • 某电商平台有20个子系统,用户每换一个系统就要重新登录,客服电话被打爆
  • 某企业内部OA系统SSO配置错误,员工登录后5分钟就自动踢出,全公司怨声载道
  • 某教育平台SSO跨域问题没处理好,Chrome浏览器登录正常,IE浏览器直接白屏

SSO单点登录,听起来很美好,实现起来全是坑。今天就结合自己从日活1万到500万的踩坑经历,跟大家聊聊SSO到底该怎么设计,让你一次登录,全网通行!

一、SSO到底是个啥?为啥大家都在用?

SSO(Single Sign-On)单点登录的核心就是:一次登录,全网通行

为啥SSO这么香?

  • 用户体验好:登录一次,访问所有系统都不用再输密码
  • 运维成本低:统一管理用户身份,不用每个系统都维护一套用户体系
  • 安全性高:统一认证中心,集中管理安全策略
  • 逼格高:用户觉得你们技术很牛逼

二、SSO的5步架构设计,一步走错就全军覆没!

SSO就像建房子,地基不稳,整栋楼都会塌。下面这5步,每一步都是血的教训。

第1步:认证中心设计,这是SSO的"地基"

认证中心是整个SSO的核心,就像支付宝一样,所有系统都要来找它确认身份。

核心架构

@RestController
public class AuthCenterController {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    /**
     * 用户登录认证
     */
    @PostMapping("/auth/login")
    public AuthResponse authenticate(@RequestBody LoginRequest request) {
        // 1. 验证用户名密码
        User user = userService.validateUser(request.getUsername(), request.getPassword());
        if (user == null) {
            return AuthResponse.fail("用户名或密码错误");
        }
        
        // 2. 生成全局会话token
        String globalToken = UUID.randomUUID().toString();
        
        // 3. 存储用户会话信息(30分钟过期)
        String sessionKey = "sso:session:" + globalToken;
        redisTemplate.opsForHash().put(sessionKey, "user", user);
        redisTemplate.opsForHash().put(sessionKey, "loginTime", System.currentTimeMillis());
        redisTemplate.expire(sessionKey, Duration.ofMinutes(30));
        
        // 4. 生成JWT token给各个子系统使用
        String jwtToken = jwtTokenUtil.generateToken(user);
        
        return AuthResponse.success(globalToken, jwtToken);
    }
    
    /**
     * 验证token有效性
     */
    @GetMapping("/auth/validate")
    public ValidateResponse validateToken(@RequestParam String token) {
        String sessionKey = "sso:session:" + token;
        User user = (User) redisTemplate.opsForHash().get(sessionKey, "user");
        
        if (user == null) {
            return ValidateResponse.invalid("token无效或已过期");
        }
        
        // 刷新过期时间
        redisTemplate.expire(sessionKey, Duration.ofMinutes(30));
        
        return ValidateResponse.valid(user);
    }
}

第2步:业务系统集成,这是SSO的"水电安装"

每个业务系统都要集成SSO客户端,就像每个房间都要安装水电一样。

SSO客户端过滤器

@Component
public class SSOAuthFilter implements Filter {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // 1. 检查是否已经登录
        String localToken = getCookieValue(httpRequest, "LOCAL_TOKEN");
        if (localToken != null && validateLocalToken(localToken)) {
            chain.doFilter(request, response);
            return;
        }
        
        // 2. 检查是否有SSO全局token
        String globalToken = getCookieValue(httpRequest, "GLOBAL_TOKEN");
        if (globalToken != null) {
            // 向认证中心验证token
            User user = validateWithAuthCenter(globalToken);
            if (user != null) {
                // 生成本地token
                String newLocalToken = generateLocalToken(user);
                setCookie(httpResponse, "LOCAL_TOKEN", newLocalToken, 3600);
                chain.doFilter(request, response);
                return;
            }
        }
        
        // 3. 跳转到SSO登录页面
        String currentUrl = httpRequest.getRequestURL().toString();
        String redirectUrl = "https://sso.yourdomain.com/login?redirect=" + 
                           URLEncoder.encode(currentUrl, "UTF-8");
        httpResponse.sendRedirect(redirectUrl);
    }
}

第3步:跨域问题处理,这是SSO的"防火措施"

跨域是SSO最大的坑,处理不好直接全军覆没。

跨域配置方案

@Configuration
public class CorsConfig {
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*.yourdomain.com"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

// Cookie跨域设置
@GetMapping("/auth/login")
public AuthResponse login(@RequestBody LoginRequest request, HttpServletResponse response) {
    AuthResponse authResponse = authenticate(request);
    
    if (authResponse.isSuccess()) {
        // 设置全局token Cookie,支持所有子域名
        Cookie globalCookie = new Cookie("GLOBAL_TOKEN", authResponse.getGlobalToken());
        globalCookie.setDomain(".yourdomain.com"); // 关键:设置主域名
        globalCookie.setPath("/");
        globalCookie.setHttpOnly(true);
        globalCookie.setSecure(true); // HTTPS环境必须设置
        response.addCookie(globalCookie);
    }
    
    return authResponse;
}

第4步:登出同步,这是SSO的"安全门"

用户在一个系统登出,所有系统都要同步登出,否则就像家门没锁。

全局登出实现

@RestController
public class LogoutController {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @PostMapping("/auth/logout")
    public LogoutResponse globalLogout(@RequestParam String globalToken, HttpServletResponse response) {
        // 1. 删除全局会话
        String sessionKey = "sso:session:" + globalToken;
        redisTemplate.delete(sessionKey);
        
        // 2. 广播登出消息给所有子系统
        broadcastLogoutEvent(globalToken);
        
        // 3. 清除Cookie
        Cookie cookie = new Cookie("GLOBAL_TOKEN", "");
        cookie.setDomain(".yourdomain.com");
        cookie.setPath("/");
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        
        return LogoutResponse.success();
    }
    
    /**
     * 处理子系统的登出回调
     */
    @PostMapping("/auth/logout/callback")
    public void logoutCallback(@RequestParam String systemId, @RequestParam String globalToken) {
        String key = "sso:logout:" + globalToken;
        redisTemplate.opsForSet().add(key, systemId);
        redisTemplate.expire(key, Duration.ofMinutes(5));
    }
}

第5步:高可用架构,这是SSO的"防震设计"

认证中心挂了,整个公司的系统都登录不了,这责任你背不起。

Redis集群配置

# Redis集群配置
spring:
  redis:
    cluster:
      nodes:
        - redis-node1:6379
        - redis-node2:6379
        - redis-node3:6379
        - redis-node4:6379
        - redis-node5:6379
        - redis-node6:6379
      max-redirects: 3
    password: your_redis_password
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
        max-wait: 1000ms

# JWT配置
jwt:
  secret: "your_jwt_secret_key_must_be_very_long_and_secure"
  expiration: 3600 # 1小时
  refresh-expiration: 86400 # 24小时

Nginx负载均衡配置

upstream sso_backend {
    least_conn;
    server 10.0.1.10:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 weight=2 max_fails=3 fail_timeout=30s;
    
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name sso.yourdomain.com;
    
    ssl_certificate /etc/nginx/ssl/yourdomain.crt;
    ssl_certificate_key /etc/nginx/ssl/yourdomain.key;
    
    location / {
        proxy_pass http://sso_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # 超时设置
        proxy_connect_timeout 5s;
        proxy_send_timeout 10s;
        proxy_read_timeout 10s;
    }
}

三、实战案例:某电商平台SSO架构演进血泪史

下面分享一个真实的电商平台从日活1万到500万的SSO架构演进过程。

阶段1:单体架构(日活1万)

问题:20个子系统各自为政,用户每换一个系统就要重新登录

解决方案

  • 搭建独立的SSO认证中心
  • 每个系统集成SSO客户端
  • 使用Redis存储会话信息

代码实现

// 最简单的SSO实现
@GetMapping("/sso/login")
public String ssoLogin(@RequestParam String redirectUrl, HttpSession session) {
    if (session.getAttribute("user") != null) {
        // 已登录,生成ticket
        String ticket = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set("ticket:" + ticket, session.getAttribute("user"), 5, TimeUnit.MINUTES);
        return "redirect:" + redirectUrl + "?ticket=" + ticket;
    }
    return "login";
}

阶段2:分布式架构(日活10万)

问题:单点Redis挂了,整个SSO系统瘫痪

解决方案

  • Redis主从复制 + 哨兵模式
  • JWT Token替代session
  • 引入消息队列处理登出同步

阶段3:微服务架构(日活100万)

问题:系统越来越多,SSO认证中心压力巨大

解决方案

  • 认证中心集群部署
  • 引入OAuth2.0协议
  • 使用Spring Cloud Gateway统一网关

阶段4:亿级架构(日活500万)

问题:跨地域访问延迟高,用户体验差

解决方案

  • 多地部署SSO节点
  • DNS就近访问
  • 用户数据分片存储
  • 引入CDN加速静态资源

四、7个避坑指南,90%的人都踩过这些坑!

坑1:Cookie域名设置错误

问题:子系统收不到Cookie,登录状态无法共享

解决方案

// 正确设置域名,注意前面的点
cookie.setDomain(".yourdomain.com");
cookie.setPath("/");

坑2:Token过期时间设置不当

问题:Token过期太快,用户频繁登录;过期太慢,安全性差

解决方案

  • 全局Token:30分钟
  • JWT Token:1小时
  • Refresh Token:7天

坑3:跨域预检请求处理不当

问题:OPTIONS请求被拦截,导致跨域失败

解决方案

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    HttpServletResponse httpResponse = (HttpServletResponse) response;
    httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
    httpResponse.setHeader("Access-Control-Max-Age", "3600");
    
    if ("OPTIONS".equals(((HttpServletRequest) request).getMethod())) {
        httpResponse.setStatus(HttpServletResponse.SC_OK);
        return;
    }
    
    chain.doFilter(request, response);
}

坑4:登出同步不完整

问题:用户在一个系统登出,其他系统还能访问

解决方案

  • 使用Redis存储登出状态
  • 所有子系统定时检查
  • 引入WebSocket实时通知

坑5:敏感信息泄露

问题:JWT Token被解码,用户信息泄露

解决方案

  • JWT payload不要放敏感信息
  • 使用HTTPS传输
  • Token定期刷新

坑6:并发登录问题

问题:同一账号多人同时登录,相互踢出

解决方案

// 限制并发登录数
String concurrentKey = "user:concurrent:" + userId;
Set<String> tokens = redisTemplate.opsForSet().members(concurrentKey);
if (tokens != null && tokens.size() >= 3) {
    // 踢出最早登录的token
    String oldestToken = tokens.iterator().next();
    redisTemplate.opsForSet().remove(concurrentKey, oldestToken);
    redisTemplate.delete("sso:session:" + oldestToken);
}

坑7:移动端适配问题

问题:移动端Cookie不生效,SSO登录失败

解决方案

  • 移动端使用Header传递Token
  • 提供专门的移动端登录接口
  • 使用LocalStorage存储Token

五、5个核心监控指标,让你的SSO稳如老狗!

1. 登录成功率监控

@Component
public class LoginMetrics {
    
    private final MeterRegistry meterRegistry;
    
    @EventListener
    public void handleLoginSuccess(LoginSuccessEvent event) {
        meterRegistry.counter("sso.login.success", 
            "system", event.getSystemId()).increment();
    }
    
    @EventListener
    public void handleLoginFailure(LoginFailureEvent event) {
        meterRegistry.counter("sso.login.failure", 
            "reason", event.getFailureReason()).increment();
    }
}

2. Token验证延迟监控

@Around("@annotation(ValidateToken)")
public Object monitorTokenValidation(ProceedingJoinPoint joinPoint) {
    return meterRegistry.timer("sso.token.validation").recordCallable(() -> {
        return joinPoint.proceed();
    });
}

3. 系统健康检查

@RestController
public class HealthCheckController {
    
    @GetMapping("/health")
    public HealthResponse checkHealth() {
        Map<String, Object> checks = new HashMap<>();
        
        // 检查Redis连接
        try {
            redisTemplate.getConnectionFactory().getConnection().ping();
            checks.put("redis", "UP");
        } catch (Exception e) {
            checks.put("redis", "DOWN");
        }
        
        // 检查数据库连接
        try {
            jdbcTemplate.queryForObject("SELECT 1", Integer.class);
            checks.put("database", "UP");
        } catch (Exception e) {
            checks.put("database", "DOWN");
        }
        
        return new HealthResponse(checks);
    }
}

六、性能指标参考(实测数据)

经过500万日活用户的真实验证:

指标 数据 说明
登录接口QPS 2万 峰值可支持
Token验证QPS 10万 平均响应时间5ms
登录成功率 99.95% 包含网络异常
平均登录延迟 200ms 包含网络传输
系统可用性 99.9% 全年故障时间<8小时

七、总结:SSO成功的3个关键点

  1. 架构先行:先把认证中心、客户端、存储方案设计清楚,再动手编码
  2. 监控到位:登录成功率、Token验证延迟、系统健康度一个都不能少
  3. 容灾完备:Redis集群、应用集群、数据库主从,一个都不能缺

SSO单点登录不是简单的登录功能,而是一个完整的身份认证体系。设计好了,用户体验飞升;设计不好,全公司陪你背锅。

记住一句话:SSO设计得好是神器,设计不好就是定时炸弹!

举报

相关推荐

0 条评论