作为一名后端开发,经历过太多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个关键点
- 架构先行:先把认证中心、客户端、存储方案设计清楚,再动手编码
- 监控到位:登录成功率、Token验证延迟、系统健康度一个都不能少
- 容灾完备:Redis集群、应用集群、数据库主从,一个都不能缺
SSO单点登录不是简单的登录功能,而是一个完整的身份认证体系。设计好了,用户体验飞升;设计不好,全公司陪你背锅。
记住一句话:SSO设计得好是神器,设计不好就是定时炸弹!