用户账号不可同时两处登入系统的控制

本文介绍了一个确保用户账号在单系统中不能被同时登录的机制。通过在ServletContext中维护一个全局变量来跟踪当前登录的用户会话,并在用户登出或会话超时时释放资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

==========================2017/03/28========================

今天测试出了一个问题,日志:

java.lang.IllegalStateException: invalidate: Session already invalidated
这个是因为在我的 logout 里面执行了一次会话销毁:

// 登出
@RequestMapping(value="logout", method=RequestMethod.GET)
@ResponseBody
public BaseResponse logout(HttpServletRequest request){		
	BaseResponse resp = new BaseResponse(RESPONSE_STATUS.SUCCESS);
	// 注销会话
	request.getSession().invalidate();
	
	resp.setMsg("成功退出系统");
	return resp;
}

但是我在 web.xml 里面把下面这个 Listener 注释掉了。
<listener>
	<listener-class>com.hebta.vinci.interceptor.VinciContextLoaderListener</listener-class>
</listener>
这就导致了 logout 里面的 invalidate() 完了后并没有执行 VinciSessionListener.sessionDestroyed 里面的清理工作。然后在 login 方法栈的 doSingleLogin 里,被销毁的会话再一次执行了 invalidate(),造成重复销毁一个会话的异常。


还有一个问题,就是 Session Timeout 后,sessionDestroyed() 方法没有被调用,这个很奇怪。由于没有从我们的 Map<String, Pair<String, HttpSession>> 里面删除对应记录,用户登录时, doSingleLogin() 里面的 old.invalidate(); 一调就报这个异常。由于一时搞不清问题所在,只能 try-catch 这个代码,然后清除记录:

for (String key : map.keySet()){
	Pair<String, HttpSession> pair = map.get(key);
	if (pair.getLeft().equalsIgnoreCase(loginName)){  
		logger.debug("曾经登录过,注销前面的会话");
		HttpSession old = pair.getRight();
		try {
			old.invalidate();
		} catch (Exception e) {
			logger.debug("可能是会话超时,服务器已经删除过了");
			map.remove(key);
		}
		break;
	}
}

==========================2017/03/23========================

3月16日的方法有个问题,如果前端把浏览器关掉了,就没办法再登录了,因为服务端不知道这个事件,就不能销毁会话,用户只能傻等着会话超时。

所以这次微调下:使用 Map<String, Pair<String, HttpSession>>, Pair 关联用户的登录账号和会话,如果用户再次登录,那么前面那个会话被销毁,将新会话注册到应用上下文。

新的 login 逻辑代码如下:

public User login(User user){
	// other work
	if(isCorrectPassword){
		doSingleLogin(user.getLoginName()); 
		
		return dbUser;
	}
	return null;
}

private void doSingleLogin(String loginName) {
	ServletContext ctx = SessionUtil.getServletContext();
	Map<String, Pair<String, HttpSession>> map = (Map<String, Pair<String, HttpSession>>)ctx.getAttribute(VinciConstants.SINGLE_LOGIN);
	
	for (String key : map.keySet()){
		Pair<String, HttpSession> pair = map.get(key);
		if (pair.getLeft().equalsIgnoreCase(loginName)){  
			logger.debug("如果曾经登录过,就让前面的会话注销");
			HttpSession old = pair.getRight();
			old.invalidate();
			break;
		}
	}		
	
	logger.debug("注册新的会话到应用上下文"); 
	HttpSession session = SessionUtil.getRequest().getSession();
	map.put(session.getId(), Pair.of(loginName, session));
}


// 重新登录,或者主动登出,都会触发会话注销,所以这里集中从应用上下文里删除老的会话
	@Override
	public void sessionDestroyed(HttpSessionEvent se) {
		String id = se.getSession().getId();
		logger.debug("session destroyed :: " + id);

		Object singleLoginAttr = SessionUtil.getServletContext().getAttribute(VinciConstants.SINGLE_LOGIN);
		if (singleLoginAttr != null) {
			Map<String, Pair<String, HttpSession>> map = (Map<String, Pair<String, HttpSession>>) singleLoginAttr;
			map.remove(id);
		} else {
			logger.warn("没有找到会话里的属性SINGLE_LOGIN");
		}
	}


====================2017/03/16=========================

需求是:如果账号已经在被使用,就不可再用此账号登录系统,需要等待前面那个用户主动退出或会话超时才可以登录系统。

实现方案:在不考虑分布式的情况下,往ServletContext 放一个全局的变量,用来存储登录过的账号,每次用户登录,都去检查该账号是否在这个变量里,有就说明正在使用,需要等待。否则就成功登录并且把自己的SessionId 和账号用户名存放到这个全局变量里。

1. 首先在 Spring 启动的时候就初始化全局变量。这里自定义一个类,继承 Spring 的 ContextLoaderListener 监听器,然后重写 contextInitialized 方法。这个全局变量是一个 Map<String, String>, key 保存用户登录后的 SessionId, value 则是用户帐号的用户名。

public class VinciContextLoaderListener extends ContextLoaderListener {
	private static final Logger logger = Logger.getLogger(VinciContextLoaderListener.class);
	/**
	 * Initialize the root web application context.
	 */
	@Override
	public void contextInitialized(ServletContextEvent event) {
		logger.debug("添加一个全局的Map变量,用来防止一个用户账号多处登录");
		ServletContext servletContext = event.getServletContext();
		
		// 这里key放SessionId, value放用户名
		Map<String, String> singleLogin = new HashMap<>();
		servletContext.setAttribute(VinciConstants.SINGLE_LOGIN, singleLogin);
		
		initWebApplicationContext(servletContext);
	}
}

2. 在用户登录的处理逻辑里,如果账号登录过并且还在会话期内,就不可登录,否则注册会话号和用户名。

public User login(User user){
	logger.debug("先检查账号是否被使用,如果被使用了就不可以重复登录");
	Map<String, String> map = (Map<String, String>)SessionUtil.getServletContext().getAttribute(VinciConstants.SINGLE_LOGIN);
	if (map.values().contains(user.getLoginName())){
		throw new RuntimeException("该账号正在被使用");
	}
	// other work
	if(isCorrectPassword){
		// 将已登录的账号保存到全局变量里,注销则在会话失效时
		map.put(SessionUtil.getRequest().getSession().getId(), user.getLoginName()); 
		
		return dbUser;
	}
	return null;
}


3. 再定义一个监听会话的监听器,无论用户是主动退出还是会话超时,都会被这个监听器监听到,所以在会话销毁的时候将该账号的可用性从全局变量注销掉。
public class VinciSessionListener implements HttpSessionListener {
	private static final Logger logger = Logger.getLogger(VinciSessionListener.class);

	@Override
	public void sessionCreated(HttpSessionEvent se) {
		logger.debug("session created :: " + se.getSession().getId());
	}

	@Override
	public void sessionDestroyed(HttpSessionEvent se) {
		logger.debug("session destroyed :: " + se.getSession().getId());
		logger.debug("从全局变量里删除登录账号");
		Object attr = se.getSession().getServletContext().getAttribute(VinciConstants.SINGLE_LOGIN);
		Map<String, String> map = (Map<String, String>)attr;
		map.remove(se.getSession().getId());
	}
}

至此,单系统内一个账号在一个时间点只能由一个用户使用。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值