SpringSession 独立使用
疯狂创客圈 Java 高并发【 亿级流量聊天室实战】实战系列 【博客园总入口 】
架构师成长+面试必备之 高并发基础书籍 【Netty Zookeeper Redis 高并发实战 】
前言
Crazy-SpringCloud 微服务脚手架 &视频介绍:
Crazy-SpringCloud 微服务脚手架,是为 Java 微服务开发 入门者 准备的 学习和开发脚手架。并配有一系列的使用教程和视频,大致如下:
高并发 环境搭建 图文教程和演示视频,陆续上线:
| 中间件 | 链接地址 | 
|---|---|
| Linux Redis 安装(带视频) | Linux Redis 安装(带视频) | 
| Linux Zookeeper 安装(带视频) | Linux Zookeeper 安装, 带视频 | 
| Windows Redis 安装(带视频) | Windows Redis 安装(带视频) | 
| RabbitMQ 离线安装(带视频) | RabbitMQ 离线安装(带视频) | 
| ElasticSearch 安装, 带视频 | ElasticSearch 安装, 带视频 | 
| Nacos 安装(带视频) | Nacos 安装(带视频) | 
Crazy-SpringCloud 微服务脚手架 图文教程和演示视频,陆续上线:
| 组件 | 链接地址 | 
|---|---|
| Eureka | Eureka 入门,带视频 | 
| SpringCloud Config | springcloud Config 入门,带视频 | 
| spring security | spring security 原理+实战 | 
| Spring Session | SpringSession 独立使用 | 
| 分布式 session 基础 | RedisSession (自定义) | 
| 重点: springcloud 开发脚手架 | springcloud 开发脚手架 | 
| SpingSecurity + SpringSession 死磕 (写作中) | SpingSecurity + SpringSession 死磕 | 
小视频以及所需工具的百度网盘链接,请参见 疯狂创客圈 高并发社群 博客
SpringSession 独立使用 的场景和问题
当Zuul网关接收到http请求后,当请求进入对应的Filter进行过滤,通过 SpringSecurity 认证后,提取 SessionID,转发给各个微服务,通过Spring-Session创建的分布式微服务,实现Session共享!
特点:
(1)浏览器和移动端,和Nginx代理,token 是可见的,但是 session 不可见。
(2)各个微服务,用到共享Session,sessionId是可见的。
(3)各个微服务,可以通过自定义的 SessionHolder 共享类,可以静态的取得分布式Session的公共数据,比如基础的用户信息。提升编程的效率。 具体请参见 SpringCloud 开发脚手架。
具体场景的请求处理流程:

问题:
问题一:需要定制ID解析器
场景1 :如果Rest请求从Zuul 过来,Zuul 会在头部设置 sessionID,就是这个场景首先从head中去取
    String headerValue = request.getHeader(this.headerName);
场景2: 如果是 单体微服务直接访问 ,就是这个场景 SpringSecurity 会将 sessionID,放在 attribute中。这种场景,直接从从attribute中去取sessionID
  headerValue = (String) request.getAttribute(SessionConstants.SESSION_SEED);
SpringSession自带的 ID解析器 ,不能满足要求,需要重新定制一个。关于ID解析器,请参见 疯狂创客圈 的另一博文 SpringSession自带的 ID解析器 最全解读
问题二:需要定制sessionRepository 存储器
sessionRepository 负责存储 session 到Redis,需要修改模式为立即提交,以免setAttribute的属性,不能及时写入Redis,这是笔者调试了几个小时发现的坑
问题三:需要定制SessionRepositoryFilter 过滤器
将Session请求,保持到 SessionHolder 的 ThreadLocal 本地变量中,方便统一获取,方便编程。例如:
SessionHolder.getSessionUser().getLoginName());
直接从redissession,读取用户的名称,多方便呀。
总之: 使用集成的默认的SpringSession ,没有办法深入的解决问题。 有两种方法。
- 第一种是自制 分布式 Session。
 
