背景
- 项目采用阿里云负载均衡,基于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());