通过session_id限制仅一个用户登录

本文介绍了一种单点登录机制的实现方法,通过在用户登录时记录session_id,并在每次验证用户信息时检查session_id的一致性,确保账户在唯一地点登录。详细展示了使用PHP实现的代码示例。

需求:

我们有的时候,希望一个账户,仅在一个地方登录。即,在别的地方登录该账户时,当前用户会被自动注销。

思路:

用户数据表admin

idnamepasswordsession_id
1root63a9f0ea7bb98050796b649e854818453olcdjkj5jjaq2u9t30mbuna96

我们实现该功能只需要在普通用户验证的基础上,加一层session_id的验证就可以了。我们在用户登录的时候,将此时的session_id写入数据表,session_id在本次session有效期是不会变化的。如果在别的地方,登录了该账户,则新的session_id值会写入数据表,则在验证用户信息的时候,当前用户的session_id和数据表里面的session_id不一致。

核心代码:

indexController.class.php

<?php

/**
 * Created by PhpStorm.
 * User: koastal
 * Date: 2016/5/15
 * Time: 19:28
 */
class indexController extends Controller { function __construct() { parent::__construct(); } /** * 显示用户信息页面 */ function index() { $loginController = new loginController(); $loginController->isLogin(); $this->smarty->assign("name",$_SESSION['name']); $this->smarty->assign("id",$_SESSION['id']); $this->smarty->display("info.html"); } }

loginController.class.php

<?php

/**
 * Created by PhpStorm.
 * User: koastal
 * Date: 2016/5/28
 * Time: 20:37
 */
class loginController extends Controller { private $loginModel; function __construct() { parent::__construct(); $this->loginModel = new loginModel(); } /** * @return bool * 判断当前登录用户是否合法 */ function isLogin(){ $res = $this->loginModel->getUserInfoById($_SESSION['id']); if(empty($res)){ echo "未登录";exit; }else{ $sql_token = md5($res['name'].$res['password']); if($sql_token != $_SESSION['token']){ echo "用户验证失败";exit; }else{ if(session_id()!=$res['session_id']){ echo "该账户已在别处登录";exit; }else{ return true; } } } } /** * 显示登录表单 */ function form(){ $this->smarty->display("login.html"); } /** * 执行登录操作 */ function action(){ $name = $_POST['name']; $password = md5($_POST['password']); $session_id = session_id(); $res = $this->loginModel->loginCheck($name,$password,$session_id); if($res){ $_SESSION['id'] = $res['id']; $_SESSION['name'] = $name; $_SESSION['token'] = md5($name.$password); header("Location:http://login.com/index.php/index/index"); }else{ header("Location:http://login.com/index.php/login/form"); } } }

loginModel.class.php

<?php

/**
 * Created by PhpStorm.
 * User: koastal
 * Date: 2016/5/28
 * Time: 19:56
 */
class loginModel extends Model { /** * @param $id * @return bool */ function getUserInfoById($id){ $sparam = array('id','name','password','session_id'); $wparam = array("id[=]"=>$id); $data = $this->select("admin",$sparam,$wparam); if(empty($data)){ return false; }else{ return $data[0]; } } /** * @param $name * @param $password * @param $session_id * @return array/bool */ function loginCheck($name,$password,$session_id){ $sparam = array('id','name','password',"session_id"); $wparam = array("name[=]"=>$name); $data = $this->select("admin",$sparam,$wparam); if(empty($data)){ return false; }else{ $info = $data[0]; if($info['password']!=$password){ //验证失败 return false; }else{ //验证成功,更新session $newdata = ["session_id"=>$session_id]; $wparam = ["id[=]"=>$info['id']]; $this->update("admin",$newdata,$wparam); return $info; } } } }

项目代码下载:

链接:http://pan.baidu.com/s/1kVAPJjp 密码:04iv

部署说明:

