Spring Security框架中踢人下線技術探索

xiaoymin 發表於 2021-04-20

1.背景

在某次專案的開發中,使用到了Spring Security許可權框架進行後端許可權開發的許可權校驗,底層整合Spring Session元件,非常方便的整合Redis進行分散式Session的會話叢集部署。系統正式上線後,各個部署節點能夠非常方便的進行叢集部署,使用者的Session會話資訊全部儲存在Redis中介軟體庫中,開發者不用關心具體的實現,Spring Session元件已經全部整合好了。

但是在系統的使用者管理模組中,提供了對系統使用者賬號的刪除功能以及禁用功能,針對這兩個功能,需求方給出的具體要求是:

  • 刪除:當管理員刪除當前使用者賬號時,如果當前賬號已經登入系統,則需要剔除下線,並且不可登入
  • 禁用:當管理員對當前賬號禁用操作時,如果當前賬號已經登入系統,則需要剔除下線,並且登入時,提示當前賬號已禁用

2.需求分析

從上面的需求來看,不管是刪除還是禁用功能,都需要實現,如果當前賬號已經登入系統,則需要剔除下線,而禁用操作只需要再登入時給出提示資訊即可,這個在業務登入方法中就可以實現,不必從底層框架進行考慮。

因此,從底層技術測進行考慮時,我們需要探索如何在Spring Security許可權框架中實現踢人下線的功能。

既然需求已經明確,從功能的實現可能性方面入手,我們則需要從幾個方面進行考慮:

  • 1)、在Spring Security框架中,使用者登入的Session會話資訊儲存在哪裡?
  • 2)、在Spring Security框架中,Session會話如何儲存,主要儲存哪些資訊?
  • 3)、如何根據賬號收集當前該賬號登入的所有Session會話資訊?
  • 4)、如何在服務端主動銷燬Session物件?

1)、在Spring Security框架中,使用者登入的Session會話資訊儲存在哪裡?

如果我們不考慮分散式Session會話的情況,單體Spring Boot專案中,服務端Session會話肯定儲存在記憶體中,這樣的弊端是如果當前應用需要做負載均衡進行部署時,使用者請求服務端介面時,會存在Session會話丟失的情況,因為使用者登入的會話資訊都存在JVM記憶體中,沒有程式之間的共享互通。

為了解決分散式應用Session會話不丟失的問題,Spring Session元件釋出了,該元件提供了基於JDBC\Redis等中介軟體的方式,將使用者端的Session會話儲存在中介軟體中,這樣分散式應用獲取使用者會話時,都會從中介軟體去獲取會話Session,這樣也保證了服務可以做負載部署以保證Session會話不丟失。本文主要討論的也是這種情況,整合Redis中介軟體用來儲存使用者會話資訊。

2)、在Spring Security框架中,Session會話如何儲存,主要儲存哪些資訊?

由於我們使用了Redis中介軟體,所以,在Spring Security許可權框架中產生的Session會話資訊,肯定儲存與Redis中,這點毫無疑問,那麼儲存了哪些資訊呢?我會在接下來的原始碼分析中進行介紹

3)、如何根據賬號收集當前該賬號登入的所有Session會話資訊?

我們從上面的需求分析中已經得知Session會話已經儲存在Redis中,那麼我們是否可以做這樣的假設,我們只需要根據Spring Security中在Redis中儲存的鍵值,找到和登入使用者名稱相關的Redis快取資料,就可以通過呼叫Security封裝的方法進行獲取,得到當前登入賬號的會話資訊呢?這個我們需要在原始碼中去找到答案

4)、如何在服務端主動銷燬Session物件?

如果是單體的Spring Boot應用,Session資訊肯定儲存在JVM的記憶體中,服務端要主動銷燬Session物件只需要找到Security許可權框架如何儲存的就可以進行刪除。

在分散式的Spring Boot應用中,我們從上面已經得知Session會話資訊以及儲存在Redis中介軟體中,那麼我們只需要得到當前登入的Session在Redis中的鍵值,就可以呼叫方法進行刪除操作,從而主動在服務端銷燬Session會話

3.原始碼分析

