Spring Session 关于 SessionRepositoryFilter SessionRepository HttpSessionIdResolver 等

前言

最近因为项目应用,对 Spring Session 做了些许了解,以此文总结记录

SessionRepositoryFilter

@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {

	// ...

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

		/**
		 * 把 HttpServletRequest 和 HttpServletResponse 装饰成 SessionRepositoryRequestWrapper
		 * 		和 SessionRepositoryResponseWrapper,让它们走接下来的 filterChain
		 */
		SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
		SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
				response);

		try {
			filterChain.doFilter(wrappedRequest, wrappedResponse);
		}
		finally {
			wrappedRequest.commitSession();
		}
	}

	// ...

}
  • Spring Session 的核心就是这个 SessionRepositoryFilter
  • 它把 HttpServletRequestHttpServletResponse 装饰成 SessionRepositoryRequestWrapperSessionRepositoryResponseWrapper,它们在后续的处理就会接管 session 的管理

SessionRepositoryRequestWrapper

	private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {

		// ...

		@Override
		public HttpSessionWrapper getSession(boolean create) {

			// 先从 request attribute 获取,同一个 request 的 session 会放在这
			HttpSessionWrapper currentSession = getCurrentSession();
			if (currentSession != null) {
				return currentSession;
			}

			// 获取不到,就解析 sessionId,然后基于此从 repository 获取
			S requestedSession = getRequestedSession();
			if (requestedSession != null) {
				// 获取到后的一些处理,比如放到上面的 request attribute 里
				// ...
			}
			else {
				setAttribute(INVALID_SESSION_ID_ATTR, "true");
			}
			if (!create) {
				return null;
			}
			
			// create == true 就创建新的 session
			// sessionRepository.createSession()
			S session = SessionRepositoryFilter.this.sessionRepository.createSession();
			session.setLastAccessedTime(Instant.now());
			currentSession = new HttpSessionWrapper(session, getServletContext());
			setCurrentSession(currentSession);
			return currentSession;
		}

		@Override
		public HttpSessionWrapper getSession() {
			return getSession(true);
		}

		private S getRequestedSession() {
			if (!this.requestedSessionCached) {
				// httpSessionIdResolver.resolveSessionIds
				// 基于 httpSessionIdResolver 解析 sessionId,比如从 Cookie RequestHeader
				List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
				for (String sessionId : sessionIds) {
					if (this.requestedSessionId == null) {
						this.requestedSessionId = sessionId;
					}

					// 基于上述 sessionid 获取 Session
					S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
					if (session != null) {
						this.requestedSession = session;
						this.requestedSessionId = sessionId;
						break;
					}
				}
				this.requestedSessionCached = true;
			}
			return this.requestedSession;
		}

		private void commitSession() {
			HttpSessionWrapper wrappedSession = getCurrentSession();
			if (wrappedSession == null) {
				// 过期的话调用 httpSessionIdResolver.expireSession
				if (isInvalidateClientSession()) {
					SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
				}
			}
			else {
				/**
				 * sessionRepository.save 保存session
				 * httpSessionIdResolver.setSessionId 将 sessionId 反馈给 客户端
				 */
				S session = wrappedSession.getSession();
				clearRequestedSessionCache();
				SessionRepositoryFilter.this.sessionRepository.save(session);
				String sessionId = session.getId();
				if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
					SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
				}
			}
		}

		// ...

	}
  • SessionRepositoryRequestWrapperSessionRepositoryFilter 的内部类,装饰 HttpSevletRequest 以接管对 Session 的管理,几个代表性的方法:
  • getSession,简单地说,这里会根据客户端请求解析对应解析对应的 sessionId,然后从 SessionRepository 获取对应 session,如果获取不到,由SessionRepository 创建新的 session
  • commitSession 方法会把上面创建的 session 存储到 SessionRepository

SessionRepository

public interface SessionRepository<S extends Session> {

	// 创建新的 session
	S createSession();

	// 保存创建的 session
	void save(S session);

	// 基于 sessionId 获取对应的 session
	S findById(String id);

	// 基于 sessionId 删除对应的 session
	void deleteById(String id);

}
  • SessionRepositoryFilter 把对 session 的操作就委托到 SessionRepository 上了,包括 持久化管理
  • createSession 方法为新的回话创建一个新的 session
  • save 方法把新创建的 session 持久化管理起来
  • findById 方法基于解析的 sessionId 获取对应的 session,比如从 redis 读取
  • deleteById 方法基于解析的 sessionId 删除对应的 session
  • 最常用的实现莫过于基于 redisRedisSessionRepository RedisIndexedSessionRepository 等,后者是使用 @EnableRedisHttpSession 注解时默认注册的 SessionRepository,功能更强大

HttpSessionIdResolver

public interface HttpSessionIdResolver {

