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

背景

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

一个账户只对应一个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 sessionMap;

    private SessionContext() {
        sessionMap = new ConcurrentHashMap();
    }

    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 operations = new ArrayList();
            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 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());

你可能感兴趣的:(javaEE,开源框架)