在上面的需求分析中,我們已經提出了假設,並且根據假設,做出來技術性的判斷,接下來我們需要從Spring Security以及Spring Session元件的原始碼中,去尋找我們需要的答案。

首先,我們在原始碼分析前,我們需要找到入口,也就是我們在使用Spring Security框架,並且使用Spring Session元件時,我們如何使用的。

pom.xml檔案中引入元件的依賴是必不可少的,如下:

<!--Spring Security元件-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Spring針對Redis操作元件-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--Spring Session整合Redis分散式Session會話-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

接下來,我們在Spring Boot專案中,需要新增@EnableRedisHttpSession註解,以開啟Redis元件對Session會話的支援,該註解我們需要制定Spring Session在Redis中儲存的Redis名稱空間,已經Session會話的時效性,示例程式碼如下:

@SpringBootApplication
@EnableRedisHttpSession(redisNamespace = "fish-admin:session",maxInactiveIntervalInSeconds = 7200)
public class FishAdminApplication {

    static Logger logger= LoggerFactory.getLogger(FishAdminApplication.class);

    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<S extends Session> {

	/**
	 * Creates a new {@link Session} that is capable of being persisted by this
	 * {@link SessionRepository}.
	 *
	 * <p>
	 * 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.
	 * </p>
	 * @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.
	 *
	 * <p>
	 * 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.
	 * </p>
	 * @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<S extends Session> extends SessionRepository<S> {

	/**
	 * 當前儲存的使用者名稱字首,使用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<Object> 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方法中,最核心的是最後一個判斷,分別執行handleDeletedhandleExpired方法,從原始碼中我們可以看到,噹噹前Session會話被刪除或者失效時,Spring Session會通過ApplicationEventPublisher廣播一個事件,分別處理SessionExpiredEventSessionDeletedEvent事件

這是Spring Session元件為開發者預留的針對Session會話的Event事件,如果開發者對於當前的Sesssion會話的刪除或者失效有特殊的處理需求,則可以通過監聽該事件進行處理。

例如,開發者針對Session會話的操作都需要做業務操作,記錄日誌儲存到DB資料庫中,此時,開發者只需要使用Spring提供的EventListener實現就可以很輕鬆的實現,示例程式碼如下:

@Component
public class SecuritySessionEventListener {

    @EventListener
    public void sessionDestroyed(SessionDestroyedEvent event) {
        //session銷燬事件處理方法...
    }

    @EventListener
    public void sessionCreated(SessionCreatedEvent event) {
        //session建立會話事件處理方法...
    }

    @EventListener
    public void sessionExired(SessionExpiredEvent event) {
        //session會話過期事件處理方法...
    }
}

4.解決方案

我們分析了Spring Session針對Session基於Redis的實現,接下來,我們從原始碼中已經知道了該如何查詢Session會話以及銷燬會話的方法,此時,我們可以來改造我們的框架程式碼了

建立SessionService介面,程式碼如下:

public interface SessionService {

    /**
     *
     * @param account
     * @return
     */
    boolean hasLogin(String account);

    /**
     * 根據賬號查詢當前session會話
     * @param account 賬號
     * @return
     */
    Map<String, ? extends Session> loadByAccount(String account);

    /**
     * 銷燬當前session會話
     * @param account
     */
    void destroySession(String account);
}

宣告該介面主要包含3個方法:

  • hasLogin:通過傳遞登入賬號,判斷該賬號是否已經登入過,該方法是一個業務的延伸,比如我們對當前賬號判斷是否已經登入過,如果登入則提示需要退出才能繼續登入的操作等
  • loadByAccount:根據登入賬號獲取當前已經登入的Session會話Map集合
  • destroySession:根據登入賬號銷燬當前所有該賬號的Session會話資訊,此介面和產品經理要求的踢人下線操作一致

接下來就是實現類,由於我們是基於Redis來處理,因此,我們需要將原始碼分析中的RedisIndexedSessionRepository實體Bean進行引入,藉助該類實現該介面方法

RedisSessionService方法實現如下:

/**
 * SpringSession整合底層Redis實現,如果底層分散式會話保持方式不是基於Redis,則該類無法正常使用
 * @author <a href="mailto:[email protected]">[email protected]</a>
 * 2021/04/20 16:23
 * @since:fish 1.0
 */
public class RedisSessionService implements SessionService {

