Spring Security框架中踢人下线技术探索,java高并发秒杀面试题

public static void main(String[] args) throws UnknownHostException {

ConfigurableApplicationContext application=SpringApplication.run(FishAdminApplication.class, args);

Environment env = application.getEnvironment();

String host= InetAddress.getLocalHost().getHostAddress();

String port=env.getProperty(“server.port”);

logger.info(“\n----------------------------------------------------------\n\t” +

“Application ‘{}’ is running! Access URLs:\n\t” +

“Local: \t\thttp://localhost:{}\n\t” +

“External: \thttp://{}:{}\n\t”+

“Doc: \thttp://{}:{}/doc.html\n\t”+

“----------------------------------------------------------”,

env.getProperty(“spring.application.name”),

env.getProperty(“server.port”),

host,port,

host,port);

}

在上面的代码中,我们指定Redis的命名空间是fish-admin:session,默认最大失效7200秒。

如果开发者默认不指定这两个属性的话,命名空间默认值是spring:session,默认最大时效则是1800秒

在上面我们已经说过了,既然是看源码,我们需要找到入口,这是看源码最好的方式,我们在使用Spring Session组件时,需要使用@EnableRedisHttpSession注解,那么该注解就是我们需要重点关注的对象,我们需要搞清楚,该注解的作用是什么?

EnableRedisHttpSession.java部分源码如下:

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

@Documented

@Import(RedisHttpSessionConfiguration.class)

@Configuration(proxyBeanMethods = false)

public @interface EnableRedisHttpSession {

//more property…

}

在该注解中,我们可以看到,最关键的是在该注解之上,使用@Import注解导入了RedisHttpSessionConfiguration.java配置类,如果你经常翻看Spring Boot相关的源码,你会敏锐地察觉到,该配置类就是我们最终要找的类

先来看该类的UML图,如下:

Spring Security框架中踢人下线技术探索

该类实现了Spring框架中很多Aware类型接口,Aware类型的接口我们都知道,Spring容器在启动创建实体Bean后,会调用Aware系列的set方法传参赋值

当然,最核心的,我们从源码中可以看到,是Spring Session组件会向Spring容器中注入两个实体Bean,代码如下:

@Bean

public RedisIndexedSessionRepository sessionRepository() {

RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();

RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);

sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);

if (this.indexResolver != null) {

sessionRepository.setIndexResolver(this.indexResolver);

}

if (this.defaultRedisSerializer != null) {

sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);

}

sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);

if (StringUtils.hasText(this.redisNamespace)) {

sessionRepository.setRedisKeyNamespace(this.redisNamespace);

}

sessionRepository.setFlushMode(this.flushMode);

sessionRepository.setSaveMode(this.saveMode);

int database = resolveDatabase();

sessionRepository.setDatabase(database);

this.sessionRepositoryCustomizers

.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));

return sessionRepository;

}

@Bean

public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(

RedisIndexedSessionRepository sessionRepository) {

RedisMessageListenerContainer container = new RedisMessageListenerContainer();

container.setConnectionFactory(this.redisConnectionFactory);

if (this.redisTaskExecutor != null) {

container.setTaskExecutor(this.redisTaskExecutor);

}

if (this.redisSubscriptionExecutor != null) {

container.setSubscriptionExecutor(this.redisSubscriptionExecutor);

}

container.addMessageListener(sessionRepository,

Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),

new ChannelTopic(sessionRepository.getSessionExpiredChannel())));

container.addMessageListener(sessionRepository,

Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + “*”)));

return container;

}

RedisIndexedSessionRepository以及RedisMessageListenerContainer的实体Bean

  • RedisMessageListenerContainer:该类是Redis的消息通知回调机制实体类,Redis提供了针对不同Key的操作回调消息通知,比如常见的删除key、key过期等事件的回调,在Spring Session组件中注入该实体Bean,从代码中也可以看出是用来监听处理Session会话的过期以及删除事件

  • RedisIndexedSessionRepository:该类是Spring Session组件提供基于Redis的针对Session会话一系列操作的具体实现类,是我们接下来源码分析的重点。

先来看RedisIndexedSessionRepository类的UML类图结构,如下图:

Spring Security框架中踢人下线技术探索

RedisIndexedSessionRepository实现了FindByIndexNameSessionRepository接口,而FindByIndexNameSessionRepository接口又继承Spring Security权限框架提供的顶级SessionRepository接口,UML类图中,我们可以得到几个重要的信息:

  • RedisIndexedSessionRepository拥有创建Session会话、销毁删除Session会话的能力

  • RedisIndexedSessionRepository由于实现自FindByIndexNameSessionRepository接口,而该接口提供了根据PrincipalName查找Session会话的能力

  • 拥有Redis回调事件的处理消息能力,因为实现了MessageListener接口

