1. 程式人生 > >spring security 在沒實現session共享的叢集環境下 防止使用者多次登入的 實現思路

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());