Spring Security OAuth2与Spring Session Redis整合实现微服务会话共享

Spring Security OAuth2与Spring Session Redis整合实现微服务会话共享

jonathan
2018-11-15 / 0 评论

Spring Security OAuth2与Spring Session Redis整合实现微服务会话共享

引言

在微服务架构中,会话管理是一个常见的挑战。当用户通过OAuth2认证后,如何在多个微服务之间共享会话信息成为一个亟待解决的问题。本文将详细介绍如何利用Spring Security OAuth2结合Spring Session Redis实现微服务架构下的会话共享方案。

技术栈

  • Spring Boot
  • Spring Security OAuth2
  • Spring Session
  • Redis
  • Spring Cloud (可选,用于服务发现等)

为什么选择Redis作为会话存储

在分布式系统中,使用Redis存储会话有以下优势:

  1. 高性能:Redis是基于内存的数据库,读写速度极快
  2. 数据持久化:支持数据持久化,防止数据丢失
  3. 丰富的数据结构:支持多种数据类型,适合存储复杂的会话数据
  4. 原生的过期机制:便于管理会话生命周期

实现步骤

1. 添加依赖

首先,我们需要在各个微服务的pom.xml文件中添加必要的依赖:

<dependencies>
    <!-- Spring Boot 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- 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>
    
    <!-- Spring Session Redis 依赖 -->
    <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>
</dependencies>

2. 配置Redis与Spring Session

在每个微服务的application.yml文件中添加Redis和Spring Session的配置:

spring:
  redis:
    host: localhost
    port: 6379
    password: # 如有密码则填写
    database: 0
  session:
    store-type: redis
    redis:
      namespace: spring:session  # Redis中存储会话的命名空间前缀
    timeout: 1800               # 会话超时时间(秒)
    
  security:
    oauth2:
      client:
        registration:
          custom-client:
            client-id: client-id
            client-secret: client-secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: read,write
        provider:
          custom-provider:
            authorization-uri: http://auth-server/oauth/authorize
            token-uri: http://auth-server/oauth/token
            user-info-uri: http://auth-server/userinfo
            user-name-attribute: name
            
server:
  servlet:
    session:
      cookie:
        http-only: true
        secure: true       # 在生产环境中应设置为true

3. 启用Spring Session

在主应用类上添加@EnableRedisHttpSession注解:

package com.example.microservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@SpringBootApplication
@EnableRedisHttpSession
public class MicroserviceApplication {

    public static void main(String[] args) {
        SpringApplication.run(MicroserviceApplication.class, args);
    }
}

4. 配置Spring Security OAuth2

创建Security配置类,配置OAuth2认证:

package com.example.microservice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .defaultSuccessUrl("/dashboard")
                .failureUrl("/login?error=true")
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout=true")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            );

        return http.build();
    }
}

5. 创建会话信息存储器

为了确保OAuth2认证信息能够在会话中正确存储和共享,创建自定义的会话信息存储器:

package com.example.microservice.session;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;

@Component
public class OAuth2SessionRepository<S extends Session> {

    private final SessionRepository<S> sessionRepository;

    public OAuth2SessionRepository(SessionRepository<S> sessionRepository) {
        this.sessionRepository = sessionRepository;
    }

    public void saveOAuth2Authentication(HttpServletRequest request, Authentication authentication) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            String sessionId = session.getId();
            S redisSession = sessionRepository.findById(sessionId);
            if (redisSession != null) {
                if (authentication instanceof OAuth2AuthenticationToken) {
                    OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
                    OAuth2User oauth2User = oauthToken.getPrincipal();
                    
                    // 存储用户信息和授权信息
                    redisSession.setAttribute("user_name", oauth2User.getAttribute("name"));
                    redisSession.setAttribute("user_email", oauth2User.getAttribute("email"));
                    redisSession.setAttribute("oauth2_provider", oauthToken.getAuthorizedClientRegistrationId());
                    
                    // 存储OAuth2授权信息(可根据需求扩展)
                    Map<String, Object> attributes = oauth2User.getAttributes();
                    for (Map.Entry<String, Object> entry : attributes.entrySet()) {
                        if (entry.getValue() instanceof String) {
                            redisSession.setAttribute("oauth2_attr_" + entry.getKey(), entry.getValue());
                        }
                    }
                    
                    sessionRepository.save(redisSession);
                }
            }
        }
    }

    public OAuth2User getOAuth2User(String sessionId) {
        S redisSession = sessionRepository.findById(sessionId);
        if (redisSession != null) {
            // 根据存储的会话信息重建OAuth2User
            // 这里仅为示例,实际实现需要根据具体情况调整
            return null; // 此处需实现具体逻辑
        }
        return null;
    }
}

6. 创建认证成功处理器

实现一个认证成功处理器,在用户成功认证后将OAuth2信息保存到Redis会话中:

package com.example.microservice.handler;

import com.example.microservice.session.OAuth2SessionRepository;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final OAuth2SessionRepository<?> oAuth2SessionRepository;

    public CustomAuthenticationSuccessHandler(OAuth2SessionRepository<?> oAuth2SessionRepository) {
        this.oAuth2SessionRepository = oAuth2SessionRepository;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 
                                       Authentication authentication) throws IOException, ServletException {
        // 保存OAuth2认证信息到Redis会话
        oAuth2SessionRepository.saveOAuth2Authentication(request, authentication);
        
        // 重定向到成功页面
        response.sendRedirect("/dashboard");
    }
}

7. 更新Security配置,使用自定义认证成功处理器

package com.example.microservice.config;

