spring security 在没实现session共享的集群环境下 防止用户多次登录的 实现思路

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: spring security 在没实现session共享的集群环境下 防止用户多次登录的 实现思路

背景

  • 项目采用阿里云负载均衡,基于cookie的会话保持。
  • 没有实现集群间的session共享。
  • 项目采用spring security 并且配置了session策略如下:
<bean
                    class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
                    <constructor-arg ref="sessionRegistry" />
                    <property name="maximumSessions" value="1" />
                    <property name="exceptionIfMaximumExceeded" value="false" />
                </bean>

一个账户只对应一个session,也就是一个用户在不同浏览器登陆,后登陆的会导致前面登陆的session失效。


问题分析

集群环境下,导致maximumSessions的配置失效。并不能实现预期的目标。因为session没有共享。


解决思路

  • 采用spring data redis session解决实现session共享,统一管理。
  • 但是由于项目集成了过多的开源框架,由于版本原因,很难整合到一起。并且项目测试已经接近尾声,因此没有采用。
  • zookeeper监听用户session 方式,后登陆时操作对应节点,触发监听事件,使其先创建的session失效。


最终采用zookeeper监听session方式

具体代码

session上下文保持

package com.raymon.cloudq.util;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SessionContext {
    private static SessionContext instance;
    private Map<String, HttpSession> sessionMap;
    private SessionContext() {
        sessionMap = new ConcurrentHashMap<String, HttpSession>();
    }
    public synchronized static SessionContext getInstance() {
        if (instance == null) {
            instance = new SessionContext();
        }
        return instance;
    }
    public  void addSession(HttpSession session) {
        if (session != null) {
            sessionMap.put(session.getId(), session);
        }
    }
    public  void delSession(HttpSession session) {
        if (session != null) {
            sessionMap.remove(session.getId());
        }
    }
    public  void delSession(String sessionId) {
         sessionMap.remove(sessionId);
    }
    public  HttpSession getSession(String sessionId) {
        if (sessionId == null)
            return null;
        return sessionMap.get(sessionId);
    }
}

基于zookeeper的session控制接口

**
 * 由于session在集群中没有实现共享,一个账户只能对应一个session
 * 基于zookeeper监听的 来控制
 */
public interface ClusterSessionsCtrlService {
    /**
     * 设置监听
     */
    void setMaximumSessionsListener();
    /**
     * 注册zookeeper sesssion数据
     * 后登陆的用户会删除先登录的节点,触发监听,让先登陆的session失效
     * @param empId
     * @param httpSession
     */
    void registerZookeeperSession(Integer empId,HttpSession httpSession);
    /**
     * session超时,删除zookeeper注册数据
     * @param sessionId
     */
    void deleteZookeeperSessionRegister(String sessionId);
}

session控制接口实现类

package com.raymon.cloudq.service.impl;
import com.raymon.cloudq.service.ClusterSessionsCtrlService;
import com.raymon.cloudq.util.SessionContext;
import org.apache.commons.lang.StringUtils;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.transaction.CuratorOp;
import org.apache.curator.framework.api.transaction.CuratorTransactionResult;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Service
public class ClusterSessionsCtrlServiceImpl implements ClusterSessionsCtrlService, InitializingBean {
    @Value(" ${zookeeperhostName}")
    private String zookeeperConnectionString;
    private static String sessionPath = "/session";
    private static String sessionReaPath = "/sessionrea";
    protected static org.slf4j.Logger logger = LoggerFactory.getLogger(ClusterSessionsCtrlServiceImpl.class);
    private CuratorFramework client = null;
    @Override
    public void afterPropertiesSet() throws Exception {
        setMaximumSessionsListener();
    }
    @Override
    public void setMaximumSessionsListener() {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        client = CuratorFrameworkFactory.builder().connectString(zookeeperConnectionString)
                .sessionTimeoutMs(8000).retryPolicy(retryPolicy).build();
        client.start();
        try {
            Stat stat = client.checkExists().forPath(sessionPath);
            if (stat == null) {
                client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(sessionPath);
            }
            stat = client.checkExists().forPath(sessionReaPath);
            if (stat == null) {
                client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(sessionReaPath);
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("zookeeper创建/session失败,原因{}", e.toString());
        }
        PathChildrenCache cache = null;
        try {
            cache = new PathChildrenCache(client, sessionPath, true);
            cache.start();
        } catch (Exception e) {
            e.printStackTrace();
            logger.error(e.toString());
        }
        PathChildrenCacheListener cacheListener = new PathChildrenCacheListener() {
            @Override
            public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
                logger.info("事件类型:" + event.getType());
                if( event.getData()!=null){
                    logger.info("节点数据:" + event.getData().getPath() + " = " + new String(event.getData().getData()));
                }
                if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) {
                    HttpSession httpSession = SessionContext.getInstance().getSession(new String(event.getData().getData()));
                    if (httpSession == null) {
                        return;
                    }
                    httpSession.invalidate();
                }
            }
        };
        cache.getListenable().addListener(cacheListener);
    }
    @Override
    public void deleteZookeeperSessionRegister(String sessionId) {
        try {
            SessionContext.getInstance().delSession(sessionId);
            String empId = null;
            Stat stat = client.checkExists().forPath(sessionReaPath + "/" + sessionId);
            if (stat != null) {
                empId = new String(client.getData().forPath(sessionReaPath + "/" + sessionId));
                client.delete().forPath(sessionReaPath + "/" + sessionId);
                logger.info("delete session:" + sessionReaPath + "/" + sessionId);
            }
         /*   stat = client.checkExists().forPath(sessionPath + "/" + empId);
            if (StringUtils.isNotEmpty(empId) && stat != null) {
                client.delete().forPath(sessionPath + "/" + empId);
                logger.info("delete session:" + sessionPath + "/" + empId);
            }*/
        } catch (Exception e) {
            e.printStackTrace();
            logger.error(e.toString());
        }
    }
    @Override
    public void registerZookeeperSession(Integer empId, HttpSession httpSession) {
        try {
            SessionContext.getInstance().addSession(httpSession);
            Stat stat = client.checkExists().forPath(sessionPath + "/" + empId);
            List<CuratorOp> operations = new ArrayList<CuratorOp>();
            if (stat != null) {
                CuratorOp deleteOpt  = client.transactionOp().delete().forPath(sessionPath + "/" + empId);
                operations.add(deleteOpt);
            }
            CuratorOp createSessionPathOpt = client.transactionOp().create().withMode(CreateMode.EPHEMERAL).forPath(sessionPath + "/" + empId, httpSession.getId().getBytes());
            CuratorOp createSessionReaPathOpt = client.transactionOp().create().withMode(CreateMode.EPHEMERAL).forPath(sessionReaPath + "/" + httpSession.getId(), String.valueOf(empId).getBytes());
            operations.add(createSessionPathOpt);
            operations.add(createSessionReaPathOpt);
            Collection<CuratorTransactionResult> results = client.transaction().forOperations(operations);
            for (CuratorTransactionResult result : results) {
                logger.info(result.getForPath() + " - " + result.getType());
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error(e.toString());
        }
    }
}