  1. host添加 127.0.0.1 login.com
  2. 在本地新建数据库test,新建表admin

转载于:https://www.cnblogs.com/haohaoyuan/p/7797941.html

<think>我们讨论的是如何限制一个sessionID对应的WebSocket连接个数。 根据之前的讨论,我们知道: - WebSocket连接建立时会携带Cookie(含SessionID),从而关联到用户的会话(session)[^1]。 - 在应用层,我们通常需要自己维护WebSocketSession,例如使用一个Map来存储用户ID(或sessionID)与WebSocketSession的映射关系[^2][^3]。 现在需求是:限制一个sessionID(即同一个用户会话)同时建立的WebSocket连接数量。 思路: 1. 我们需要在服务端维护一个数据结构,记录每个sessionID当前活跃的WebSocket连接数(或连接实例)。 2. 在建立新的WebSocket连接时(在`afterConnectionEstablished`等方法中),检查该sessionID当前的连接数是否已达到上限。 3. 如果达到上限,则拒绝新的连接(例如,关闭新建立的连接)。 4. 在连接关闭时(在`afterConnectionClosed`方法中),减少该sessionID的连接计数。 具体步骤(以Spring WebSocket为例): 步骤1:定义一个全局的连接计数器 使用一个线程安全的Map来存储每个sessionID的连接计数(或者存储每个sessionID对应的连接集合,然后通过集合大小判断)。例如: ```java private final ConcurrentMap<String, AtomicInteger> sessionConnectionCounts = new ConcurrentHashMap<>(); // 或者存储连接集合(如果需要操作具体的连接实例) // private final ConcurrentMap<String, Set<WebSocketSession>> sessionConnections = new ConcurrentHashMap<>(); ``` 步骤2:在连接建立时增加计数并检查 ```java @Override public void afterConnectionEstablished(WebSocketSession session) { // 从session中获取sessionID(这里注意:WebSocketSessionid是唯一的连接标识,不是HTTP SessionID) // 我们需要的是HTTP SessionID,它通常通过握手时的属性传递过来(在握手拦截器中设置) // 假设我们在握手拦截器中已经将HTTP SessionID设置到了WebSocketSession的attributes中,键为"sessionId" String sessionId = (String) session.getAttributes().get("sessionId"); // 如果sessionId为null,可能是未登录,这里可以根据需要处理(例如关闭连接) if (sessionId == null) { session.close(CloseStatus.NOT_ACCEPTABLE); return; } // 获取当前sessionId的连接计数(原子操作) AtomicInteger count = sessionConnectionCounts.computeIfAbsent(sessionId, k -> new AtomicInteger(0)); int currentCount = count.incrementAndGet(); // 设置每个sessionID的最大连接数(例如3) int maxConnections = 3; if (currentCount > maxConnections) { // 超过上限,递减计数(因为刚刚已经增加了)并关闭连接 count.decrementAndGet(); session.close(CloseStatus.POLICY_VIOLATION); return; } // 连接被接受,继续其他操作(如加入会话管理) } ``` 步骤3:在连接关闭时减少计数 ```java @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { String sessionId = (String) session.getAttributes().get("sessionId"); if (sessionId != null) { AtomicInteger count = sessionConnectionCounts.get(sessionId); if (count != null) { // 减少计数,如果计数为0,则从map中移除(避免内存泄漏) int currentCount = count.decrementAndGet(); if (currentCount <= 0) { sessionConnectionCounts.remove(sessionId, count); // 使用remove(key, value)确保安全 } } } } ``` 注意事项: - **线程安全**:使用`ConcurrentHashMap`和`AtomicInteger`保证线程安全。 - **内存泄漏**:需要确保在连接关闭时减少计数,并在计数为0时移除条目。 - **握手拦截器中设置sessionId**:在握手阶段,我们需要将HTTP SessionID(或用户标识)设置到WebSocketSession的属性中。可以通过实现`HandshakeInterceptor`,在`beforeHandshake`方法中设置: ```java @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { // 从HTTP请求中获取session(例如通过HttpSession) if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; HttpSession httpSession = servletRequest.getServletRequest().getSession(false); if (httpSession != null) { // 将HTTP SessionID存入WebSocketSession的属性中 attributes.put("sessionId", httpSession.getId()); } } return true; } ``` - **分布式环境**:上述方案适用于单机。在集群环境下,需要将计数存储在分布式缓存(如Redis)中,并使用原子操作(如Redis的INCR/DECR)来保证一致性。 分布式环境下的调整: 使用Redis存储每个sessionId的连接计数,并利用Redis的原子操作来增减计数。例如: - 连接建立时:使用`INCR`命令增加计数,然后判断是否超过最大值,如果超过则使用`DECR`并关闭连接。 - 连接关闭时:使用`DECR`减少计数。 但要注意,在分布式环境下,增减计数和判断是否超过最大值需要保证原子性(可以使用Lua脚本,或者使用Redis的`INCR`和`GET`操作,但要注意在非原子操作时可能出现的并发问题)。一种简单的方式是使用Lua脚本: ```lua local key = KEYS[1] -- sessionId local max = tonumber(ARGV[1]) -- 最大连接数 local current = redis.call('GET', key) or 0 if tonumber(current) >= max then return -1 -- 超过上限 else return redis.call('INCR', key) -- 返回增加后的值 end ``` 在Java中,使用Spring Data Redis执行该脚本,如果返回-1则关闭连接。 总结:限制一个sessionID的WebSocket连接个数需要开发者在应用层实现计数和限制逻辑,包括连接建立时的检查、连接关闭时的清理,并注意线程安全和分布式环境下的扩展。 相关问题: 1. 在分布式系统中如何确保WebSocket连接计数的原子性? 2. 如何优雅地关闭超过连接数限制的WebSocket连接? 3. 除了连接计数,还有哪些方法可以限制一个用户的WebSocket连接数? </think> ### 如何限制一个SessionID的WebSocket连接个数? 在WebSocket应用中,限制同一SessionID的连接数量是防止资源滥用和提升系统稳定性的关键策略。以下是具体实现方案: #### **1. 核心实现原理** 需要**在服务端维护连接计数器**,基于SessionID进行连接数统计和限制: ```java // 全局连接计数器:SessionID -> 当前连接数 private final ConcurrentMap<String, AtomicInteger> sessionConnections = new ConcurrentHashMap<>(); private static final int MAX_CONNECTIONS_PER_SESSION = 3; // 单Session最大连接数 ``` #### **2. 连接建立时的计数校验** 在`afterConnectionEstablished`回调中执行检查: ```java @Override public void afterConnectionEstablished(WebSocketSession session) { String sessionId = getSessionId(session); // 从Cookie或Header获取SessionID AtomicInteger count = sessionConnections.computeIfAbsent( sessionId, k -> new AtomicInteger(0) ); // 连接数超限处理 if (count.incrementAndGet() > MAX_CONNECTIONS_PER_SESSION) { count.decrementAndGet(); // 回滚计数 session.close(CloseStatus.POLICY_VIOLATION); // 强制关闭连接 return; } // 正常处理连接... } ``` #### **3. 连接关闭时的计数清理** 在`afterConnectionClosed`回调中更新计数器: ```java @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { String sessionId = getSessionId(session); AtomicInteger count = sessionConnections.get(sessionId); if (count != null && count.decrementAndGet() <= 0) { sessionConnections.remove(sessionId); // 清理空计数器 } } ``` #### **4. SessionID获取方法** 通过握手拦截器从HTTP请求中提取SessionID: ```java public class AuthInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { // 从Cookie或Header解析SessionID String sessionId = extractSessionId(request); attributes.put("sessionId", sessionId); // 存入WebSocketSession属性 return true; } } ``` #### **5. 分布式环境扩展** 在集群中需使用分布式计数器(如Redis): ```java // 使用Redis原子操作 public boolean checkConnectionLimit(String sessionId) { String key = "ws:limit:" + sessionId; Long count = redisTemplate.opsForValue().increment(key, 1); if (count > MAX_CONNECTIONS_PER_SESSION) { redisTemplate.decrement(key, 1); // 回滚 return false; } redisTemplate.expire(key, 1, TimeUnit.HOURS); // 设置过期时间 return true; } ``` #### **6. 关键注意事项** - **线程安全**:单机使用`ConcurrentHashMap` + `AtomicInteger` - **僵尸连接处理**:配置`setIdleTimeout()`自动清理死链 - **失效Session清理**:定时任务清除无活跃连接的计数器 - **连接拒绝策略**:返回`1008(政策违规)`状态码 > **示例场景**:当用户SessionID=xyz)尝试建立第4个连接时,服务端主动关闭新连接并返回: > ``` > CloseStatus: 1008 (Policy Violation) > Reason: "Exceeded max connections per session (3)" > ``` --- ### 相关问题 1. 如何防止WebSocket连接数的分布式计数器出现脏读? 2. 不同设备登录同一账户时,SessionID相同会导致连接数限制吗? 3. WebSocket连接限制与HTTP请求限流机制有何本质区别? 4. 高并发场景下如何优化连接计数器的性能? : WebSocket协议通过空闲超时自动清理僵尸连接 [^2]: 拒绝连接时返回RFC-6455定义的策略违规状态码
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值