【源码在文末】SpringSession实战使用(基于SpringBoot项目)

发布时间:2022-08-18 18:44

spring-boot 整合 spring-session 的自动配置可谓是开箱即用,极其简洁和方便。这篇文章即介绍 spring-boot 整合 spring-session,这里只介绍基于 RedisSession 的实战。

考虑到 RedisSession 模块与 spring-session v2.0.6 版本的差异很小,且能够与 spring-boot v2.0.0 兼容,所以实战篇是基于 spring-boot v2.0.0 基础上配置 spring-session。

配置 spring-session

引入 spring-session 的 pom 配置,由于 spring-boot 包含 spring-session 的 starter 模块,所以 pom 中依赖:


 org.springframework.session
 spring-session-data-redis


编写 spring boot 启动类 SessionExampleApplication

/**
 * 启动类
 *
 * @author huaijin
 */
@SpringBootApplication
public class SessionExampleApplication {

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

配置 application.yml

spring:
  session:
    redis:
      flush-mode: on_save
      namespace: session.example
      cleanup-cron: 0 * * * * *
    store-type: redis
    timeout: 1800
  redis:
    host: localhost
    port: 6379
    jedis:
      pool:
        max-active: 100
        max-wait: 10
        max-idle: 10
        min-idle: 10
    database: 0

编写 controller

编写登录控制器,登录时创建 session,并将当前登录用户存储 sesion 中。登出时,使 session 失效。

/**
 * 登录控制器
 *
 * @author huaijin
 */
@RestController
public class LoginController {

    private static final String CURRENT_USER = "currentUser";

    /**
     * 登录
     *
     * @param loginVo 登录信息
     *
     * @author huaijin
     */
    @PostMapping("/login.do")
    public String login(@RequestBody LoginVo loginVo, HttpServletRequest request) {
        UserVo userVo = UserVo.builder().userName(loginVo.getUserName())
                .userPassword(loginVo.getUserPassword()).build();
        HttpSession session = request.getSession();
        session.setAttribute(CURRENT_USER, userVo);
        System.out.println("create session, sessionId is:" + session.getId());
        return "ok";
    }

    /**
     * 登出
     *
     * @author huaijin
     */
    @PostMapping("/logout.do")
    public String logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        session.invalidate();
        return "ok";
    }
}

编写查询控制器,在登录创建 session 后,使用将 sessionId 置于 cookie 中访问。如果没有 session 将返回错误。

/**
 * 查询
 *
 * @author huaijin
 */
@RestController
@RequestMapping("/session")
public class QuerySessionController {

    @GetMapping("/query.do")
    public String querySessionId(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return "error";
        }
        System.out.println("current's user is:" + session.getId() +  "in session");
        return "ok";
    }
}

编写 Session 删除事件监听器

Session 删除事件监听器用于监听登出时使 session 失效的事件源。

/**
 * session事件监听器
 *
 * @author huaijin
 */
@Component
public class SessionEventListener implements ApplicationListener {

    private static final String CURRENT_USER = "currentUser";

    @Override
    public void onApplicationEvent(SessionDeletedEvent event) {
        Session session = event.getSession();
        UserVo userVo = session.getAttribute(CURRENT_USER);
        System.out.println("invalid session's user:" + userVo.toString());
    }
}

验证测试

编写 spring-boot 测试类,测试 controller,验证 spring-session 是否生效。


