瘋狂創客圈 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 面試題 一網打盡**
- 瘋狂創客圈 【 博客園 總入口 】
