SpringSession 独立使用

疯狂创客圈 《SpringCloud Nginx 高并发核心编程》高并发 架构师 必备【链接

在这里插入图片描述

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 面试题 一网打尽**


posted @ 2019-12-14 12:10  疯狂创客圈  阅读(2309)  评论(0编辑  收藏  举报