/**
 * 测试Spring-Session:
 * 1.登录时创建session
 * 2.使用sessionId能正常访问
 * 3.session过期销毁,能够监听销毁事件
 *
 * @author huaijin
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SpringSessionTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testLogin() throws Exception {
        LoginVo loginVo = new LoginVo();
        loginVo.setUserName("admin");
        loginVo.setUserPassword("admin@123");
        String content = JSON.toJSONString(loginVo);

        // mock登录
        ResultActions actions = this.mockMvc.perform(post("/login.do")
                .content(content).contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()).andExpect(content().string("ok"));
        String sessionId = actions.andReturn()
                .getResponse().getCookie("SESSION").getValue();

        // 使用登录的sessionId mock查询
        this.mockMvc.perform(get("/session/query.do")
                .cookie(new Cookie("SESSION", sessionId)))
                .andExpect(status().isOk()).andExpect(content().string("ok"));

        // mock登出
        this.mockMvc.perform(post("/logout.do")
                .cookie(new Cookie("SESSION", sessionId)))
                .andExpect(status().isOk()).andExpect(content().string("ok"));
    }
}

测试类执行结果:

create session, sessionId is:429cb0d3-698a-475a-b3f1-09422acf2e9c
current's user is:429cb0d3-698a-475a-b3f1-09422acf2e9cin session
invalid session's user:UserVo{userName='admin', userPassword='admin@123'

登录时创建 Session,存储当前登录用户。然后在以登录响应返回的 SessionId 查询用户。最后再登出使 Session 过期。

spring-boot 整合 spring-session 自动配置原理

前两篇文章介绍 spring-session 原理时,总结 spring-session 的核心模块。这节中探索 spring-boot 中自动配置如何初始化 spring-session 的各个核心模块。

spring-boot-autoconfigure 模块中包含了 spinrg-session 的自动配置。包 org.springframework.boot.autoconfigure.session 中包含了 spring-session 的所有自动配置项。

其中 RedisSession 的核心配置项是 RedisHttpSessionConfiguration 类。

@Configuration
@ConditionalOnClass({ RedisTemplate.class, RedisOperationsSessionRepository.class })
@ConditionalOnMissingBean(SessionRepository.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@Conditional(ServletSessionCondition.class)
@EnableConfigurationProperties(RedisSessionProperties.class)
class RedisSessionConfiguration {

 @Configuration
 public static class SpringBootRedisHttpSessionConfiguration
   extends RedisHttpSessionConfiguration {

  // 加载application.yml或者application.properties中自定义的配置项:
  // 命名空间:用于作为session redis key的一部分
  // flushmode:session写入redis的模式
  // 定时任务时间:即访问redis过期键的定时任务的cron表达式
  @Autowired
  public void customize(SessionProperties sessionProperties,
    RedisSessionProperties redisSessionProperties) {
   Duration timeout = sessionProperties.getTimeout();
   if (timeout != null) {
    setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
   }
   setRedisNamespace(redisSessionProperties.getNamespace());
   setRedisFlushMode(redisSessionProperties.getFlushMode());
   setCleanupCron(redisSessionProperties.getCleanupCron());
  }

 }

}

RedisSessionConfiguration 配置类中嵌套 SpringBootRedisHttpSessionConfiguration 继承了 RedisHttpSessionConfiguration 配置类。首先看下该配置类持有的成员。

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
  implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
  SchedulingConfigurer {

 // 默认的cron表达式,application.yml可以自定义配置
 static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";

 // session的有效最大时间间隔, application.yml可以自定义配置
 private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

 // session在redis中的命名空间,主要为了区分session,application.yml可以自定义配置
 private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE;

 // session写入Redis的模式,application.yml可以自定义配置
 private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;

 // 访问过期Session集合的定时任务的定时时间,默认是每整分运行任务
 private String cleanupCron = DEFAULT_CLEANUP_CRON;

 private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();

 // spring-data-redis的redis连接工厂
 private RedisConnectionFactory redisConnectionFactory;

 // spring-data-redis的RedisSerializer,用于序列化session中存储的attributes
 private RedisSerializer defaultRedisSerializer;

 // session时间发布者,默认注入的是AppliationContext实例
 private ApplicationEventPublisher applicationEventPublisher;

 // 访问过期session键的定时任务的调度器
 private Executor redisTaskExecutor;

 private Executor redisSubscriptionExecutor;

 private ClassLoader classLoader;

 private StringValueResolver embeddedValueResolver;
}

 
   

该配置类中初始化了 RedisSession 的最为核心模块之一 RedisOperationsSessionRepository。

@Bean
public RedisOperationsSessionRepository sessionRepository() {
 // 创建RedisOperationsSessionRepository
 RedisTemplate redisTemplate = createRedisTemplate();
 RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
   redisTemplate);
 // 设置Session Event发布者。如果对此迷惑,传送门:https://www.cnblogs.com/lxyit/p/9719542.html
 sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
 if (this.defaultRedisSerializer != null) {
  sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
 }
 // 设置默认的Session最大有效期间隔
 sessionRepository
   .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
 // 设置命名空间
 if (StringUtils.hasText(this.redisNamespace)) {
  sessionRepository.setRedisKeyNamespace(this.redisNamespace);
 }
 // 设置写redis的模式
 sessionRepository.setRedisFlushMode(this.redisFlushMode);
 return sessionRepository;
}

同时也初始化了 Session 事件监听器 MessageListener 模块

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
 // 创建MessageListener容器,这属于spring-data-redis范畴,略过
 RedisMessageListenerContainer container = new RedisMessageListenerContainer();
 container.setConnectionFactory(this.redisConnectionFactory);
 if (this.redisTaskExecutor != null) {
  container.setTaskExecutor(this.redisTaskExecutor);
 }
 if (this.redisSubscriptionExecutor != null) {
  container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
 }
 // 模式订阅redis的__keyevent@*:expired和__keyevent@*:del通道,
 // 获取redis的键过期和删除事件通知
 container.addMessageListener(sessionRepository(),
   Arrays.asList(new PatternTopic("__keyevent@*:del"),
     new PatternTopic("__keyevent@*:expired")));
 // 模式订阅redis的${namespace}:event:created:*通道,当该向该通道发布消息,
 // 则MessageListener消费消息并处理
 container.addMessageListener(sessionRepository(),
   Collections.singletonList(new PatternTopic(
     sessionRepository().getSessionCreatedChannelPrefix() + "*")));
 return container;
}

上篇文章中介绍到的 spring-session event 事件原理,spring-session 在启动时监听 Redis 的 channel,使用 Redis 的键空间通知处理 Session 的删除和过期事件和使用 Pub/Sub 模式处理 Session 创建事件。

关于 RedisSession 的存储管理部分已经初始化,但是 spring-session 的另一个基础设施模块 SessionRepositoryFilter 是在 RedisHttpSessionConfiguration 父类 SpringHttpSessionConfiguration 中初始化。

@Bean
public  SessionRepositoryFilter springSessionRepositoryFilter(
  SessionRepository sessionRepository) {
 SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter<>(
   sessionRepository);
 sessionRepositoryFilter.setServletContext(this.servletContext);
 sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
 return sessionRepositoryFilter;
}

spring-boot 整合 spring-session 配置的层次:

RedisSessionConfiguration
 |_ _ SpringBootRedisHttpSessionConfiguration
   |_ _ RedisHttpSessionConfiguration
     |_ _ SpringHttpSessionConfiguration

回顾思考 spring-boot 自动配置 spring-session,非常合理。