session监听

/**
 * session监听
 * 
 *
 */
public class SessionListener  implements HttpSessionListener,  HttpSessionAttributeListener{
    private SessionContext context = SessionContext.getInstance();
    Logger log = LoggerFactory.getLogger(SessionListener.class);
    @Override
    public void attributeAdded(HttpSessionBindingEvent arg0) {
    }
    @Override
    public void attributeRemoved(HttpSessionBindingEvent arg0) {
    }
    @Override
    public void attributeReplaced(HttpSessionBindingEvent arg0) {
    }
    @Override
    public void sessionCreated(HttpSessionEvent arg0) {
        if(log.isDebugEnabled()) {
            log.debug("创建session");
        }
    }
    @Override
    public void sessionDestroyed(HttpSessionEvent arg0) {
        context.delSession(arg0.getSession());
        ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(arg0.getSession().getServletContext());
        ClusterSessionsCtrlService clusterSessionsCtrlService = ctx.getBean(ClusterSessionsCtrlService.class);
        clusterSessionsCtrlService.deleteZookeeperSessionRegister(arg0.getSession().getId());
    }
}

用户登陆成功需要注册session监听

clusterSessionsCtrlService.registerZookeeperSession(empId,request.getSession());
相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
相关文章
|
2天前
|
安全 Java 数据安全/隐私保护
|
6天前
|
安全 Java API
第7章 Spring Security 的 REST API 与微服务安全(2024 最新版)(上)
第7章 Spring Security 的 REST API 与微服务安全(2024 最新版)
24 0
第7章 Spring Security 的 REST API 与微服务安全(2024 最新版)(上)
|
6天前
|
存储 安全 Java
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)(下)
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)
17 2
|
6天前
|
安全 Cloud Native Java
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)(上)
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)
22 2
|
6天前
|
安全 Java API
第5章 Spring Security 的高级认证技术(2024 最新版)(上)
第5章 Spring Security 的高级认证技术(2024 最新版)
29 0
|
6天前
|
存储 安全 Java
第3章 Spring Security 的用户认证机制(2024 最新版)(下)
第3章 Spring Security 的用户认证机制(2024 最新版)
29 0
|
6天前
|
存储 安全 Java
第2章 Spring Security 的环境设置与基础配置(2024 最新版)(下)
第2章 Spring Security 的环境设置与基础配置(2024 最新版)(下)
16 0
|
6天前
|
安全 Java 数据库
第2章 Spring Security 的环境设置与基础配置(2024 最新版)(上)
第2章 Spring Security 的环境设置与基础配置(2024 最新版)
28 0
|
10月前
|
Java Nacos Spring
使用Spring Boot的Profile功能来实现不同环境使用不同的Nacos Namespace的配置
使用Spring Boot的Profile功能来实现不同环境使用不同的Nacos Namespace的配置
387 1
|
11月前
|
Java 测试技术 Nacos
Spring Cloud Alibaba - 18 Nacos Config配置中心加载相同微服务的不同环境下的通用配置
Spring Cloud Alibaba - 18 Nacos Config配置中心加载相同微服务的不同环境下的通用配置
96 0