具体请参考 疯狂创客圈 博客 分布式RedisSession 自制
这种方法的优点:简陋。 缺点:过于简陋。
在流程和思想上,和第下面的第二种是类似的,可供学习使用,方便理解。
- 第二种是 SpringSession 独立使用。
 
就是本文的内容。
说明: 第二种在流程和思想上第一种是类似的,可供学习使用,方便理解,建议先了解第一种,第二种就好掌握多了。
理论基础: springSession 原理
spring-session分为以下核心模块:
过滤器 SessionRepositoryFilter:Servlet规范中Filter的实现,用 Spring Session 替换原来的 HttpSession,具体的方式是使用了自己的两个包装器: HttpServletRequest 和HttpServletResponse。
包装器 HttpServerletRequest/HttpServletResponse/HttpSessionWrapper:包装原有的HttpServletRequest、HttpServletResponse和Spring Session,实现切换Session和透明继承HttpSession的关键之所在
Session:Spring Session模块
存储器 SessionRepository:负责 Spring Session的存储
具体见下图:
Spring Session模块
spring-session中则抽象出单独的Session层接口,让后再使用适配器模式将Session适配层Servlet规范中的HttpSession。
类图如下:
RedisSession 的本质:
内部封装一个 MapSession,MapSession 本质是一个  map。而 RedisSession 的主要职责:负责 MapSession中  Map  的K-V内容的  Redis 存储。
spring-session 原理,请参见博文
第1步: ID解析器 自定义
场景1 :如果Rest请求从Zuul 过来,Zuul 会在头部设置 sessionID,就是这个场景首先从head中去取
    String headerValue = request.getHeader(this.headerName);
场景2: 如果是 单体微服务直接访问 ,就是这个场景 SpringSecurity 会将 sessionID,放在 attribute中。这种场景,直接从从attribute中去取sessionID
  headerValue = (String) request.getAttribute(SessionConstants.SESSION_SEED);
实现 HttpSessionIdResolver 接口,定义一个完整的ID解析器,代码如下:
package com.crazymaker.springcloud.standard.config;
//...省略import
@Data
public class CustomedSessionIdResolver implements HttpSessionIdResolver {
    private RedisTemplate<Object, Object> redisTemplet = null;
    private static final String HEADER_AUTHENTICATION_INFO = "Authentication-Info";
    private final String headerName;
    /**
     * The name of the header to obtain the session id from.
     *
     */
    public CustomedSessionIdResolver() {
        //设置 head头的名称
        this.headerName = SessionConstants.SESSION_SEED;
        if (headerName == null) {
            throw new IllegalArgumentException("headerName cannot be null");
        }
    }
    @Override
    public List<String> resolveSessionIds(HttpServletRequest request) {
        //step1:首先从head中去取sessionID
        // 如果从Zuul 过来,就是这个场景
        String headerValue = request.getHeader(this.headerName);
        //step1:首先从attribute中去取sessionID
        // 如果是 单体微服务直接访问 ,就是这个场景
        //SpringSecurity 会将  sessionID,放在  attribute中
        if (StringUtils.isEmpty(headerValue)) {
            headerValue = (String) request.getAttribute(SessionConstants.SESSION_SEED);
            if (!StringUtils.isEmpty(headerValue)) {
                headerValue = SessionConstants.getRedisSessionID(headerValue);
            }
        }
        return (headerValue != null) ?
                Collections.singletonList(headerValue) : Collections.emptyList();
    }
    @Override
    public void setSessionId(HttpServletRequest request, HttpServletResponse response,
                             String sessionId) {
        //不需要返回sessionId
        //到前端
        response.setHeader(this.headerName, "");
        //        response.setHeader(this.headerName, sessionId);
    }
    @Override
    public void expireSession(HttpServletRequest request, HttpServletResponse response) {
        response.setHeader(this.headerName, "");
    }
    //....省略其他
}
第2步:自定义一个SessionRepositoryFilter
这一步,不是必须的。
主要作用: 在过滤器的处理方法 doFilterInternal(....), 要将 redis session 保存到 SessionHolder 类中,方便后面访问。代码如下:
    SessionHolder.setRequest(wrappedRequest);
    SessionHolder.setSession(wrappedRequest.getSession());