	// 从客户端请求解析 sessionId
	List<String> resolveSessionIds(HttpServletRequest request);

	// 告诉客户端新创建 session 的 sessionId,比如放到 Cookie 里
	void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId);

	// 告知客户端 session 过期,比如从 Cookie 里移除对应的 sessionId
	void expireSession(HttpServletRequest request, HttpServletResponse response);

}
  • 该接口主要负责 sessionId 的解析、处理工作
  • resolveSessionIds 方法解析客户端请求对应的 sessionId,比如从 Cookie 解析、 RequestHeader 解析
  • setSessionId 方法将创建的 sessionId 返回给客户端,比如放到 CookieResponseHeader
  • expireSession 方法告知客户端 session 已过期,比如从 Cookie 移除、删除对应的 ResponseHeader
  • Spring 提供的实现有 CookieHttpSessionIdResolverHeaderHttpSessionIdResolver,前者是基于 Cookie 后者基于 Header,默认的是 CookieHttpSessionIdResolver

SpringHttpSessionConfiguration

@Configuration(proxyBeanMethods = false)
public class SpringHttpSessionConfiguration implements ApplicationContextAware {

	// ...

	private CookieHttpSessionIdResolver defaultHttpSessionIdResolver = new CookieHttpSessionIdResolver();

	/**
	 * 注册核心的 SessionRepositoryFilter,指定两大组件:
	 * 		sessionRepository
	 * 		httpSessionIdResolver
	 */
	@Bean
	public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
			SessionRepository<S> sessionRepository) {
		SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
		sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
		return sessionRepositoryFilter;
	}

	// 支持用户自定义 httpSessionIdResolver
	@Autowired(required = false)
	public void setHttpSessionIdResolver(HttpSessionIdResolver httpSessionIdResolver) {
		this.httpSessionIdResolver = httpSessionIdResolver;
	}

	// ...

}
  • Spring 提供的配置类,该配置类基于 @EnableSpringHttpSession 引入
  • 注册核心组件 SessionRepositoryFilter,指定 sessionRepositoryhttpSessionIdResolver,其中 sessionRepository 自然是由具体的实现决定
  • httpSessionIdResolver 组件支持覆盖,默认 CookieHttpSessionIdResolver

RedisHttpSessionConfiguration

@Configuration(proxyBeanMethods = false)
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
	
	// ...

	// 默认创建一个 RedisIndexedSessionRepository,支持事件
	@Bean
	public RedisIndexedSessionRepository sessionRepository() {
		RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
		RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
		
		// ...

		/**
		 * 收集容器中所有的 SessionRepositoryCustomizer<RedisIndexedSessionRepository> 对
		 * 		sessionRepository 自定义处理
		 */
		this.sessionRepositoryCustomizers
				.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
		return sessionRepository;
	}

	/**
	 * 收集容器里的 @SpringSessionRedisConnectionFactory 的 RedisConnectionFactory Bean 组件
	 * 		作为 Spring Session 的 RedisConnectionFactory
	 * 如果没有就降级为全局的 RedisConnectionFactory
	 * 这个地方旨在使用者定义不同的 redis 数据源,但是实际在整合了 spring-boot-data-redis 的场景里,
	 * 		如果指定了 @SpringSessionRedisConnectionFactory 的 RedisConnectionFactory Bean 组件,
	 * 		则 RedisAutoConfiguration 就不会自动装配 RedisConnectionFactory 了,因此并不好用
	 */
	@Autowired
	public void setRedisConnectionFactory(
			@SpringSessionRedisConnectionFactory ObjectProvider<RedisConnectionFactory> springSessionRedisConnectionFactory,
			ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
		RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory.getIfAvailable();
		if (redisConnectionFactoryToUse == null) {
			redisConnectionFactoryToUse = redisConnectionFactory.getObject();
		}
		this.redisConnectionFactory = redisConnectionFactoryToUse;
	}

	// ...

}
  • 该配置类基于 @EnableRedisHttpSession 注解引入,它是 SpringHttpSessionConfiguration 的子类,因此核心的组件也都会引入
  • 该配置类专注基于 redissession 管理,因此注册默认的 sessionRepositoryRedisIndexedSessionRepository
  • 对应的 redisConnectionFactory 可由 @SpringSessionRedisConnectionFactory 注解指定
  • 使用该注解且同时整合 spring-boot-data-redis 时不太好用(具体可见注释说明),因此个人偏向于基于 @EnableSpringHttpSession 注解引入,即自己创建 sessionRepositoryredisConnectionFactory
  • 当然如果应用本身不使用 redis 或者不需要与 session 分离,则基于 @EnableRedisHttpSession 更加方便

SessionAutoConfiguration

// 引入 spring-session 时自动装配
@ConditionalOnClass(Session.class)
@ConditionalOnWebApplication
@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class, WebFluxProperties.class })
public class SessionAutoConfiguration {

