SpringBoot实现单点登录(SSO)的4种方案

单点登录(Single Sign-On,SSO)是企业应用系统中常见的用户认证方案,它允许用户使用一组凭证访问多个相关但独立的系统,无需重复登录。对于拥有多个应用的企业来说,SSO可以显著提升用户体验并降低凭证管理成本。

一、基于Cookie-Session的传统SSO方案

原理

这是最基础的SSO实现方式,其核心是将用户认证状态存储在服务端Session中,并通过Cookie在客户端保存Session标识符。当用户登录SSO服务器后,服务器创建Session存储用户信息,并将SessionID通过Cookie设置在顶级域名下,使所有子域应用都能访问该Cookie并验证同一个Session。

实现方案

  1. 创建SSO服务端
@RestController
@RequestMapping("/sso")
public class SsoServerController {

    @Autowired
    private UserService userService;
    
    @PostMapping("/login")
    public ResponseEntity<String> login(
            @RequestParam String username,
            @RequestParam String password,
            HttpServletRequest request,
            HttpServletResponse response,
            @RequestParam(required = false) String redirect) {
        
        // 验证用户凭证
        User user = userService.authenticate(username, password);
        if (user == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
        }
        
        // 创建session并存储用户信息
        HttpSession session = request.getSession(true);
        session.setAttribute("USER_INFO", user);
        session.setMaxInactiveInterval(3600); // Session有效期1小时
        
        // 获取SessionID
        String sessionId = session.getId();
        
        // 设置Cookie
        Cookie cookie = new Cookie("SSO_SESSION_ID", sessionId);
        cookie.setMaxAge(3600); // 1小时有效期
        cookie.setPath("/");
        cookie.setDomain(".example.com"); // 关键:顶级域名,使所有子域都能访问
        cookie.setHttpOnly(true);
        cookie.setSecure(true); // 仅通过HTTPS传输
        
        response.addCookie(cookie);
        
        // 如果有重定向URL,则重定向回原应用
        if (redirect != null && !redirect.isEmpty()) {
            return ResponseEntity.status(HttpStatus.FOUND)
                .header("Location", redirect)
                .build();
        }
        
        return ResponseEntity.ok("Login successful");
    }
    
