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。

类图如下:

img

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 高并发实战

img


疯狂创客圈 Java 死磕系列

  • Java (Netty) 聊天程序【 亿级流量】实战 开源项目实战

Java 面试题 一网打尽**


原文地址:https://www.cnblogs.com/crazymakercircle/p/12038664.html