	// ...

	// servlet 环境
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnWebApplication(type = Type.SERVLET)
	@Import({ ServletSessionRepositoryValidator.class, SessionRepositoryFilterConfiguration.class })
	static class ServletSessionConfiguration {

		// ...

		// ServletSessionConfigurationImportSelector 会引入 sevlet 环境的所有配置类
		@Configuration(proxyBeanMethods = false)
		@ConditionalOnMissingBean(SessionRepository.class)
		@Import({ ServletSessionRepositoryImplementationValidator.class,
				ServletSessionConfigurationImportSelector.class })
		static class ServletSessionRepositoryConfiguration {

		}

	}

	/**
	 * 根据 web 类型加载对应的配置类
	 * 比如 servlet 环境下基于 redis 的实现则使用配置类 RedisSessionConfiguration
	 * 详情:
	 * private static final Map<StoreType, Configurations> MAPPINGS;
	 * static {
	 * 		Map<StoreType, Configurations> mappings = new EnumMap<>(StoreType.class);
	 * 		mappings.put(StoreType.REDIS,
	 * 				new Configurations(RedisSessionConfiguration.class, RedisReactiveSessionConfiguration.class));
	 * 		mappings.put(StoreType.MONGODB,
	 * 				new Configurations(MongoSessionConfiguration.class, MongoReactiveSessionConfiguration.class));
	 * 		mappings.put(StoreType.JDBC, new Configurations(JdbcSessionConfiguration.class, null));
	 * 		mappings.put(StoreType.HAZELCAST, new Configurations(HazelcastSessionConfiguration.class, null));
	 * 		mappings.put(StoreType.NONE,
	 * 				new Configurations(NoOpSessionConfiguration.class, NoOpReactiveSessionConfiguration.class));
	 * 		MAPPINGS = Collections.unmodifiableMap(mappings);
	 * }
	 */
	abstract static class SessionConfigurationImportSelector implements ImportSelector {

		protected final String[] selectImports(WebApplicationType webApplicationType) {
			return Arrays.stream(StoreType.values())
					.map((type) -> SessionStoreMappings.getConfigurationClass(webApplicationType, type))
					.toArray(String[]::new);
		}

	}

	static class ServletSessionConfigurationImportSelector extends SessionConfigurationImportSelector {

		@Override
		public String[] selectImports(AnnotationMetadata importingClassMetadata) {
			return super.selectImports(WebApplicationType.SERVLET);
		}

	}

	// ...

}
  • Spring Boot 自然也会提供一站式的 自动装配 功能
  • 在我们引入 spring-session 依赖时,该类会自动装配
  • 这里比较核心的一点就是通过 ImportSelector 基于 web 环境装配对应配置类
  • 比如 servlet 环境加载对应的配置类,又根据 spring.session.redis.store-type 加载配置类 RedisSessionConfiguration

RedisSessionConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RedisTemplate.class, RedisIndexedSessionRepository.class })
// 用户没有注册 SessionRepository 时装配
@ConditionalOnMissingBean(SessionRepository.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@Conditional(ServletSessionCondition.class)
@EnableConfigurationProperties(RedisSessionProperties.class)
class RedisSessionConfiguration {

	// ...

	/**
	 * 继承 RedisHttpSessionConfiguration,在其基础上允许基于 spring.session.redis 
	 * 		前缀指定对应属性
	 */
	@Configuration(proxyBeanMethods = false)
	public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {

		@Autowired
		public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
				ServerProperties serverProperties) {
			Duration timeout = sessionProperties
					.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout());
			if (timeout != null) {
				setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
			}
			setRedisNamespace(redisSessionProperties.getNamespace());
			setFlushMode(redisSessionProperties.getFlushMode());
			setSaveMode(redisSessionProperties.getSaveMode());
			setCleanupCron(redisSessionProperties.getCleanupCron());
		}

	}

}
  • 这里的模式,跟 spring-boot-webmvc 模块的自动装配类似,不是直接使用,而是提供拓展的 RedisHttpSessionConfiguration
  • 提供了 SpringBootRedisHttpSessionConfiguration,主要是支持基于 spring.session.redis 前缀来配置对应的属性
  • 不难理解,跟 webMVC 模块的配置一样,如果你的应用使用了类似 @EnableSpringHttpSession @EnableRedisHttpSession 注解,那自动装配就失效了

demo

最后,提供一个自定义数据源的配置示例做个总结

// 基于 SpringHttpSessionConfiguration 拓展
@EnableSpringHttpSession
public class RedisSessionConfig {

    /**
     * 自定义的 redis 连接属性
     */
    @Value("${uasa.redis.host}")
    String host;

    @Value("${uasa.redis.username:}")
    String username;

    @Value("${uasa.redis.password:}")
    String password;

    @Value("${uasa.redis.port}")
    int port;

