ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaWeb登录注册实战:Servlet+JSP+验证码+JDBC全链路解析

JavaWeb登录注册实战:Servlet+JSP+验证码+JDBC全链路解析 简介一份面向JavaWeb初学者的登录注册完整实例教程以Servlet、JSP、MySQL、JDBCTemplate、Druid、BeanUtils和Tomcat技术栈为主线完整覆盖登录注册页面设计、验证码生成与校验、关系数据库表设计、用户名主键约束、Druid连接池配置、JDBC工具类封装以及Servlet登录校验和注册写入等环节并附带运行效果图与常见错误提示方便按步骤理解并动手实践。压缩包内只有1个PDF文件大小148KB内容集中但步骤完整适合课程设计、毕业设计或初学Web开发时对照参考。目前已有4792人学习下载对想快速上手JavaWeb登录注册模块的开发者而言是一份高性价比的参考资料。1. 一个能跑通的 ServletJSP 登录注册先看验证码怎么骗过浏览器缓存我拆这个 JavaWeb 登录注册实例是因为它把验证码、Session、JDBC 这几块初学者最容易绕晕的东西串在了一条完整链路上。你搜“JavaWeb 登录注册”时缺的不是零散的 API 用法而是一个能从头跑到尾的最小闭环前端 JSP 发请求Servlet 接收参数验证码画出来并在 Session 里比对查库校验用户名密码最终重定向到成功页或带着错误提示转发回登录页。这个实例正好把这条链路完整走了一遍用的是 Servlet JSP MySQL JDBCTemplate Druid 连接池这套经典技术栈适合刚学完 Servlet 和 JSP、正卡在“各个知识点都会但拼不成一个项目”阶段的读者也适合做课设时需要一个基础底子再往上加功能的同学。你照着部署跑通一次之后再回头看自己之前零零散散写的那些 Servlet会清楚很多。2. 技术栈选型与整体请求链路为什么是 Servlet JSP JDBCTemplate Druid2.1 技术栈选型的理由这套组合解决什么问题这个实例的技术栈不是随便拼的每选一个组件都有明确的针对性。Servlet 负责接收 HTTP 请求和做流程控制JSP 负责渲染页面两者通过 request 转发、response 重定向、Session 存储来完成状态传递。MySQL 存用户数据JDBCTemplate 封装 JDBC 操作让 DAO 层不需要写大量 try-catch-finally 来开关连接。Druid 连接池管理数据库连接避免每次请求都重新创建连接带来的性能损耗。BeanUtils 在这个实例里其实用得不算深项目里是手动 new User 再 set 属性不过它被列进技术栈说明了一个思路从 request 参数到 JavaBean 的映射本可以更自动化。这套组合的最大价值是“每一层都能看到清晰边界”。JSP 只干展示和接收输入的活Servlet 只干流程控制的活DAO 只干 SQL 的活实体类只干数据封装的活。对于学习阶段来说这种分层越“老实”越容易在后面引入 Spring MVC 时理解 MVC 到底解决了什么问题。你用 SSM 或者 Spring Boot 时Controller 里做的事和这里的 LoginServlet 几乎一一对应只是换了个壳。Tomcat 的作用是提供 Servlet 容器。你在自己电脑上部署时把项目打成 war 包放进 webapps 目录或者直接在 IDEA 里配置 Tomcat 运行本质上都是让 Tomcat 接管 Servlet 的生命周期。这个实例里的 WebServlet 注解是 Servlet 3.0 以上的写法省去了 web.xml 里逐个映射 Servlet 的配置。2.2 一次登录请求的完整流转过程我把这条链路的每一步拆开你看完就知道每个文件在干什么了。用户在 login.jsp 填写用户名、密码、验证码点击登录按钮表单以 POST 方式提交到 /daydayup/loginServlet。Tomcat 根据 WebServlet(/loginServlet) 注解找到 LoginServlet调用 doPost 方法。LoginServlet 先设置 request.setCharacterEncoding(utf-8)然后取出 username、password、checkcode 三个参数。从 Session 里取出之前由 CheckCodeServlet 存进去的 checkcode_session取出来后立即 removeAttribute。比对验证码。不一致则往 request 里塞 checkcode_error 提示信息转发回 login.jsp。验证码一致则把用户名密码封装进 User 对象调用 UserDao.login 方法查库。查得到用户则把 username 存进 Session然后 response.sendRedirect 到 success.jsp。查不到则往 request 里塞 login_error 提示信息转发回 login.jsp。注意第 5 步和第 8 步用的是 forward 转发第 7 步用的是 sendRedirect 重定向这个区别后面讲。验证码的生成发生在另一个独立的 Servlet——CheckCodeServlet它在登录页加载时通过 标签的 src 被请求到每次请求生成一张新图片并把验证码字符串存到 Session。2.3 开发环境与部署步骤先说环境版本我拆的时候用的配置是 JDK 8、Tomcat 8.5、MySQL 5.7你在 JDK 11 或 Tomcat 9 下跑大概率也没问题但如果你用的是 Tomcat 10注意 javax.servlet 包名要换成 jakarta.servlet这个实例没有做迁移直接跑会报 ClassNotFound。这是个容易卡住的版本差异点放到后面避坑里细说。# 1. 创建数据库和用户表 CREATE DATABASE IF NOT EXISTS daydayup DEFAULT CHARSET utf8mb4; USE daydayup; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表结构要和 UserDao 里执行的 SQL 对应上。select * from user where username ? and password ?查询的是 username 和 password 两列insert into user (username,password) values(...)写入的也是这两列。id 是自增主键不手工赋值。然后配置 Druid 连接池的 druid.properties 文件放到 src 目录下保证编译后出现在 classes 根路径里。driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/daydayup?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot password你的数据库密码 initialSize5 maxActive10 maxWait3000key 名必须写 driverClassName、url 这些DruidDataSourceFactory 就是按这套 key 解析的。如果你数据库密码是空的直接把 password 那行删掉或者留空都可以但建议还是设一个密码再连。注意 MySQL 8.x 需要把驱动类换成 com.mysql.cj.jdbc.Driver且驱动 jar 版本要匹配否则会报 ClassNotFoundException 或者 SSL 连接错误。在 IDEA 里新建 Web 项目把源码目录复制进去再引入依赖 jar 包。需要的有mysql-connector-java、druid、spring-jdbcJdbcTemplate 在这个包里、spring-beans、spring-core、spring-tx以及 Tomcat 自带的 servlet-api。用 Maven 的话pom.xml 里加对应依赖没有 Maven 的话就把 jar 包丢到 WEB-INF/lib 下。这个实例原版没有用 Maven是直接拷 jar 的模式跑通后再迁 Maven 也不难。3. 验证码生成与校验从画一张图到 Session 比对3.1 CheckCodeServlet 逐行拆解图片是怎么画出来的CheckCodeServlet 是这个实例里最有辨识度的一段代码它用纯 Java 2D 绘图 API 在内存中生成一张带干扰线的验证码图片。核心逻辑是创建 BufferedImage 对象拿到 Graphics 画笔填充背景色画边框随机取字符画上去最后画几条干扰线再通过 ImageIO.write 输出到响应流。int width 100; int height 50; BufferedImage image new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR); Graphics g image.getGraphics(); g.setColor(Color.ORANGE); g.fillRect(0, 0, width, height); g.setColor(Color.BLUE); g.drawRect(0, 0, width - 1, height - 1);这段代码里最关键的是BufferedImage.TYPE_INT_BGR它指定了图像的色彩模型。用 TYPE_INT_BGR 时像素按 BGR 顺序存储生成的图片在浏览器里显示颜色是正常的。画边框用drawRect(0, 0, width - 1, height - 1)如果你写成width和height会裁掉右边缘和下边缘的边框线这是无伤大雅但肉眼可见的细节差异。接着是随机取字符的部分。注意验证码的字符集集合是一个字符串前后包含了大写字母、数字、小写字母总共 62 个字符。随机生成 4 个字符的逻辑是这样的String str QWERTYUIOPASDFGHJKLZXCVBNM1234567890zxcvbnmlkjhgfdsaqwertyuiop; StringBuilder sb new StringBuilder(); Random ran new Random(); for (int i 1; i 4; i) { int index ran.nextInt(str.length()); char ch str.charAt(index); sb.append(ch); g.drawString(ch , width / 5 * i, height / 2); } String checkcode_session sb.toString(); request.getSession().setAttribute(checkcode_session, checkcode_session);这里有两个技术点值得注意。一是ran.nextInt(str.length())的调用方式如果你用Math.random()要自己处理浮点到整数的转换而nextInt(n)直接返回 0 到 n-1 之间的整数省一步。二是g.drawString(ch , width / 5 * i, height / 2)里字符的水平位置是按照 i 从 1 到 4 等分图片宽度5 个等分点去掉第一个让字符稍微偏左视觉上更居中。你可以改成width / 5 * i - 5微调偏移量让字符在各自的小格子里真正居中。干扰线是紧接着验证码字符画的线段的端点坐标全用ran.nextInt(width)或ran.nextInt(height)随机画 6 条红色线条。干扰线的意义在于增加 OCR 识别的难度但这只是一个最基础的风控手段后面我在最后一张会说它的局限。request.getSession().setAttribute(checkcode_session, checkcode_session)这一行是整个验证码机制的核心生成的字符串不是存在客户端也不是放在请求参数里而是放在 Session 中。login.jsp 里的 标签请求这个 Servlet 时服务器端就会把验证码字符串和当前会话绑定起来用户提交时再拿表单里的值和 Session 里的值比对。3.2 前端点击刷新验证码的时间戳技巧login.jsp 里这段 JavaScript 是验证码体验的关键也是很多初学 JavaWeb 时会忽略的细节window.onload function () { document.getElementById(img).onclick function () { this.src /daydayup/checkCodeServlet?time new Date().getTime(); }; };这段代码的作用是点击图片时重新请求验证码图片。中间没有加问号参数名等号也就是?time后直接拼时间戳出参格式并不标准但 CheckCodeServlet 里并没有读取任何名为 time 的参数所以这行代码的真正作用不是传参而是让每次请求的 URL 不一样。这涉及浏览器 HTTP 缓存机制如果图片的 src 地址不变浏览器可能直接返回缓存的旧图片看到的验证码就永远不刷新。加上时间戳后 URL 每次都不同浏览器无法命中缓存只能重新请求。这是一个很典型的前端配合技巧后端不需要做任何改动就能让图片刷新机制生效。不过这段代码有个小问题window.onload function(){...}这个写法会在页面所有资源加载完毕后绑定点击事件。如果页面加载过程中有延迟用户可能看到图片后点击时事件尚未绑定成功。稳妥的做法是用内联 onclick 或事件委托。但在这个实例的简单场景下实测基本没问题因为页面只有一个外链图片资源。3.3 校验逻辑为什么要先比对验证码再查数据库LoginServlet 里的校验顺序是刻意安排的。它先取验证码参数再取 Session 中存储的验证码字符串然后立即 removeAttribute最后做比对。这个顺序背后的逻辑是验证码比对是纯内存操作不需要访问数据库。如果先查数据库再去校验验证码就白白浪费一次数据库连接资源而且攻击者可以通过响应时间的差异来判断用户名是否存在。String checkcode_session (String) session.getAttribute(checkcode_session); session.removeAttribute(checkcode_session); if (checkcode_session ! null checkcode_session.equalsIgnoreCase(checkcode)) { // 验证码正确继续登录逻辑 } else { request.setAttribute(checkcode_error, 验证码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); }两个细节值得展开讲。第一是removeAttribute放在比对之前而不是比对之后。这确保了验证码是一次性的——用户第一次提交后无论验证码对不对Session 里的验证码都被销毁。这能防止重放攻击用户刷新页面重新加载验证码图片后旧验证码无法再次使用。第二是equalsIgnoreCase忽略了大小写差异这意味着用户输入小写也能通过降低了输入成本但让验证码的防御能力稍微变弱了一点。工程上这个取舍很常见避免的是用户被大小写折腾的挫败感。4. 注册功能与 UserDao主键约束兜底SQL 拼接的隐患4.1 注册流程的代码路径注册功能从 register.jsp 的提交开始请求到达 RegisterServlet。这个 Servlet 做的事很简单取出表单参数封装到 User 对象传给 UserDao.add 方法然后根据返回值往 request 里放不同提示再转发回 register.jsp 显示。注意这里用的是 forward叫“返回注册页”但请求地址栏 URL 不会变化。protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); String username request.getParameter(username); String password request.getParameter(password); User registerUser new User(); registerUser.setUsername(username); registerUser.setPassword(password); boolean flag new UserDao().add(registerUser); if (flag) { request.setAttribute(register_success, 注册成功); } else { request.setAttribute(register_error, 账号已被注册,注册失败); } request.getRequestDispatcher(/register.jsp).forward(request, response); }这个 Servlet 里有一个容易被忽略的问题它没有判断用户名和密码是否为空。用户名或密码为空时也会进入 DAO 层执行 SQL插入空字符串。数据库层面如果用户名是主键那么第二次用空字符串注册时会因为主键冲突而失败但第一次空字符串是可以成功插入的。如果你要在生产上复用这个代码第一件事就是补上前端必填校验和服务端非空校验。4.2 UserDao.add 方法的执行流程与返回逻辑UserDao 里维护了一个 JdbcTemplate 成员变量初始化时传入 Druid 连接池的数据源。JdbcTemplate 是 Spring 对 JDBC 的封装它内部自己管理连接的开与关因此 add 方法里又手动获取连接就显得很怪。下面这个 add 方法就是项目原本的写法public boolean add(User user) { String sql insert into user (username,password) VALUES( user.getUsername() , user.getPassword() ); boolean flag false; int num 0; Connection conn null; Statement state null; try { conn JDBCUtils.getConnection(); state conn.createStatement(); num state.executeUpdate(sql); } catch (SQLException e) { e.printStackTrace(); } if (num 0) flag true; return flag; }这段代码有一个明显不合常理的地方JdbcTemplate 已经在类初始化时建好了add 方法里却完全没有用它而是回到最原始的 Statement 手动操作。这说明这个方法的编写方式与 login 方法的风格不统一。实际生产上不推荐这种写法有两个原因。第一是 SQL 字符串拼接存在注入风险用户名里如果含有单引号比如oconner拼接出来的 SQL 会把字符串截断甚至可以通过构造参数改写 SQL 语义。第二是 Statement 每次执行都要由数据库重新解析 SQL性能不如 PreparedStatement 的预编译机制。如果是我来改会统一用 JdbcTemplate 的 update 方法配合占位符public boolean add(User user) { String sql insert into user (username,password) values(?,?); int num template.update(sql, user.getUsername(), user.getPassword()); return num 0; }这样既消除了注入风险又利用了 JdbcTemplate 对连接的自动管理还少写了 Connection 和 Statement 的开关。项目里同样的问题也存在于 login 方法虽然它的select * from user where username ? and password ?用了 PreparedStatement 占位符但 add 方法这个特例很明显是后来补的没有统一风格。4.3 注册失败判定用户名唯一约束是最后一道防线这个实例里“用户名不能重复”的校验并不是 Servlet 提前查了一遍库而是靠表结构里 username 字段的 UNIQUE KEY 约束。当第二次插入相同用户名时数据库会抛出 DuplicateEntry 异常add 方法捕获到 SQLException 后返回 falseRegisterServlet 就显示“账号已被注册,注册失败”。这个设计思路是可以学习的依赖数据库约束做最后兜底而不是完全依赖业务代码里的先查再插。先查再插存在竞态条件两个请求同时查到用户名不存在然后同时插入结果总有一个失败。唯一约束可以保证在数据库层面多个请求并发时只有一次插入成功。工程上常见的做法是“先查一次插入时再用 try-catch 捕获冲突”也就是把业务校验和数据库兜底结合起来。如果你的表没建唯一索引那这里的重复用户名校验就会失效注册的 bug 会以很难看的方式呈现。4.4 数据库连接池的配置要点JDBCUtils 静态代码块里做的事情是标准的加载 classpath 下的 druid.properties用 DruidDataSourceFactory 创建 DataSource。这里必须注意静态代码块在类加载时执行一次之后整个应用共享同一个 DataSource。druid.properties 里 initialSize 是启动时创建的物理连接数maxActive 是最大活跃连接数maxWait 是获取连接的等待超时。如果你设置 maxActive10有 11 个并发请求同时访问数据库第 11 个请求会等待超过 maxWait 后抛出获取连接超时异常。学习阶段跑单用户调试没问题但如果模拟并发测试要注意调整这些参数。5. 登录 Servlet 的分步逻辑从请求参数到 Session 传递用户状态5.1 登录的完整 Java 代码与关键分支说明WebServlet(/loginServlet) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); String username request.getParameter(username); String password request.getParameter(password); String checkcode request.getParameter(checkcode); HttpSession session request.getSession(); String checkcode_session (String) session.getAttribute(checkcode_session); session.removeAttribute(checkcode_session); if (checkcode_session ! null checkcode_session.equalsIgnoreCase(checkcode)) { User loginUser new User(); loginUser.setUsername(username); loginUser.setPassword(password); User user new UserDao().login(loginUser); if (user ! null) { session.setAttribute(user, username); response.sendRedirect(request.getContextPath() /success.jsp); } else { request.setAttribute(login_error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } else { request.setAttribute(checkcode_error, 验证码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); } }这段代码有三个必须看懂的细节。第一是 Session 的获取方式request.getSession()在无参时表示当前请求有 Session 就用现有的没有就新建一个。对应地如果登录失败后转发回 login.jsp响应里会带上新的 JSESSIONID Cookie。第二是成功后的数据传递方式登录成功后跳转到 success.jsp 时浏览器会发起一个新的 HTTP 请求这个新请求里没有原始表单参数所以要在跳转前把用户名塞到 Session 里success.jsp 再从 Session 取出来显示。这是为什么这里用 Session 而不是 request 的根本原因——跨请求传递数据。第三是request.getContextPath()的引入这会让路径兼容部署时改了应用名的情况不要写死成重定向到 /daydayup/success.jsp。5.2 queryForObject 的异常处理机制与坑UserDao 的 login 方法用的是template.queryForObject。这个方法要求 SQL 查询结果恰好返回一行如果查到 0 行或查到多于 1 行都会抛异常。当用户名或密码错误时查询结果为空queryForObject 会抛出 EmptyResultDataAccessException它是 DataAccessException 的子类被代码里的 catch 捕获后返回 null。这种用异常控制业务分支的写法“能跑”但不推荐。调优方向是先用 queryForList 或者 query 方法接返回值判断集合大小避免拿异常当普通分支来用。不过我实操中见过很多老项目都这么写原因是 JdbcTemplate 的 queryForObject 在只查一行时确实简洁尤其在学习示例里。阅读这段代码时要记住queryForObject 查不到数据时会抛异常不是返回 null。5.3 request 转发与 response 重定向的路径差异登录失败时用的是 request.getRequestDispatcher(/login.jsp).forward(request, response)登录成功时用的是 response.sendRedirect(request.getContextPath() /success.jsp)。两种跳转方式的差别在路径写法和状态保持上。forward 发生在服务器内部浏览器地址栏不变请求参数保留但转发两次会出现表单重复提交的问题刷新页面时浏览器会重新发送原请求。sendRedirect 返回 302浏览器会向新地址再发一次新请求地址栏变为目标地址之前的 request 对象不会带到新请求里所以数据传递要靠 Session 或 URL 参数。这个实例里把登录失败设计成转发让 login.jsp 能用${requestScope.login_error}和${requestScope.checkcode_error}显示错误。这两个变量存在 request 域里转发的目标页面能直接通过 EL 表达式取到。如果改成重定向request 里的属性就失效了必须换成 Session 或其他方案。这是理解这个项目为什么“失败用转发、成功用重定向”的关键也是面试时经常被追问的点。5.4 success.jsp 的 Session 取值与 EL 表达式的选择h1%request.getSession().getAttribute(user)%,欢迎您/h1success.jsp 里用 Java 脚本片段直接输出 Session 属性。代码里还留着那段注释掉的${requestScope.user}说明原来也可能想用 EL 表达式。用 EL 写会简洁很多。但后端存的是字符串 username在 EL 里直接写${sessionScope.user}即可。如果你想显示更多用户信息需要在 Servlet 里把整个 User 对象存入 Session逻辑上不需要额外查一次库。6. 避坑指南我跑这个项目时踩过的六个坑这章不写原理全是我实际部署时踩过的坑按“现象 → 原因 → 解决”的格式列出来。6.1 一直报 ClassNotFoundException: com.mysql.jdbc.Driver现象Tomcat 启动后第一个请求就报ClassNotFoundException: com.mysql.jdbc.DriverDruid 初始化失败。原因MySQL 驱动 jar 没有放到 WEB-INF/lib 目录下。如果你用的是 MySQL 8.x 驱动类名还叫 com.mysql.jdbc.Driver 但已经不建议使用。如果是用 IDEA 的 Libraries 方式引入依赖没有选择打包到 WEB-INF/libTomcat 运行时也找不到。解决确认数据库版本。MySQL 5.7 用 mysql-connector-java 5.x驱动类名是 com.mysql.jdbc.Driver。MySQL 8.x 必须用 8.x 驱动且把 properties 里的 driverClassName 改成 com.mysql.cj.jdbc.Driver。同时在 IDEA 的 Artifacts 配置里确认依赖已经输出到 WEB-INF/lib。6.2 登录页能打开但点登录按钮没反应现象login.jsp 正常显示点击登录按钮后页面没有跳转控制台也不报错。原因表单里没有写 method 属性默认提交方式为 GET而 LoginServlet 虽然覆写了 doGet 并转发到 doPost但如果表单 action 写错路径404 时页面可能直接显示错误。更常见的场景是按钮类型写成了button但没有显式指定 type 属性在某些浏览器中默认 type 是 submit某些是 reset行为不一致。解决登录按钮显式写成input typesubmit value登录。同时检查表单 action 的路径要和 WebServlet 注解完全匹配注意应用上下文路径前缀。6.3 验证码图片永远不变刷新多少次都一样现象点击验证码图片刷新后图片还是同一个。原因浏览器缓存。图片 src 的 URL 没有变化时浏览器直接从缓存读取。另一个原因是 Session 没有更新图片虽然重新生成了但写入 Session 的验证码没有变——这种情况发生在你访问的会话 ID 没有变化而 CheckCodeServlet 每次都认同一个会话。解决确认 login.jsp 里 JavaScript 的图片 src 带了时间戳即?time 时间戳。另外如果 CheckCodeServlet 里没有先处理掉旧的 Session 属性多次刷新同一个页面时只要用户不提交表单Session 里的验证码会不断被覆盖这是正常的不会导致“不变化”的问题。6.4 验证码正确但登录一直失败数据库有这条记录现象验证码输入正确用户名密码确认多次无误数据库里也能查到但就是提示用户名或密码错误。原因编码问题。前端页面提交的是 UTF-8 编码但数据库连接的 url 里没有指定 characterEncodingutf8或者 JDBC 驱动拿到的字节流没有正确转码导致中文无法比对成功。如果你的用户名是英文和数字这个问题不明显一旦用户名是中文几乎必现。解决在 druid.properties 的 url 后面加characterEncodingutf8。同时确认 MySQL 数据库和表都是 utf8mb4 编码。还可以在 LoginServlet 进入正题前打印一次 username 和 password 的字符序列看是否为乱码。6.5 注册时第一次成功第二次相同用户名报 500现象第一次注册成功第二次用相同用户名注册页面没有返回“账号已被注册”而是直接跳到了 500 错误页。原因没有配置 unwind 检查。项目里的 SQLException 被 catch 后只打印了堆栈没有记录到日志数据库的唯一约束冲突异常也没有被转换成业务提示。还有一个可能是 Druid 的异常被 JdbcTemplate 包装后重新抛出原本 catch SQLException 的地方没接住。解决不管用哪种方式把 DAO 的 add 方法返回 boolean 的逻辑改为捕获 DataAccessException 而不是 SQLException。或者更常见的做法是在 RegisterServlet 中捕获异常将 DuplicateEntry 错误转成 register_error 提示。最稳的方案是 Spring 的异常翻译机制配合日志框架把堆栈存下来。6.6 Tomcat 10 下运行项目报 javax.servlet 相关错误现象把项目放到 Tomcat 10 下运行启动失败或访问时直接报 NoClassDefFoundError 关于 javax.servlet 的类。原因Tomcat 10 是 Jakarta EE 9 的实现Servlet 的包名从javax.servlet改成了jakarta.servlet。这个实例代码用 WebServlet、HttpServlet 等全部基于 javax 包直接跑必然报错。解决两种方案。一是换回 Tomcat 8.5 或 9.0这是最省事的二是把项目中所有import javax.servlet改成import jakarta.servlet并把对应的 servlet-api jar 换成 Tomcat 10 自带的。这个迁移不算复杂但你要是还在学习阶段用 Tomcat 8.5 就好。7. 进阶验证压测 Druid 连接池参数并给验证码加随机颜色这个实例跑通之后我习惯再加一层自己的验证不然总觉得代码只是“能跑”没说清楚“跑得好不好”。第一个验证是连接池参数是否合理。druid.properties 里 initialSize5、maxActive10单用户调试完全够用。但你可以在登录成功后打开 Druid 的监控页面看活跃连接数、闲置连接数、获取连接耗时等指标。在这个实例里没引入 Druid 监控 Servlet你可以临时在 web.xml 里加一个 DruidStatServlet或者用代码临时打印连接池状态。更简单的验证方式是开 20 个并发线程同时请求登录接口看控制台有没有报连接租用超时。如果 maxWait3000 设得太短并发一上来就会抛异常。把这组参数按你实际的并发量调整才算真正把这个例子的数据库层吃透了。第二个验证是验证码的随机颜色。原版验证码字符统一用默认的前景色也就是黑色。工程上验证码的抗识别能力依赖字符颜色随机变化和扭曲程度。我一般会这样改 CheckCodeServlet让每个字符的颜色随机生成Random ran new Random(); for (int i 1; i 4; i) { int index ran.nextInt(str.length()); char ch str.charAt(index); sb.append(ch); g.setColor(new Color(ran.nextInt(256), ran.nextInt(256), ran.nextInt(256))); g.drawString(ch , width / 5 * i, height / 2); }注意new Color(ran.nextInt(256), ran.nextInt(256), ran.nextInt(256))生成的随机颜色可能和橙色背景撞色如果你背景是橙色字符再用橙色就会看不清。可以限定颜色范围比如红色通道在 20 到 200 之间跳动确保和背景有足够的对比度。这种“颜色随机化”只是把 OCR 识别的门槛抬高了一点点真正的验证码安全要靠字符扭曲、背景噪点、行为验证等手段但在这个学习阶段的项目里把颜色随机这一步加上已经比原版看起来专业不少。第三个验证是把所有密码改成加密存储。原版代码里密码是明文存 MySQL这在生产环境是不可接受的。常见做法是注册时用 MD5 或 BCrypt 加密后再入库登录时把用户输入的密码加同样的盐再和数据库比对。改动量不大只需要在 RegisterServlet 里加一步加密在 LoginServlet 里加一步加密。如果你用 BCrypt还要引入 spring-security-crypto 依赖。这也是我拿到任何登录注册项目后第一件会做的事密码明文落库属于一发生产就出事的安全雷。这三个点做完这个项目不只是“能跑”而是一个能拿得出手的课设或小系统骨架。从那以后我每次拿到别人的登录注册示例都会强制走一遍这三步先看连接池参数是否扛得住并发再看验证码有没有随机化处理最后看密码是不是明文存储。三步都没问题我才会放心往下加业务功能。希望帮到你。本文还有配套的精品资源点击获取
返回列表