    @GetMapping("/validate")
    public ResponseEntity<UserInfo> validateSession(
            @CookieValue(name = "SSO_SESSION_ID", required = false) String sessionId) {
        
        if (sessionId == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        
        // 验证Session有效性并获取用户信息
        HttpSession session = sessionRegistry.getSession(sessionId);
        if (session == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        
        // 从Session获取用户信息
        User user = (User) session.getAttribute("USER_INFO");
        if (user == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        
        // 返回用户信息
        UserInfo userInfo = new UserInfo(user.getId(), user.getUsername(), user.getRoles());
        return ResponseEntity.ok(userInfo);
    }
    
    @PostMapping("/logout")
    public ResponseEntity<Void> logout(
            @CookieValue(name = "SSO_SESSION_ID", required = false) String sessionId,
            HttpServletResponse response,
            @RequestParam(required = false) String redirect) {
        
        // 使Session失效
        if (sessionId != null) {
            HttpSession session = sessionRegistry.getSession(sessionId);
            if (session != null) {
                session.invalidate();
            }
        }
        
        // 删除Cookie
        Cookie cookie = new Cookie("SSO_SESSION_ID", null);
        cookie.setMaxAge(0);
        cookie.setPath("/");
        cookie.setDomain(".example.com");
        response.addCookie(cookie);
        
        // 如果有重定向URL,则重定向
        if (redirect != null && !redirect.isEmpty()) {
            return ResponseEntity.status(HttpStatus.FOUND)
                .header("Location", redirect)
                .build();
        }
        
        return ResponseEntity.ok().build();
    }
}
  1. 客户端应用集成
@Component
public class SsoFilter implements Filter {

    @Autowired
    private RestTemplate restTemplate;
    
    private static final String SSO_SERVER_URL = "https://sso.example.com";
    private static final String SSO_VALIDATION_URL = SSO_SERVER_URL + "/sso/validate";
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // 检查当前应用是否已有本地Session
        HttpSession currentSession = httpRequest.getSession(false);
        if (currentSession != null && currentSession.getAttribute("USER_INFO") != null) {
            // 已有本地Session,继续请求
            chain.doFilter(request, response);
            return;
        }
        
        // 获取SSO Session Cookie
        Cookie[] cookies = httpRequest.getCookies();
        String ssoSessionId = null;
        
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("SSO_SESSION_ID".equals(cookie.getName())) {
                    ssoSessionId = cookie.getValue();
                    break;
                }
            }
        }
        
        // 未找到SSO Cookie,重定向到SSO登录页
        if (ssoSessionId == null) {
            String redirectUrl = SSO_SERVER_URL + "/login?redirect=" + 
                                URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8");
            httpResponse.sendRedirect(redirectUrl);
            return;
        }
        
        // 验证SSO Session有效性
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.add("Cookie", "SSO_SESSION_ID=" + ssoSessionId);
            HttpEntity<String> entity = new HttpEntity<>(headers);
            
            ResponseEntity<UserInfo> responseEntity = restTemplate.exchange(
                    SSO_VALIDATION_URL, HttpMethod.GET, entity, UserInfo.class);
            
            if (responseEntity.getStatusCode() == HttpStatus.OK) {
                // SSO Session有效,创建本地Session
                UserInfo userInfo = responseEntity.getBody();
                HttpSession session = httpRequest.getSession(true);
                session.setAttribute("USER_INFO", userInfo);
                
                chain.doFilter(request, response);
            } else {
                // SSO Session无效,重定向到登录页
                String redirectUrl = SSO_SERVER_URL + "/login?redirect=" + 
                                    URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8");
                httpResponse.sendRedirect(redirectUrl);
            }
        } catch (Exception e) {
            // 验证过程出错,重定向到登录页
            String redirectUrl = SSO_SERVER_URL + "/login?redirect=" + 
                                URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8");
            httpResponse.sendRedirect(redirectUrl);
        }
    }
}

优缺点

优点:

  • 实现相对简单,遵循传统Web开发模式
  • 服务端完全控制会话状态和生命周期
  • 客户端无需存储和管理复杂状态
  • 支持即时会话失效和撤销

缺点:

  • 受同源策略限制,仅适用于同一顶级域名下的应用
  • 依赖Cookie机制,在某些环境可能受限(如移动应用)
  • 存在CSRF风险

二、基于JWT的无状态SSO方案

原理

JWT(JSON Web Token)是一种紧凑的、自包含的令牌格式,可以在不同应用间安全地传递信息。使用JWT实现SSO时,认证服务器在用户登录后生成JWT令牌,其中包含用户相关信息和签名。由于JWT可以独立验证而无需查询中央服务器,它非常适合构建无状态的SSO系统。

实现方案

  1. 创建JWT认证服务

首先添加依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

实现JWT工具类:

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secretKey;
    
    @Value("${jwt.expiration}")
    private long expirationTime;
    
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }
    
    public String generateToken(User user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", user.getId());
        claims.put("username", user.getUsername());
        claims.put("email", user.getEmail());
        claims.put("roles", user.getRoles());
        
        return createToken(claims, user.getUsername());
    }
    
    private String createToken(Map<String, Object> claims, String subject) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expirationTime);
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS512)
                .compact();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    public Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

实现认证控制器:

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private UserService userService;
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        // 验证用户凭证
        User user = userService.authenticate(
                loginRequest.getUsername(), 
                loginRequest.getPassword()
        );
        
        if (user == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(new ErrorResponse("Invalid credentials"));
        }
        
        // 生成JWT
        String jwt = jwtUtil.generateToken(user);
        
        // 设置刷新令牌(可选)
        String refreshToken = UUID.randomUUID().toString();
        userService.saveRefreshToken(user.getId(), refreshToken, 30); // 30天有效期
        
        // 返回Token
        return ResponseEntity.ok(new JwtResponse(jwt, refreshToken));
    }
    
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
        String refreshToken = request.getRefreshToken();
        
        // 验证刷新令牌
        User user = userService.findUserByRefreshToken(refreshToken);
        if (user == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(new ErrorResponse("Invalid refresh token"));
        }
        
        // 生成新的JWT
        String newToken = jwtUtil.generateToken(user);
        
        return ResponseEntity.ok(new JwtResponse(newToken, refreshToken));
    }
    
    @PostMapping("/validate")
    public ResponseEntity<?> validateToken(@RequestBody TokenValidationRequest request) {
        String token = request.getToken();
        
        if (!jwtUtil.validateToken(token)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(new ErrorResponse("Invalid token"));
        }
        
        // 提取用户信息
        Claims claims = jwtUtil.extractAllClaims(token);
        Map<String, Object> userInfo = new HashMap<>(claims);
        
        return ResponseEntity.ok(userInfo);
    }
    
    @PostMapping("/logout")
    public ResponseEntity<?> logout(@RequestBody LogoutRequest request) {
        // 删除刷新令牌
        userService.removeRefreshToken(request.getRefreshToken());
        
        // JWT本身无法撤销,客户端需要丢弃令牌
        return ResponseEntity.ok(new SuccessResponse("Logout successful"));
    }
}
  1. 客户端应用集成

JWT过滤器:

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        
        final String authorizationHeader = request.getHeader("Authorization");
        
        String username = null;
        String jwt = null;
        
        // 从Authorization头提取JWT
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            try {
                // 验证令牌并提取用户信息
                if (jwtUtil.validateToken(jwt)) {
                    Claims claims = jwtUtil.extractAllClaims(jwt);
                    username = claims.getSubject();
                    
                    // 构建认证对象
                    List<String> roles = (List<String>) claims.get("roles");
                    List<GrantedAuthority> authorities = roles.stream()
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());
                    
                    UserDetails userDetails = new User(username, "", authorities);
                    
                    UsernamePasswordAuthenticationToken authentication = 
                            new UsernamePasswordAuthenticationToken(
                                    userDetails, null, userDetails.getAuthorities());
                                    
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            } catch (Exception e) {
                // JWT验证失败,不设置认证信息
            }
        }
        
        chain.doFilter(request, response);
    }
}