SessionRepository是Spring Security提供的顶级接口,源码如下:

public interface SessionRepository {

/**

  • Creates a new {@link Session} that is capable of being persisted by this

  • {@link SessionRepository}.

  • This allows optimizations and customizations in how the {@link Session} is

  • persisted. For example, the implementation returned might keep track of the changes

  • ensuring that only the delta needs to be persisted on a save.

  • @return a new {@link Session} that is capable of being persisted by this

  • {@link SessionRepository}

*/

S createSession();

/**

  • Ensures the {@link Session} created by

  • {@link org.springframework.session.SessionRepository#createSession()} is saved.

  • Some implementations may choose to save as the {@link Session} is updated by

  • returning a {@link Session} that immediately persists any changes. In this case,

  • this method may not actually do anything.

  • @param session the {@link Session} to save

*/

void save(S session);

/**

  • Gets the {@link Session} by the {@link Session#getId()} or null if no

  • {@link Session} is found.

  • @param id the {@link org.springframework.session.Session#getId()} to lookup

  • @return the {@link Session} by the {@link Session#getId()} or null if no

  • {@link Session} is found.

*/

S findById(String id);

/**

  • Deletes the {@link Session} with the given {@link Session#getId()} or does nothing

  • if the {@link Session} is not found.

  • @param id the {@link org.springframework.session.Session#getId()} to delete

*/

void deleteById(String id);

}

该接口提供四个方法:

  • createSession:创建Session会话

  • save:保存Session会话

  • findById:根据SessionId查找获取Session会话对象信息

  • deleteById:根据SessionId进行删除

FindByIndexNameSessionRepository源码主要是提供根据账号名称进行查询的功能,如下:

public interface FindByIndexNameSessionRepository extends SessionRepository {

/**

  • 当前存储的用户名前缀,使用Redis进行存储时,存储的key值是:redisNamespace+

*/

String PRINCIPAL_NAME_INDEX_NAME = FindByIndexNameSessionRepository.class.getName()

.concat(“.PRINCIPAL_NAME_INDEX_NAME”);

/**

  • Find a {@link Map} of the session id to the {@link Session} of all sessions that

  • contain the specified index name index value.

  • @param indexName the name of the index (i.e.

  • {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME})

  • @param indexValue the value of the index to search for.

  • @return a {@code Map} (never {@code null}) of the session id to the {@code Session}

  • of all sessions that contain the specified index name and index value. If no

  • results are found, an empty {@code Map} is returned.

*/

Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);

/**

  • Find a {@link Map} of the session id to the {@link Session} of all sessions that

  • contain the index with the name

  • {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME} and the

  • specified principal name.

  • @param principalName the principal name

  • @return a {@code Map} (never {@code null}) of the session id to the {@code Session}

  • of all sessions that contain the specified principal name. If no results are found,

  • an empty {@code Map} is returned.

  • @since 2.1.0

*/

default Map<String, S> findByPrincipalName(String principalName) {

return findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);

}

}

该接口最核心的功能是提供了根据用户名查找获取Session会话的接口,这对我们后面实现踢人功能很有帮助。

通过查看SessionRepository接口以及FindByIndexNameSessionRepository接口的源码我们得知:

  • Redis的实现最终实现了这两个接口,因此获得了基于Redis中间件创建及销毁Session会话的能力

  • 根据账号去查找当前的所有登录会话Session符合我们最终需要服务端主动踢人下线的功能需求。

接下来我们只需要关注RedisIndexedSessionRepository的实现即可。首先来看findByPrincipalName方法,源码如下:

@Override

public Map<String, RedisSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {

//如果名称不匹配,则直接反馈空集合Map

if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {

return Collections.emptyMap();

}

//获取拼装的Key值

String principalKey = getPrincipalKey(indexValue);

//从Redis中获取该Key值的成员数

Set sessionIds = this.sessionRedisOperations.boundSetOps(principalKey).members();

//初始化Map集合

Map<String, RedisSession> sessions = new HashMap<>(sessionIds.size());

//循环遍历

for (Object id : sessionIds) {

//根据id查找Session会话

RedisSession session = findById((String) id);

if (session != null) {

sessions.put(session.getId(), session);

}

}

return sessions;

}

String getPrincipalKey(String principalName) {

return this.namespace + “index:” + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + “:”

  • principalName;

}

