简介:这份资源是一套基于JavaWeb技术栈实现用户登录与注册功能的完整项目源码,面向正在学习JSP、Servlet与MySQL数据库开发的初学者及课程实践者,帮助理解Web应用中用户认证模块的完整实现流程。压缩包共34个文件,约3.56MB,包含7个Java源文件与7个编译后的class文件、3个JSP页面、2个XML配置、2个properties配置以及SQL脚本、jar依赖和前端JS等,覆盖从视图层到数据访问层的核心代码。项目围绕JSP构建登录注册表单界面,通过Servlet处理请求并借助JDBC连接MySQL完成用户信息校验与存储,同时涉及session状态保持、密码加密及参数化查询防注入等安全处理思路。目前已有3049人学习下载,适合对照源码梳理项目结构、理解请求响应流程,并在此基础上扩展错误提示与用户体验优化。
1. 从零手搓一套 JSP + JavaBean + MySQL 登录注册:这套源码到底能帮你省下多少时间
如果你正在搜「javaweb项目完整案例mysql」,大概率不是想听 Servlet 生命周期这种教科书内容,而是想找一套能直接跑起来、能改、能交作业或者能当项目脚手架的东西。这套基于 JavaWeb 实现的登录及注册功能,技术栈就是最经典的 JSP + Servlet + JavaBean + MySQL,没有 Spring 那一层封装,也没有 Maven 多模块的复杂度,打开 IDEA 配好 Tomcat 就能跑。它解决的核心问题很具体:用户表设计、前端表单校验、后端参数接收、数据库增删改查、Session 状态保持、登录失败提示,这一整条链路全部打通。适合谁?刚学完 JSP 语法但不知道怎么串成完整项目的人,需要交课程设计但不想从零搭环境的人,以及想回头补一补原生 Servlet 请求流转细节的开发者。这套东西不高级,但它把「一个请求从浏览器到数据库再回来」的每一步都摊开给你看了,这种透明感在 Spring 全家桶里反而很难获得。
2. 环境搭建与数据库建表:别让第一步就翻车
2.1 JDK、Tomcat、MySQL 的版本咬合关系
这套项目对版本不算挑剔,但有几个组合是经过验证比较稳的:JDK 8 或 JDK 11,Tomcat 8.5 或 9.0,MySQL 5.7 或 8.0。我一般会推荐 JDK 8 + Tomcat 8.5 + MySQL 5.7 这个组合,原因是网上大部分 JavaWeb 老教程都基于这个环境,你遇到问题时搜到的答案匹配度最高。如果你用 JDK 17 配 Tomcat 10,Servlet 的包名从javax.servlet变成了jakarta.servlet,这套源码里的 import 全部要改,新手很容易在这里卡住。
IDEA 里配置 Tomcat 的步骤:打开 Run/Debug Configurations,点左上角加号选 Tomcat Server → Local,在 Server 标签页里找到 Tomcat 安装目录,在 Deployment 标签页点加号选 Artifact,把项目的 war exploded 加进去。Application context 建议设成/login,这样浏览器访问路径就是http://localhost:8080/login/。
注意:IDEA 社区版没有 Tomcat Server 选项,这是很多人第一次就卡住的地方。社区版只能用 Smart Tomcat 插件或者手动部署到 Tomcat 的 webapps 目录。
2.2 用户表设计:字段不多,但每个都有讲究
数据库建表看起来简单,但字段类型和约束如果一开始没设对,后面写代码时会出现各种玄学问题。下面是我一般会用的建表语句:
CREATE DATABASE IF NOT EXISTS javaweb_login DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE javaweb_login; CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增', `username` VARCHAR(50) NOT NULL COMMENT '用户名,登录账号', `password` VARCHAR(64) NOT NULL COMMENT '密码,建议存MD5值', `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱,注册时可选填', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';这段 SQL 里有三个点值得展开说。第一,字符集用utf8mb4而不是utf8,因为 MySQL 的utf8其实是阉割版,存不了 emoji 和部分生僻字,注册时用户名里带特殊字符就会报错。第二,username上加了唯一索引uk_username,这是防止重复注册的第一道防线,但代码里仍然要在插入前查一次,因为靠数据库报错来提示用户「用户名已存在」体验很差。第三,password字段长度给了 64,因为如果你用 MD5 存密码,32 位十六进制字符串刚好放得下,如果后面想升级成 SHA-256 也不用改表结构。
2.3 项目目录结构与依赖 jar 包
原生 JavaWeb 项目没有 Maven 帮你管依赖,所有 jar 包要手动放进WEB-INF/lib目录。这套登录注册功能需要的 jar 包不多,核心就是 MySQL 驱动:
项目根目录/ ├── src/ │ └── com/ │ └── login/ │ ├── dao/ # 数据访问层 │ │ └── UserDao.java │ ├── entity/ # 实体类 │ │ └── User.java │ ├── servlet/ # 控制层 │ │ ├── LoginServlet.java │ │ └── RegisterServlet.java │ └── util/ # 工具类 │ └── DBUtil.java ├── web/ │ ├── WEB-INF/ │ │ ├── lib/ │ │ │ └── mysql-connector-java-8.0.28.jar │ │ └── web.xml │ ├── css/ │ │ └── style.css │ ├── login.jsp │ ├── register.jsp │ └── success.jspMySQL 驱动版本要和你装的 MySQL 服务端版本匹配。MySQL 5.7 用mysql-connector-java-5.1.x,MySQL 8.0 用mysql-connector-java-8.0.x。用错了会在启动时报No suitable driver found或者连接时抛Communications link failure。把 jar 包放进WEB-INF/lib之后,还要在 IDEA 的 Project Structure → Modules → Dependencies 里确认这个目录被加进了依赖,否则编译时能找到类但运行时找不到。
3. 从表单到数据库:登录注册的完整请求链路拆解
3.1 前端 JSP 页面:表单提交的细节决定后端要不要擦屁股
登录和注册页面看起来就是两个表单,但表单的method、action和输入框的name属性如果写错,后端收到的就是 null。先看登录页面的核心代码:
<!-- login.jsp --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>用户登录</title> </head> <body> <h2>登录</h2> <!-- 表单提交到 LoginServlet,用 POST 方法 --> <form action="LoginServlet" method="post"> <label>用户名:</label> <input type="text" name="username" required><br> <label>密 码:</label> <input type="password" name="password" required><br> <input type="submit" value="登录"> <a href="register.jsp">没有账号?去注册</a> </form> <!-- 登录失败时显示错误信息 --> <p style="color:red;">${requestScope.msg}</p> </body> </html>这里有几个容易忽略的点。action="LoginServlet"用的是相对路径,实际请求地址是/login/LoginServlet,这个映射关系要在web.xml或者@WebServlet注解里配好。method="post"是必须的,用 get 的话密码会出现在 URL 里,既不安全也不符合基本规范。两个 input 的name属性值username和password必须和 Servlet 里request.getParameter()的参数名完全一致,大小写都不能差。required是 HTML5 自带的非空校验,能挡住一部分空提交,但不能替代后端校验,因为用户可以禁用 JavaScript 或者直接构造请求绕过。
注册页面比登录多一个确认密码和邮箱字段,结构类似,但多了一个前端校验的逻辑:
<!-- register.jsp 核心部分 --> <form action="RegisterServlet" method="post" onsubmit="return checkForm()"> <label>用户名:</label> <input type="text" name="username" id="username" required><br> <label>密 码:</label> <input type="password" name="password" id="password" required><br> <label>确认密码:</label> <input type="password" name="confirmPassword" id="confirmPassword" required><br> <label>邮 箱:</label> <input type="email" name="email"><br> <input type="submit" value="注册"> </form> <script> function checkForm() { var pwd = document.getElementById("password").value; var confirmPwd = document.getElementById("confirmPassword").value; if (pwd !== confirmPwd) { alert("两次输入的密码不一致"); return false; // 阻止表单提交 } if (pwd.length < 6) { alert("密码长度不能少于6位"); return false; } return true; } </script>onsubmit="return checkForm()"这个写法里,return false会阻止表单提交,return true才放行。如果只写checkForm()不写return,那么无论函数返回什么,表单都会提交,这是很多人踩过的坑。前端校验的目的是提升用户体验,减少无效请求,但后端必须再校验一遍,因为前端代码在浏览器里,用户想改就能改。
3.2 后端 Servlet 处理:参数接收、业务判断、页面跳转
登录 Servlet 是整个链路的核心,它要做四件事:接收参数、调用 DAO 查数据库、判断结果、决定跳转。先看代码:
// LoginServlet.java @WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置请求和响应的编码,防止中文乱码 request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); // 1. 接收参数 String username = request.getParameter("username"); String password = request.getParameter("password"); // 2. 后端非空校验(前端校验可被绕过) if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { request.setAttribute("msg", "用户名和密码不能为空"); request.getRequestDispatcher("login.jsp").forward(request, response); return; } // 3. 调用 DAO 查询用户 User user = userDao.findByUsernameAndPassword(username, password); // 4. 判断结果并跳转 if (user != null) { // 登录成功,把用户信息存入 Session request.getSession().setAttribute("currentUser", user); response.sendRedirect("success.jsp"); } else { // 登录失败,转发回登录页并带错误提示 request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } } }这段代码里有几个关键决策点。request.setCharacterEncoding("UTF-8")必须放在所有getParameter()之前,否则不生效,这是 Servlet 规范决定的。response.setContentType("text/html;charset=UTF-8")是告诉浏览器用什么编码解析返回的 HTML。登录成功用sendRedirect而不是forward,是因为重定向会改变浏览器地址栏,用户刷新页面不会重复提交表单;登录失败用forward,是因为需要把错误信息通过request域带到 JSP 页面上显示。request.getSession()会自动创建 Session 对象,把 User 对象存进去之后,其他页面可以通过session.getAttribute("currentUser")判断用户是否已登录。
注册 Servlet 的逻辑比登录多一步「查重」:
// RegisterServlet.java 核心逻辑 @WebServlet("/RegisterServlet") public class RegisterServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); String email = request.getParameter("email"); // 先查用户名是否已存在 if (userDao.findByUsername(username) != null) { request.setAttribute("msg", "用户名已被注册"); request.getRequestDispatcher("register.jsp").forward(request, response); return; } // 构建 User 对象并插入数据库 User user = new User(); user.setUsername(username); user.setPassword(password); // 实际项目应存 MD5 值 user.setEmail(email); boolean success = userDao.insert(user); if (success) { request.setAttribute("msg", "注册成功,请登录"); request.getRequestDispatcher("login.jsp").forward(request, response); } else { request.setAttribute("msg", "注册失败,请稍后重试"); request.getRequestDispatcher("register.jsp").forward(request, response); } } }注册成功后转发到登录页而不是直接跳转到 success 页,是因为注册只是创建了账号,还没有建立登录态。如果注册后直接进主页,Session 里没有用户信息,后续操作会出问题。
3.3 DAO 层与数据库连接:JDBC 工具类的封装与资源释放
DAO 层负责和数据库打交道,这套项目里我一般会把 JDBC 连接封装成一个工具类,避免在每个方法里重复写驱动加载和连接获取:
// DBUtil.java public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/javaweb_login" + "?useUnicode=true&characterEncoding=utf8&useSSL=false" + "&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "你的数据库密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 释放资源,顺序不能错:ResultSet → Statement → Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }连接 URL 里的参数每一个都有作用。useUnicode=true&characterEncoding=utf8保证中文不乱码。useSSL=false关掉 SSL 警告,本地开发用不上。serverTimezone=Asia/Shanghai是 MySQL 8.0 必须加的,不加会报时区错误。驱动类名com.mysql.cj.jdbc.Driver是 MySQL 8.0 的写法,5.1 版本用com.mysql.jdbc.Driver。
UserDao 里两个核心方法:
// UserDao.java public class UserDao { // 登录查询:根据用户名和密码查用户 public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT * FROM user WHERE username = ? AND password = ?"; Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); rs = ps.executeQuery(); if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setEmail(rs.getString("email")); return user; } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(conn, ps, rs); } return null; } // 注册插入:新增一条用户记录 public boolean insert(User user) { String sql = "INSERT INTO user(username, password, email) VALUES(?, ?, ?)"; Connection conn = null; PreparedStatement ps = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } finally { DBUtil.close(conn, ps, null); } } }用PreparedStatement而不是Statement拼接字符串,是为了防止 SQL 注入。如果写成"SELECT * FROM user WHERE username = '" + username + "'",用户在用户名输入框里填' OR '1'='1,就能绕过密码验证直接登录。PreparedStatement的?占位符会把参数当作纯字符串处理,不会解析成 SQL 语法。
4. 避坑与排查:这套登录注册跑不起来时先看这几个地方
4.1 中文乱码:POST 和 GET 的处理方式不一样
现象:注册时输入中文用户名,存进数据库变成???,或者登录时提示「用户名或密码错误」但数据库里明明有这个用户。
原因:POST 请求的乱码可以通过request.setCharacterEncoding("UTF-8")解决,但 GET 请求的参数在 URL 里,Tomcat 8.5 之前默认用 ISO-8859-1 解码,setCharacterEncoding对 GET 无效。
解决:Tomcat 8.5 及以上版本在conf/server.xml的 Connector 标签里加了URIEncoding="UTF-8"默认配置,GET 乱码基本不会出现。如果用的是更老的 Tomcat,要么改 server.xml,要么在代码里手动转码:new String(username.getBytes("ISO-8859-1"), "UTF-8")。数据库连接 URL 里的characterEncoding=utf8也要确认加上。
4.2 ClassNotFoundException:驱动 jar 包放了但没生效
现象:启动 Tomcat 后访问登录页面,提交表单时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。
原因:jar 包虽然放进了WEB-INF/lib,但 IDEA 的项目结构里这个目录没有被识别为依赖库,编译时能找到类(因为 IDE 索引了),运行时 Tomcat 的类加载器找不到。
解决:打开 Project Structure → Modules → Dependencies,点加号选 JARs or directories,把WEB-INF/lib目录加进去,Scope 选 Provided 或 Compile。另外确认 Artifact 里WEB-INF/lib下的 jar 包被包含进了输出布局,否则部署到 Tomcat 的 war 包里没有这些 jar。
4.3 表单提交后 404:Servlet 映射路径对不上
现象:点击登录按钮后浏览器显示 404,地址栏是http://localhost:8080/login/LoginServlet。
原因:@WebServlet("/LoginServlet")注解里的路径和表单action里的路径不一致,或者web.xml里配了<url-pattern>但注解和 xml 同时存在导致冲突。
解决:确认@WebServlet的值和 form 的 action 完全一致,包括大小写。如果项目里同时有web.xml和注解,检查web.xml的metadata-complete属性,如果是true,注解会被忽略,只认 xml 配置。建议二选一,不要混用。
4.4 登录成功但刷新页面又回到登录页:Session 没设对
现象:登录后跳转到 success.jsp 正常显示,但按 F5 刷新后跳回了登录页。
原因:success.jsp 里判断登录态的代码写错了,比如用了request.getAttribute("currentUser")而不是session.getAttribute("currentUser"),request 域在一次请求结束后就失效了。
解决:在 success.jsp 开头加判断:
<% Object currentUser = session.getAttribute("currentUser"); if (currentUser == null) { response.sendRedirect("login.jsp"); return; } %>同时确认 LoginServlet 里存的是request.getSession().setAttribute("currentUser", user),key 的名字两边要一致。
4.5 MySQL 8.0 时区报错:The server time zone value is unrecognized
现象:连接数据库时抛异常java.sql.SQLException: The server time zone value '?D1ú±ê×?ê±??' is unrecognized。
原因:MySQL 8.0 的驱动要求明确指定时区,不指定就会用服务器默认时区,中文 Windows 系统下会返回乱码的时区名。
解决:在 JDBC URL 里加serverTimezone=Asia/Shanghai,或者改成serverTimezone=GMT%2B8。如果用的是 MySQL 5.7 驱动连 8.0 服务端,也建议加上这个参数。
5. 密码加密与登录态校验:从能跑到能用的最后一步
5.1 用 MD5 给密码加一层,别让数据库裸奔
前面 DAO 层代码里密码是明文存的,这在教学项目里常见,但如果你要把这套代码用到任何真实场景,必须改。最简单的做法是注册时对密码做 MD5 摘要,登录时对输入的密码做同样处理再比对:
// MD5Util.java public class MD5Util { public static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("MD5算法不可用", e); } } }注册时把user.setPassword(password)改成user.setPassword(MD5Util.md5(password)),登录时把userDao.findByUsernameAndPassword(username, password)改成userDao.findByUsernameAndPassword(username, MD5Util.md5(password))。这样数据库里存的就是 32 位十六进制字符串,即使数据库被拖库,攻击者也不能直接拿到明文密码。
注意:MD5 已经不是安全的密码哈希算法了,彩虹表可以反查常见密码。真实项目应该用 BCrypt 或 Argon2,但在这个教学项目里,MD5 的目的是让你理解「密码不能明文存」这件事,而不是提供一个生产级方案。
5.2 登录拦截:别让未登录用户直接访问 success.jsp
现在 success.jsp 虽然加了 Session 判断,但每个需要登录才能访问的页面都要写一遍太麻烦。更常见的做法是用一个 Filter 统一拦截:
// LoginFilter.java @WebFilter("/*") public class LoginFilter implements Filter { // 不需要登录就能访问的路径 private static final String[] WHITE_LIST = {"/login.jsp", "/register.jsp", "/LoginServlet", "/RegisterServlet", "/css/"}; @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String path = request.getRequestURI().substring(request.getContextPath().length()); // 白名单路径直接放行 for (String white : WHITE_LIST) { if (path.startsWith(white)) { chain.doFilter(req, resp); return; } } // 非白名单路径检查 Session Object user = request.getSession().getAttribute("currentUser"); if (user != null) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() + "/login.jsp"); } } }@WebFilter("/*")表示拦截所有请求,白名单里的路径直接放行,其余路径检查 Session 里有没有currentUser。这个 Filter 的坑在于静态资源也会被拦截,所以 css、js、图片目录都要加进白名单,否则登录页面样式会丢失。
5.3 验证这套代码是否真的跑通了
部署到 Tomcat 之后,按这个顺序走一遍:访问http://localhost:8080/login/register.jsp,填一个没注册过的用户名和密码,提交后应该跳回登录页并提示「注册成功,请登录」。打开数据库执行SELECT * FROM user,确认多了一条记录且密码字段是 MD5 值。回到登录页,用刚才的账号登录,应该跳转到 success.jsp 并显示用户名。直接访问http://localhost:8080/login/success.jsp,如果 Filter 配好了,应该被重定向回登录页。最后试一下用错误的密码登录,确认提示「用户名或密码错误」而不是 500 错误页。
从那以后我每次拿到一套 JavaWeb 老项目,都会先确认三件事:JDBC URL 里有没有serverTimezone,WEB-INF/lib有没有被加进 Artifact 输出,Filter 白名单有没有漏掉静态资源目录。这三步走完,百分之八十的「跑不起来」问题都能定位到。希望帮到你。
本文还有配套的精品资源,点击获取