复制源码中的 SessionRepositoryFilter 类,改名为 CustomedSessionRepositoryFilter, 简单的修改一下,代码如下:
package com.crazymaker.springcloud.standard.security.filter;
//.....
public class CustomedSessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
    private static final String SESSION_LOGGER_NAME = CustomedSessionRepositoryFilter.class
            .getName().concat(".SESSION_LOGGER");
   //....
   //默认的ID解析器,需要替换掉
    private HttpSessionIdResolver httpSessionIdResolver = new CookieHttpSessionIdResolver();
    /**
     * Creates a new instance.
     *
     * @param sessionRepository the <code>SessionRepository</code> to use. Cannot be null.
     */
    public CustomedSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
        if (sessionRepository == null) {
            throw new IllegalArgumentException("sessionRepository cannot be null");
        }
        this.sessionRepository = sessionRepository;
    }
    /**
     * Sets the {@link HttpSessionIdResolver} to be used. The default is a
     * {@link CookieHttpSessionIdResolver}.
     *
     * @param httpSessionIdResolver the {@link HttpSessionIdResolver} to use. Cannot be
     *                              null.
     */
    public void setHttpSessionIdResolver(HttpSessionIdResolver httpSessionIdResolver) {
        if (httpSessionIdResolver == null) {
            throw new IllegalArgumentException("httpSessionIdResolver cannot be null");
        }
        this.httpSessionIdResolver = httpSessionIdResolver;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        if(this.servletContext==null)
        {
            this.servletContext=request.getServletContext();
        }
        SessionRepositoryRequestWrapper wrappedRequest =
                new SessionRepositoryRequestWrapper(request, response, this.servletContext);
        SessionRepositoryResponseWrapper wrappedResponse =
                new SessionRepositoryResponseWrapper(wrappedRequest, response);
        /**
         * 将Session请求,保持到  SessionHolder 的 ThreadLocal 本地变量中,方便统一获取
         */
        SessionHolder.setRequest(wrappedRequest);
        SessionHolder.setSession(wrappedRequest.getSession());
        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            wrappedRequest.commitSession();
        }
    }
    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }
    /**
     * Allows ensuring that the session is saved if the response is committed.
     *
     * @author Rob Winch
     * @since 1.0
     */
    private final class SessionRepositoryResponseWrapper
            extends OnCommittedResponseWrapper {
     //.....
    }
    /**
     * A {@link javax.servlet.http.HttpServletRequest} that retrieves the
     * {@link javax.servlet.http.HttpSession} using a
     * {@link org.springframework.session.SessionRepository}.
     *
     * @author Rob Winch
     * @since 1.0
     */
    private final class SessionRepositoryRequestWrapper
            extends HttpServletRequestWrapper {
       //....
    }
  static   class HttpSessionAdapter<S extends Session> implements HttpSession {
     //....
    }
}
第3步:自动配置 Configuration 的定制
简单粗暴,将springsession 默认的自动配置,废掉了。
复制一份 RedisHttpSessionConfiguration, 名字叫做 CustomedRedisHttpSessionConfiguration ,主要作用:
(1) 创建 CustomedSessionIdResolver ID解析器的IOC Bean
(2) 创建 sessionRepository 保存器 的IOC Bean时,修改模式为立即提交
package com.crazymaker.springcloud.standard.config;
//....
@Configuration
@EnableScheduling
public class CustomedRedisHttpSessionConfiguration
        implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
        SchedulingConfigurer {
    static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
    //......
    @DependsOn("httpSessionIdResolver")
    @Bean
    public RedisOperationsSessionRepository sessionRepository(CustomedSessionIdResolver httpSessionIdResolver) {
        RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
        RedisOperationsSessionRepository sessionRepository =
                new RedisOperationsSessionRepository(redisTemplate);
        sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
        if (this.defaultRedisSerializer != null) {
            sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
        }
        sessionRepository
                .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
        if (StringUtils.hasText(this.redisNamespace)) {
            sessionRepository.setRedisKeyNamespace(this.redisNamespace+":"+SessionConstants.REDIS_SESSION_KEY_PREFIX);
        }
        //修改模式为立即提交
        sessionRepository.setRedisFlushMode(RedisFlushMode.IMMEDIATE);
//        sessionRepository.setRedisFlushMode(this.redisFlushMode);
        int database = resolveDatabase();
        sessionRepository.setDatabase(database);
        httpSessionIdResolver.setRedisTemplet(redisTemplate);
        this.sessionRepository = sessionRepository;
        return sessionRepository;
    }
//....
    /**
     * 配置 ID 解析器,从 header  解析id
     *
     * @return
     */
    @Bean("httpSessionIdResolver")
    public CustomedSessionIdResolver httpSessionIdResolver() {
        return new CustomedSessionIdResolver(SessionConstants.SESSION_ID);
    }
}
第4步: 在SpringSecurityConfig中,使用过滤器
package com.crazymaker.springcloud.user.info.config;
//....
import javax.annotation.Resource;
import java.util.Arrays;
@EnableWebSecurity()
public class UserProviderWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private UserLoginService userLoginService;
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(
                        "/v2/api-docs",
                        "/swagger-resources/configuration/ui",
                        "/swagger-resources",
                        "/swagger-resources/configuration/security",
                        "/swagger-ui.html",
                        "/api/user/login/v1",
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().disable()
                .sessionManagement().disable()
                .cors()
                .and()
                .addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
                .apply(new JsonLoginConfigurer<>()).loginSuccessHandler(jsonLoginSuccessHandler())
                .and()
                .apply(new JwtLoginConfigurer<>()).tokenValidSuccessHandler(jwtRefreshSuccessHandler()).permissiveRequestUrls("/logout")
                .and()
                .logout()
                .addLogoutHandler(tokenClearLogoutHandler())
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
                .and()
                .addFilterBefore(springSessionRepositoryFilter(), SessionManagementFilter.class)
                .sessionManagement().disable()
        ;
    }
    @Resource
    RedisOperationsSessionRepository sessionRepository;
    @Resource
    public CustomedSessionIdResolver httpSessionIdResolver;
    @DependsOn({"sessionRepository","httpSessionIdResolver"})
    @Bean("jwtAuthenticationProvider")
    protected AuthenticationProvider jwtAuthenticationProvider() {
        return new JwtAuthenticationProvider(sessionRepository,httpSessionIdResolver);
    }