    Logger logger= LoggerFactory.getLogger(RedisSessionService.class);

    final RedisIndexedSessionRepository redisIndexedSessionRepository;

    final ApplicationEventPublisher applicationEventPublisher;

    public RedisSessionService(RedisIndexedSessionRepository redisIndexedSessionRepository, ApplicationEventPublisher applicationEventPublisher) {
        this.redisIndexedSessionRepository = redisIndexedSessionRepository;
        this.applicationEventPublisher = applicationEventPublisher;
    }


    @Override
    public boolean hasLogin(String account) {
        return CollectionUtil.isNotEmpty(loadByAccount(account));
    }

    @Override
    public Map<String, ? extends Session> loadByAccount(String account) {
        logger.info("收集當前登入會話session,賬號:{}",account);
        return redisIndexedSessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,account);
    }

    @Override
    public void destroySession(String account) {
        logger.info("銷燬當前登入session會話,賬號:{}",account);
        Map<String,? extends Session> sessionMap=loadByAccount(account);
        if (CollectionUtil.isNotEmpty(sessionMap)){
            logger.info("當前登入會話size:{}",sessionMap.size());
            for (Map.Entry<String,? extends Session> sessionEntry:sessionMap.entrySet()){
                String key=sessionEntry.getKey();
                Session session=sessionEntry.getValue();
                logger.info("destroy session key:{}",key);
                //刪除
                redisIndexedSessionRepository.deleteById(session.getId());
                //廣播Session會話銷燬事件
                applicationEventPublisher.publishEvent(new SessionDestroyedEvent(redisIndexedSessionRepository,session));
            }
        }
    }
}

destroySession方法實現中,首先根據賬號獲取當前所有登入會話資訊,如果會話不為空,則遍歷會話Map集合,執行刪除會話操作,並且通過applicationEventPublisher廣播一個會話被銷燬的事件。該廣播事件非必須,但是從程式碼的全域性進行考慮,還是需要加上

接下來,我們就可以將該類注入到Spring的容器中的,注入實體Bean程式碼如下:

@Bean
public RedisSessionService sessionService(RedisIndexedSessionRepository redisIndexedSessionRepository, ApplicationEventPublisher applicationEventPublisher){
    return new RedisSessionService(redisIndexedSessionRepository,applicationEventPublisher);
}

PS:我們為什麼需要建立介面而不是直接建立class的方式通過@Service等註解進行注入,而是通過抽象介面實現類的方式,最終通過JavaConfig的方式進行注入呢?從程式碼的耦合度上來看,由於Spring Session提供處理基於Redis的能力處理Session會話之外,還提供了諸如JDBC\mongo等多元化的擴充套件方式,因此,為了程式碼解耦,通過抽象介面的方式是更合理的。

接下來,我們在我們的使用者管理的業務Service方法中就可以進行操作了

刪除使用者的業務Service方法

/**
* 根據主鍵id刪除使用者管理
* @param id 主鍵id
* @return 是否刪除成功
*/
@Override
public RestfulMessage<String> delete(Integer id) {
    logger.info("根據主鍵id刪除使用者管理,id:{}",id);
    FishUserInfo fishUserInfo=fishUserInfoMapper.selectByPrimaryKey(id);
    assertArgumentNotEmpty(fishUserInfo,"請求資料非法");
    int ret=fishUserInfoMapper.deleteByPrimaryKey(id);
    //刪除成功,如果該角色線上,則強制剔除下線
    if (ret>0){
        logger.info("使用者會話剔除下線");
        sessionService.destroySession(fishUserInfo.getAccount());
    }
    return ret>0?RestfulMessage.success("刪除成功"):RestfulMessage.error("刪除失敗");
}

禁用使用者

禁用使用者其實操作方法和刪除一樣,區別在於禁用操作只是將使用者在資料庫中的狀態進行變更,而刪除則是將該使用者的資料從資料庫DB中進行刪除。更新庫的使用者狀態後,呼叫destroySession刪除該賬號的所有Session會話操作即可