背景

项目中使用了spring-session作为分布式session,session存储采用了sprign-session-jdbc。

同时使用了spring-security作为安全框架

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
*
* @author kewen
* @since 2024-08-26
*/
@Configuration
public class SessionConfig {
private static final Logger log = LoggerFactory.getLogger(SessionConfig.class);

/**
* 使用 Header方式获取sessionID
*
* @return
*/
@Bean
HttpSessionIdResolver sessionIdResolver() {
return new HeaderHttpSessionIdResolver("Authorization");
}

/**
* 兼容SpringSecurity的记住我
*
* @return
*/
@Bean
SpringSessionRememberMeServices springSessionRememberMeServices() {
return new SpringSessionRememberMeServices();
}

/**
* 兼容SpringSecurity的SessionRegistry
*
* @return
*/
@Bean
SpringSessionBackedSessionRegistry springSessionBackedSessionRegistry(FindByIndexNameSessionRepository repository) {
return new SpringSessionBackedSessionRegistry(repository);
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@Configuration
@EnableConfigurationProperties({SecurityProperties.class})
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//......其余的配置略
.sessionManagement(sessionManagementConfigurer -> {
//如果有SessionRegistry则注入
sessionManagementConfigurer.sessionConcurrency(concurrentSession -> {
//容器中的SessionRegistry添加到配置中
getApplicationContext().getBeanProvider(SessionRegistry.class).ifAvailable(sessionRegistry -> {
//配置session的并发数和满了的行为
concurrentSession.sessionRegistry(sessionRegistry);
});
});
});
}
}

现在有一个需求是统计在线人数,于是想到了通过统计session来获取人数。

根据一般的方法,只需要在spring-security中添加事件发布,然后再添加一个自定义的监听器即可

  1. spring-security的配置中添加session的Bean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Configuration
    @EnableConfigurationProperties({SecurityProperties.class})
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
    * 加入监听器,session销毁时才会触发 spring容器的感知,否则 security监听不到销毁
    * @return
    */
    @Bean
    HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
    }

    //其余spring-security配置省略

    }
  2. 创建一个自定义的监听器,监听session事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//记录session存储的容器,可以从这里拿到session数量
@WebListener
public class SecuritySessionContainer implements HttpSessionListener {

private static final ConcurrentHashSet<String> SESSION_IDS = new ConcurrentHashSet<>();

@Override
public void sessionCreated(HttpSessionEvent se) {
SESSION_IDS.add(getSessionId(se));
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
SESSION_IDS.remove(getSessionId(se));
}
public int getCount() {
return SESSION_IDS.size();
}
private String getSessionId(HttpSessionEvent sessionEvent) {
return sessionEvent.getSession().getId();
}
}

从理论上来说这样就完成了session事件的监听

这时候问题来了,经测试,这个方法压根就没有调用到,也就是没有监听到发布的事件。

解决方案

先直接说解决方案

添加一个切面,增强SessionRepository这个接口的createSessiondeleteById方法,并发布事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

/**
* spring-session事件切面,原版的是不会触发事件的,到时相关的监听器无法监听session事件
* @author kewen
* @since 2024-10-29
*/
@Aspect
public class SessionRepositoryEventAspect implements ApplicationEventPublisherAware {

private static final Logger log = LoggerFactory.getLogger(SessionRepositoryEventAspect.class);

ApplicationEventPublisher applicationEventPublisher;

/**
* 创建session事件
* @param returnValue
*/
@AfterReturning(
pointcut = "execution(* org.springframework.session.SessionRepository.createSession(..))",
returning = "returnValue"
)
public void createSession(Object returnValue) {
//createSession
log.info("Create session");
if (returnValue instanceof Session) {
Session session = (Session) returnValue;
applicationEventPublisher.publishEvent(new SessionCreatedEvent(session,session));
}
}

/**
* 删除session事件
* @param pjp
* @param id
* @return
* @throws Throwable
*/
@Around("execution(* org.springframework.session.SessionRepository.deleteById(..)) && args(id)")
public Object deleteSession(ProceedingJoinPoint pjp, String id) throws Throwable {
//deleteById
log.info("Delete session");
//
Object target = pjp.getTarget();

Session session = (Session) target.getClass().getMethod("findById", String.class).invoke(target, id);

applicationEventPublisher.publishEvent(new SessionDestroyedEvent(session,session));

return pjp.proceed();
}

@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}

