Spring Boot项目中实现单点登录(SSO)完整指南

单点登录(Single Sign-On, SSO)是一种身份验证机制,允许用户使用一组凭证(如用户名和密码)登录多个相关但独立的系统。

一、单点登录的核心原理

SSO的核心原理使集中认证、分散授权,主要流程如下:

1.用户访问应用A

2.应用A检查本地会话,发现未登录

3.重定向到SSO认证中心

4.用户在认证中心登录

5.认证中心创建全局会话,并颁发令牌

6.用户携带令牌返回应用A

7.应用A向认证中心验证令牌

8.认证中心返回用户信息,应用A创建本地会话

9.用户访问应用B时重复2-8流程(但无需重复登录)

二、Spring Boot实现SSO的三种主流方案

方案1:基于OAuth2的实现(推荐)
1.添加依赖
xml 复制代码
<!-- Spring Security OAuth2 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
2.认证中心配置
java 复制代码
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
            .withClient("client1")
            .secret(passwordEncoder().encode("secret1"))
            .authorizedGrantTypes("authorization_code", "refresh_token")
            .scopes("read", "write")
            .redirectUris("http://localhost:8081/login/oauth2/code/client1")
            .autoApprove(true);
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
3.资源服务配置
java 复制代码
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/api/**").authenticated()
            .anyRequest().permitAll();
    }
}
4.客户端应用配置
yaml 复制代码
# application.yml
spring:
  security:
    oauth2:
      client:
        registration:
          sso:
            client-id: client1
            client-secret: secret1
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: read,write
        provider:
          sso:
            issuer-uri: http://localhost:8080
方案2:基于JWT实现
1.添加依赖
xml 复制代码
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
2.JWT工具类
java 复制代码
public class JwtTokenUtil {
    
    private static final String SECRET = "your-secret-key";
    private static final long EXPIRATION = 86400000; // 24小时
    
    public static String generateToken(UserDetails userDetails) {
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
            .signWith(SignatureAlgorithm.HS512, SECRET)
            .compact();
    }
    
    public static String getUsernameFromToken(String token) {
        return Jwts.parser()
            .setSigningKey(SECRET)
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
}
3.认证过滤器
java 复制代码
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain chain) throws IOException, ServletException {
        String token = resolveToken(request);
        
        if (token != null && validateToken(token)) {
            String username = JwtTokenUtil.getUsernameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
        chain.doFilter(request, response);
    }
    
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
方案3:基于CAS实现
1.添加CAS客户端依赖
xml 复制代码
<dependency>
    <groupId>org.jasig.cas.client</groupId>
    <artifactId>cas-client-support-springboot</artifactId>
    <version>3.6.4</version>
</dependency>

2.CAS配置

java 复制代码
@Configuration
public class CasConfig {
    
    @Value("${cas.server.url}")
    private String casServerUrl;
    
    @Value("${cas.service.url}")
    private String serviceUrl;
    
    @Bean
    public FilterRegistrationBean<AuthenticationFilter> casAuthenticationFilter() {
        FilterRegistrationBean<AuthenticationFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new AuthenticationFilter());
        registration.addInitParameter("casServerLoginUrl", casServerUrl + "/login");
        registration.addInitParameter("serverName", serviceUrl);
        registration.addUrlPatterns("/*");
        return registration;
    }
    
    @Bean
    public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> casSingleSignOutListener() {
        return new ServletListenerRegistrationBean<>(new SingleSignOutHttpSessionListener());
    }
}

三、SSO实现的关键技术点

1.会话管理
  • 分布式会话:使用Redis存储会话信息

    java 复制代码
    @Bean
    public RedisIndexedSessionRepository sessionRepository(RedisOperations<String, Object> redisOperations) {
        return new RedisIndexedSessionRepository(redisOperations);
    }
  • Session共享配置

    yaml 复制代码
    spring:
      session:
        store-type: redis
        redis:
          flush-mode: on_save
          namespace: spring:session
2.跨域问题解决
java 复制代码
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins("*")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600);
    }
}
3.安全配置
java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/login", "/oauth/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login")
            .and()
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessUrl("/")
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID");
    }
}

四、SSO实现的最佳实践

1.安全性考虑:
  • 使用HTTPS加密所有通信
  • 实现令牌的短期有效性(设置合理的过期时间)
  • 防范CSRF攻击
2.性能优化:
  • 使用缓存减少令牌验证的数据库查询
  • 实现令牌的自动续期机制
3.用户体验:
  • 实现无缝跳转,避免多次重定向
  • 提供清晰的登录状态提示
4.监控与日志
  • 记录所有认证事件
  • 实现异常登录的告警机制

五、三种SSO方案对比

方案 优点 缺点 适用场景
OAuth2 标准协议,安全性高,扩展性强 实现复杂度较高 企业级应用,多平台集成
JWT 无状态,性能好,适合分布式系统 令牌无法主动失效 微服务架构,前后端分离
CAS 专为SSO设计,功能完善 需要额外部署CAS服务器 传统企业应用,教育系统

六、常见问题解决方案

1.令牌失效问题
  • 实现令牌刷新机制
  • 使用Redis黑名单管理已注销令牌
2.跨域会话问题
  • 设置正确的Cookie域和路径

    java 复制代码
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("JSESSIONID");
        serializer.setCookiePath("/");
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        return serializer;
    }
3.多因素认证集成
java 复制代码
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/login").permitAll()
        .antMatchers("/mfa-verify").hasRole("PRE_AUTH")
        .anyRequest().fullyAuthenticated()
        .and()
        .formLogin()
        .loginPage("/login")
        .successHandler((request, response, authentication) -> {
            if (needsMfa(authentication)) {
                response.sendRedirect("/mfa-verify");
            } else {
                response.sendRedirect("/home");
            }
        });
}
相关推荐
Mr__Miss4 分钟前
微服务中引入公共拦截器
java·微服务·架构
Asthenia041216 分钟前
ElasticSearch8.x+SpringBoot3.X联调踩坑指南
后端
刘大浪19 分钟前
JDK17 与JDK8 共同存在一个电脑上
java
gou1234123440 分钟前
【Golang进阶】第八章:并发编程基础——从Goroutine调度到Channel通信实战
开发语言·后端·golang
秋难降42 分钟前
贪心算法:看似精明的 “短视选手”,用好了也能逆袭!💥
java·算法
程序小武43 分钟前
python编辑器如何选择?
后端·python
陈随易1 小时前
薪资跳动,VSCode实时显示今日打工收入
前端·后端·程序员
阿蒙Amon1 小时前
C#数字金额转中文大写金额:代码解析
java·mysql·c#
失乐园1 小时前
电商/物流/IoT三大场景:用MongoDB设计高扩展数据架构的最佳实践
java·后端·架构
五行星辰1 小时前
Spring AI 实战:用 Java 搞 AI,从此告别调参侠
java·后端