资讯动态

JSP+Java+MySQL登录注册系统实战:从密码加密到Session与过滤器

发布时间:2026/10/28 6:18:29 来源:尧图企业网站定制
简介面向JavaWeb初学者与课程实践者的登录注册功能实现项目围绕JSP页面、Servlet后端处理与MySQL数据库展开可用于练习表单交互、身份校验、Session状态保持及注册信息入库等常见Web开发环节。压缩包共34个文件整体约3.56MB包含7个Java源文件、7个class文件与3个JSP页面并配有SQL脚本、数据库连接配置、项目构建与依赖文件结构覆盖页面展示、请求处理和数据访问。已有3051人学习说明其作为入门案例具有一定参考度。代码涉及登录验证、注册查重、参数化查询防注入、密码加密存储、错误提示与页面跳转等思路读者可据此理解Servlet请求响应、JDBC连接和Session管理也便于拆分改造为课程作业或小型系统模块。1. 登录注册系统JSPJavaMySQL 这套老技术栈为什么还值得做一遍有个学生来找我看他的登录注册项目做完之后连密码明文存储的问题都没意识到把注册表单原样insert进数据库就交差了。我当时没直接评价只问了一句如果这套代码上线第二天数据库被人拖走你能不能睡着觉。他沉默了一会儿说能不能重做一遍。这就是我为什么仍然推荐用 JSPJavaMySQL 做一遍登录注册系统。它技术栈不新但正因为简单所有安全细节都暴露在你面前没有框架帮你兜底。你要自己写过滤器、自己管 Session、自己做密码哈希、自己处理验证码和防重复提交。这些东西在 Spring Security 里一行配置就搞定但你不知道它替你做了什么自己手写一遍才真正理解“认证”这两个字背后的链条。这套方案适合三类人正在做毕设或课程设计需要一个结构完整、能讲清楚设计理由的项目刚接触 JavaWeb想搞明白请求从浏览器到数据库再回来的完整链路已经会用框架但想把底层机制补上的从业者。下面从需求拆到验收给一份可以直接照着做的完整方案。2. 需求与技术选型为什么是 JSP 而不是前后端分离2.1 先把功能边界划清楚登录注册看起来简单真拆开是两块完全不同的业务。注册是“写”流程核心是数据校验、唯一性保证、防刷登录是“读状态”流程核心是凭证比对、会话建立、状态保持。很多人把两者混在一个 Servlet 里写完后面加个找回密码功能就得推倒重来。我一般会把它拆成四个功能域。账号域负责注册、登录、退出安全域负责密码加密、验证码、登录失败计数会话域负责 Session 的创建、续期、销毁数据域负责用户表的读写和连接管理。课程设计里不用真写成四个包但心里要有这条线否则代码写到后面就是一团。功能清单我通常这样列功能输入处理输出用户注册用户名、密码、确认密码、验证码校验格式→查重→加密→入库成功跳登录 / 失败回填提示用户登录用户名、密码、验证码查询用户→比对哈希→建 Session跳首页 / 失败计数1退出登录Session销毁 Session跳登录页登录态校验请求过滤器拦截放行 / 重定向登录这张表看着朴素但它是后面所有代码的依据。写代码前先有它能避免“想到哪写到哪”的典型翻车。2.2 JSP 方案的真实取舍前后端分离是主流但对这个项目我不推荐。原因很实际毕设答辩时评委更关心你有没有把业务流程讲透而不是你用了几个前端框架。JSP 把视图和请求处理放在同一套工程里部署一个 Tomcat 就能跑调试链路短出问题好定位。而上一套 Vue后端分离的结构光是跨域配置和 Token 传递就能耗掉一半时间最后核心的认证逻辑反而没写扎实。技术分工上JSP 负责页面渲染和表单回填Servlet 负责请求分发和业务编排JavaBean 承载用户实体JDBC 直连 MySQL。这套组合的好处是每一层职责都清晰可见JSP 里不写 SQLServlet 里不拼 HTML实体类只做数据搬运。提示JDBC 直连只适合教学和小型项目。真实生产环境要用连接池这里的写法是为了让你看清连接的生命周期。2.3 数据库表怎么设计才不留坑用户表的设计有两个常见错误一是密码字段长度给 32后来换哈希算法存不下二是用户名不加唯一索引靠代码查重并发下必然出重复数据。CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL COMMENT 登录名唯一, password VARCHAR(128) NOT NULL COMMENT 哈希后的密码非明文, salt VARCHAR(32) NOT NULL COMMENT 每用户独立盐值, nickname VARCHAR(32) DEFAULT NULL COMMENT 展示名, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, fail_count INT NOT NULL DEFAULT 0 COMMENT 连续登录失败次数, lock_until DATETIME DEFAULT NULL COMMENT 锁定截止时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段说明几句。password给到 128 是因为哈希结果是十六进制字符串长度比原始密码长得多给窄了后期改表很麻烦。salt单独存一列而不是拼在密码串里是为了将来换算法时能平滑迁移。fail_count和lock_until是防暴力破解用的登录失败累加超过阈值锁定一段时间这两个字段在答辩时是加分项因为它证明你想过攻击场景。uk_username唯一索引是最后一道防线代码查重是为了给用户友好提示索引才是真正保证数据不重复的东西。字符集用utf8mb4而不是utf8因为后者在 MySQL 里实际是三字节的存不了某些特殊字符和表情用户昵称一带表情就报错这种问题排查起来很费时间。3. 总体架构与请求链路一次登录到底经过了什么3.1 分层结构与包组织工程按职责分四层。表现层是 JSP 页面加 Servlet 控制器服务层放业务逻辑比如注册时的查重与加密、登录时的失败计数数据访问层封装 JDBC 操作对外只暴露方法模型层是用户实体和统一响应对象。包结构大致这样组织控制层、服务层、数据访问层、实体层、工具类、过滤器。工具类里放密码哈希、验证码生成、数据库连接获取。过滤器单独一个包因为登录态校验是横切关注点不该混在业务代码里。3.2 一次登录请求的完整轨迹浏览器提交表单到/login容器把请求交给登录 ServletServlet 先取验证码比对不一致直接返回一致则调服务层服务层先查用户是否存在不存在也返回统一提示存在则取出盐值和哈希后的密码用同样算法算一遍比对一致则重置失败计数创建 Session 写入用户标识重定向首页不一致则失败计数加一达到阈值写锁定时间。这个链路上有两个容易被忽略的点。第一用户不存在和密码错误必须返回同一句提示否则攻击者能通过提示差异枚举出哪些用户名存在这是很典型的踩坑。第二重定向必须在 Session 写完之后用sendRedirect而不是forward否则用户刷新页面会重复提交表单。3.3 会话状态怎么维持Session 是这套方案的核心。登录成功后request.getSession()拿到会话往里塞用户 id容器自动下发 JSESSIONID 的 Cookie。后续请求带着这个 Cookie 来服务端就能认出是谁。会话有效期在web.xml里配置默认 30 分钟教学项目可以设短一点方便验证。要注意 Session 不是绝对安全的用户拿到别人的 JSESSIONID 就能冒充所以登录成功后应该调用session.invalidate()销毁旧会话再新建这个操作叫防会话固定很多人不知道。退出登录时同样要销毁光把属性移除不够Session 对象还在内存里占着。// 登录成功后重建会话防止会话固定攻击 HttpSession oldSession request.getSession(false); if (oldSession ! null) { oldSession.invalidate(); // 废弃旧会话 } HttpSession session request.getSession(true); // 创建新会话 session.setAttribute(loginUserId, user.getId()); session.setMaxInactiveInterval(30 * 60); // 30 分钟过期参数说明getSession(false)表示没有会话时返回 null 而不新建避免无谓创建invalidate()让旧会话立即失效setMaxInactiveInterval单位是秒从最后一次访问开始计时。这段逻辑放在登录成功分支里是整个认证流程最不能省的一步。4. 关键模块实现注册、登录、过滤器与验证码4.1 注册流程与密码加密密码必须加密存储这点没有商量余地。教学项目里常见两种做法MD5 加盐或者 BCrypt。MD5 已经不安全但加盐后至少能防彩虹表如果学校环境不允许引入第三方库MD5随机盐是及格线能引依赖的话优先 BCrypt它自带盐值且计算慢抗暴力破解能力强。public class PasswordUtil { // 生成随机盐值每个用户独立 public static String generateSalt() { byte[] bytes new byte[16]; new SecureRandom().nextBytes(bytes); return HexFormat.of().formatHex(bytes); } // MD5(盐值 明文)用于教学演示生产环境建议 BCrypt public static String hash(String rawPassword, String salt) { try { MessageDigest md MessageDigest.getInstance(MD5); md.update(salt.getBytes(StandardCharsets.UTF_8)); byte[] digest md.digest(rawPassword.getBytes(StandardCharsets.UTF_8)); return HexFormat.of().formatHex(digest); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(哈希算法不可用, e); } } // 比对时用同样盐值重新计算不要明文比较 public static boolean matches(String rawPassword, String salt, String storedHash) { return hash(rawPassword, salt).equalsIgnoreCase(storedHash); } }这段代码的关键在于注册时先generateSalt()拿盐再hash()得密文两个值分别存进salt和password列登录时用库里的盐重新算一遍再比对。注意matches里不能写rawPassword.equals(storedHash)那等于没加密。另外哈希结果统一转小写十六进制存储比对时用equalsIgnoreCase避免大小写差异导致登录失败这种小问题排查起来能耗一晚上。注册的校验顺序也有讲究先校验格式用户名长度、密码复杂度再校验两次密码一致再校验验证码最后才查数据库。顺序反了会让无效请求打到数据库白白浪费连接。4.2 登录 Servlet 的完整控制流登录逻辑要处理五种分支验证码错、用户不存在、账号锁定、密码错、成功。分支多了容易写乱我习惯用提前返回的方式处理每个失败分支直接return主流程始终在最外层读起来清爽。protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); String captcha req.getParameter(captcha); // 1. 验证码校验验证码存在 Session 里 String expected (String) req.getSession().getAttribute(captcha); req.getSession().removeAttribute(captcha); // 一次性使用用完即删 if (expected null || !expected.equalsIgnoreCase(captcha)) { req.setAttribute(error, 验证码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } UserService service new UserService(); User user service.findByUsername(username); // 2. 用户不存在与密码错误统一提示避免用户名枚举 if (user null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 3. 锁定检查 if (user.getLockUntil() ! null user.getLockUntil().after(new Date())) { req.setAttribute(error, 账号已锁定请稍后再试); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 4. 密码比对 if (!PasswordUtil.matches(password, user.getSalt(), user.getPassword())) { service.recordFail(user.getId()); // 失败计数1达阈值写锁定时间 req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 5. 成功重置计数、重建会话、跳转 service.resetFail(user.getId()); HttpSession old req.getSession(false); if (old ! null) old.invalidate(); HttpSession session req.getSession(true); session.setAttribute(loginUserId, user.getId()); resp.sendRedirect(req.getContextPath() /index.jsp); }几个细节值得单独说。验证码取出来立刻removeAttribute保证一码一用否则同一码能反复提交。所有失败分支都用forward回到登录页并带上错误信息保留用户输入体验只有成功才sendRedirect避免刷新重复提交。recordFail里要判断累计次数超过五次就写lock_until为当前时间加十分钟这个阈值和时长可以按需调。4.3 过滤器实现登录态校验登录态校验不能写在每个 Servlet 里否则漏一个就是一个未授权入口。用 Filter 统一拦截最稳妥。WebFilter(/*) public class AuthFilter implements Filter { // 白名单登录页、注册页、登录注册接口、静态资源 private static final SetString WHITE Set.of( /login.jsp, /register.jsp, /login, /register, /captcha, /css/, /js/, /images/ ); public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String path req.getRequestURI().substring(req.getContextPath().length()); // 白名单或已登录则放行 boolean white WHITE.stream().anyMatch(path::startsWith); boolean logged req.getSession().getAttribute(loginUserId) ! null; if (white || logged) { chain.doFilter(request, response); } else { resp.sendRedirect(req.getContextPath() /login.jsp); } } }白名单用前缀匹配而不是全等匹配是因为静态资源路径带文件名全等匹配必然漏。注意白名单里要放/captcha否则验证码接口本身会被拦截登录页永远显示不出来验证码这个坑非常典型。另外过滤器的拦截顺序由web.xml或注解的匹配规则决定有多个过滤器时要用filter-mapping显式排序别指望自动。4.4 验证码与防重复提交验证码用 Java 2D 画图生成宽度 120、高度 40四位字符随机旋转和干扰线。生成后把文本存 Session图片流写给浏览器。前端img的src指向/captcha点击时在 URL 后拼个时间戳强制刷新否则浏览器走缓存用户看到的还是旧码。防重复提交有两种思路。一种是表单里埋一个隐藏的 token服务端校验后删除跟验证码原理相同另一种是注册成功后直接sendRedirect到结果页让浏览器的后退刷新不构成重复 POST。教学项目用第二种就够token 方案可以在答辩时作为优化点提。4.5 数据库访问层封装JDBC 代码最烦的是资源关闭和异常处理。我一般写个工具类集中管理PreparedStatement必须用拼接 SQL 等于把 SQL 注入口子敞开。public class UserDao { public User findByUsername(String username) { String sql SELECT id, username, password, salt, status, fail_count, lock_until FROM t_user WHERE username ?; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); // 参数绑定杜绝注入 try (ResultSet rs ps.executeQuery()) { return rs.next() ? mapRow(rs) : null; } } catch (SQLException e) { throw new DataAccessException(查询用户失败, e); } } }try-with-resources保证连接、语句、结果集按逆序自动关闭不用写一堆finally。setString的参数从 1 开始这个下标搞错会导致“参数个数不匹配”的报错初学者常犯。数据库连接信息放配置文件而不是硬编码在类里改环境时不用重新编译。5. 避坑与排查这几个问题我见过太多次5.1 中文乱码从请求到响应全链路现象是注册的用户名存进库变成问号或者页面回显是乱码。原因通常是三层编码不一致JSP 页面pageEncoding、请求体解码方式、数据库连接串字符集。解决方式是三层全对齐到 UTF-8。JSP 头部写% page contentTypetext/html;charsetUTF-8 %Servlet 里在取参数前调用request.setCharacterEncoding(UTF-8)注意这行必须在getParameter之前之后再设无效连接串加上useUnicodetruecharacterEncodingutf8建库建表统一utf8mb4。Tomcat 8 以后 GET 请求默认按 UTF-8 解码老版本还得改server.xml的URIEncoding这一处最容易被漏掉。5.2 验证码永远提示错误现象是输入框里的码和图片明明一致提交却报验证码错误。原因一般是 Session 对不上图片是浏览器发的另一个请求两次请求的 JSESSIONID 不同或者验证码生成后又被重置。排查时先看浏览器开发者工具里/captcha请求和/login请求携带的 Cookie 是不是同一个。另一个高频原因是验证码存的是大写、比对时用的原文或者removeAttribute放的位置不对导致取不到。还有一种情况是forward之后页面重新加载又触发了一次验证码生成把 Session 里的值覆盖了这个链路要顺着走一遍。5.3 登录成功后刷新页面又跳回登录页现象很迷惑明明登录成功了点一下刷新就被踢出来。原因通常是重定向地址写错或者 Session 属性名前后不一致比如存的时候叫userId、过滤器里查的是loginUserId。这种问题用全局搜索属性名就能定位。还有一种可能是 Cookie 的 Path 设置不对导致 JSESSIONID 没带上检查方式是在浏览器里看每次请求的 Cookie 头。要是用了sendRedirect到绝对路径却漏了getContextPath()部署到带上下文的应用下就会 404这种在小项目里很常见。5.4 数据库连接泄漏把 Tomcat 拖死现象是系统跑一段时间后所有请求卡住日志里冒出一堆连接超时。原因是某处Connection没关或者异常分支跳过了关闭逻辑。JDBC 直连没有连接池兜底泄漏几次就耗尽。解决方式是所有数据库操作一律try-with-resources不要手写finally开发阶段可以把连接获取和关闭打日志跑一轮注册登录看连接数是否配对。真上线一定换连接池Druid 或 HikariCP 都行配置里加上连接泄漏检测阈值超过时间没归还就打印堆栈能直接定位到哪一行没关。5.5 密码改完老密码还能登现象是用户改了密码用旧密码依然能登录成功。原因通常是更新语句写成了update t_user set password ?但忘记同步更新salt或者更新时用的还是旧盐值算出来的哈希。改密码的正确流程是生成新盐、用新盐算新哈希、两个字段一起更新。还有人把更新条件写错where后面漏了 id结果全表密码被改成同一个值这种事故在教学项目里都发生过。写完更新语句一定先在测试库验证影响行数executeUpdate返回 0 就说明条件没匹配上。6. 验收与答辩怎么证明这套系统真的能用验收不是跑一遍注册登录就完事要拿证据说话。我建议按四个维度准备功能正确性、安全性、异常处理、代码规范。功能层面准备一份用例表正向流程跑通只是及格重点在反向用例——空用户名、超长密码、重复注册、错误验证码、连续五次错误密码触发锁定、登录后直接访问受保护页面被拦截。每一条都要有截图或录屏答辩时按表讲比临场演示稳得多。安全性这块最容易被问到的是“密码怎么存的”。你要能当场指出数据库里存的是哈希值加盐值并能解释为什么不存明文、为什么每个用户盐不同。其次是 SQL 注入演示在登录框输入 or 11被挡住说明用了PreparedStatement。再就是会话安全讲清楚登录成功后重建 Session 的原因。这三条讲明白基本能覆盖评委的追问。异常处理看两点出错时用户看到的是友好提示而不是 Tomcat 的堆栈页后台日志能定位到具体哪一步失败。可以故意把数据库密码改错演示系统给出“服务暂时不可用”而不是白屏同时日志里有完整异常链。代码规范不是虚的评委翻代码主要看命名、分层和注释。类名方法名能不能自解释、Servlet 里有没有混 SQL、工具类有没有重复造轮子这些一眼就能看出来。验收项合格标准验证方式注册正向流程注册成功后可用新账号登录手动执行重复用户名提示已存在不产生重复数据并发提交两次密码存储库中无明文盐值每用户不同查库截图SQL 注入特殊字符输入不改变查询语义手工注入测试未登录访问重定向到登录页直接访问受保护 URL连续失败锁定五次错误后锁定十分钟连续提交错误密码退出登录Session 销毁后退无法访问退出后点浏览器后退答辩陈述我一般建议按“需求—设计—实现—验证”四段走每段两分钟。设计段重点讲分层和表结构实现段挑密码加密和过滤器两个点深入验证段直接放用例表。别把时间花在介绍 JSP 是什么上评委不关心这个关心的是你的设计决策有没有依据。从那以后我每次带人做这类项目都强制要求先写用例表再写代码功能可以少但每条功能必须有对应的反向用例和验证方式否则不算完成。这套习惯比任何框架都值钱。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