ARTICLE DETAIL

资讯详情

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

JavaWeb登录注册实例:Servlet+JSP+MySQL+Druid完整链路解析

JavaWeb登录注册实例:Servlet+JSP+MySQL+Druid完整链路解析 简介这是一份面向JavaWeb初学者的登录注册完整实例代码文档聚焦“带验证码的用户登录与注册”这一经典场景演示从页面搭建到数据库写入、登录校验的完整流程。资源以PDF形式收录了登录页、注册页、成功页的JSP代码以及验证码生成、JDBCUtils工具类、Servlet处理逻辑等关键实现适合课程设计、毕业设计或自学参考。压缩包共1个文件类型为PDF大小148KB便于直接打开阅读。目前已有4793人学习浏览实用性获得不少初学者认可。内容覆盖需求分析、技术栈选型、验证码防机器人机制、MySQL用户表设计、Druid连接池配置与BeanUtils封装并包含常见错误信息的页面回显思路。读者可通过完整代码快速理解ServletJSPMySQL的协作方式也可将其改造为带注册校验和安全机制的项目基础模块。1. JavaWeb 登录注册实例先搞清这套代码解决什么问题JavaWeb 登录注册是后端学习者绕不开的第一个完整闭环用户在页面输账号密码服务端校验验证码、比对数据库、写 Session、跳转回显一次请求跑完整个 Servlet 生命周期。这套实例代码用 Servlet JSP MySQL JDBCTemplate Druid BeanUtils Tomcat 把链路完整串起来不引 Spring、不做前后端分离代码量可控部署到 Tomcat 就能跑。适合正在学 JavaWeb 的人、需要交课程设计的学生以及拿带验证码的登录注册做底子的人。下面按我拆项目的习惯把工程结构、核心代码和踩过的坑逐条过一遍重点说清验证码为什么要先校验、Session 与 Request 传参的差别、注册时主键冲突这条隐藏链路。2. 技术选型与工程结构JDBCTemplate Druid Servlet 的搭配逻辑2.1 分层逻辑Servlet 管请求、JSP 管展示、Dao 管数据先看整体分层。这个项目没有用框架是经典的 JSP Servlet 三层cn.code.servlet 包里的 LoginServlet、RegisterServlet 承担控制器职责负责取参数、调 Dao、决定跳转cn.code.checkcode 里的 CheckCodeServlet 负责生成验证码图片cn.code.dao 里的 UserDao 只做数据库读写cn.code.util 的 JDBCUtils 管理数据源cn.code.domain 的 User 是实体类属性对应表字段。JSP 页面放在 web 根目录属于最朴素的 Servlet 时代结构。之所以不直接上 SpringMVC 或 SpringBoot是因为学习阶段用原生 Servlet你才能看清楚 request、session、forward、redirect 这些对象的真实行为。框架会把细节藏进黑匣子出了问题反而难排查。等这套跑通了再套 SpringMVC 只是换了一层壳底层逻辑一模一样。工程目录在 IDEA 里大致长这样daydayup/ ├── src/cn/code/ │ ├── checkcode/CheckCodeServlet.java # 生成验证码图片 │ ├── dao/UserDao.java # 登录查询、注册插入 │ ├── domain/User.java # 用户实体 │ ├── servlet/LoginServlet.java # 登录控制 │ ├── servlet/RegisterServlet.java # 注册控制 │ └── util/JDBCUtils.java # Druid 连接池工具类 ├── web/ │ ├── login.jsp # 登录页 │ ├── register.jsp # 注册页 │ └── success.jsp # 登录成功页 └── resources/ └── druid.properties # 连接池与数据库配置数据访问层用了 JDBCTemplate 而不是手写原始 JDBC这是我认为这套代码最值得抄的地方。原始 JDBC 每次查询都要自己写连接获取、ResultSet 遍历、finally 关资源代码量大且容易漏。JDBCTemplate 把模板化工作收走一行 queryForObject 加一个 BeanPropertyRowMapper 就能把查询结果映射成 User 对象。技术选型里写的 BeanUtils 在代码中对应的其实是 BeanPropertyRowMapper——它内部按列名和实体属性名做反射赋值和 Apache BeanUtils 的 copyProperties 是同一类工作理解成Bean 属性自动映射即可。提示如果你之前用 MyBatis 比较多看这套代码会觉得很朴素但理解列名到属性名的映射规则对你看懂 MyBatis 的驼峰配置反而有帮助。2.2 数据库设计为什么把用户名设为主键原实例的需求写得很清楚用户名不能重复设为主键重复注册直接失败。对应的建表 SQL 是这样的-- 按原实例设计username 直接作为主键插入重复值会抛主键冲突 CREATE TABLE user ( username varchar(20) NOT NULL, password varchar(20) DEFAULT NULL, PRIMARY KEY (username) ) ENGINEInnoDB DEFAULT CHARSETutf8;注意原代码的实体类还有一个 id 字段但建表可以不建 id因为插入语句只写了 username 和 password 两列。把用户名直接当主键最直接的好处是注册时不用先 select 查重——插入重复主键会抛异常UserDao.add 捕获异常返回 falseRegisterServlet 就把账号已被注册回显到页面。这一条链路是整套代码里最容易被忽略的巧妙点。代价也要说清楚用户名做主键意味着用户没法改名索引基于字符串且不是自增如果未来要做用户系统订单、评论、权限外键引用会非常别扭。我一般会在这个实例的基础上改成自增 id 主键 username 唯一索引业务去重效果一样但扩展性好得多。学习阶段先按原设计跑通理解主键冲突即注册失败的思想后续再升级即可。2.3 JDBCUtils 与 druid.properties连接池初始化数据库连接不能每次请求现连这是项目引入 Druid 的原因。resources 下的 druid.properties 是连接池和数据库的全部配置# 注意 MySQL 8 要换成 com.mysql.cj.jdbc.Driver并配 serverTimezone driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/daydayup?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot password123456 initialSize5 maxActive10 maxWait3000JDBCUtils 用静态代码块加载这份配置并创建数据源全项目共享一个连接池对象package cn.code.util; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.util.Properties; public class JDBCUtils { private static DataSource ds; static { try { Properties pro new Properties(); // 通过类加载器读取 classpath 下的 druid.properties InputStream is JDBCUtils.class.getClassLoader().getResourceAsStream(druid.properties); pro.load(is); ds DruidDataSourceFactory.createDataSource(pro); } catch (Exception e) { e.printStackTrace(); } } public static DataSource getDataSource() { return ds; } public static Connection getConnection() throws SQLException { return ds.getConnection(); } }逻辑说明静态块在类第一次加载时执行把 druid.properties 读成 Properties 对象再交给 DruidDataSourceFactory 创建数据源。getDataSource 供 JdbcTemplate 使用getConnection 供手写 JDBC 的地方使用。参数 initialSize 是连接池初始连接数maxActive 是最大连接数maxWait 是拿不到连接时的最大等待毫秒数。注意 MySQL 8 的驱动类要换成 com.mysql.cj.jdbc.DriverURL 里 serverTimezone 必须指定否则即使连上也会在日期操作时报错——这也是老项目迁移到新版 MySQL 最常见的翻车点。3. 核心实现拆解从验证码画图到登录注册落库的完整链路3.1 CheckCodeServlet用 BufferedImage 在内存里画验证码验证码部分是整个登录注册的安全门槛也是最容易写飘的一段。CheckCodeServlet 的思路是在内存中创建一张图片用 Graphics 画笔往上画背景、边框、字符和干扰线然后把图片以 jpg 格式输出到响应流同时把字符原文存进 session。核心代码WebServlet(/checkCodeServlet) public class CheckCodeServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 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); // 边框减 1 防止边框被裁掉 g.drawRect(0, 0, width - 1, height - 1); 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); // 四个字符均分 } // 验证码原文存 session登录时比对的就是它 request.getSession().setAttribute(checkcode_session, sb.toString()); g.setColor(Color.RED); // 干扰线 for (int i 0; i 6; i) { int x1 ran.nextInt(width); int x2 ran.nextInt(width); int y1 ran.nextInt(height); int y2 ran.nextInt(height); g.drawLine(x1, y1, x2, y2); } // 把图片写进响应浏览器直接显示 ImageIO.write(image, jpg, response.getOutputStream()); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); } }参数说明width100、height50 是图片尺寸验证码 4 个字符字符池里大小写和数字混排因为登录校验用 equalsIgnoreCase大小写不影响比对结果。drawString 的横坐标 width/5*i 让 4 个字符均匀分布在图片上干扰线 6 条颜色随机红蓝橙组合起来能挡住简单的 OCR但防不住专门训练过的识别模型——这个实例的定位是课程设计和入门项目生产环境建议换滑块验证码。图片输出用的是 response 的输出流这个 Servlet 不需要 forward 到任何 JSP访问 /checkCodeServlet 拿到的直接就是 jpg 字节。登录页给图片加了点击刷新功能这里有一个浏览器缓存的小技巧document.getElementById(img).onclick function () { this.src /daydayup/checkCodeServlet?time new Date().getTime(); };逻辑说明浏览器对同一个 URL 默认会缓存直接重设 src 很可能拿到同一张旧图。加一个 time 时间戳参数后每次请求的 URL 都不同浏览器只能重新向服务器要图这是最朴素的强制刷新方案。3.2 先验证码后账号LoginServlet 的校验顺序登录逻辑里最容易忽略的是校验顺序。LoginServlet 先取参数再从 session 里取验证码原文注意它一进来就把 session 里的验证码删掉了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); }逻辑说明先 removeAttribute 再比对意味着验证码只能被成功使用一次——用户输错一次session 里的旧验证码就作废了必须点图片刷新后再输。校验顺序上验证码不过就根本不查数据库这样机器人拿字典撞密码时数据库压力被挡在第一道闸门外。equalsIgnoreCase 是对大小写容错用户输入 qwer 和 QWER 都能过。这里还有请求转发与重定向的关键差异验证码错、用户名错这些失败场景用的是 request.setAttribute forward因为 forward 是服务器内部跳转request 对象存活login.jsp 里才能用 ${requestScope.login_error} 取到提示。而登录成功用 sendRedirect这是第二次请求request 里的数据全没了所以用户名要放 sessionsuccess.jsp 里从 session 取。很多新手在 success.jsp 里写 ${requestScope.user} 拿不到值根因就在这。3.3 注册落库UserDao 的两个方法与主键冲突链路UserDao 登录方法用 JdbcTemplate 的占位符查询这一版是安全的public User login(User loginUser) { String sql select * from user where username ? and password ?; try { return template.queryForObject(sql, new BeanPropertyRowMapperUser(User.class), loginUser.getUsername(), loginUser.getPassword()); } catch (DataAccessException e) { return null; // 查不到记录统一返回 null } }说明queryForObject 查不到记录会抛 EmptyResultDataAccessException属于 DataAccessException 的子类catch 后返回 nullLoginServlet 根据 null 提示用户名或密码错误。这里用 ? 占位符JdbcTemplate 内部走 PreparedStatement登录查询不存在 SQL 注入问题。注册方法就有隐患了原文是拿 Statement 拼 SQLpublic boolean add(User user) { String sql insert into user (username,password) VALUES( user.getUsername() , user.getPassword() ); int num 0; try { Connection conn JDBCUtils.getConnection(); Statement state conn.createStatement(); num state.executeUpdate(sql); } catch (SQLException e) { e.printStackTrace(); } return num 0; }逻辑说明用户名或密码里带单引号SQL 结构就会被破坏这是典型的拼接注入点。代码能跑通是因为注册失败时 SQLException 被 catch 住返回 falseRegisterServlet 再提示账号已注册。主键冲突正是通过这个异常被转成业务提示的——这条隐藏链路是原代码的精髓但拼接 SQL 的方式我建议立刻改成带参写法避坑章节会给出具体改法。RegisterServlet 整体是个薄壳取参数、封装 User、调 add、按布尔结果往 request 塞提示、forward 回 register.jsp。register.jsp 的 form 没有验证码这是简化处理如果你想严格一点注册页也应该加做法和登录页完全一样。3.4 前端页面的两个细节错误回显与成功页取数login.jsp 的布局是朴素的 table 表格四个控件用户名、密码、验证码输入框、验证码图片。错误提示用两个 div 承接${requestScope.login_error} 和 ${requestScope.checkcode_error}。注册按钮用 window.open 新开一个 register.jsp 窗口这种写法在学习项目里很常见但也容易在窗口拦截器上出幺蛾子正式项目更推荐在当前页面跳转。注意原代码把密码框写成了 input typetext这是明显的问题——密码明文可见部署给别人看的时候一定要改成 typepassword换一个属性值而已。成功页更简单整页就一句欢迎语h1%request.getSession().getAttribute(user)%,欢迎您/h1说明这里用 session 取用户名而不是 EL 表达式正是因为重定向导致 request 失效。原代码里注释掉的那行 ${requestScope.user} 是典型的错误示范留着注释反而是个很好的教学点——看到这句就该意识到重定向后request 已经不是原来那个 request 了。4. 避坑指南五个必踩的坑与对应的排查方法这一章全是拆这份代码时实打实踩过的坑每条按现象、原因、解决三个步骤写照着排查能少走很多弯路。4.1 验证码明明输对了还是提示验证码错误现象验证码图片能正常显示输入看起来也没问题但提交后一直报验证码错误。原因排查下来九成是两个情况。一是 session key 不一致CheckCodeServlet 存的是 checkcode_sessionLoginServlet 取的时候手滑打成别的名字字符串一比对肯定失败。二是验证码的一次性机制——只要点过一次登录session 里的验证码就被 removeAttribute 删了用户没刷新图片、直接拿旧验证码再提交必然报错。解决把验证码的 key 定义成常量在项目里共用避免拼写漂移。前端在登录失败后自动触发一次 img.click() 刷新图片让用户拿新验证码输入体验会好很多。4.2 登录成功跳转后success.jsp 显示 null现象数据库里账号密码都对登录页也跳转了但成功页显示null,欢迎您。原因登录成功分支里写的是 response.sendRedirect这是第二次请求request.setAttribute 放进去的用户名早就随第一次请求销毁了。如果 success.jsp 里用的是 ${requestScope.user}拿到的必然是 null。解决跨请求传登录态用 session登录成功后 session.setAttribute(user, username)success.jsp 从 session 取值。退出登录时记得调用 session.invalidate() 或 removeAttribute(user)否则浏览器关掉再开、session 还在直接就能进成功页。4.3 用户名里带单引号注册直接报错甚至被注入现象注册一个用户名为 ab 的账号页面报 SQL 语法异常用 or 11 这类字符串测试登录查询可能绕过正常逻辑。原因UserDao.add 用 Statement 拼接字符串单引号破坏了 SQL 结构。登录方法因为有占位符是安全的但注册方法暴露了完整的 SQL 拼接风险这是整套代码里最需要改的隐患。解决改成带参的 updateJdbcTemplate 内部走 PreparedStatementpublic boolean add(User user) { String sql insert into user (username,password) values(?,?); try { int num template.update(sql, user.getUsername(), user.getPassword()); return num 0; } catch (DataAccessException e) { return false; // 主键冲突或 SQL 异常都走这里 } }逻辑说明update 返回受影响行数用户名重复时主键冲突抛 DuplicateKeyException它是 DataAccessException 的子类catch 住返回 falseRegisterServlet 的账号已注册提示依然成立业务链路不用动。4.4 中文用户名存进 MySQL 变成 ???现象注册页填中文Navicat 里看是问号登录时怎么都匹配不上。原因字符集乱在了一层或多层。JSP 页面本身没声明 UTF-8、Servlet 里没调 setCharacterEncoding、JDBC URL 没带 characterEncoding、或者表字符集建成了 latin1。还要注意 request.setCharacterEncoding 只对 POST 请求生效GET 请求的乱码得改 Tomcat 的 URIEncoding四层错一层就乱。解决JSP 顶部写 % page contentTypetext/html;charsetUTF-8 %Servlet 里第一行 request.setCharacterEncoding(utf-8)JDBC URL 加 characterEncodingutf8建表用 DEFAULT CHARSETutf8mb4比 utf8 更全支持 emoji。这四个地方对齐后中文就不会再出问题。4.5 部署到 Tomcat 一直 404/daydayup 路径对不上现象访问 http://localhost:8080/daydayup/login.jsp 报 404或者 form action 提交后 404。原因IDEA 部署时项目上线文路径不是 daydayup而是默认的 /javaweb_war_exploded 之类或者部署的是改名后的工程导致 JSP 里的 /daydayup 前缀和表单 action 全部失配。解决IDEA 里 Run → Edit Configurations → Deployment 标签把 Application context 改成 /daydayup所有带 /daydayup 前缀的路径统一下来。更稳的做法是用 request.getContextPath() 动态拼比如 response.sendRedirect(request.getContextPath() /success.jsp)这样无论工程名改成什么都能对上。改完配置记得 clean 一遍再重启 Tomcat别拿热部署的旧状态排查。5. 验证与进阶跑通之后还需要改的四个地方5.1 三分钟自测清单项目部署完我习惯按下面这张表把全链路过一遍任何一步不符预期都说明有个环节没理解透步骤操作预期结果1访问 /daydayup/login.jsp登录页显示验证码图片2不输验证码直接点登录页面提示验证码错误3点击验证码图片图片换成一张新图4输正确验证码、错密码提示用户名或密码错误5注册新账号提示注册成功6重复注册同名账号提示账号已注册7用新账号登录跳转 success.jsp 并显示用户名8退出后直接访问 success.jsp仍能显示用户名说明 session 登录态没清理第 8 步也许出乎意料——它恰恰说明 session 的登录态没有被清理正式项目必须在退出时把 session 作废。5.2 值得优先改的四个点第一注册方法改成带参 update顺手把密码从明文改成 BCrypt 哈希登录时校验哈希而不是比对原文。第二给验证码加过期时间session 里同时存生成时刻校验时超过 60 秒直接作废session.setAttribute(checkcode_time, System.currentTimeMillis()); // 登录校验时判断 long age System.currentTimeMillis() - (Long) session.getAttribute(checkcode_time); if (age 60 * 1000) { // 提示验证码过期需要刷新图片 }第三登录加失败次数限制连续错 5 次就锁定 15 分钟这个状态可以先放 session以后有需要再换 Redis。第四配置 Druid 的监控过滤器访问 /druid 页面看连接池的活跃连接数再回头调 maxActive——这套代码规模小但监控习惯值钱。这套自测清单后来被我固化成了接手登录注册项目的固定动作先手点把正常流程走一遍再用错验证码、错密码、重复用户名三个异常路径各打一轮最后去数据库看一眼落库数据。从那以后我每次写完登录注册都强制走一遍这套流程表面上是验证功能实际上是在确认自己对 session 生命周期、转发与重定向、异常转业务提示这三件事的理解没有偷懒。希望帮到你。本文还有配套的精品资源点击获取
返回列表