Java Web 单一登录踢人:Filter + 全局 Session 注册表实现
简介面向Java Web开发者的账号单一登录实现资料围绕同一账号仅允许一处在线、后登录者自动踢出前者的核心需求讲解如何通过Filter过滤器配合HttpSession完成用户状态校验与强制下线。内容从需求背景、实现思路到具体步骤层层展开覆盖登录页AJAX提交、服务器端会话检查及踢人逻辑并结合示例说明轮询读取Session方案的资源占用与异步返回顺序冲突问题同时给出优缺点分析和加入验证码、双因素认证等安全扩展方向。资源为单个PDF文档大小149KB篇幅紧凑包含登录页与主页面的示例代码片段、jQuery请求写法及运行验证要点适合为管理系统或内部平台增加单点登录约束的初中级Java工程师参考。已有3377人学习下载方案可直接迁移到实际项目中快速实现防重复登录的踢人效果。1. 账号单一登录的“踢人”需求Filter 方案比轮询 Session 更值得学做 Java web 账号登录模块单一登录几乎是必选项同一账号不允许同时在两台设备上保持登录态后登录的人要把先登录的人踢下线效果类似 QQ 的“踢人”。网上常见的实现手段是写个定时器每隔 500ms 去读后台 Session 做校验但这种轮询方案不仅在浪费资源还会在 AJAX 并发时出现返回值错乱——请求 1 还没返回请求 2 先到了前端拿到的是错配的数据。这份资源用 Filter 全局 Session 注册表来实现踢人逻辑干净适合直接跑通验证效果也适合想把这套机制移植到自己项目里的人。2. 核心实现全局 Session 注册表与踢人逻辑的完整拆解2.1 为什么用 static HashMap 存 Session先看最核心的一行声明它放在 LoginServlet 里public static MapString, HttpSession user_Session new HashMapString, HttpSession();在 Servlet 容器中Servlet 本身是单例static 字段在整个 JVM 里只有一份所以所有请求都能访问到这份注册表。key 存用户名value 存该用户当前登录产生的 HttpSession。这样第二次登录时就能通过用户名找到旧会话并把它作废。维度设计说明key用户名业务上的唯一标识valueHttpSession旧登录的完整会话对象生命周期随应用进程存在不随单个请求结束而清空注意 HashMap 不是线程安全的。单机 demo 里并发量低问题不大如果放到生产环境两个请求同时操作这个 Map可能出现 put 覆盖后又被 remove 的竞态。常见做法是换成 ConcurrentHashMap或者对 put 和 remove 操作加同步锁。另外为什么不建议用 sessionId 做 key因为 sessionId 每次登录都会变第二次登录时拿不到“同一个用户”的旧会话也就无法实现踢人。2.2 LoginServlet 登录流程先踢旧人再存新人完整的 LoginServlet 核心逻辑如下这里省略了 doGet、doDelete 等模板方法重点看 doPost 和两个私有方法SuppressWarnings(serial) WebServlet(urlPatterns{/LoginServlet}) public class LoginServlet extends HttpServlet { private PrintWriter out; private String user; private String method; private HttpSession session; public static MapString, HttpSession user_Session new HashMapString, HttpSession(); 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(); } 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(); } } }doPost 先解析出用户名、方法名和当前请求的 session然后按 method 分发。登录走 mLogin第一行先调用 removeUser把旧 session 失效然后把新 session 的属性 name 设置为用户名再放进全局注册表。顺序很重要如果先 put 再 invalidate注册表里存的是旧 session旧 session 被 invalidate 之后新登录的 session 反而进不了 Map最终新会话也是失效状态。out.println(1) 返回给前端一个固定值 1前端拿它判断登录是否成功。这个值是可以自己定的只要前后端约定一致就行。2.3 踢人真正的执行点不在 Filter而在 invalidate很多人第一次看这套代码会以为 Filter 是踢人的地方其实是理解偏了。过滤器只负责“检查当前请求是否还有效”真正把旧登录干掉的是 removeUser 里的invalidate()。当用户 B 带着同一账号再次登录时mLogin 里 removeUser 会从注册表中找到用户 A 的 session 并立即作废。作废之后用户 A 的浏览器再发起 SubmitServlet 请求过滤器里request.getSession().getAttribute(name)拿到的是 null于是返回自定义状态码 900前端就认为登录失效了。这样设计的优势在于不需要任何后台轮询线程也不会出现“请求 1 还没返回请求 2 先返回”这种响应错位。旧会话只在它自己发起下一次请求时才被校验校验动作和业务请求是串行的天然避免了并发冲突。这个点理解透了后面很多问题都能自己排查。3. 前端 AJAX 与 Servlet 联动登录、提交、900 状态码处理3.1 两个 JSP 页面登录页与主页面的结构登录页 index.jsp 为了演示被刻意简化没有密码只有一个用户名输入框和登录按钮body input idusername nameusername typetext button idbtnlogin namebtnlogin登录/button script typetext/javascript srcjs/jquery-1.11.3.js/script script typetext/javascript srcjs/jsSubmit.js/script /body主页面 singlecount.jsp 更简单只放了一个提交按钮body 已登录. br button idbtnsubmit namesubmit提交/button script typetext/javascript srcjs/jquery-1.11.3.js/script script typetext/javascript srcjs/jsSubmit.js/script /body两个页面都引入 jquery-1.11.3.js 和 jsSubmit.js。演示项目里不做密码校验是为了把注意力集中在 session 管理和踢人机制上实际项目中替换成真实账号密码校验即可登录成功后同样用 AJAX 拿到确认值再跳转。3.2 jsSubmit.js登录、提交与被踢回调所有前端逻辑都集中在 jsSubmit.js 里AJAX 回调处理了三件事登录成功跳转、提交结果提示、被踢后返回登录页。$(document).ready(function() { // 登录按钮 $(#btnlogin).click(function() { $.ajax({ url: LoginServlet?methodlogin, type: post, data: {username: $(input[nameusername]).val()}, success: function(msg) { if (msg 1) { window.location.href singlecount.jsp; } }, error: function() { window.alert(错误); } }); }); // 提交按钮 $(#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; } } }); }); });登录 AJAX 把用户名通过 data 参数提交给 LoginServlet返回 1 就跳到主页。提交 AJAX 访问 SubmitServlet成功后弹出当前提交总数。被踢的判断在 error 回调里jqXHR.status 等于 900 时提示登录失效并跳回项目根路径。这里有两个细节值得注意。第一dataType 没有设置成 json因为后端返回的是纯数字字符串按 text 处理最不容易出问题如果设置成 json反而可能因为返回类型不匹配走 error 分支。第二被踢后跳转的/OneLogin是项目部署时的 context path如果你的工程改了名字这个路径必须同步改否则会跳到一个不存在的地址。3.3 Filter 与 web.xml 的配合方式SessionFilter 的职责是拦截需要登录态保护的请求判断 session 中是否有 name 属性。核心代码如下WebFilter(/SessionFilter) public class SessionFilter implements Filter { Override public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) arg0; HttpServletResponse response (HttpServletResponse) arg1; String strURL request.getRequestURL().toString(); if (strURL.indexOf(SubmitServlet) ! -1 || strURL.indexOf(singlecount.jsp) ! -1) { if (request.getSession().getAttribute(name) null) { request.getSession().invalidate(); response.sendError(900, 登录失效请重新登录); return; } else { arg2.doFilter(arg0, arg1); } } else { arg2.doFilter(arg0, arg1); } } }过滤器判断请求路径里是否包含 SubmitServlet 或 singlecount.jsp包含则检查 session 属性。属性为 null 说明用户已经被踢或从未登录此时先 invalidate 当前 session再通过 sendError 返回 900。前端 AJAX 的 error 回调会捕获这个状态码从而弹出重新登录提示。web.xml 里面的配置决定了过滤器具体拦哪些 URLfilter 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这里有一个容易翻车的细节SessionFilter 类上写了WebFilter(/SessionFilter)但真正生效的是 web.xml 里两个 filter-mapping。注解里的路径/SessionFilter实际并不存在它不会拦截 SubmitServlet。如果部署环境只支持注解扫描这个过滤器会完全失效。后面避坑章我会专门说这个问题。4. 避坑排查AJAX 返回值错乱、过滤器边界与测试盲区4.1 现象AJAX 并发时请求 1 拿到请求 2 的返回值有博客实现的踢人方式是定期读后台 session比如每隔 500ms 检查一次。这种办法除了额外消耗资源还会踩到 AJAX 响应顺序的坑。请求 1 先提交但请求 2 先返回前端就收到了错配的数据。后台两个请求都执行成功了前端逻辑却是乱的。原因在于多线程并发下session 校验线程和业务线程是并行执行的返回顺序完全不可控。解决方式就是本资源用的这种同步踢人在登录请求里就完成旧 session 的 invalidate不要在后台开轮询。旧页面下一次发起请求时才被拦截整个过程没有额外线程参与也就不存在返回值串台的窗口。4.2 现象过滤器把静态资源也拦截页面 JS 加载不出来如果把 filter-mapping 的 url-pattern 写成/*jquery-1.11.3.js 和 jsSubmit.js 都会经过过滤器。第一次访问页面时 session 里还没有 name 属性所有静态资源都会被 sendError 900前端脚本根本加载不出来。解决方法是只对需要保护的后端接口和页面配置拦截。示例里只拦 singlecount.jsp 和 SubmitServlet非常精准。如果项目有多个需要保护的路径建议用前缀匹配比如/admin/*并且在过滤器里放行静态文件if (strURL.endsWith(.js) || strURL.endsWith(.css) || strURL.endsWith(.png)) { arg2.doFilter(arg0, arg1); return; }记住一条原则过滤器拦的是访问入口不是让页面变干净的工具。该放行的资源一定要放行。4.3 现象同一浏览器开两个标签页测不出踢人这是我在最早调试这套代码时踩过的坑。同一个浏览器里两个标签页共享同一个 JSESSIONID Cookie。第二个标签页登录时request.getSession() 拿到的还是第一个标签页的 session。removeUser 里 invalidate 把这个 session 作废然后第二次登录又基于这个已经失效的 session 继续操作最终两个页面同时失效根本不出现“新登录踢掉旧登录”的效果。解决办法很简单用两个不同浏览器测试Chrome 一个窗口、Firefox 一个窗口或者用浏览器的隐身模式。这样才能产生两个独立的 JSESSIONID模拟两台真实客户端。4.4 现象WebFilter(/SessionFilter)导致过滤器不生效原工程的 SessionFilter 类上写了WebFilter(/SessionFilter)web.xml 里又配置了 filter-mapping。注解里的路径是随意写的实际不存在这个 URL真正起作用的是 web.xml。如果换个只扫描注解的部署环境Filter 不会拦截 SubmitServlet登录状态检查就完全失效了。解决方法是二选一保留 web.xml 就把WebFilter注解删掉用注解扫描就把路径改成真实要拦的资源WebFilter(urlPatterns{/SubmitServlet, /singlecount.jsp})不要在一个工程里同时依赖两种配置方式很容易让人误判到底哪段配置生效排查问题时多花一晚上。4.5 现象被踢后旧页面跳转 404前端在被踢后跳转的是/OneLogin这是部署时的 context path。如果项目部署名称不叫 OneLogin比如打成 war 包后改叫demo那跳转就变成/demo才能访问原来的固定路径就会 404。解决方法是不要在前端写死路径。用 JSP 动态拼 basePath跳转时使用相对路径window.location.href %basePath%;或者在前端直接跳相对路径index.jsp让浏览器基于当前路径自动补全。做登录模块时凡是涉及页面跳转的字符串都值得检查一下是不是硬编码。5. 进阶技巧给踢人加友好提示并给多节点部署留好后路5.1 未登录直接访问主页面时的自动跳转原资源里用户被踢后只有点击提交按钮才会看到“登录状态失效”的提示。如果用户直接在地址栏输入http://localhost:8080/OneLogin/singlecount.jsp过滤器会返回 900 状态码浏览器显示的是错误页体验很生硬。常见的做法是在主页面加载时就做一次会话探活。在 singlecount.jsp 中加入一段初始化 AJAX请求 LoginServlet 新增的 check 分支$(document).ready(function() { $.ajax({ url: LoginServlet?methodcheck, type: post, success: function(msg) { if (msg ! 1) { window.location.href index.jsp; } }, error: function(jqXHR) { if (jqXHR.status 900) { window.location.href index.jsp; } } }); });对应在 LoginServlet 的 doPost 里增加一个分支case check: if (session.getAttribute(name) ! null) { out.println(1); } else { out.println(0); } break;这样无论是被踢的用户还是没登录就手动输入地址的用户打开主页都会在几百毫秒内被带回登录页不会看到一段冷冰冰的状态码。5.2 单机 HashMap 撑不了多节点注册表要换 Redisstatic HashMap 的注册表只在单个 JVM 里有意义。一旦项目部署在多台 Tomcat 后面加 Nginx 负载均衡用户第一次登录落在节点 A第二次登录落在节点 B两边各有一份 user_Session踢人逻辑就彻底失效了。常见做法是把注册表从 JVM 内存搬到 Redis用 userId 作为 keysessionId 作为 value。登录时写入过滤时判断// 登录时 jedis.set(login: userId, sessionId); jedis.expire(login: userId, 1800); // 过滤时 String current jedis.get(login: userId); if (current ! null !current.equals(session.getId())) { response.sendError(900, 登录失效请重新登录); return; }TTL 设置为 1800 秒和 Tomcat 默认 session 超时保持一致避免 Redis 里的记录永久堆积。这样即使两次请求落在不同节点也能通过 Redis 判断同一账号是否已经在别处登录。再往下做还可以用消息队列通知旧节点及时失效 session不过对小项目来说Redis 注册表这一层已经足够。从那以后我每次做登录模块都会先想清楚注册表的存储边界和过滤器的拦截范围再写前端回调。这套 Filter Session 注册表的写法虽然基础但把职责拆清楚之后后面加 Redis、加权限控制都顺理成章。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

FTP与WWW服务器配置:Linux双服务联调与故障排查实战

FTP与WWW服务器配置:Linux双服务联调与故障排查实战

简介:在Linux服务器运维中,FTP与WWW服务是文件共享和网站托管的两大基础应用。搭建这两个服务时,管理员不仅需要懂得vsftpd和Apache的基本配置,更要理解被动模式端口规划、SELinux策略、防火墙放行规则等底层原理,才能…

2026/10/9 4:17:41 阅读更多 →
FTP与WWW服务器配置实战:从vsftpd到nginx的完整链路

FTP与WWW服务器配置实战:从vsftpd到nginx的完整链路

简介:这是一份以Windows 2000环境为背景的实验四学习笔记型PDF,围绕FTP服务器与WWW服务器配置展开,适合网络技术初学者、高校实训学生以及需要复习IIS建站流程的IT运维人员。文档共1个文件,压缩包约1.34MB,主要为PDF格…

2026/10/9 4:17:41 阅读更多 →
Claude长期记忆缺失怎么办?claude-mem外挂记忆层搭建与实战

Claude长期记忆缺失怎么办?claude-mem外挂记忆层搭建与实战

做AI工具的这大半年,我最大的感受是:Claude单次对话的能力再强,也架不住它每次都“失忆”。上午刚跟它敲定的技术方案,下午换一个新会话窗口,它连项目代号都不记得;上周让它整理的文档要点,这周…

2026/10/9 4:16:40 阅读更多 →

最新新闻

oneTBB concurrent_hash_map 非成员二元比较运算符(operator== / operator!=)详解

oneTBB concurrent_hash_map 非成员二元比较运算符(operator== / operator!=)详解

并发编程高性能计算 【免费下载链接】oneTBB oneAPI Threading Building Blocks (oneTBB) 项目地址: https://gitcode.com/gh_mirrors/on/oneTBB 点击查看 免费下载 导读 本文聚焦 oneAPI Threading Building Blocks(oneTBB)中 oneapi::tbb…

2026/10/9 4:49:04 阅读更多 →
Claude Code 命令速查手册:高频命令、快捷键与高效工作流

Claude Code 命令速查手册:高频命令、快捷键与高效工作流

1. 为什么需要一个命令速查手册刚接触 Claude Code 的人,十有八九会经历这么一个阶段:装好了,敲了个claude进去,然后对着那个闪烁的光标发呆——接下来该干嘛?官方文档当然有,但文档是线性的,从…

2026/10/9 4:49:04 阅读更多 →
ESP32 SoC与模组选型指南:从芯片架构到量产料号

ESP32 SoC与模组选型指南:从芯片架构到量产料号

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 4:49:04 阅读更多 →
Claude Code Mods扩展开发:工具挂载与终端界面渲染实战

Claude Code Mods扩展开发:工具挂载与终端界面渲染实战

1. 从终端里的AI助手说起:为什么需要给它加装工具和界面很多人第一次接触命令行里的AI编程助手时,感受往往是矛盾的。一方面,它能理解自然语言、能读写文件、能执行命令,确实比传统补全工具强出一大截;另一方面&#x…

2026/10/9 4:49:03 阅读更多 →
SSM框架2025年真实处境与Spring Boot渐进式迁移实战

SSM框架2025年真实处境与Spring Boot渐进式迁移实战

直接开写 说实话,每次在技术群里看到有人问“SSM框架还能打吗”,我就知道问这问题的十有八九是两种人:一种是刚接手了祖传项目、天天被XML配置折磨得想跑路的年轻开发,另一种是还在用SSM做老系统维护、看着外面的技术新闻越来越焦…

2026/10/9 4:49:03 阅读更多 →
LRE框架:重构AI智能体的时间感知与因果记忆机制

LRE框架:重构AI智能体的时间感知与因果记忆机制

1. 这不是“给AI加个备忘录”,而是重构智能体的时间感知能力很多人第一次看到“AI智能体记忆管理”这个词,下意识会想:不就是让大模型多存点上下文、加个向量数据库当外挂硬盘吗?我试过——在某个模拟项目X里,给一个任…

2026/10/9 4:48:03 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →