首页 > 编程语言 > 详细

Spring-Session 剖析

时间:2020-10-03 11:08:50      阅读:57      评论:0      收藏:0      [点我收藏+]

1.前置知识: Keyspace Notifications (redis 键空间通知)

http://www.redis.cn/topics/notifications.html

https://redis.io/topics/notifications

https://www.jianshu.com/p/2c3f253fb4c5

 

2.应用示例

2.1 pom.xml

<!-- <version>2.2.6.RELEASE</version> -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

 

2.2 application.yml

spring:
  redis:
    host: 192.168.0.156
    port: 6379
  session:
    store-type: redis
    timeout: 30m
    redis:
      # 表示不支持 键空间事件; 默认值是 notify_keyspace_events 表示支持
      # 这里有个问题就是, 如果一开始启用了该功能, 后期想关闭该功能, 仅把此处设置为 none 是不行的, 必须重启 redis, 再讲此处设置为 none.
      # 再研究下, 看是 bug 提个 issue, 还是说 还有其他方案.
      configure-action: none
      cleanup-cron: 0 * * * * * # 清理过期 session 的定时任务
      namespace: spring:session # 前缀
server:
  port: 8081
  servlet:
    session:
      timeout: 30m
      cookie:
        http-only: true
#        domain: zy.com
        path: /
#        secure: true
        name: authId

 

2.3 模拟登录登出的controller

package com.zy.controller;

import com.zy.vo.TbUser;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;

/**
 * 分别模拟登录和登出
 */
@RestController
@RequestMapping("/user/")
public class UserController {
    public static final String USER_ATTRIBUTE_NAME = "user_session";
    @RequestMapping("login")
    public ResponseEntity<String> login(@RequestBody TbUser user, HttpServletRequest request) {
        request.getSession().setAttribute(USER_ATTRIBUTE_NAME, user);
        return ResponseEntity.ok().body("successfully to login.\n sessionId is: " + request.getSession().getId());
    }
    @RequestMapping("logout")
    public ResponseEntity<String> logout(HttpServletRequest request) {
        request.getSession(false).invalidate();
        return ResponseEntity.ok().body("successfully to logout..\n sessionId is: " + request.getSession().getId());
    }
}

 

2.4 监听Session删除事件的监听器

package com.zy.config;

import com.zy.controller.UserController;
import org.springframework.context.ApplicationListener;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.stereotype.Component;

@Component
public class SessionDeleteEventListener implements ApplicationListener<SessionDeletedEvent> {
    @Override
    public void onApplicationEvent(SessionDeletedEvent event) {
        System.out.println("--------------------------------");
        Object user = event.getSession().getAttribute(UserController.USER_ATTRIBUTE_NAME);
        System.out.println("SessionDeletedEvent, user is: " + user);
        System.out.println("--------------------------------");
    }
}

 

2.5 MockMvc 进行mock测试

package com.zy;

import com.alibaba.fastjson.JSON;
import com.zy.vo.TbUser;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import javax.servlet.http.Cookie;

@SpringBootTest
@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
public class TestUserController {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testLoginAndLogout() throws Exception {
        TbUser user = new TbUser();
        user.setUsername("tom");
        user.setPassword("123456");

        ResultActions actions = this.mockMvc.perform(MockMvcRequestBuilders.post("/user/login")
                .content(JSON.toJSONString(user))
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk());
        String sessionId = actions.andReturn().getResponse().getCookie("authId").getValue();

        ResultActions resultActions = this.mockMvc.perform(MockMvcRequestBuilders.post("/user/logout")
                .cookie(new Cookie("authId", sessionId)))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}

 

3.源码分析

3.1 框架初始化阶段

3.1.1 加载 yml 中的配置文件

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
}

@ConfigurationProperties(prefix = "spring.session")
public class SessionProperties {
}

 

3.1.2 加载相关自动装配的类

核心类1 SessionAutoConfiguration
// 构建 DefaultCookieSerializer 
SessionAutoConfiguration.ServletSessionConfiguration#cookieSerializer

 

核心类2 RedisSessionConfiguration

// 构建 ConfigureRedisAction (即redis键空间通知相关行为)
RedisSessionConfiguration#configureRedisAction
>> NOTIFY_KEYSPACE_EVENTS
>> NONE