    @Value("${uasa.redis.database}")
    int database;

    @Value("${uasa.redis.maxInactiveIntervalInSeconds}")
    int maxInactiveIntervalInSeconds;

    @Bean
    public SessionRepository sessionRepository() {

        // redis 配置
        RedisStandaloneConfiguration configuration
                = new RedisStandaloneConfiguration();
        configuration.setHostName(host);
        configuration.setPort(port);
        configuration.setUsername(username);
        configuration.setPassword(RedisPassword.of(password));
        configuration.setDatabase(database);

        // connectionFactory 创建
        LettuceConnectionFactory connectionFactory
                = new LettuceConnectionFactory(configuration);
        // 相当于交给 Spring 容器初始化
        connectionFactory.afterPropertiesSet();

        // RedisTemplate 创建
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // 相当于交给 Spring 容器初始化
        redisTemplate.afterPropertiesSet();

        // SessionRepository 创建
        RedisSessionRepository sessionRepository = new RedisSessionRepository(redisTemplate);
        // 自定义过期时间
        sessionRepository.setDefaultMaxInactiveInterval(
                Duration.ofSeconds(maxInactiveIntervalInSeconds)
        );
        // 自定义 session 前缀
        sessionRepository.setRedisKeyNamespace("uasa:session");

        return sessionRepository;
    }

    @Bean
    public HttpSessionIdResolver sessionIdResolver() {

        // 覆盖默认的 CookieHttpSessionIdResolver
        return new HeaderHttpSessionIdResolver("token");
    }
}
  • 本示例基于 @EnableSpringHttpSession 使用,然后指定自己的 SessionRepository
  • 这样就不跟 spring-boot-data-redis 自动装配的 RedisConnectionFactory 冲突了,可以实现与主业务 redis 的完美隔离
  • 当然如果主应用不使用或者与 spring-session 使用同一个 redis 数据源,则基本不需要配置

总结

spring-session 作为 session 共享的主要 解决方案 之一,进行稍微深入的了解还是很有必要的

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Security和Spring Session的结合可以提供更强大的安全性和会话管理功能。Spring Security提供了身份验证和授权的功能,而Spring Session提供了跨多个请求的会话管理。 在结合使用时,可以使用Spring Session的`SessionRepositoryFilter`将会话存储在Redis或数据库等外部存储中,并在Spring Security中使用`SessionManagementConfigurer`配置会话管理。以下是一个简单的示例: ``` @Configuration @EnableWebSecurity @EnableRedisHttpSession public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private RedisConnectionFactory redisConnectionFactory; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login").permitAll() .and() .logout().permitAll(); } @Bean public RedisOperationsSessionRepository sessionRepository() { return new RedisOperationsSessionRepository(redisConnectionFactory); } @Bean public SessionManagementConfigurer<HttpSecurity> sessionManagementConfigurer() { return new SessionManagementConfigurer<HttpSecurity>() { @Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement() .sessionAuthenticationStrategy(sessionAuthenticationStrategy()) .maximumSessions(1) .maxSessionsPreventsLogin(true) .sessionRegistry(sessionRegistry()); } }; } @Bean public SessionAuthenticationStrategy sessionAuthenticationStrategy() { return new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry()); } @Bean public SessionRegistry sessionRegistry() { return new SpringSessionBackedSessionRegistry<>(new RedisOperationsSessionRepository(redisConnectionFactory)); } } ``` 在上面的配置中,`@EnableRedisHttpSession`注解启用了Spring Session,并使用`RedisOperationsSessionRepository`将会话存储在Redis中。`SessionManagementConfigurer`配置了会话管理,包括最大并发会话数和会话注册表。 需要注意的是,Spring Session默认使用一个名为`SESSION`的Cookie来跟踪会话。如果需要自定义Cookie名称和其他会话属性,可以使用`@EnableRedisHttpSession`的`cookieName`和`redisNamespace`属性进行配置。 在使用Spring Security和Spring Session结合时,还需要确保在各个请求中正确地暴露会话信息。可以使用Spring Session的`SessionRepositoryFilter`来完成这个任务,例如: ``` @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public FilterRegistrationBean<SessionRepositoryFilter> sessionRepositoryFilterRegistration() { FilterRegistrationBean<SessionRepositoryFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new SessionRepositoryFilter(sessionRepository())); registration.addUrlPatterns("/*"); registration.setOrder(Ordered.HIGHEST_PRECEDENCE); return registration; } @Bean public RedisOperationsSessionRepository sessionRepository() { return new RedisOperationsSessionRepository(redisConnectionFactory); } } ``` 在上面的配置中,`SessionRepositoryFilter`将会话信息暴露在所有请求中。需要注意的是,`SessionRepositoryFilter`应该注册为具有最高优先级的过滤器,以确保会话数据在其他过滤器之前暴露。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值