安全配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtRequestFilter jwtRequestFilter;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/public/**", "/auth/login", "/auth/refresh").permitAll()
            .anyRequest().authenticated()
            .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
  1. 前端处理JWT
// 在登录成功后存储JWT令牌
function handleLoginSuccess(response) {
    const { token, refreshToken } = response.data;
    localStorage.setItem('jwtToken', token);
    localStorage.setItem('refreshToken', refreshToken);
    
    // 设置默认Authorization头
    axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
    
    // 重定向到应用页面
    window.location.href = '/dashboard';
}

// 添加请求拦截器自动附加令牌
axios.interceptors.request.use(config => {
    const token = localStorage.getItem('jwtToken');
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});

// 添加响应拦截器处理令牌过期
axios.interceptors.response.use(
    response => response,
    async error => {
        const originalRequest = error.config;
        
        // 如果是401错误且不是刷新令牌请求,尝试刷新令牌
        if (error.response.status === 401 && !originalRequest._retry) {
            originalRequest._retry = true;
            
            try {
                const refreshToken = localStorage.getItem('refreshToken');
                const response = await axios.post('/auth/refresh', { refreshToken });
                
                const { token } = response.data;
                localStorage.setItem('jwtToken', token);
                axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
                
                return axios(originalRequest);
            } catch (err) {
                // 刷新失败,重定向到登录页
                localStorage.removeItem('jwtToken');
                localStorage.removeItem('refreshToken');
                window.location.href = '/login';
                return Promise.reject(err);
            }
        }
        
        return Promise.reject(error);
    }
);

优缺点

优点:

  • 完全无状态,服务器不需要存储会话信息
  • 跨域支持,适用于分布式系统和微服务
  • 可扩展性好,JWT可包含丰富的用户信息
  • 不依赖Cookie,避免CSRF问题
  • 适用于各种客户端(Web、移动应用、API)

缺点:

  • 无法主动失效已颁发的令牌(除非使用黑名单机制)
  • JWT可能较大,增加网络传输负担
  • 令牌管理需要客户端介入
  • 刷新令牌机制较复杂
  • 存在令牌被盗用的风险

三、基于OAuth 2.0/OpenID Connect的SSO方案

原理

OAuth 2.0是一个授权框架,而OpenID Connect(OIDC)是建立在OAuth 2.0之上的身份认证层。这是目前最标准化和完善的SSO解决方案,特别适合企业级应用和需要第三方集成的场景。它提供了丰富的授权流程选项和安全特性。

实现方案

在SpringBoot中,可以使用Spring Security OAuth2来实现OAuth 2.0/OIDC服务器和客户端。

  1. 搭建授权服务器

首先添加依赖:

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.6.8</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

配置授权服务器:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private DataSource dataSource;
    
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("your-signing-key");
        return converter;
    }
    
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }
    
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new CustomTokenEnhancer();
    }
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(
                Arrays.asList(tokenEnhancer(), accessTokenConverter()));
                
        endpoints
            .tokenStore(tokenStore())
            .tokenEnhancer(tokenEnhancerChain)
            .authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService)
            .reuseRefreshTokens(false);
    }
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource)
            .withClient("web-client")
            .secret(passwordEncoder.encode("web-client-secret"))
            .authorizedGrantTypes("password", "refresh_token", "authorization_code")
            .scopes("read", "write")
            .redirectUris("http://app1.example.com/login/oauth2/code/custom",
                          "http://app2.example.com/login/oauth2/code/custom")
            .accessTokenValiditySeconds(3600)
            .refreshTokenValiditySeconds(86400)
            .and()
            .withClient("mobile-client")
            .secret(passwordEncoder.encode("mobile-client-secret"))
            .authorizedGrantTypes("password", "refresh_token")
            .scopes("read")
            .accessTokenValiditySeconds(7200)
            .refreshTokenValiditySeconds(259200);
    }
    
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
            .tokenKeyAccess("permitAll()")
            .checkTokenAccess("isAuthenticated()")
            .allowFormAuthenticationForClients();
    }
}

// 自定义令牌增强器,添加额外的用户信息
public class CustomTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        if (authentication.getPrincipal() instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            Map<String, Object> additionalInfo = new HashMap<>();
            
            // 添加额外的用户信息
            if (userDetails instanceof CustomUserDetails) {
                CustomUserDetails customUserDetails = (CustomUserDetails) userDetails;
                additionalInfo.put("userId", customUserDetails.getId());
                additionalInfo.put("email", customUserDetails.getEmail());
                additionalInfo.put("fullName", customUserDetails.getFullName());
            }
            
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        }
        
        return accessToken;
    }
}

配置资源服务器:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/api/public/**").permitAll()
            .antMatchers("/api/user/**").hasRole("USER")
            .antMatchers("/api/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated();
    }
}
  1. 客户端应用集成

客户端配置(application.yml):

spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: web-client
            client-secret: web-client-secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: read,write
        provider:
          custom:
            authorization-uri: http://auth-server.example.com/oauth/authorize
            token-uri: http://auth-server.example.com/oauth/token
            user-info-uri: http://auth-server.example.com/userinfo
            jwk-set-uri: http://auth-server.example.com/.well-known/jwks.json
            user-name-attribute: sub

客户端安全配置:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests(authorizeRequests ->
                authorizeRequests
                    .antMatchers("/", "/login/**", "/error", "/webjars/**").permitAll()
                    .anyRequest().authenticated()
            )
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .loginPage("/login")
                    .defaultSuccessUrl("/home")
                    .userInfoEndpoint()
                    .userService(oAuth2UserService())
            )
            .logout(logout ->
                logout
                    .logoutSuccessUrl("http://auth-server.example.com/logout?client_id=web-client")
                    .invalidateHttpSession(true)
                    .clearAuthentication(true)
                    .deleteCookies("JSESSIONID")
            );
    }
    
    @Bean
    public OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService() {
        DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
        
        return userRequest -> {
            OAuth2User oAuth2User = delegate.loadUser(userRequest);
            
            // 自定义用户信息处理
            Map<String, Object> attributes = oAuth2User.getAttributes();
            String userNameAttributeName = userRequest.getClientRegistration()
                    .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
            
            return new DefaultOAuth2User(
                    oAuth2User.getAuthorities(),
                    attributes,
                    userNameAttributeName);
        };
    }
}
  1. 完整的登出流程
@Controller
public class AuthController {

    @GetMapping("/logout")
    public String logout(HttpServletRequest request,
                         HttpServletResponse response,
                         @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
        
        // 清除本地会话
        SecurityContextHolder.clearContext();
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        
        // 清除Cookies
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                cookie.setValue("");
                cookie.setPath("/");
                cookie.setMaxAge(0);
                response.addCookie(cookie);
            }
        }
        
        // 重定向到OAuth2服务器的注销端点
        String logoutUrl = "http://auth-server.example.com/logout?client_id=" +
                authorizedClient.getClientRegistration().getClientId() +
                "&post_logout_redirect_uri=" +
                URLEncoder.encode("http://app.example.com/", StandardCharsets.UTF_8);
        
        return "redirect:" + logoutUrl;
    }
}

优缺点

优点:

  • 成熟的安全协议,广泛采用的行业标准
  • 支持多种认证流程(授权码、隐式、密码等)
  • 令牌撤销机制完善
  • 可扩展性极好,适合企业级应用
  • 明确分离认证与授权职责

缺点:

  • 实现复杂度高,小型应用可能不合适
  • 配置和理解学习曲线陡峭

四、基于Spring Session的共享会话SSO方案

原理

Spring Session提供了一个将会话数据存储在共享外部存储(如Redis)中的框架,使得不同的应用之间能够共享会话信息。这种方式特别适合基于Spring的同构系统,可以在保持简单实现的同时解决分布式会话共享问题。

实现方案

  1. 添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  1. 配置Spring Session
@Configuration
@EnableRedisHttpSession
public class SessionConfig {

    @Bean
    public LettuceConnectionFactory connectionFactory() {
        // Redis连接配置
        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
        redisConfig.setHostName("redis-host");
        redisConfig.setPort(6379);
        
        return new LettuceConnectionFactory(redisConfig);
    }
    
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("SESSION"); // 使用统一的Cookie名
        serializer.setCookiePath("/");
        serializer.setDomainNamePattern("^.+?\.(\w+\.[a-z]+)$"); // 支持子域
        serializer.setUseSecureCookie(true);
        serializer.setUseHttpOnlyCookie(true);
        
        return serializer;
    }
    
    @Bean
    public HttpSessionIdResolver httpSessionIdResolver() {
        return new CookieHttpSessionIdResolver();
    }
}
  1. 创建中央认证服务
@Controller
@RequestMapping("/auth")
public class CentralAuthController {

    @Autowired
    private UserService userService;
    
    @GetMapping("/login")
    public String loginPage(@RequestParam(required = false) String redirect, Model model) {
        model.addAttribute("redirectUrl", redirect);
        return "login";
    }
    
    @PostMapping("/login")
    public String login(@RequestParam String username,
                       @RequestParam String password,
                       @RequestParam(required = false) String redirect,
                       HttpSession session,
                       RedirectAttributes redirectAttrs) {
        
        // 验证用户凭证
        User user = userService.authenticate(username, password);
        if (user == null) {
            redirectAttrs.addFlashAttribute("error", "Invalid credentials");
            return "redirect:/auth/login";
        }
        
        // 将用户信息存入共享会话
        UserInfo userInfo = new UserInfo(user.getId(), user.getUsername(), user.getRoles());
        session.setAttribute("USER_INFO", userInfo);
        
        // 如果有重定向URL,则重定向回原应用
        if (redirect != null && !redirect.isEmpty()) {
            return "redirect:" + redirect;
        }
        
        return "redirect:/dashboard";
    }
    
    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpSession session) {
        // 使会话失效
        session.invalidate();
        
        // 可选:获取要重定向的URL
        String referer = request.getHeader("Referer");
        
        return "redirect:/auth/login";
    }
}
  1. 创建应用内认证过滤器
@Component
public class SessionAuthenticationFilter implements Filter {

    private static final String LOGIN_PAGE = "http://auth.example.com/auth/login";
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // 公开路径不需要认证
        String path = httpRequest.getRequestURI();
        if (isPublicPath(path)) {
            chain.doFilter(request, response);
            return;
        }
        
        // 检查session中是否有用户信息
        HttpSession session = httpRequest.getSession(false);
        boolean authenticated = session != null && session.getAttribute("USER_INFO") != null;
        
        if (authenticated) {
            // 用户已认证,继续请求
            chain.doFilter(request, response);
        } else {
            // 未认证,重定向到登录页面
            String redirectUrl = LOGIN_PAGE + "?redirect=" + 
                                URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8");
            httpResponse.sendRedirect(redirectUrl);
        }
    }
    
    private boolean isPublicPath(String path) {
        return path.startsWith("/public/") || 
               path.startsWith("/resources/") || 
               path.equals("/error");
    }
}
  1. 配置应用内WebMVC
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private SessionAuthenticationFilter sessionAuthenticationFilter;
    
    @Bean
    public FilterRegistrationBean<SessionAuthenticationFilter> sessionFilterRegistration() {
        FilterRegistrationBean<SessionAuthenticationFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(sessionAuthenticationFilter);
        registration.addUrlPatterns("/*");
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); // 在Spring Security之后执行
        return registration;
    }
}
  1. 使用会话信息
@Controller
@RequestMapping("/dashboard")
public class DashboardController {

    @GetMapping
    public String dashboard(HttpSession session, Model model) {
        UserInfo userInfo = (UserInfo) session.getAttribute("USER_INFO");
        
        model.addAttribute("user", userInfo);
        
        return "dashboard";
    }
}

优缺点

优点:

  • 实现简单,易于理解
  • 与Spring生态无缝集成
  • 会话可包含丰富的信息

缺点:

  • 依赖中央存储(如Redis)
  • 会话数据需要序列化/反序列化
  • 依赖Cookie,不适合非Web应用

五、方案选择与最佳实践

选择建议

方案类型 推荐场景 不适合场景
Cookie-Session 同域名下的小型应用,简单的认证需求 跨域应用,移动应用集成,高安全性需求
JWT 分布式微服务,前后端分离应用 需要即时撤销令牌的场景,极高安全性要求
OAuth 2.0/OIDC 企业级应用,需要第三方集成,多租户系统 小型应用,资源受限环境,急速开发需求
Spring Session Spring技术栈的应用,中型企业应用 异构技术栈,非Web应用集成

六、总结

从简单的基于Cookie-Session的方案,到复杂的OAuth 2.0/OIDC实现,SSO方案的选择应该基于业务需求、安全要求、用户体验目标和技术约束进行综合考量。

本文来自 风象南

下载说明:① 请不要相信网站的任何广告;② 当你使用手机访问网盘时,网盘会诱导你下载他们的APP,大家不要去下载,直接把浏览器改成“电脑模式/PC模式”访问,然后免费普通下载即可;③ 123云盘限制,必须登录后才能下载,且限制每人每天下载流量1GB,下载 123云盘免流量破解工具

版权声明:
小编:吾乐吧
链接:https://wuleba.com/49.html
来源:吾乐吧软件站
本站资源仅供个人学习交流,请于下载后 24 小时内删除,不允许用于商业用途,否则法律问题自行承担。

THE END
分享
二维码
打赏
< <上一篇
下一篇>>