//....
}
具体,请关注 Java 高并发研习社群 【博客园 总入口 】
最后,介绍一下疯狂创客圈:疯狂创客圈,一个Java 高并发研习社群 【博客园 总入口 】
疯狂创客圈,倾力推出:面试必备 + 面试必备 + 面试必备 的基础原理+实战 书籍 《Netty Zookeeper Redis 高并发实战》
疯狂创客圈 Java 死磕系列
- Java (Netty) 聊天程序【 亿级流量】实战 开源项目实战
 
- Netty 源码、原理、JAVA NIO 原理
 - Java 面试题 一网打尽
 - 疯狂创客圈 【 博客园 总入口 】
 
Java 面试题 一网打尽**
- 疯狂创客圈 【 博客园 总入口 】
 
SpringSession 独立使用的更多相关文章
- SpringSession   header/cookie/attribute存放 session id
		
SpringSession header/cookie/attribute存放 SessionID(死磕) 疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 架构师成长 ...
 - SpringBoot  SpringCloud 热部署  热加载  热调试
		
疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 架构师成长+面试必备之 高并发基础书籍 [Netty Zookeeper Redis 高并发实战 ] Crazy-Sp ...
 - Eureka 入门,带视频
		
疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 架构师成长+面试必备之 高并发基础书籍 [Netty Zookeeper Redis 高并发实战 ] 前言 Crazy ...
 - springcloud Config 入门,带视频
		
疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 架构师成长+面试必备之 高并发基础书籍 [Netty Zookeeper Redis 高并发实战 ] 前言 Crazy ...
 - spring security 原理+实战
		
疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 架构师成长+面试必备之 高并发基础书籍 [Netty Zookeeper Redis 高并发实战 ] 前言 Crazy ...
 - RedisSession  (自定义)
		
RedisSession (自定义) 疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 架构师成长+面试必备之 高并发基础书籍 [Netty Zookeeper Red ...
 - SpringCloud 脚手架
		
疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 架构师成长+面试必备之 高并发基础书籍 [Netty Zookeeper Redis 高并发实战 ] 前言 Crazy ...
 - SpringCloud  亿级流量 架构演进
		
疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 架构师成长+面试必备之 高并发基础书籍 [Netty Zookeeper Redis 高并发实战 ] 前言 Crazy ...
 - Zuul 详解,带视频
		
疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 架构师成长+面试必备之 高并发基础书籍 [Netty Zookeeper Redis 高并发实战 ] 前言 Crazy ...
 
随机推荐
- MySQL5.7.18自动化安装脚本
			
背景 很好的朋友邱启明同学,擅长MySQL,目前任职某大型互联网业MySQL DBA,要来一套MySQL自动安装的Shell脚本,贴出来保存一些. 此版本为 MySQL 5.7.18 ###### 自 ...
 - 寻找键盘bug
			
被这句阻拦了
 - “洞察千里”,华为云HiLens如何让无人车智行天下
			
作者:华为云 Rosie 随着人工智能的普及和渗透,"无人"的场景越来越丰富,无人超市.无人车.无人机等已经融入我们的生活. 乘着这股热浪,华为云携手上海交通大学学生创新中心举办了 ...
 - where 和having 的区别
			
where : 约束声明,在查询结果返回之前对数据库中的查询条件进行约束 其后不能写聚合函数 having 过滤声明,在查询结果返回之后进行过滤,
 - luogu P1082 同余方程 |扩展欧几里得
			
题目描述 求关于 x的同余方程 ax≡1(modb) 的最小正整数解. 输入格式 一行,包含两个正整数 a,ba,b,用一个空格隔开. 输出格式 一个正整数 x,即最小正整数解.输入数据保证一定有解. ...
 - luogu P2343 宝石管理系统 |分块+堆
			
题目描述 GY君购买了一批宝石放进了仓库.有一天GY君心血来潮,想要清点他的宝石,于是把m个宝石都取出来放进了宝石管理系统.每个宝石i都有一个珍贵值vi,他希望你能编写程序查找到从大到小第n珍贵的宝石 ...
 - Crush 算法以及PG和PGP调整经验
			
PG和PGP调整经验调整前准备为了降低对业务的影响,需要调整以下参数ceph tell osd.* injectargs ‘–osd-max-backfills 1’ceph tell osd.* i ...
 - Python3 类的继承小练习
			
1.打印并解释结果 class Parent(object): x = 1 class Child1(Parent): pass class Child2(Parent): pass print(Pa ...
 - 记录我的 python 学习历程-Day07 基础数据类型进阶 / 数据类型之间的转换 / 基础数据类型总结 / 编码的进阶
			
基础数据类型 str(字符串) str:补充方法练习一遍就行 s.capitalize() 首字母大写,其余变小写 s = 'dyLAn' print(s.capitalize()) # Dylan ...
 - CCF-CSP题解 201809-3 元素选择器
			
题目要求写一个简易的CSS Selector. 首先用结构体\(<lev,label[],hasId,id[]>\)存储元素.其中\(lev\)表示元素在html树中的深度(这个是因为逻辑 ...