接下来我们看删除Session会话的方法实现:

@Override

public void deleteById(String sessionId) {

//根据sessionId获取Session会话

RedisSession session = getSession(sessionId, true);

if (session == null) {

return;

}

//从Redis中移除所有存储的针对principal的key值

cleanupPrincipalIndex(session);

//Redis中删除SessionId所对应的key值

this.expirationPolicy.onDelete(session);

//移除Session会话创建时,存储的过期key值

String expireKey = getExpiredKey(session.getId());

this.sessionRedisOperations.delete(expireKey);

//设置当前session会话最大存活时间为0

session.setMaxInactiveInterval(Duration.ZERO);

//执行save方法

save(session);

}

从上面的代码中,我们已经知道了Spring Session组件对于Session相关的处理方法,其实我们基于上面的两个核心方法,我们已经获得了踢人下线的能力,但是,既然RedisIndexedSessionRepository实现了MessageListener接口,我们需要继续跟踪一下该接口的具体实现方法,我们直接来看onMessage方法,代码如下:

@Override

public void onMessage(Message message, byte[] pattern) {

byte[] messageChannel = message.getChannel();

byte[] messageBody = message.getBody();

String channel = new String(messageChannel);

if (channel.startsWith(this.sessionCreatedChannelPrefix)) {

// TODO: is this thread safe?

@SuppressWarnings(“unchecked”)

Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());

handleCreated(loaded, channel);

return;

}

String body = new String(messageBody);

if (!body.startsWith(getExpiredKeyPrefix())) {

return;

}

boolean isDeleted = channel.equals(this.sessionDeletedChannel);

if (isDeleted || channel.equals(this.sessionExpiredChannel)) {

int beginIndex = body.lastIndexOf(“:”) + 1;

int endIndex = body.length();

String sessionId = body.substring(beginIndex, endIndex);

RedisSession session = getSession(sessionId, true);

if (session == null) {

logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId);

return;

}

if (logger.isDebugEnabled()) {

logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);

}

cleanupPrincipalIndex(session);

if (isDeleted) {

handleDeleted(session);

}

else {

handleExpired(session);

}

}

}

private void handleDeleted(RedisSession session) {

publishEvent(new SessionDeletedEvent(this, session));

}

private void handleExpired(RedisSession session) {

publishEvent(new SessionExpiredEvent(this, session));

}

private void publishEvent(ApplicationEvent event) {

try {

this.eventPublisher.publishEvent(event);

}

catch (Throwable ex) {

logger.error("Error publishing " + event + “.”, ex);

}

}

在onMessage方法中,最核心的是最后一个判断,分别执行handleDeleted和handleExpired方法,从源码中我们可以看到,当当前Session会话被删除或者失效时,Spring Session会通过ApplicationEventPublisher广播一个事件,分别处理SessionExpiredEvent和SessionDeletedEvent事件

这是Spring Session组件为开发者预留的针对Session会话的Event事件,如果开发者对于当前的Sesssion会话的删除或者失效有特殊的处理需求,则可以通过监听该事件进行处理。

例如,开发者针对Session会话的操作都需要做业务操作,记录日志保存到DB数据库中,此时,开发者只需要使用Spring提供的EventListener实现就可以很轻松的实现,示例代码如下:

@Component

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后

分享一些系统的面试题,大家可以拿去刷一刷,准备面试涨薪。

这些面试题相对应的技术点:

  • JVM
  • MySQL
  • Mybatis
  • MongoDB
  • Redis
  • Spring
  • Spring boot
  • Spring cloud
  • Kafka
  • RabbitMQ
  • Nginx

大类就是:

  • Java基础
  • 数据结构与算法
  • 并发编程
  • 数据库
  • 设计模式
  • 微服务
  • 消息中间件

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

数据结构与算法

  • 并发编程
  • 数据库
  • 设计模式
  • 微服务
  • 消息中间件

[外链图片转存中…(img-dSPRWkXr-1712184596483)]

[外链图片转存中…(img-lR1kJeSO-1712184596483)]

[外链图片转存中…(img-ycTuNYYG-1712184596483)]

[外链图片转存中…(img-nM4TqrT0-1712184596484)]

[外链图片转存中…(img-1Dc4XhiT-1712184596484)]

[外链图片转存中…(img-EugKNc88-1712184596484)]

[外链图片转存中…(img-UdhuUI0m-1712184596485)]

[外链图片转存中…(img-4XKBTGXx-1712184596485)]

[外链图片转存中…(img-gFQYVjUZ-1712184596485)]

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值