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 面試題 一網打盡**



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM