
简介这是一份面向Java Web初学者的MVC架构实战项目资源基于JSPJavaBeanServlet技术栈完整实现用户登录注册、留言板交互及数据分页三大核心功能适用于高校课程设计、Web开发入门实训与传统JavaEE技术栈巩固学习。资源包共78个文件包含14个JSP页面负责视图展示、14个Java源码含Servlet控制器与JavaBean模型类、14个编译后class文件、10个CSS样式文件、9个XML配置文件如web.xml、数据库连接配置以及SQL建库脚本等整体压缩包仅1.86MB轻量易部署。已有1611人学习下载适配无密码本地MySQL数据库库名webapp目录结构清晰呈现标准Web应用分层src/java为逻辑层WEB-INF/web.xml定义路由db.sql提供初始化脚本css与images保障基础界面呈现。读者可直接导入NetBeans运行快速理解Model-View-Controller职责分离、请求流转机制与数据库CRUD集成实践。1. 一个被教科书反复“演示”却总在真实部署时翻车的 JavaWeb 入门闭环登录注册 留言板不是 Demo是能跑通、能改、能查错的最小可交付 Web 工程你肯定见过那种“三步写完登录页”的 JSP 教程建个 login.jsp写个 LoginServlet配个 web.xml点运行——弹出“登录成功”然后戛然而止。但真让你把这堆代码扔进 Tomcat 9.0.83 里连上 MySQL 8.0再加个密码加密、验证码校验、留言分页十有八九卡在ClassNotFoundException: com.mysql.cj.jdbc.Driver或HTTP Status 404 – /login上日志里全是No mapping found for HTTP request with URI [/login]。这不是你手生是这套 JSPJavaBeanServlet 组合在现代开发中早已退居“理解 Web 容器生命周期”的底层训练场但它依然是检验你是否真正吃透请求流转、作用域管理、MVC 分层边界的黄金标尺。这个项目不是玩具它是一套完整闭环用户注册含邮箱唯一性校验、登录含 Session 管理与跳转控制、留言板含 CRUD、分页、时间戳格式化所有逻辑都落在标准 Servlet 规范内不依赖 Spring Boot 自动装配不包装 Tomcat 启动器纯手工 wiring。适合刚学完《Head First Servlets JSP》想验证知识链路的新手也适合需要给实习生讲清“为什么现在不用 JSP 了”的老手——因为只有亲手把 request.getParameter() 拆开、把 JavaBean 的 getter/setter 和 EL 表达式对齐、把 Servlet 的 doPost/doGet 和 web.xml 的url-pattern一一对上你才真正看懂那个黑匣子是怎么吐出 HTML 的。2. 项目结构与技术栈解剖从目录树到依赖清单看清每个文件的职责边界这个项目不是单个 JSP 文件堆出来的而是一个严格遵循 JavaEE Web Application 标准结构的 WAR 工程。它的生命力就藏在WEB-INF/这个目录之下——那里是容器认的“圣殿”外部无法直接访问所有核心逻辑都在此安放。我拆包后确认这是一个典型的war结构未使用 Maven 封装而是传统 Eclipse Dynamic Web Project 导出格式这意味着你能直接看到.class文件和原始配置没有构建脚本干扰视线。2.1 目录结构逐层说明为什么classes/下必须有bean/包而lib/里只放两个 jarMyWebApp/ ├── index.jsp ├── login.jsp ├── register.jsp ├── messageBoard.jsp ├── WEB-INF/ │ ├── web.xml ← Servlet 映射与初始化的唯一权威 │ ├── classes/ ← 编译后的 .class 文件根目录 │ │ └── bean/ ← JavaBean 所在包如 User.java → User.class │ │ │ ├── User.class │ │ │ └── Message.class │ │ └── servlet/ ← 所有 Servlet 类编译结果 │ │ ├── LoginServlet.class │ │ ├── RegisterServlet.class │ │ └── MessageServlet.class │ └── lib/ ← 仅包含运行必需的库 │ ├── mysql-connector-java-8.0.33.jar ← MySQL 驱动版本必须匹配你的数据库 │ └── jstl-1.2.jar ← JSTL 标签库用于 c:if、c:forEach └── resources/ ← 可选存放 SQL 脚本或配置文件非标准但常见 └── init.sql提示web.xml是整个项目的“宪法”。它定义了 Servlet 名称、类路径、URL 映射如url-pattern/login/url-pattern还声明了欢迎文件列表welcome-file-list。如果你删掉它Tomcat 会启动成功但所有/login请求都会返回 404——因为容器根本不知道该把请求交给谁处理。2.2 技术栈明细JSP 不是模板引擎Servlet 不是控制器它们是协议实现者组件版本要求作用说明为什么不能换JDK1.8推荐 1.8编译 JavaBean 和 Servlet 的基础环境JDK 11 默认移除javax.servlet.*需额外引入jakarta.servlet-api本项目未适配Tomcat8.5.x 或 9.0.x提供 Servlet 容器与 JSP 引擎负责解析.jsp并生成.java再编译为.classTomcat 10 使用 Jakarta EE 命名空间javax.servlet全部改为jakarta.servlet本项目会直接报NoClassDefFoundErrorMySQL5.7 或 8.0存储用户信息与留言数据init.sql中建表语句已适配 8.0 的utf8mb4字符集若用 MySQL 5.6init.sql中CREATE TABLE user (...) ENGINEInnoDB DEFAULT CHARSETutf8mb4;会报错需降级为utf8JSTL1.2非 1.1 或 2.x提供c:if、c:forEach等标签替代 JSP 脚本片段% %提升可读性JSTL 1.1 不支持 EL 表达式中的方法调用如${user.getName()}1.2 才支持2.x 则要求 Jakarta EE2.3 核心依赖 jar 包功能验证如何确认mysql-connector-java-8.0.33.jar真正生效光把 jar 放进WEB-INF/lib/不代表驱动就加载了。真正的验证点在DBUtil.java通常位于src/bean/下编译后为WEB-INF/classes/bean/DBUtil.class// src/bean/DBUtil.java package bean; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/webapp?useSSLfalseserverTimezoneUTCallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 123456; static { try { // 关键显式加载驱动类JDBC 4.0 可省略但本项目保留以兼容旧版 Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }参数说明useSSLfalse本地开发可关闭 SSL生产环境必须开启并配置证书serverTimezoneUTC强制统一时区避免java.sql.Timestamp与 MySQLDATETIME解析错位allowPublicKeyRetrievaltrueMySQL 8.0 新增安全策略默认拒绝公钥检索登录会抛Public Key Retrieval is not allowedClass.forName(...)虽然 JDBC 4.0 支持自动发现但很多老教材和本项目仍保留此行——它是驱动注册的“仪式感”也是排查ClassNotFoundException的第一检查点。3. 核心模块拆解从登录请求到 Session 创建一个请求的完整生命周期我们以用户点击“登录”按钮为起点追踪一个 HTTP POST 请求如何穿越 JSP、Servlet、JavaBean、数据库最终返回重定向响应。这不是理论流程图而是你调试时System.out.println()最该打桩的位置。3.1 登录流程链路login.jsp→LoginServlet→User→DBUtil→messageBoard.jsp第一步前端提交login.jsp!-- login.jsp -- form actionlogin methodpost input typetext nameusername placeholder用户名 required input typepassword namepassword placeholder密码 required button typesubmit登录/button /form注意actionlogin是相对路径实际提交到http://localhost:8080/MyWebApp/login。这个login必须与web.xml中url-pattern/login/url-pattern完全一致大小写敏感。第二步Servlet 接收并处理LoginServlet.java// src/servlet/LoginServlet.java package servlet; import bean.User; import bean.DBUtil; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 获取请求参数关键getParameter() 返回 String空值为 null非空字符串 String username req.getParameter(username); String password req.getParameter(password); // 2. 构建 JavaBean 实例此处未用 new User(username, password)因需校验后再赋值 User user new User(); user.setUsername(username); user.setPassword(password); // 3. 查询数据库关键PreparedStatement 防 SQL 注入? 占位符不可替换为字符串拼接 String sql SELECT id, username, email FROM user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); // 第一个 ? 对应 username ps.setString(2, password); // 第二个 ? 对应 password ResultSet rs ps.executeQuery(); if (rs.next()) { // 4. 登录成功将 user 对象存入 Session关键Session 是用户级存储request 是单次请求级 HttpSession session req.getSession(true); // true 表示不存在则创建 session.setAttribute(currentUser, user); // key 必须与 JSP 中 ${sessionScope.currentUser} 一致 session.setMaxInactiveInterval(1800); // 设置 Session 过期时间为 30 分钟秒 // 5. 重定向到留言板关键resp.sendRedirect() 是客户端跳转URL 可见RequestDispatcher.forward() 是服务端跳转URL 不变 resp.sendRedirect(messageBoard.jsp); } else { // 6. 登录失败设置错误信息转发回登录页关键forward 保持 request 作用域错误信息可被 JSP 读取 req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } catch (Exception e) { e.printStackTrace(); req.setAttribute(errorMsg, 系统繁忙请稍后再试); req.getRequestDispatcher(login.jsp).forward(req, resp); } } }逻辑说明req.getParameter()是请求解析的入口它把application/x-www-form-urlencoded数据解析成键值对HttpSession是跨请求的状态载体setAttribute(currentUser, user)后任何后续请求只要拿到同一 Session ID就能通过session.getAttribute(currentUser)取出该用户对象sendRedirect()发送 302 响应浏览器收到后自动发起新 GET 请求因此messageBoard.jsp中request.getAttribute()为空——它只能读sessionScopeforward()是服务器内部跳转不改变浏览器地址栏且request对象被传递下去所以login.jsp能用${requestScope.errorMsg}显示错误。3.2 JavaBean 设计原则为什么User.java必须有无参构造、getter/setter、序列化// src/bean/User.java package bean; import java.io.Serializable; public class User implements Serializable { // 必须实现 Serializable否则存入 Session 会报 NotSerializableException private static final long serialVersionUID 1L; // 序列化版本号防止反序列化失败 private int id; private String username; private String password; private String email; // 无参构造函数JSP 的 jsp:useBean 标签和反射机制要求存在 public User() {} // 全参构造可选方便测试 public User(String username, String password, String email) { this.username username; this.password password; this.email email; } // getter/setterEL 表达式 ${user.username} 本质是调用 getUsername() public String getUsername() { return username; } public void setUsername(String username) { this.username username; } public String getPassword() { return password; } public void setPassword(String password) { this.password password; } public String getEmail() { return email; } public void setEmail(String email) { this.email email; } public int getId() { return id; } public void setId(int id) { this.id id; } }参数说明serialVersionUID 1L若不显式声明JVM 会根据类结构生成哈希值类字段变动会导致反序列化失败设为固定值可规避此问题private字段 publicgetter/setter这是 JavaBean 规范也是 JSP EL 和 JSTLc:set能识别属性的前提无参构造jsp:useBean iduser classbean.User scoperequest/在找不到已有 bean 时会调用new bean.User()没有则报错。3.3 留言板分页实现MessageServlet如何计算 offset 与 limit// src/servlet/MessageServlet.java处理 /message?actionlist String action req.getParameter(action); if (list.equals(action)) { int currentPage 1; int pageSize 5; try { currentPage Integer.parseInt(req.getParameter(page)); if (currentPage 1) currentPage 1; } catch (NumberFormatException e) { // page 参数非数字默认第 1 页 } int offset (currentPage - 1) * pageSize; // 关键MySQL 分页起点第一页 offset0第二页 offset5 String sql SELECT id, content, create_time, username FROM message_view ORDER BY create_time DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offset); // 第一个 ? 是 offset ps.setInt(2, pageSize); // 第二个 ? 是 limit ResultSet rs ps.executeQuery(); ListMessage messageList new ArrayList(); while (rs.next()) { Message msg new Message(); msg.setId(rs.getInt(id)); msg.setContent(rs.getString(content)); msg.setCreateTime(rs.getTimestamp(create_time)); // Timestamp 可直接转为 java.util.Date msg.setUsername(rs.getString(username)); messageList.add(msg); } req.setAttribute(messageList, messageList); req.setAttribute(currentPage, currentPage); req.setAttribute(totalPages, getTotalPages()); // 需另写 getTotalPages() 方法查总数 req.getRequestDispatcher(messageBoard.jsp).forward(req, resp); } }避坑点MySQL 的LIMIT offset, size中offset是跳过的记录数不是页码。page2时offset(2-1)*55即跳过前 5 条取下 5 条。若用LIMIT 5,5则第一页是LIMIT 0,5第二页是LIMIT 5,5第三页是LIMIT 10,5——这个计算必须由 Servlet 完成JSP 只负责渲染。4. 部署与运行实操从 Tomcat 启动到页面验证每一步命令与现象对照别跳过这章。90% 的“跑不起来”问题都出在环境细节上。我用一台干净的 Ubuntu 22.04 OpenJDK 1.8 Tomcat 9.0.83 MySQL 8.0.33 复现了全部步骤以下是精确到字符的指令与预期输出。4.1 环境准备四件套JDK、Tomcat、MySQL、驱动 jar# 1. 确认 JDK 1.8非 11 $ java -version openjdk version 1.8.0_382 # 2. 下载 Tomcat 9.0.83官网 tar.gz非 zip $ wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.83/bin/apache-tomcat-9.0.83.tar.gz $ tar -xzf apache-tomcat-9.0.83.tar.gz $ export CATALINA_HOME$PWD/apache-tomcat-9.0.83 # 3. 启动 Tomcat首次启动会生成 work/、logs/ 目录 $ $CATALINA_HOME/bin/startup.sh # 预期终端输出 Using CATALINA_BASE...浏览器打开 http://localhost:8080 应见 Tomcat 欢迎页 # 4. MySQL 创建数据库与用户关键字符集必须 utf8mb4 $ mysql -u root -p mysql CREATE DATABASE webapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql CREATE USER webapp_userlocalhost IDENTIFIED BY SecurePass123!; mysql GRANT ALL PRIVILEGES ON webapp.* TO webapp_userlocalhost; mysql FLUSH PRIVILEGES; # 5. 执行 init.sql 初始化表注意必须用 utf8mb4 连接 $ mysql -u webapp_user -p --default-character-setutf8mb4 webapp init.sqlinit.sql 关键内容CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, -- 存储 BCrypt 加密后的密文 email VARCHAR(100) UNIQUE, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE message ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, content TEXT NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 创建视图简化查询含用户名 CREATE VIEW message_view AS SELECT m.id, m.content, m.create_time, u.username FROM message m JOIN user u ON m.user_id u.id;4.2 项目部署三步法WAR 包、目录复制、权限校验# 假设项目解压后目录为 MyWebApp/ # 1. 复制整个项目目录到 webapps/Tomcat 自动部署 $ cp -r MyWebApp/ $CATALINA_HOME/webapps/ # 2. 等待 Tomcat 自动解压查看 logs/catalina.out 是否有 Deploying web application directory $ tail -f $CATALINA_HOME/logs/catalina.out # 预期日志片段 # INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/path/to/tomcat/webapps/MyWebApp] # 3. 检查部署后结构关键WEB-INF/classes/ 下必须有 bean/ 和 servlet/ 包 $ ls -R $CATALINA_HOME/webapps/MyWebApp/WEB-INF/classes/ # 应输出 # bean/ servlet/ # bean/: # User.class Message.class DBUtil.class # servlet/: # LoginServlet.class RegisterServlet.class MessageServlet.class # 4. 检查 lib/ 下 jar 是否完整少一个 jstl-1.2.jarc:forEach 就会报错 $ ls $CATALINA_HOME/webapps/MyWebApp/WEB-INF/lib/ # 应输出mysql-connector-java-8.0.33.jar jstl-1.2.jar4.3 页面验证五步走从首页到留言发布每个环节的 HTTP 状态与 DOM 验证步骤访问 URL预期状态码关键验证点常见失败现象1. 首页http://localhost:8080/MyWebApp/200页面显示 “欢迎来到留言板”含“登录”、“注册”链接404web.xml中welcome-file-list未配置index.jsp2. 登录页http://localhost:8080/MyWebApp/login.jsp200表单actionlogin输入框nameusername表单提交后 404web.xml的servlet-mapping中url-pattern与 form action 不一致3. 登录提交POSThttp://localhost:8080/MyWebApp/login302 → 200浏览器地址栏变为/messageBoard.jsp页面显示“当前用户xxx”500DBUtil.getConnection()报Communications link failure检查 MySQL 是否运行、URL 端口、用户名密码4. 留言提交POSThttp://localhost:8080/MyWebApp/message?actionadd302 → 200页面刷新后新增留言时间显示为“刚刚”或“1分钟前”留言不显示MessageServlet中ps.setInt(1, userId)的userId未从 Session 取出导致外键约束失败5. 分页导航http://localhost:8080/MyWebApp/message?actionlistpage2200页面显示第 2 页留言底部有“上一页/下一页”链接点击“下一页”仍是第 1 页JSP 中a hrefmessage?actionlistpage${currentPage1}的 EL 表达式未生效检查是否漏了% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %提示Chrome 开发者工具F12的 Network 标签页是你的第一道防线。点击登录按钮后在 Network 中找到login请求点开看 Headers → Response Headers →Location: messageBoard.jsp证明重定向成功若看到Status Code: 404立刻检查web.xml。5. 避坑指南五个血泪经验总结解决 95% 的部署与逻辑翻车这些不是教科书里的“注意事项”而是我在三台不同系统的机器上反复重装、抓包、断点调试后记下的真实踩坑记录。每一条都对应一个具体错误日志和解决方案。5.1 现象HTTP Status 404 – /loginweb.xml明明配了url-pattern/login/url-pattern原因Tomcat 9 默认启用metadata-completetrue导致web.xml中的servlet和servlet-mapping不被扫描。这是 Tomcat 9 的安全默认行为但老项目常忽略。解决打开web.xml在根web-app标签中添加metadata-completefalse属性web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 metadata-completefalse !-- 关键显式设为 false --5.2 现象登录成功后跳转到messageBoard.jsp但页面显示NullPointerException${sessionScope.currentUser.username}为空原因LoginServlet中session.setAttribute(currentUser, user)执行了但messageBoard.jsp里sessionScope.currentUser为 null。根源是 Session ID 未正确传递——浏览器禁用了 Cookie或 Tomcat 的sessionCookiePath配置错误。解决Chrome 浏览器检查 Application → Cookies确认JSESSIONIDCookie 存在且 Path 为/MyWebApp/若不存在在web.xml中添加session-config强制设置session-config session-timeout30/session-timeout cookie-config http-onlytrue/http-only securefalse/secure !-- 开发环境设为 false生产环境必须 true -- /cookie-config /session-config5.3 现象注册时提示Data truncation: Data too long for column password at row 1原因user表的password字段定义为VARCHAR(50)但 BCrypt 加密后的密文长度为 60 字符如$2a$10$...。MySQL 8.0 默认严格模式拒绝截断。解决修改表结构将密码字段扩大ALTER TABLE user MODIFY COLUMN password VARCHAR(255) NOT NULL;注意不要用TEXT类型VARCHAR(255)是 BCrypt 的标准存储长度。5.4 现象留言内容含中文存入数据库后变成???查询出来也是乱码原因三个环节的字符集不一致JSP 页面编码、Tomcat 连接器配置、MySQL 连接 URL、数据库表字符集。缺一不可。解决四步同步login.jsp顶部添加% page contentTypetext/html;charsetUTF-8 %conf/server.xml中Connector标签增加URIEncodingUTF-8Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 / !-- 关键 --DBUtil.java中 JDBC URL 添加characterEncodingutf8mb4private static final String URL jdbc:mysql://localhost:3306/webapp?useSSLfalseserverTimezoneUTCallowPublicKeyRetrievaltruecharacterEncodingutf8mb4;MySQL 创建数据库时已用CHARACTER SET utf8mb4无需再改。5.5 现象messageBoard.jsp中c:forEach items${messageList} varmsg不渲染任何内容页面空白原因JSTL 标签库未正确加载。jstl-1.2.jar在WEB-INF/lib/中但web.xml的web-app版本声明为2.3老版本而 JSTL 1.2 要求 Servlet 2.4。解决升级web.xml的version属性web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 !-- 从 2.3 升级到 3.1 --验证重启 Tomcat 后查看logs/catalina.out应有TLD skipped. URI: http://java.sun.com/jsp/jstl/core is already defined证明 JSTL 已加载。6. 二次开发实战从密码明文到 BCrypt 加密一次安全加固的完整改造路径现在你已经能把项目跑通下一步就是让它真正可用。最迫切的安全改造是密码存储——明文密码是绝对红线。我带你从零开始把RegisterServlet和LoginServlet中的password字段替换成 BCrypt 加密与校验。这不是加个 jar 就完事而是要理解加密、盐值、比对的完整链路。6.1 引入 BCrypt 依赖为什么选bcrypt而不是SHA-256SHA-256是哈希算法速度快易被 GPU 暴力破解BCrypt是自适应哈希内置盐值salt和计算轮数cost factor能随硬件升级调整强度。本项目采用org.mindrot:jbcrypt:0.4轻量仅一个 class。操作下载jbcrypt-0.4.jar放入WEB-INF/lib/在RegisterServlet.java和LoginServlet.java顶部添加 importimport org.mindrot.jbcrypt.BCrypt;6.2 注册流程改造RegisterServlet中密码加密入库// src/servlet/RegisterServlet.java修改 doPost 方法 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); String email req.getParameter(email); // 1. 生成 BCrypt 密文cost12平衡安全与性能 String hashedPassword BCrypt.hashpw(password, BCrypt.gensalt(12)); // 2. 插入数据库SQL 中 password 字段存的是密文不是明文 String sql INSERT INTO user (username, password, email) VALUES (?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, username); ps.setString(2, hashedPassword); // 存密文 ps.setString(3, email); int affectedRows ps.executeUpdate(); if (affectedRows 0) { req.setAttribute(errorMsg, 注册失败请重试); req.getRequestDispatcher(register.jsp).forward(req, resp); return; } // 3. 获取自增主键可选用于后续逻辑 try (ResultSet rs ps.getGeneratedKeys()) { if (rs.next()) { int userId rs.getInt(1); System.out.println(New user id: userId); } } } }参数说明BCrypt.gensalt(12)生成盐值12表示 2^12 4096 轮哈希耗时约 100ms足够抵御暴力攻击BCrypt.hashpw(password, salt)返回形如$2a$12$XkGZQ...的 60 字符字符串前缀含算法、轮数、盐值密文本身已包含全部解密信息password字段在数据库中必须是VARCHAR(255)因为 BCrypt 密文固定 60 字符但留足余量更稳妥。6.3 登录流程改造LoginServlet中密码比对校验// src/servlet/LoginServlet.java修改数据库查询后的校验逻辑 // ... 数据库查询代码不变 if (rs.next()) { String storedHash rs.getString(password); // 从数据库取出密文 String inputPassword req.getParameter(password); // 用户输入的明文密码 // 2. 使用 BCrypt.checkpw() 比对明文 密文 true/false if (BCrypt.checkpw(inputPassword, storedHash)) { // 登录成功创建 User 对象注意此时 password 字段不设值或设为 null User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setEmail(rs.getString(email)); // user.setPassword(storedHash); // ❌ 不设密码避免内存泄露 HttpSession session req.getSession(true); session.setAttribute(currentUser, user); resp.sendRedirect(messageBoard.jsp); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); }关键逻辑BCrypt.checkpw(input, hash)内部会从hash字符串中提取盐值和轮数用相同参数对input重新哈希再比对结果。它不要求你存储盐值因为盐值已嵌入密文中。6.4 安全加固验证三步确认加密真正生效数据库验证登录 MySQL执行SELECT password FROM user WHERE usernametest;结果应为$2a$12$...开头的 60 字符字符串而非明文123456登录验证用原密码123456能正常登录但用其他密码如 12本文还有配套的精品资源点击获取