// 构建 RedisIndexedSessionRepository 前的属性设置
RedisSessionConfiguration.SpringBootRedisHttpSessionConfiguration#customize
>> setMaxInactiveIntervalInSeconds
>> setRedisNamespace
>> setFlushMode
>> setSaveMode
>> setCleanupCron

 

核心类3 RedisHttpSessionConfiguration

// 构建  RedisIndexedSessionRepository
RedisHttpSessionConfiguration#sessionRepository
在其构造器中构建了 RedisSessionExpirationPolicy (核心类), 其核心方法:
>> onDelete
>> onExpirationUpdated
>> cleanExpiredSessions


// 构建 RedisMessageListenerContainer
RedisHttpSessionConfiguration#redisMessageListenerContainer
在该对象中 addMessageListener (核心方法), 主要添加了:
>> SessionDeletedTopic
>> SessionExpiredTopic
>> SessionCreatedTopic.
这些 Topic 最终被 RedisIndexedSessionRepository 实现了其 
onMessage 方法, 监听 Session 的创建, 删除, 过期事件.
该onMessage 方法 在 RedisMessageListenerContainer#executeListener 中调用时触发.
该类中同时有一个内部SubscriptionTask, 该线程主要是订阅redis事件.


// 构建 EnableRedisKeyspaceNotificationsInitializer
// 这里如果 redis 不支持 键空间通知 功能, 启动时便会抛异常
RedisHttpSessionConfiguration#enableRedisKeyspaceNotifications
>> 如果上述步骤(核心类1)中 ConfigureRedisAction 是 NONE, 则不向 redis 发送指令, 不再支持"键空间通知";
>> 如果 ConfigureRedisAction 是 NOTIFY_KEYSPACE_EVENTS,则向redis发送指令, 支持"键空间通知";


// 构建清理过期的 sessions 的调度任务 (定时任务, cron 表达式)
RedisHttpSessionConfiguration.SessionCleanupConfiguration

 

核心类4 SpringHttpSessionConfiguration

// 核心点: 构建 SessionEventHttpSessionListenerAdapter
SpringHttpSessionConfiguration#sessionEventHttpSessionListenerAdapter

// 核心点: 构建 SessionRepositoryFilter
SpringHttpSessionConfiguration#springSessionRepositoryFilter
其默认优先级是: Integer.MIN_VALUE + 50; 值越小, 优先级越高.

 

核心类5 SessionRepositoryFilterConfiguration

// 将 SessionRepositoryFilter 注册到 filter 链中
SessionRepositoryFilterConfiguration#sessionRepositoryFilterRegistration
默认的拦截路径是: /*

 

3.2 运行时拦截阶段

 

 

 

5.一些问题

5.1 关于spring-boot整合方式的问题

spring-boot2里:
个人不太建议 使用@EnableRedisHttpSession 注解了. 如果用, 则RedisSessionConfiguration 配置类将不被加载.
此时若是配置 spring.session.redis.configure-action=none (即不启用键空间通知事件), 将不生效.

 

5.2 启用键空间事件无法关闭的bug

#spring-boot 版本2.2.6.RELEASE
如果spring-boot中一开始支持"键空间通知" 
其实是通过RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer#afterPropertiesSet中发送 config set命令来使得redis支持的.
如果后期想关闭该功能. 仅仅配置spring.session.redis.configure-action=none是不起作用的, 需要重启 redis服务器.
(也许还有其他方案, 也许可以提个 issue).

 

5.3 spring-session在redis集群下监听expired事件失败

spring-session的expired事件有时监听会丢失,spring-session不支持redis集群场景。

其原因在于:
spring-session默认会随机订阅redis集群中所有主备节点中一台,
而创建带ttl参数的session连接只会hash到所有主节点中一台。
只有订阅和session创建连接同时连接到一台redis节点才能监听到这个ttl session产生的expired事件。

解决方案:
自行对所有redis集群主备节点进行expired事件订阅。

https://github.com/spring-projects/spring-session/issues/478

 

 

 

参考资源

https://www.cnblogs.com/lxyit/archive/2004/01/13/9720159.html

https://www.cnblogs.com/tinywan/p/5903988.html

https://blog.csdn.net/gqtcgq/article/details/50808729

 

Spring-Session 剖析

原文:https://www.cnblogs.com/psy-code/p/13763629.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!