资讯动态

Java Web实现同一账号登录互踢:Filter+Session方案详解

发布时间:2026/9/29 4:16:53 来源:尧图企业网站定制
简介面向Java Web开发者的账号单一登录实现参考文档重点解决同一账号重复登录带来的安全隐患与数据紊乱问题。方案采用过滤器Filter配合HttpSession检查当前用户登录状态当新会话登录成功后主动使旧会话失效形成类似QQ登录的踢人效果确保同一时刻仅允许一个账号在线。文档从需求背景与实现思路讲起逐条给出具体实现步骤并配以登录页面、主页面、AJAX请求提交、JS封装等关键代码片段按需求分析、实现方法、优缺点及拓展总结等模块组织方便读者快速理解会话管理、过滤器应用与前后端交互的完整链路。压缩包内共1个PDF文件大小约149KB内容紧凑适合需要为系统增加登录互斥控制的中初级Java开发者。目前已有3377人学习下载对掌握过滤器实现的单一登录机制具有较强实操参考价值。1. 单一登录的需求没那么简单重复登录不只是踢人问题你有没有遇到过这种情况一个账号在多台电脑上同时登录结果后登录的人把先登录的人挤下线先登录那个人再点任何操作突然弹出一条「登录失效」然后被踢回登录页——这就是 QQ 那种「踢人效果」。Java web 里实现这个功能网上搜出来的方案五花八门有的做法是定时轮询 session有的做法是存 token 到 Redis但真正在一个小型 Java web 项目里落地最快、最稳的其实是基于 Filter 和 session 映射表的方式。这篇笔记我要拆的这个工程就是典型的 Filter 实现方案不依赖 Redis、不引入 Spring Security纯 Servlet JSP jQuery 就够代码量不多但把「同一账号登录互踢」「未登录访问受保护页跳转」「会话失效提示」这几个核心场景都覆盖了而且有一个很关键的细节它用自定义 HTTP 状态码 900 配合 AJAX error 回调而不是在 success 里判断返回值。这个细节很多人第一次写会踩坑。适合刚接触 Java web 过滤器机制、或者正在做课程设计需要演示单一登录效果的同学直接下载工程跑起来看效果比自己从零写要省不少时间。2. 方案选型为什么是 Filter Map 而不是轮询 Session2.1 轮询思路为什么会被否决网上不少博客讲单一登录给出的思路是前端隔一段时间比如 500ms向后端发一个请求检查当前 session 是否还有效如果发现这个账号在别处登录了就把当前会话标记为失效然后提示用户重新登录。这个思路听起来直接但实际用起来有两个硬伤。第一是资源占用每个在线用户每 500ms 就要产生一个 HTTP 请求100 个在线用户就是每秒 200 个请求Tomcat 的连接池和线程池压力不小而且这些请求本身没有业务含义纯粹是「探活」。第二是 AJAX 响应顺序错乱的问题原作者在博客里也提到过假设页面同时发起了两个 AJAX 请求请求 1 先发出请求 2 后发出但 AJAX 是异步的服务器处理速度和网络传输速度都有波动可能出现请求 2 的响应先回到前端这时候请求 1 的回调函数接收到的却是请求 2 的返回值。如果两个请求的返回类型一样前端拿到的数据就是错的而后台日志看每个请求都成功返回了。这种问题很难排查因为它不是稳定复现而是偶发。所以原作者在这个工程里直接放弃了轮询思路改用「登录时踢旧会话」的主动策略不需要前端探活只要账号再次登录旧 session 立刻失效旧会话的下一次请求到达过滤器时发现 session 里没有用户信息直接返回错误状态码前端收到后跳转登录页。2.2 Filter 在这里扮演的角色Filter 在 Java web 里是 Servlet 规范中的一种组件请求先经过 Filter 链再进入 Servlet。也就是说Filter 可以在请求到达业务代码之前做拦截判断。这个工程里Filter 只拦两个路径/singlecount.jsp和/SubmitServlet。拦下来的请求先做一次 session 检查如果 session 里没有name这个属性说明当前会话没有登录直接response.sendError(900, 登录失效请重新登录)把请求挡回去如果 session 里有name说明是合法会话放行。这里有一个容易忽略的点sendError(900, ...)不是sendRedirect它不会跳转页面只是给浏览器返回一个 HTTP 错误状态码。浏览器的默认行为是显示错误页但我们的前端用了 AJAX所以这个错误码会被 AJAX 的error回调捕获而不是success回调。这是这个工程里一个非常关键的设计后面我会单独展开讲。2.3 Map 存 Session 的设计意图原工程里LoginServlet 定义了一个静态 Mappublic static MapString, HttpSession user_Session new HashMapString, HttpSession();这个 Map 的 key 是用户名value 是对应登录用户的 HttpSession 对象。登录逻辑很简单先调用removeUser(user)检查 Map 里有没有同名的 session有就把它invalidate()掉然后再把当前用户的 session 放进去。这样做的好处是无论同一账号在多少个地方登录Map 里始终只保留最后一次登录的 session。旧 session 已经invalidate()了旧会话后续的请求即使到了 Filter也拿不到name属性直接被拦截。新会话则正常放行业务操作互不干扰。这里提一下静态 Map 的隐患它放在 JVM 内存里没有持久化一旦 Tomcat 重启Map 就清空了所有在线用户都要重新登录。对小型项目或课程设计来说完全可以接受但如果是生产环境更合适的做法是用 Redis 存 sessionId 和用户映射或者用 Spring Session 做分布式会话管理。这个工程的定位是演示单一登录原理所以静态 Map 是最直观、最容易理解的方案。3. 工程结构与运行准备拿到代码后先跑通再改3.1 下载后的工程目录长什么样这个工程是一个标准的 Eclipse 或 MyEclipse 动态 Web 工程不是 Maven 工程。下载解压后你会看到类似这样的目录结构OneLogin/ ├── src/ │ ├── filter/ │ │ └── SessionFilter.java │ └── servlet/ │ ├── LoginServlet.java │ └── SubmitServlet.java ├── WebContent/ │ ├── index.jsp │ ├── singlecount.jsp │ ├── js/ │ │ ├── jquery-1.11.3.js │ │ └── jsSubmit.js │ └── WEB-INF/ │ └── web.xmlsrc 目录下是两个包filter 包放 SessionFilterservlet 包放 LoginServlet 和 SubmitServlet。WebContent 下是 JSP 页面和 jQuery 资源WEB-INF 下是 web.xml。整体结构比较清晰没有冗余文件。3.2 部署到 Tomcat 的具体步骤我用的是 Tomcat 8.5 JDK 1.8Eclipse 里直接右键Import → Existing Projects into Workspace选工程的根目录就能导入。如果你用的是 IDEA操作路径略有不同File → New → Project from Existing Sources选工程目录后IDEA 会识别出这是一个 Web 工程需要你在Project Structure → Artifacts里确认一下是否已经生成了 war exploded 的部署配置。导入完成后确认三件事第一项目的 Compiler level 要设置为 1.8这个工程的代码里用到了WebServlet和WebFilter注解这是 Servlet 3.0 的特性如果编译级别低于 1.8可能报注解解析错误。第二确认Project Facets里 Dynamic Web Module 版本至少是 3.0这样WebServlet注解才会被扫描到。如果 Dynamic Web Module 版本是 2.5注解是不生效的必须在 web.xml 里手动配置servlet和servlet-mapping。第三部署到 Tomcat 时Context Path 要设置为/OneLogin。为什么是这个名字因为工程里的 web.xml 中display-name是 OneLogin而且前端代码里有硬编码的跳转路径window.location.href /OneLogin;如果 Context Path 不是/OneLogin这句跳转会变成 404。你可以改成自己的路径但记得三处要同步Tomcat 部署时的 Application context、web.xml 的display-name、以及 jsSubmit.js 里所有的硬编码路径。3.3 启动后先跑两个验证场景部署成功后启动 Tomcat浏览器访问http://localhost:8080/OneLogin/会看到登录页输入框下面是一个「登录」按钮由于工程简化了密码直接输入任意账号名点击登录即可。第一个验证场景用 Chrome 登录账号admin登录成功后会跳转到 singlecount.jsp 主页面点「提交」按钮页面弹出提示「提交总数1」说明会话有效。第二个验证场景不要关闭 Chrome再开一个 Firefox 或 Edge用同一个账号admin再次登录。此时回到 Chrome 的页面再点一次「提交」按钮会弹出「登录状态失效请重新登录」的提示并且页面自动跳回登录页。这个效果就是「踢人」后登录的会话把先登录的会话踢下线。注意顺序——是先登录的被踢不是后登录的被拒。这个语义跟一些业务系统的「禁止重复登录」不同后者是第二次登录直接被拒绝前者是第二次登录成功、第一次登录失效类似 QQ 的移动端和网页端互踢。4. 核心代码走读LoginServlet 与踢人逻辑4.1 doPost 的分发结构LoginServlet 的 doPost 方法做了一件很典型的事用method参数做方法分发而不是每个功能写一个 Servlet。这样在小型项目里能让 Servlet 数量减少但代价是如果方法多了switch 会越来越长。Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(text/html); req.setCharacterEncoding(utf-8); resp.setCharacterEncoding(utf-8); out resp.getWriter(); user req.getParameter(username); method req.getParameter(method); session req.getSession(); switch (method) { case login: mLogin(); break; default: break; } out.flush(); out.close(); }这段代码有三个细节值得注意。第一req.getSession()如果当前请求没有 sessionServlet 容器会主动创建一个新的 HttpSession。这个行为是 Servlet 规范定义的但你得知道这里调用的时机——是在登录成功之前所以就算用户是在登录页也会先拿到一个 session这个 session 后面会被赋上name属性并放进 Map。第二编码处理放在getWriter()之后是没问题的但更稳妥的做法是把setCharacterEncoding放在setContentType之前。Tomcat 8 之后默认使用 UTF-8问题不大但如果你部署环境里改过 URIEncoding中文用户名可能会出现乱码可以把req.setCharacterEncoding(utf-8)提到最前面。第三这个 switch 分支没有case logout的处理。如果你想加退出登录功能需要自己补一个分支从 Map 里移除当前用户然后session.invalidate()再out.println(0)返回给前端。4.2 mLogin 与 removeUser 的配合逻辑登录成功后的核心代码其实只有几行private void mLogin() { removeUser(user); session.setAttribute(name, user); user_Session.put(user, session); System.out.println(user_Session); out.println(1); } private void removeUser(String user) { if (user_Session.containsKey(user)) { user_Session.get(user).invalidate(); } }整个踢人逻辑的关键在于removeUser和user_Session.put的顺序。先执行removeUser把同名的旧 session 作废再把当前 session 放进 Map。这样 Map 里一个用户永远只对应一个 session这个 session 一定是最后一次登录产生的。这里有一个隐患如果同一个账号在同一个浏览器里连续登录两次第二次登录时removeUser会把第一次登录的 session invalidate但第二次登录用的 session 是同一个吗不一定。req.getSession()在浏览器有 JSESSIONID 的前提下返回的是同一个 session所以第二次登录时旧 session 已经被 invalidate 了——等等这里就出问题了。如果你在同一个浏览器里重复登录第一次登录的 session 没关第二次登录时req.getSession()返回的还是那个旧 session而旧 session 在removeUser里已经被 invalidate你对一个已失效的 session 调用setAttribute会抛IllegalStateException。这就是一个典型的踩坑点。我在本地复现时确实遇到了这个问题同一个浏览器窗口反复点登录第二次开始后台报错。解决方式有两种一是在mLogin里先用req.getSession(false)判断当前请求是否已有 session有就先把旧的 invalidate 再创建新 session二是干脆session.invalidate()之后再req.getSession()主动拿一个全新的 session。但要注意这种场景只在同一个浏览器测试时才会触发用两个不同浏览器模拟多端登录是不会踩到的所以很多人写完跑一下双浏览器测试觉得没问题一旦用户在同一浏览器里退出再登录就会出岔子原工程没有做这个防护。我建议你在用的时候加一行private void mLogin() { if (session ! null) { session.invalidate(); // 确保旧会话彻底废弃 } session req.getSession(); // 重新创建全新会话 removeUser(user); session.setAttribute(name, user); user_Session.put(user, session); out.println(1); }注意我这里先 invalidate 再 getSession这样能保证当前浏览器拿到的是一个完全全新的 session不会跟旧 session 的状态纠缠在一起。4.3 静态 Map 的并发隐患public static MapString, HttpSession user_Session是线程不安全的。如果多个用户同时登录HashMap 的 put 操作可能出现竞态问题极端情况下两个线程同时 put 不同 key 但落在同一个桶上可能导致其中一个丢失。这个工程规模小并发量低不出来也正常。但如果你把这个方案用到选课系统或者实训项目里建议至少加一个ConcurrentHashMap或者用Collections.synchronizedMap包一层。另外removeUser的检查和删除不是原子的containsKey之后invalidate()之间如果有另一个线程 put 了同名用户可能会导致旧 session 被误删。更严格的写法是用ConcurrentHashMap的remove(key, value)配合锁但对课程设计来说用ConcurrentHashMap就已经比原生 HashMap 好很多了。5. 过滤器与前端联动自定义状态码 900 的完整链路5.1 SessionFilter 如何拦截请求SessionFilter 实现了javax.servlet.Filter接口用WebFilter(/SessionFilter)注解标注。但注意这个注解的写法跟实际配置方式其实是有出入的——真正的过滤路径是在 web.xml 里配置的filter filter-namefilter.SessionFilter/filter-name filter-classfilter.SessionFilter/filter-class /filter filter-mapping filter-namefilter.SessionFilter/filter-name url-pattern/singlecount.jsp/url-pattern /filter-mapping filter-mapping filter-namefilter.SessionFilter/filter-name url-pattern/SubmitServlet/url-pattern /filter-mapping也就是说Filter 只拦两个路径singlecount.jsp 页面本身和 SubmitServlet。这样设计的目的是用户未登录时直接访问主页面http://localhost:8080/OneLogin/singlecount.jsp会被过滤器拦截返回 900 错误未登录时直接请求 SubmitServlet同样被拦截。我在第一次看这段代码时觉得有点绕Filter 里先取request.getSession().getAttribute(name)如果为 null 就invalidate()再sendError(900)。但getSession()在没有 session 时会创建一个新 session所以对未登录用户来说每一次被拦截的请求都会凭空创建一个无效 session然后再被 invalidate 掉。虽然最终结果是对的但会白白产生一次 session 创建和销毁。更干净的写法是HttpSession session request.getSession(false); if (session null || session.getAttribute(name) null) { response.sendError(900, 登录失效请重新登录); return; }getSession(false)如果当前请求没有携带合法 session会返回 null而不会创建新 session。这样就不会产生垃圾 session。这个细节对服务器压力有实际影响如果拦截的是恶意刷的请求每次都会触发无效 session 创建时间长了对内存不友好。5.2 前端如何处理 900 状态码前端在 jsSubmit.js 里面对提交请求的处理是$(#btnsubmit).click(function() { $.ajax({ url: SubmitServlet?methodsubmit, type: post, success: function(msg) { if (msg 1) { window.alert(提交总数 msg); } }, error: function(jqXHR) { if (jqXHR.status 900) { window.alert(登录状态失效请重新登录); window.location.href /OneLogin; } } }); });这里的设计很典型后台用sendError(900)主动制造一个 HTTP 错误响应前端的success回调不会触发转而触发error回调。jqXHR.status就是 HTTP 状态码判断等于 900 后先弹窗提示再跳回登录页。为什么不用 401 或 403因为 401 在部分浏览器里会触发原生认证弹窗如果你在 Firefox 里用 401它可能弹出账号密码输入框403 语义上是「没权限」而不是「会话失效」而且有些代理服务器会对 4xx 状态码做特殊处理。用一个自定义的 900 码语义清晰不会被浏览器劫持也不会被框架内置逻辑干扰。5.3 未登录直跳主页面时的链路过窄问题SessionFilter 在strURL.indexOf(SubmitServlet) ! -1 || strURL.indexOf(singlecount.jsp) ! -1的条件下才做校验其他请求一律放行。这意味着如果系统里有其他业务 Servlet比如查询列表的ListServlet只要路径里不包含 SubmitServlet 字样就不会被拦截。这样做的初衷是「只保护需要登录的资源」但实际开发中你可能会发现业务越加越多过滤器要保护的路径也越来越多。常见做法是反过来写所有业务请求都被过滤器拦截然后放行静态资源和登录接口。比如只放行/LoginServlet、/index.jsp、/js/*、/css/*其他路径全部校验 session。这样比维护一个「拦截白名单」更安全因为新加的 Servlet 默认被保护不会因为忘了配置而裸奔。原工程里还有一个需要在 JSP 上手动补的步骤如果你希望用户未登录时直接访问 singlecount.jsp 能跳转到登录页而不是显示错误页面需要在 jsSubmit.js 的$(document).ready里加一段$.ajax({ url: LoginServlet?methodcheckLogin, type: post, success: function(data) { if (data 0) { window.location.href /OneLogin; } } });但这个方法需要后台额外加一个 checkLogin 接口原工程没有实现只说明了思路。实际上更优雅的做法是继续依赖 Filter在过滤器里判断 session 为空时不调用sendError而是直接response.sendRedirect(request.getContextPath() /index.jsp)。但这是页面跳转走的是浏览器地址栏跟 AJAX 请求的行为是不同的。你要区分一个场景如果被拦截的请求是用户直接在地址栏访问 singlecount.jsp这时候sendRedirect可以正常工作但如果被拦截的请求是 AJAX 发的sendRedirect返回的 302 会被 AJAX 框架自动跟随最终拿到的是 index.jsp 页面的 HTML然后你会感到困惑——success回调拿到的数据是一整页 HTML而不是预期的 JSON。所以原工程用 900 状态码是对的它绕开了 302 带来的 AJAX 自动跳转问题。5.4 避坑清单复现时容易翻车的 5 个地方现象一同一个浏览器窗口里退出登录后再用同一个账号登录后台报IllegalStateException: getAttribute() called after invalidate()。原因是mLogin里先执行removeUser(user)把当前浏览器持有的 session invalidate 掉了然后session.setAttribute(name, user)还在往这个已失效的 session 上塞数据。解决方式是在mLogin开头主动 invalidate 旧 session 并重新获取新 session保证当前会话是一个全新对象。现象二未登录时访问 singlecount.jsp浏览器显示的是 Tomcat 的错误页面而不是跳回登录页。原因是sendError(900)只会返回错误码不会执行页面跳转。这是设计如此不是 bug。如果你想要未登录访问主页面直接跳登录页用sendRedirect而不是sendError但要注意前面说的 AJAX 302 问题两个场景分开处理。现象三点击登录后页面没有跳转打开浏览器控制台看到 JS 报错window.location.href /OneLogin404。原因是 Context Path 不是/OneLogin。Tomcat 部署时如果改了 Application context比如配成了/test这个硬编码路径就打不开了。把所有硬编码路径换成%request.getContextPath()%动态拼接或者在 JS 里用相对路径。现象四提交按钮点击后没有任何反应F12 看网络请求发现 SubmitServlet 返回 404。原因是WebServlet注解没有被识别。检查两处工程 Faces 里 Dynamic Web Module 版本是否 ≥3.0WebServlet的 urlPatterns 是否与 web.xml 中的 servlet-mapping 重复配置了。如果 web.xml 里也配了一个相同路径的 servlet会出现冲突选择一种配置方式即可不要同时配。现象五用同一个浏览器开两个标签页测试踢人第二个标签页登录后第一个标签页点提交依然可以正常提交没有被踢下线。原因是同一个浏览器共享 JSESSIONID cookie。第二次登录时两个标签页实际上是同一个 sessionremoveUser把旧 session invalidate 后等于把当前浏览器唯一的 session 也废了但第二次登录后又创建了新 session 放进了 Map第一个标签页点提交时请求携带的还是同一个 JSESSIONID自然能通过过滤器。这不是踢人逻辑的问题而是测试方法的问题。务必用两个不同浏览器Chrome Firefox或者 Chrome Edge否则验证不了互踢效果。6. 从「能跑」到「敢用」验证方法、改造方向与一个调试习惯拿到这个工程后建议你按下面的顺序做一轮完整的验证。用 Chrome 和 Firefox 分别打开登录页在 Chrome 登录账号admin进入主页面点击「提交」按钮确认弹出「提交总数1」。接着在 Firefox 里用同一个账号登录此时 Firefox 进入主页面点击提交正常。再切回 Chrome点击提交会弹出「登录状态失效请重新登录」并跳回登录页。这一步验证的是踢人逻辑是否生效。第二步不开任何登录态直接访问http://localhost:8080/OneLogin/singlecount.jsp确认被过滤器挡下来。注意观察浏览器地址栏是否保持了 singlecount.jsp页面显示的是 Tomcat 错误页——这是sendError的正常表现。如果你希望这里跳回登录页改造方式是在 Filter 里区分请求来源直接把sendError改成sendRedirect(request.getContextPath() /index.jsp)但这只对地址栏直接访问有效对 AJAX 请求会有 302 追随问题所以这个改造要谨慎评估。第三步验证 AJAX 错误码链路。按 F12 打开开发者工具Network 面板里勾选 Preserve log然后在 Chrome 被踢之后点提交观察 SubmitServlet 请求的响应状态码是否为 900。如果状态码不是 900检查 Filter 有没有正确拦截比如 web.xml 里的 url-pattern 是否与实际请求路径匹配。这个工程的定位是演示和教学如果你想把它改造成能上生产的样子我建议从三个方向入手。第一是把静态 Map 换成 Redis以用户名为 key、sessionId 为 value每次登录时取旧 sessionId 并调用HttpSession的 invalidate或者更彻底一点用 Redis 的 expire 机制让会话有过期时间这样即使用户不主动退出会话也会在有效时间后失效不用依赖容器 session 的默认超时配置。第二是把自定义状态码 900 换成语义更清晰的 JSON 响应体比如返回{code: 900, msg: session invalid}前端用jqXHR.responseJSON.code判断这样前后端契约更明确也方便后续统一异常处理。第三是把方法分发的 switch 改为反射或路由表在 Servlet 里用private MapString, Function... handlers new HashMap()来注册处理函数新增方法时不用改 switch 结构。我要特别强调一个调试习惯所有涉及 session 和过滤器的改动改完后先在浏览器无痕模式 两个不同的浏览器里重跑完整流程不要只在同一个浏览器里刷新验证。原因很简单session 这个机制本身就是「一次会话」的概念同一个浏览器默认共享同一个会话它会掩盖掉所有 Session 层面的 bug。我就吃过这个亏——在某次改造里我把req.getSession()改成了req.getSession(false)单浏览器测试一切正常因为第二次请求时 session 已经在第一次登录时创建好了但换了另一个浏览器一测直接 NullPointerException查了半天才发现是未登录用户首次访问时拿不到 session 对象导致的。从那以后我每次改完 session 相关的代码都强制自己在两个浏览器加无痕模式各走一遍完整的「登录 → 操作 → 互踢 → 重登」流程确认无误才算通过。这个习惯看起来笨但真的能帮你拦住不少隐蔽的会话问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