import com.example.microservice.handler.CustomAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomAuthenticationSuccessHandler authenticationSuccessHandler;

    public SecurityConfig(CustomAuthenticationSuccessHandler authenticationSuccessHandler) {
        this.authenticationSuccessHandler = authenticationSuccessHandler;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .successHandler(authenticationSuccessHandler)
                .failureUrl("/login?error=true")
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout=true")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            );

        return http.build();
    }
}

8. 创建一个拦截器,用于在请求中恢复认证信息

package com.example.microservice.interceptor;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class OAuth2AuthenticationInterceptor implements HandlerInterceptor {

    private final SessionRepository<? extends Session> sessionRepository;

    public OAuth2AuthenticationInterceptor(SessionRepository<? extends Session> sessionRepository) {
        this.sessionRepository = sessionRepository;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 检查是否已经有认证信息
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            HttpSession httpSession = request.getSession(false);
            if (httpSession != null) {
                String sessionId = httpSession.getId();
                Session redisSession = sessionRepository.findById(sessionId);
                
                if (redisSession != null) {
                    // 从Redis会话中恢复OAuth2认证信息
                    // 这里需要实现具体的逻辑,根据存储的信息重建OAuth2AuthenticationToken
                    OAuth2User oauth2User = createOAuth2UserFromSession(redisSession);
                    String registrationId = (String) redisSession.getAttribute("oauth2_provider");
                    
                    if (oauth2User != null && registrationId != null) {
                        OAuth2AuthenticationToken authentication = 
                            new OAuth2AuthenticationToken(oauth2User, oauth2User.getAuthorities(), registrationId);
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
        }
        return true;
    }

    private OAuth2User createOAuth2UserFromSession(Session session) {
        // 根据会话中存储的信息重建OAuth2User对象
        // 这里需要实现具体逻辑
        return null; // 实际实现中需要返回有效的OAuth2User
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                          ModelAndView modelAndView) {
        // 如果需要,可以在这里添加后处理逻辑
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                               Object handler, Exception ex) {
        // 如果需要,可以在这里添加完成后的逻辑
    }
}

9. 注册拦截器

package com.example.microservice.config;

import com.example.microservice.interceptor.OAuth2AuthenticationInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.SessionRepository;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final SessionRepository<?> sessionRepository;

    public WebConfig(SessionRepository<?> sessionRepository) {
        this.sessionRepository = sessionRepository;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new OAuth2AuthenticationInterceptor(sessionRepository));
    }
}

测试会话共享

创建一个控制器来测试会话共享功能:

package com.example.microservice.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;

@RestController
public class TestController {

    @GetMapping("/api/user-info")
    public Map<String, Object> getUserInfo(@AuthenticationPrincipal OAuth2User oauth2User, HttpSession session) {
        Map<String, Object> userInfo = new HashMap<>();
        
        if (oauth2User != null) {
            userInfo.put("name", oauth2User.getAttribute("name"));
            userInfo.put("email", oauth2User.getAttribute("email"));
            userInfo.put("authorities", oauth2User.getAuthorities());
        }
        
        userInfo.put("sessionId", session.getId());
        userInfo.put("creationTime", session.getCreationTime());
        userInfo.put("lastAccessedTime", session.getLastAccessedTime());
        
        return userInfo;
    }
    
    @GetMapping("/api/session-test")
    public Map<String, Object> testSession(HttpSession session) {
        // 获取或设置会话属性
        Object count = session.getAttribute("counter");
        int newCount = 1;
        
        if (count != null) {
            newCount = (Integer) count + 1;
        }
        
        session.setAttribute("counter", newCount);
        
        Map<String, Object> result = new HashMap<>();
        result.put("sessionId", session.getId());
        result.put("counter", newCount);
        result.put("message", "此计数在所有微服务中共享");
        
        return result;
    }
}

部署架构

在实际微服务架构中,通常会通过API网关统一管理认证和会话。这里是一个简化的部署架构图:

+----------------+     +----------------+
|  API Gateway   |     | Auth Service   |
|  (Zuul/Spring  |---->| (OAuth2 Server)|
|  Cloud Gateway)|     +----------------+
+-------+--------+              
        |                      
        v                      
+-------+--------+     +-------+--------+
| Microservice A |<--->| Redis Session  |
+----------------+     | Storage        |
        ^              +----------------+
        |                      ^
        v                      |
+-------+--------+             |
| Microservice B |-------------+
+----------------+

安全注意事项

在实现会话共享时,需要注意以下安全事项:

  1. 确保Redis服务器安全配置,启用密码认证
  2. 使用SSL/TLS加密Redis连接
  3. 会话ID应使用足够长的随机字符串,防止被猜测
  4. 设置合理的会话超时时间
  5. 使用securehttpOnly标志保护会话Cookie
  6. 考虑使用JWT等无状态认证方案作为补充

性能优化

为了提高系统性能,可以考虑以下优化措施:

  1. 配置Redis连接池
  2. 使用Redis集群提高可用性
  3. 合理设置会话数据的过期时间
  4. 仅存储必要的会话信息,避免存储大量数据
  5. 监控Redis性能指标,及时进行扩容

总结

通过Spring Security OAuth2与Spring Session Redis的整合,我们实现了微服务架构下的会话共享方案。这种方案具有以下优势:

  1. 统一的认证机制:通过OAuth2提供标准的认证流程
  2. 高效的会话存储:利用Redis高性能特性存储会话数据
  3. 透明的会话共享:微服务无需感知会话存储细节
  4. 良好的扩展性:可以方便地扩展到更多微服务

这种方案适用于大多数微服务架构,特别是那些需要保持用户状态的系统。通过合理配置和优化,可以构建一个安全、高效、可扩展的会话管理系统。

评论

博主关闭了当前页面的评论