同一个用户帐号,在同一时间内只能一个人使用。

本文提供了一种使用Session监听器来解决用户重复登录问题的方法,并附带了完整的示例代码。通过维护一个保存sessionID和用户名映射的HashMap,能够有效地处理用户重复登录的情况。

发现对于登录问题的解决都说用session监听,但是没有人写出示例代码,今天写了一个,希望对大家有所帮助.  
  解决同名用户登录的问题,类似QQ,即同名用户第二次登录,第一次登录的用户就掉线.  
  SessionListener.java监听session的类,部署于/App/WEB-INF/classes/com/test下(其中App为你的应用程序目录)  
  package   com.test;  
  import   javax.servlet.http.*;  
  import   java.util.*;  
   
  public   class   SessionListener   implements   HttpSessionListener{  
          private   static   HashMap   hUserName   =   new   HashMap();//保存sessionID和username的映射  
          /**以下是实现HttpSessionListener中的方法**/  
          public   void   sessionCreated(HttpSessionEvent   se){  
          }  
           
          public   void   sessionDestroyed(HttpSessionEvent   se){  
                  hUserName.remove(   se.getSession().getId()   );  
          }  
          /*  
            *   isAlreadyEnter-用于判断用户是否已经登录以及相应的处理方法  
            *   @param   sUserName   String-登录的用户名称  
            *   @return   boolean-该用户是否已经登录过的标志  
            */  
          public   static   boolean   isAlreadyEnter(HttpSession   session,String   sUserName){  
                  boolean   flag   =   false;  
                  if(hUserName.containsValue(sUserName)){//如果该用户已经登录过,则使上次登录的用户掉线(依据使用户名是否在hUserName中)  
                          flag   =   true;  
                          //遍历原来的hUserName,删除原用户名对应的sessionID(即删除原来的sessionID和username)  
                          Iterator   iter   =   hUserName.entrySet().iterator();  
                          while   (iter.hasNext())   {  
                                  Map.Entry   entry   =   (Map.Entry)iter.next();  
                                  Object   key   =   entry.getKey();  
                                  Object   val   =   entry.getValue();  
                                  if(   (   (String)val   ).equals(sUserName)   ){  
                                          hUserName.remove(key);  
                                  }  
                          }  
                          hUserName.put(   session.getId(),sUserName   );//添加现在的sessionID和username  
                          System.out.println("hUserName   =   "   +   hUserName);  
                  }  
                  else{//如果该用户没登录过,直接添加现在的sessionID和username  
                          flag   =   false;  
                          hUserName.put(   session.getId(),sUserName   );  
                          System.out.println("hUserName   =   "   +   hUserName);  
                  }  
                  return   flag;  
          }  
          /*  
            *   isOnline-用于判断用户是否在线  
            *   @param   session   HttpSession-登录的用户名称  
            *   @return   boolean-该用户是否在线的标志  
            */  
          public   static   boolean   isOnline(HttpSession   session){  
                  boolean   flag   =   true;  
                  if(   hUserName.containsKey(   session.getId()   )   ){  
                          flag   =   true;  
                  }  
                  else{  
                          flag   =   false;  
                  }  
                  return   flag;  
          }  
  }  
   
  web.xml部署于/App/WEB-INF下  
  <?xml   version="1.0"   encoding="ISO-8859-1"?>  
   
  <!DOCTYPE   web-app  
  PUBLIC   "-//Sun   Microsystems,   Inc.//DTD   Web   Application   2.3//EN"  
  "http://java.sun.com/j2ee/dtds/web-app_2.3.dtd">  
   
  <web-app>  
   
  <listener>  
  <listener-class>  
  com.inspirer.dbmp.SessionListener  
  </listener-class>  
  </listener>  
   
  </web-app>  
  应用部分  
  1.在你的登录验证时,调用SessionListener.isAlreadyEnter(session,"admin")  
  既可以判断该用户名的用户是否登录过,又可以使上次登录的用户掉线  
  2.其他页面调用SessionListener.isOnline(session),可以判断该用户是否在线.  
   
 


 这里的讨论很热烈,不过首先得澄清,这个问题不叫单点登陆,呵呵,单点登陆指的是sso,感兴趣的朋友可以看看josso。  
   
 

