在多系统并存的现代企业环境中,反复登录认证是影响用户体验与效率的痛点。单点登录(SSO)技术旨在解决这一问题,实现“一次登录,通行所有系统”。本文将深入解析SSO的核心原理,并对比五种主流实现方案,提供从简单到企业级的实战代码示例。
一、SSO的核心价值:一次认证,处处通行
可以将其理解为统一门禁系统:在没有SSO时,进入公司的每个区域(系统)都需要单独刷卡(登录);而部署SSO后,仅需在主入口(认证中心)进行一次认证,即可畅通无阻地访问所有关联区域。
二、主流SSO实现方案详解
1. 基于Cookie的共享Session(同域简单方案)
适用场景:主域名相同下的多个子应用,如 app1.example.com 与 app2.example.com。
实现原理:
用户在一个子应用登录后,认证服务器将Session ID通过Cookie设置到根域名(如.example.com)。同一根域名下的其他应用在发起请求时会自动携带此Cookie,从而共享登录状态。
核心代码示例:
// 1. 在应用A登录,设置根域名Cookie
@PostMapping("/login")
public String login(String username, String password, HttpServletResponse response) {
// 验证用户
User user = userService.authenticate(username, password);
// 生成Session并存入共享存储(如Redis)
String sessionId = UUID.randomUUID().toString();
redis.set("session:" + sessionId, user.getId(), 3600);
// 关键:将Cookie的domain设置为根域名
Cookie cookie = new Cookie("SESSION_ID", sessionId);
cookie.setDomain(".example.com");
cookie.setPath("/");
cookie.setHttpOnly(true);
response.addCookie(cookie);
return "redirect:/home";
}
// 2. 在其他应用(如应用B)中验证Cookie
@GetMapping("/api/user")
public UserInfo getUserInfo(@CookieValue("SESSION_ID") String sessionId) {
String userId = redis.get("session:" + sessionId);
if (userId == null) {
throw new UnauthorizedException("会话已失效,请重新登录");
}
return userService.getUserInfo(userId);
}
方案优劣:
- 优点:实现极其简单,无额外网络开销。
- 缺点:严格受限於同域名;Cookie存在安全风险;对移动端原生应用支持不友好;全局注销困难。
- 适用建议:仅适用于对安全要求不高、且域名统一的简单内部系统。
2. 基于集中式Session的SSO(跨域传统方案)
适用场景:多个不同域名的系统,例如 hr.com 与 finance.com。
工作原理时序:
- 用户访问
a.com,因无本地Session被重定向至SSO中心sso.com。
- 在
sso.com完成登录,生成全局Session存入Redis,并产生一个一次性的临时凭证ticket。
- SSO中心将用户重定向回
a.com,并在URL中附带ticket。
a.com后台使用该ticket向sso.com发起验证请求,换取用户信息,并在本地创建会话。
- 用户访问
b.com时,重复上述流程。
关键实现代码:
// SSO认证中心控制器
@Controller
public class SsoController {
@PostMapping("/login")
public String login(String username, String password,
String redirectUrl, HttpServletResponse response) {
User user = userService.verify(username, password);
// 创建全局Session
String sessionId = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("sso:session:" + sessionId, user, 30, TimeUnit.MINUTES);
// 生成一次性Ticket
String ticket = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("sso:ticket:" + ticket, sessionId, 5, TimeUnit.MINUTES);
// 携带Ticket重定向回原系统
return "redirect:" + redirectUrl + "?ticket=" + ticket;
}
@GetMapping("/verify")
@ResponseBody
public User verify(@RequestParam String ticket) {
String sessionId = (String) redisTemplate.opsForValue().get("sso:ticket:" + ticket);
if (sessionId == null) {
throw new UnauthorizedException("无效的Ticket");
}
// 验证后立即删除Ticket,确保一次性使用
redisTemplate.delete("sso:ticket:" + ticket);
return (User) redisTemplate.opsForValue().get("sso:session:" + sessionId);
}
}
// 子系统(客户端)认证过滤器
@Component
public class SsoFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 检查本地Session是否存在
if (req.getSession().getAttribute("user") != null) {
chain.doFilter(request, response);
return;
}
// 检查请求中是否携带了SSO中心颁发的Ticket
String ticket = req.getParameter("ticket");
if (ticket != null) {
// 向SSO中心验证Ticket
String verifyUrl = "https://sso.com/verify?ticket=" + ticket;
try {
User user = restTemplate.getForObject(verifyUrl, User.class);
req.getSession().setAttribute("user", user);
chain.doFilter(request, response);
return;
} catch (Exception ignored) { }
}
// 既无本地Session也无有效Ticket,重定向至SSO登录页
String redirectUrl = URLEncoder.encode(req.getRequestURL().toString(), "UTF-8");
resp.sendRedirect("https://sso.com/login?redirect=" + redirectUrl);
}
}
方案优劣:
- 优点:支持跨域,安全性相比纯Cookie方案更高(Ticket一次性使用)。
- 缺点:强依赖共享的
Redis等中间件存储Session,带来了维护成本和网络调用延迟。在构建高可用架构时,Redis的稳定性至关重要。
- 适用建议:适用于有一定安全要求、且尚未全面转向前后端分离或
微服务架构的传统多域名系统。
3. 基于Token的SSO:JWT方案
适用场景:前后端分离应用、移动端APP、微服务架构。
核心思想:使用自包含的JSON Web Token (JWT) 作为认证凭证,服务端无需存储Session状态,通过验证Token签名和有效期即可确认真伪。
JWT结构示例:
Header(算法与类型).Payload(负载信息).Signature(签名)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0...SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT工具类与认证流程代码:
// JWT工具类,负责Token的生成与解析
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
public String generateToken(User user, Duration duration) {
long now = System.currentTimeMillis();
return Jwts.builder()
.setSubject(user.getId())
.claim("username", user.getUsername())
.setIssuedAt(new Date(now))
.setExpiration(new Date(now + duration.toMillis()))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
throw new UnauthorizedException("Token无效或已过期");
}
}
}
// 子系统(资源服务器)的JWT认证过滤器
@Component
public class JwtFilter implements Filter {
@Autowired
private JwtUtil jwtUtil;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String authHeader = req.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
// 本地验证Token签名和有效期,无需调用认证中心
Claims claims = jwtUtil.parseToken(token);
// 可将用户信息存入请求上下文,供后续业务使用
chain.doFilter(request, response);
return;
} catch (UnauthorizedException e) {
// Token验证失败
}
}
// 无有效Token,返回401状态码
((HttpServletResponse) response).setStatus(HttpStatus.UNAUTHORIZED.value());
}
}
方案优劣:
- 优点:无状态,减轻服务端存储压力;跨域支持好;特别适合
微服务间的认证;验证效率高。
- 缺点:Token一旦签发,在有效期内无法主动废止(需额外实现黑名单机制);Token内容虽可加密但默认仅签名,敏感信息不应放入Payload。
- 适用建议:是现代前后端分离应用和微服务的首选方案,需配合HTTPS、较短的Token有效期和Refresh Token机制来提升安全性。
4. OAuth 2.0 与 OpenID Connect (企业级标准方案)
适用场景:企业统一身份认证、第三方应用授权登录、云原生应用。
核心角色与授权码流程:
- 资源所有者 (Resource Owner):终端用户。
- 客户端 (Client):需要接入SSO的子系统。
- 授权服务器 (Authorization Server):SSO认证中心。
- 资源服务器 (Resource Server):提供API的后端服务。
标准授权码模式流程保证了高度的安全性,避免了Access Token直接暴露给前端。
使用Spring Security OAuth2的快速配置示例:
// 客户端应用配置
@Configuration
@EnableWebSecurity
public class OAuth2ClientConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login() // 启用OAuth2登录
.loginPage("/oauth2/authorization/sso-client")
.and()
.oauth2ResourceServer()
.jwt(); // 将JWT作为资源服务器Token
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(
ClientRegistration.withRegistrationId("sso-client")
.clientId("your-client-id")
.clientSecret("your-client-secret")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("http://your-app.com/login/oauth2/code/sso-client")
.scope("openid", "profile", "email")
.authorizationUri("https://sso.com/oauth2/authorize")
.tokenUri("https://sso.com/oauth2/token")
.jwkSetUri("https://sso.com/oauth2/jwks")
.clientName("SSO Client")
.build()
);
}
}
通过Spring Security等成熟框架可以极大地简化OAuth 2.0和OIDC的集成复杂度。
方案优劣:
- 优点:行业标准协议,安全性最高,生态完善,支持细粒度授权和主动令牌吊销。
- 缺点:协议本身较为复杂,实现和运维成本较高。
- 适用建议:对安全性要求严苛的企业级SSO、需要与第三方应用(如微信登录、GitHub登录)集成的场景。
5. SAML 2.0 (传统企业集成方案)
适用场景:需要与Active Directory (AD)、LDAP等传统企业目录服务集成的老牌系统。
工作原理简述:
- 用户访问服务提供商(SP)的应用。
- SP生成SAML认证请求,将用户浏览器重定向至身份提供商(IdP)。
- 用户在IdP(如公司AD)进行认证。
- IdP生成包含用户身份声明的SAML响应(XML格式),Post回SP。
- SP验证SAML响应的签名,断言用户身份,建立本地会话。
方案优劣:
- 优点:与企业目录服务集成度深,安全性高,在金融、政府等领域应用广泛。
- 缺点:基于XML,协议沉重且复杂,对现代Web和移动端支持不如OAuth/JWT灵活。
- 适用建议:主要用于维护需要与现有AD/LDAP深度绑定的传统企业级应用。
三、方案选型对比与实战建议
| 方案 |
跨域支持 |
移动端友好 |
实现复杂度 |
安全性 |
典型适用场景 |
| Cookie共享Session |
❌ |
❌ |
⭐ |
⭐ |
同根域名的简单内部系统 |
| 集中式Session |
✅ |
❌ |
⭐⭐ |
⭐⭐ |
跨域名的传统Web系统 |
| Token-Based (JWT) |
✅ |
✅ |
⭐⭐⭐ |
⭐⭐⭐ |
前后端分离、微服务、移动App |
| OAuth 2.0 + OIDC |
✅ |
✅ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
企业级SSO、第三方授权、云应用 |
| SAML 2.0 |
✅ |
△ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
传统企业、AD/LDAP集成 |
选型避坑指南:
- 避免使用Cookie方案实现跨域SSO,存在安全与兼容性问题。
- 避免使用集中式Session方案服务移动端原生应用,交互流程复杂。
- 谨慎在无额外管理手段(如黑名单)的情况下,将JWT用于需要强制下线功能的高安全系统。
- 对于新项目,优先考虑 OAuth 2.0 / OIDC 或 JWT 方案,它们更契合现代应用架构。
- 对于传统企业集成,SAML 2.0 仍是可靠选择,但需评估其长期维护成本。