找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2066

积分

0

好友

294

主题
发表于 2025-12-24 17:12:01 | 查看: 40| 回复: 0

在多系统并存的现代企业环境中,反复登录认证是影响用户体验与效率的痛点。单点登录(SSO)技术旨在解决这一问题,实现“一次登录,通行所有系统”。本文将深入解析SSO的核心原理,并对比五种主流实现方案,提供从简单到企业级的实战代码示例。

一、SSO的核心价值:一次认证,处处通行

可以将其理解为统一门禁系统:在没有SSO时,进入公司的每个区域(系统)都需要单独刷卡(登录);而部署SSO后,仅需在主入口(认证中心)进行一次认证,即可畅通无阻地访问所有关联区域。

二、主流SSO实现方案详解

1. 基于Cookie的共享Session(同域简单方案)

适用场景:主域名相同下的多个子应用,如 app1.example.comapp2.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.comfinance.com

工作原理时序

  1. 用户访问a.com,因无本地Session被重定向至SSO中心sso.com
  2. sso.com完成登录,生成全局Session存入Redis,并产生一个一次性的临时凭证ticket
  3. SSO中心将用户重定向回a.com,并在URL中附带ticket
  4. a.com后台使用该ticketsso.com发起验证请求,换取用户信息,并在本地创建会话。
  5. 用户访问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.0OIDC的集成复杂度。

方案优劣

  • 优点:行业标准协议,安全性最高,生态完善,支持细粒度授权和主动令牌吊销。
  • 缺点:协议本身较为复杂,实现和运维成本较高。
  • 适用建议:对安全性要求严苛的企业级SSO、需要与第三方应用(如微信登录、GitHub登录)集成的场景。

5. SAML 2.0 (传统企业集成方案)

适用场景:需要与Active Directory (AD)、LDAP等传统企业目录服务集成的老牌系统。

工作原理简述

  1. 用户访问服务提供商(SP)的应用。
  2. SP生成SAML认证请求,将用户浏览器重定向至身份提供商(IdP)。
  3. 用户在IdP(如公司AD)进行认证。
  4. IdP生成包含用户身份声明的SAML响应(XML格式),Post回SP。
  5. 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 / OIDCJWT 方案,它们更契合现代应用架构。
  • 对于传统企业集成SAML 2.0 仍是可靠选择,但需评估其长期维护成本。



上一篇:Falcon可靠硬件传输解析:对比CIPU eRDMA多路径
下一篇:树莓派CM0系统烧录与配置指南:从硬件准备到Home Assistant和MQTT部署
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 20:14 , Processed in 0.354304 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表