<think>根据用户需求,我们需要实现一个Spring Boot应用,限制同一用户最多只能有3个在线会话(即最多3个设备同时登录)。当用户第4次登录时,可以选择踢掉最早登录的会话或阻止登录。这里我们采用踢掉最早会话的方式。 实现思路: 1. 使用一个全局的会话注册表(SessionRegistry)来记录每个用户的会话列表。 2. 在用户登录时,检查该用户当前的会话数量,如果已经达到3个,则找到最早的一个会话并使其失效。 3. 使用HttpSessionListener来监听会话的销毁事件,以便在会话销毁时从注册表中移除。 注意:此实现适用于单机应用。如果是分布式系统,需要使用集中存储(如Redis)来管理会话状态。 步骤: 1. 创建SessionRegistry类,用于存储用户与会话的映射关系。 2. 实现登录控制逻辑:在用户登录成功后,将新会话加入注册表,并检查会话数量,如果超过3个,则使最早的一个会话失效。 3. 实现HttpSessionListener,在会话销毁时清理注册表中的对应记录。 具体代码实现: 1. SessionRegistry.java:使用ConcurrentHashMap来存储用户和会话列表的映射,以及会话和用户的映射(用于快速查找)。 2. 登录控制器:在登录成功后,将新会话加入SessionRegistry,并检查该用户的会话列表,如果超过3个,则使最早的一个会话失效。 3. SessionListener.java:监听会话销毁事件,从SessionRegistry中移除对应的会话。 注意:会话的创建时间可以通过HttpSession的getCreationTime()方法获取。 代码示例: 首先,创建SessionRegistry类: ```java import javax.servlet.http.HttpSession; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class SessionRegistry { // 存储每个用户对应的会话列表(按创建时间排序) private static final Map<String, List<HttpSession>> USER_SESSIONS = new ConcurrentHashMap<>(); // 存储会话ID和用户的映射,用于快速查找 private static final Map<String, String> SESSION_USER_MAP = new ConcurrentHashMap<>(); // 添加会话 public static void addSession(String username, HttpSession session) { List<HttpSession> sessions = USER_SESSIONS.computeIfAbsent(username, k -> new ArrayList<>()); sessions.add(session); SESSION_USER_MAP.put(session.getId(), username); } // 移除会话 public static void removeSession(HttpSession session) { String username = SESSION_USER_MAP.get(session.getId()); if (username != null) { List<HttpSession> sessions = USER_SESSIONS.get(username); if (sessions != null) { sessions.remove(session); // 如果该用户的会话列表为空,则移除用户条目 if (sessions.isEmpty()) { USER_SESSIONS.remove(username); } } SESSION_USER_MAP.remove(session.getId()); } } // 获取用户的所有会话(按创建时间排序) public static List<HttpSession> getSessionsByUser(String username) { List<HttpSession> sessions = USER_SESSIONS.get(username); if (sessions == null) { return Collections.emptyList(); } // 返回一个副本,避免外部修改内部数据 return new ArrayList<>(sessions); } // 获取用户当前在线的会话数量 public static int getSessionCount(String username) { List<HttpSession> sessions = USER_SESSIONS.get(username); return sessions == null ? 0 : sessions.size(); } } ``` 2. 登录控制器: ```java import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.Comparator; import java.util.List; @Controller public class LoginController { @PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password, HttpServletRequest request) { // 假设验证通过 if (authenticate(username, password)) { HttpSession newSession = request.getSession(); // 将新会话添加到注册表 SessionRegistry.addSession(username, newSession); // 检查该用户的会话数量 List<HttpSession> sessions = SessionRegistry.getSessionsByUser(username); // 如果会话数量超过3,则按创建时间排序,并销毁最早的一个(注意:新会话已经加入,所以现在是4个) if (sessions.size() > 3) { // 按创建时间排序(从小到大,即最早的排在最前面) sessions.sort(Comparator.comparingLong(HttpSession::getCreationTime)); // 获取最早的一个会话(注意:不能销毁当前新会话,因为新会话也在列表中,但新会话的创建时间是最新的,所以排序后它在最后) // 因此,我们取第一个(最早创建的)会话 HttpSession earliestSession = sessions.get(0); // 销毁最早的会话(注意:销毁会话会触发SessionListener的sessionDestroyed方法,从而从注册表中移除) earliestSession.invalidate(); } return "redirect:/home"; } return "redirect:/login?error"; } private boolean authenticate(String username, String password) { // 验证逻辑,这里省略 return true; } } ``` 3. 实现HttpSessionListener: ```java import org.springframework.stereotype.Component; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; @Component public class SessionListener implements HttpSessionListener { @Override public void sessionCreated(HttpSessionEvent se) { // 不需要做任何事情,因为我们在登录时添加会话 } @Override public void sessionDestroyed(HttpSessionEvent se) { // 当会话销毁时,从注册表中移除 SessionRegistry.removeSession(se.getSession()); } } ``` 注意:这里我们是在登录成功后将会话添加到SessionRegistry,而SessionListener会在会话销毁时自动清理。这样,当用户主动注销时,也会触发清理。 4. 在用户注销时,我们也可以主动使会话失效: ```java @PostMapping("/logout") public String logout(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } return "redirect:/login"; } ``` 这个实现中,我们限制了每个用户最多同时有3个活跃会话。当用户第4次登录时,最早创建的会话会被销毁。 但是,上述实现存在一个问题:在登录时,新会话已经加入,所以当检查到超过3个会话时,实际上已经有4个会话(包括新创建的)。因此,我们销毁最早的一个,保留3个。 另外,注意会话的排序:我们按照会话的创建时间排序,最早的排在最前面。然后销毁第一个。 但是,如果用户同时登录多个设备,可能会出现多个会话的创建时间非常接近,排序可能不精确(因为getCreationTime()返回的是毫秒时间戳)。不过,由于我们按创建时间排序,基本可以保证销毁最早创建的。 在分布式系统中,不能使用本地内存的SessionRegistry,而应该使用集中存储(如Redis)来存储用户会话信息。同时,会话的创建时间也需要从集中存储中获取,并且需要保证操作的原子性。 以上就是在Spring Boot中实现同一用户最多3个在线会话的方法。 参考引用: [1]: SpringBoot 实现并发登录数控制。通常系统都会限制同一个账号的登录数,多登录要么限制后者登录,要么踢出前者,Spring Security 提供了这样的功能,本文讲解一下在没有使用Security的时候如何手动实现这个功能[^1]。 相关问题: 1. 如何在分布式系统中实现用户会话的集中管理? 2. 如何优化会话注册表的性能,避免频繁的排序操作? 3. 除了按创建时间踢出会话,还有哪些会话淘汰策略?</think>### Spring Boot 实现同一用户最多3个在线会话 下面提供两种实现方案:基于内存会话管理和基于Redis的分布式方案,核心思路是维护用户与会话的映射关系,登录时检查会话数量并淘汰最早会话。 --- #### 方案一:基于内存会话管理(适合单机应用) ```java // 会话注册表(存储用户与会话映射) public class SessionRegistry { // 用户 -> 会话列表(按创建时间排序) private static final Map<String, List<HttpSession>> USER_SESSIONS = new ConcurrentHashMap<>(); // 添加会话并检查数量限制 public static void addSession(String username, HttpSession session) { List<HttpSession> sessions = USER_SESSIONS.computeIfAbsent( username, k -> new CopyOnWriteArrayList<>() ); sessions.add(session); Collections.sort(sessions, Comparator.comparingLong(HttpSession::getCreationTime)); // 超过3个会话时淘汰最早会话 if (sessions.size() > 3) { HttpSession oldestSession = sessions.remove(0); oldestSession.invalidate(); // 使旧会话失效 } } // 移除会话 public static void removeSession(HttpSession session) { USER_SESSIONS.values().forEach(sessions -> sessions.removeIf(s -> s.getId().equals(session.getId())) ); } } // 会话生命周期监听器 @Component public class SessionListener implements HttpSessionListener { @Override public void sessionDestroyed(HttpSessionEvent se) { SessionRegistry.removeSession(se.getSession()); } } // 登录控制器 @Controller public class LoginController { @PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password, HttpServletRequest request) { if (authenticate(username, password)) { HttpSession session = request.getSession(); SessionRegistry.addSession(username, session); // 注册新会话 return "redirect:/dashboard"; } return "redirect:/login?error"; } } ``` --- #### 方案二:基于Redis的分布式方案(适合集群部署) ```java // Redis会话管理服务 @Service public class RedisSessionService { @Autowired private RedisTemplate<String, String> redisTemplate; // 添加会话并限制数量 public void addSession(String username, String sessionId) { String key = "user_sessions:" + username; // 使用Lua脚本保证原子性操作 String luaScript = "local sessions = redis.call('LRANGE', KEYS[1], 0, -1)\n" + "if #sessions >= 3 then\n" + " redis.call('LTRIM', KEYS[1], 1, 2)\n" + // 保留最近2个会话 " return sessions[1]\n" + // 返回最早会话ID "end\n" + "redis.call('RPUSH', KEYS[1], ARGV[1])\n" + "return nil"; String oldestSessionId = redisTemplate.execute( new DefaultRedisScript<>(luaScript, String.class), Collections.singletonList(key), sessionId ); // 淘汰最早会话 if (oldestSessionId != null) { redisTemplate.delete("session:" + oldestSessionId); } } } // 会话拦截器 @Component public class SessionInterceptor implements HandlerInterceptor { @Autowired private RedisSessionService sessionService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { HttpSession session = request.getSession(false); if (session != null) { String username = (String) session.getAttribute("username"); String sessionId = session.getId(); sessionService.addSession(username, sessionId); } return true; } } ``` --- ### 关键实现要点 1. **会话淘汰策略**: - 使用LRU(最近最少使用)原则淘汰最早会话 - 会话列表按创建时间排序,新会话加入时淘汰最旧会话 2. **数据结构选择**: - 单机方案:`ConcurrentHashMap` + `CopyOnWriteArrayList` - 分布式方案:Redis List + Lua脚本保证原子性 3. **会话清理机制**: - 实现`HttpSessionListener`自动清理失效会话 - 主动调用`session.invalidate()`使会话失效 4. **性能优化**: - 分布式场景使用Lua脚本减少网络请求 - 会话列表最多保留3条记录,避免内存膨胀 > **注意事项**:分布式方案需要配置Redis序列化器 > ```java > @Bean > public RedisTemplate<String, String> redisTemplate() { > RedisTemplate<String, String> template = new RedisTemplate<>(); > template.setKeySerializer(new StringRedisSerializer()); > template.setValueSerializer(new StringRedisSerializer()); > return template; > } > ``` --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值