注意:记得加入到spring容器中必须要在容器里才能生效这不用多说

再来说一下原理

其原因是spring-session-jdb采用了JdbcIndexedSessionRepository这个类来处理session的创建查询等,但是这里并没有提供session的事件发布,上面的切面类就是增强,在创建和删除的方法调用时发布事件SessionDestroyedEventSessionDestroyedEvent,这样应用中监听的HttpSessionListener就能获取到数据了
spring-session-redis也是一样的原理,我们增强也就可以直接增强SessionRepository这个接口了,把接口增强就可以兼容多种

问题排查

  1. 既然是没有监听到,那么首先要看一下是否有发布事件但是没有监听事件,这里最主要的就是检查监听器有没有放入容器中,有没有实现HttpSessionListener,有没有添加@WebListener加入到监听器中,这很好排查。所以肯定不是这里的问题

  2. 再看是不是HttpSessionEvent事件没有发布呢。根据方法,找到了HttpSessionEventPublisher这个,这很明显就是发布这个事件的了。
    再往上找到调用这个事件的类SessionEventHttpSessionListenerAdapter,这个也是一个监听器,在onApplicationEvent()方法循环调用字段 List<HttpSessionListener> listeners也就是spring中的监听器因此这里需要检查的是SessionEventHttpSessionListenerAdapter有没有配置,HttpSessionEventPublisher这个监听器有没有在listeners中,
    再通过SessionEventHttpSessionListenerAdapter构造器往上找,查看创建这个类的配置SpringHttpSessionConfiguration。经过debug,SpringHttpSessionConfiguration是创建并配置了的,SessionEventHttpSessionListenerAdapter也是加入到容器中的,HttpSessionEventPublisher也加入到了SessionEventHttpSessionListenerAdapter的字段listeners中。因此这里也是没有问题的

  3. 以上都没有问题,那就说明了SessionEventHttpSessionListenerAdapter监听器并没有监听到事件,因此onApplicationEvent(AbstractSessionEvent event)方法没有执行到,所以自己定义的就没有监听到,因此需要查找AbstractSessionEvent这个事件没有发布的原因。

  4. AbstractSessionEvent是个抽象类,查找其子类直观的可以看到有SessionCreatedEvent,SessionDeletedEvent,SessionDestroyedEvent,SessionExpiredEvent这4个子类,我们选取一个SessionCreatedEvent来进行分析。

  5. SessionCreatedEvent类查找哪些引用了SessionCreatedEvent,可以惊奇的发现没有任何地方创建了这个类,因此可以判断此事件压根就没有发布,因此监听器没有监听到。虽然没有地方使用,但是我们可以找到EnableSpringHttpSession这个注解对其的引用说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /*
    * ......上面的省略
    * <li>SessionEventHttpSessionListenerAdapter - is responsible for translating Spring
    * Session events into HttpSessionEvent. In order for it to work, the implementation of
    * SessionRepository you provide must support {@link SessionCreatedEvent} and
    * {@link SessionDestroyedEvent}.</li>
    * <li>
    * </ul>
    *
    * @author Rob Winch
    * @since 1.1
    */
    @Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
    @Target({ java.lang.annotation.ElementType.TYPE })
    @Documented
    @Import(SpringHttpSessionConfiguration.class)
    @Configuration(proxyBeanMethods = false)
    public @interface EnableSpringHttpSession {

    }

    这个类很明确的说明了SessionRepository应该要支持SessionCreatedEventSessionDestroyedEvent

  6. 再看我们的spring-session-jdbc使用的为JdbcIndexedSessionRepository,而这个类并没有支持SessionCreatedEventSessionDestroyedEvent事件的发布,因此终于定位到了问题所在,因此只需要对JdbcIndexedSessionRepository的接口SessionRepository创建session和删除session时做增强即可。一般可以使用装饰器模式用一个类包装,或者使用动态代理切面增强,这里使用动态代理切面增强。

至此,就解决了session创建销毁拿不到的问题