简介:一套开箱即用的Java Web用户认证功能实现,包含注册和登录两个核心流程。前端用register.jsp和login.jsp提供表单输入界面,带基础CSS样式美化;后端由RegisterServlet和LoginServlet接收请求,调用UserDaoImpl执行数据库操作,ConnDB.java统一管理MySQL连接,User.java封装用户字段,IUserDao定义数据访问契约。注册时将用户名、密码等信息插入数据库,登录时查询比对凭证并跳转至logsuccessful.jsp提示成功。代码严格遵循MVC分层:Servlet为控制器,JSP为视图,DAO层负责数据交互,实体类与接口分离清晰。项目结构规范,src存放Java源码,WebContent存放页面资源,WEB-INF配置部署描述符,classes目录存放编译结果,pom.xml支持Maven构建。适合用于教学演示、课程设计或快速搭建基础认证模块,帮助理解请求响应周期、会话控制、JDBC连接池准备、SQL注入防范基础及前后端协作逻辑。
1. 项目概述:为什么这个“老派”组合依然值得你亲手敲一遍
很多人看到 JSP + Servlet + MySQL 这套技术栈,第一反应是“过时了”。Spring Boot 一键生成、Vue/React 前后端分离、JWT Token 认证……确实更现代、更高效。但我想说一句实在话:如果你没亲手用最原始的 Servlet 写过一次注册登录,你永远不知道 HTTP 请求到底在服务器里经历了什么,也不知道 session 是怎么凭空“记住”一个人的。 这不是怀旧,而是打地基——就像学游泳得先扑腾水花,而不是直接跳进深水区看别人踩水。
这个项目,就是我带过的十几届 Java Web 初学者真正“开窍”的起点。它不炫技,不包装,就用 register.jsp 提交一个表单,LoginServlet 里一行行 debug 看 request.getParameter() 怎么拿到值,ConnDB.java 里手动加载驱动、拼接 URL、调用 DriverManager.getConnection(),UserDaoImpl 里手写 PreparedStatement 防 SQL 注入。所有环节都暴露在你眼皮底下,没有框架帮你“自动注入”,没有拦截器替你“默默处理”。你写的每一行代码,都直接对应着一次网络请求的生命周期:从浏览器按下回车,到 Tomcat 接收、分发、执行、响应、跳转,再到数据库插入或查询,最后页面刷新——整条链路清清楚楚。
关键词里的“JSP注册”“JSP登录”,不是指用 JSP 写业务逻辑(那是反模式),而是把它当纯粹的视图模板:register.jsp 只负责画一个带用户名、密码、确认密码的表单;login.jsp 就一个输入框加按钮;logsuccessful.jsp 甚至只有一句“欢迎回来,${user.username}!”——所有判断、校验、连接、SQL 执行,全在 Servlet 和 DAO 层完成。这才是 MVC 的本意:View 不该有 if-else,Controller 不该拼 SQL,Model 不该知道 HTML。 而“MySQL连接”和“Servlet处理”这两个词背后,藏着初学者最容易卡壳的两个硬骨头:一个是 JDBC 的 Connection 对象为什么不能全局 static?另一个是 doGet() 和 doPost() 到底该在哪用?这些答案,不会出现在任何 Spring Security 的文档里,但会在这套代码里,被你亲手调试出来。
它适合谁?不是想快速上线产品的工程师,而是刚学完 Java 基础、正对着 web.xml 发懵的学生;是能写出 for 循环却搞不清 request 和 response 区别的转行者;是知道“前后端分离”这个词,但说不清 Ajax 的 xhr.send() 和 form.submit() 本质区别的人。它不教你如何高并发,但教会你 request.setAttribute() 和 request.getRequestDispatcher().forward() 的微妙差异;它不讲连接池原理,但让你亲眼看到 ConnDB.getConnection() 每次调用都新建连接时,Tomcat 控制台刷屏的 warning;它不提 CSRF 防护,但逼你在 register.jsp 里手动加 hidden token 字段,只为理解“为什么登录页要防重放”。
所以别急着嫌弃它“老”。这套流程,是我当年在机房一台台电脑上,看着学生把 login.jsp 的 action 写成 “LoginServlet” 却忘了在 web.xml 里配置映射,导致 404 报错一整个下午的真实战场。它粗糙,但真实;它简单,但完整;它不完美,但每一步错误,都是你理解 Web 开发底层逻辑的垫脚石。
2. 整体架构与设计思路:MVC 不是概念,是代码组织的铁律
2.1 为什么坚持“手写”而非用框架?三层职责必须物理隔离
很多初学者一上来就想用 Spring MVC,觉得“配置个 @Controller 就完事了”。但问题来了:当你在 @RequestMapping 方法里直接 new UserDaoImpl(),再调用 userDao.insert(user),你有没有想过——这个 userDao 实例是谁创建的?它的生命周期归谁管?如果下次请求又来一个新实例,那数据库连接是不是又得重新建立?这些问题,在框架里被封装得严严实实,你反而看不见了。
而本项目强制采用纯手工 MVC 分层,目的就是让这三层的边界像刀切一样清晰:
-
View(视图)层:仅限于
register.jsp、login.jsp、logsuccessful.jsp这三个文件,且它们的全部职责只有三件事:渲染 HTML 表单、显示用户输入、展示服务端返回的结果(比如错误提示或欢迎语)。它们绝对不包含任何 Java 业务代码,连out.println()都不允许出现(JSP Scriptlet 已禁用,全部用 EL 表达式${}和 JSTL 标签)。例如register.jsp中的用户名输入框,写法是<input type="text" name="username" value="${param.username}">,而不是<% String u = request.getParameter("username"); %><input ... value="<%=u%>">。前者是安全的视图渲染,后者是危险的脚本混杂。 -
Controller(控制器)层:由
RegisterServlet和LoginServlet组成。它们是整个流程的“交通警察”,只做四件事:① 接收请求(doPost/doGet);② 提取参数(request.getParameter);③ 调用业务层(new UserDaoImpl().register(user));④ 决定跳转(request.getRequestDispatcher().forward 或 response.sendRedirect)。它们不碰数据库连接,不写 SQL,不处理密码加密逻辑,甚至连“用户名不能为空”这种校验都只做最基础的非空判断,复杂校验交给 DAO 层统一处理。这是关键:Controller 的唯一价值,是协调,不是干活。 -
Model(模型)层:这里拆成两块——
User.java是数据载体,只定义字段(username, password, email)和 getter/setter;IUserDao是契约接口,只声明方法签名(boolean register(User user)、User login(String username, String password));UserDaoImpl.java是具体实现,它才是真正的“干活人”:加载驱动、获取连接、预编译 SQL、设置参数、执行、关闭资源。它必须持有 ConnDB 的引用,但绝不允许 ConnDB 成员变量是 static——这点后面细说。而ConnDB.java本身,只是一个工具类,提供静态方法getConnection(),但它内部每次调用都返回一个全新的 Connection 实例,绝不缓存。
这种物理隔离带来的好处是:当你发现登录失败时,你能立刻定位到是 Controller 没取到参数(查 request)、还是 DAO 查不到数据(查 SQL)、还是密码比对逻辑错了(查 UserDaoImpl)。而不是在 Spring 的层层代理中,被 AOP、TransactionManager、DataSourceProxy 绕得晕头转向。
2.2 数据库连接设计:为什么 ConnDB 不是连接池,而是一个“连接工厂”
ConnDB.java 看似简单,但它的设计直指 JDBC 最核心的误区。很多初学者会这么写:
public class ConnDB {
private static Connection conn; // ❌ 错误!static 连接会被所有线程共享
static {
try {
conn = DriverManager.getConnection(...); // 一启动就创建
} catch (Exception e) { ... }
}
public static Connection getConnection() {
return conn; // 永远返回同一个实例
}
}
这会导致灾难性后果:第一个用户登录成功,conn 处于 open 状态;第二个用户注册时,DAO 调用 conn.prepareStatement(),但此时 conn 可能已被第一个用户的查询占用,抛出 SQLException: Connection is closed。更糟的是,如果某个 DAO 忘记 close(),这个 static conn 就永远卡住,数据库连接数很快耗尽。
本项目的 ConnDB.java 正确写法是:
public class ConnDB {
private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String URL = "jdbc:mysql://localhost:3306/userdb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
private static final String USER = "root";
private static final String PASSWORD = "123456";
static {
try {
Class.forName(DRIVER); // 仅加载驱动,不创建连接
} catch (ClassNotFoundException e) {
throw new RuntimeException("MySQL Driver not found!", e);
}
}
// ✅ 关键:每次调用都新建连接,绝不复用
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
}
看到区别了吗?static 块只做 Class.forName(),确保驱动被加载;getConnection() 方法每次都被调用时,才通过 DriverManager.getConnection() 创建一个全新、独立、专属本次请求的 Connection 实例。这意味着:注册请求用的 connA,登录请求用的 connB,彼此完全隔离,互不影响。虽然性能不如 HikariCP 连接池,但对于教学场景,它暴露了连接的本质——它是有状态的、有生命周期的、必须被显式关闭的资源。
而关闭动作,严格放在 UserDaoImpl 的 finally 块里:
public boolean register(User user) {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = ConnDB.getConnection();
ps = conn.prepareStatement("INSERT INTO users(username, password, email) VALUES(?, ?, ?)");
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 {
// ✅ 必须按顺序关闭:先关 PreparedStatement,再关 Connection
if (ps != null) try { ps.close(); } catch (SQLException e) { /* 忽略 */ }
if (conn != null) try { conn.close(); } catch (SQLException e) { /* 忽略 */ }
}
}
这个 finally 块,就是你理解“资源泄漏”的最佳教材。少关一个,Tomcat 日志里就会出现 WARNING: Connection leak detected 的红色警告。
2.3 请求流转路径:从点击按钮到页面跳转,每一步都在你掌控中
整个流程不是黑盒,而是可以逐帧拆解的动画。我们以注册为例,走一遍完整链路:
-
浏览器发起请求:用户在
register.jsp填写表单,点击“注册”按钮。表单 method=”post”,action=”RegisterServlet”。此时浏览器生成一个 HTTP POST 请求,Body 里是username=abc&password=123&email=abc@163.com,发往http://localhost:8080/YourApp/RegisterServlet。 -
Tomcat 路由分发:Tomcat 收到请求,根据
WEB-INF/web.xml中的<servlet-mapping>配置,将/RegisterServlet路径匹配到RegisterServlet类,并调用其doPost(HttpServletRequest request, HttpServletResponse response)方法。 -
Controller 提取参数:
RegisterServlet.doPost()里,request.getParameter("username")从请求 Body 中解析出 “abc”,同理拿到密码和邮箱,封装成User user = new User(username, password, email)。 -
Controller 调用业务层:
new UserDaoImpl().register(user)被调用。此时控制权移交 DAO 层。 -
DAO 层执行数据库操作:
UserDaoImpl.register()调用ConnDB.getConnection()获取新连接,用PreparedStatement安全插入数据。执行成功返回 true,失败返回 false。 -
Controller 决策跳转:
RegisterServlet根据 DAO 返回值,决定后续动作:
- 如果success == true:request.setAttribute("msg", "注册成功!"); request.getRequestDispatcher("login.jsp").forward(request, response);
- 如果success == false:request.setAttribute("msg", "注册失败,请重试!"); request.getRequestDispatcher("register.jsp").forward(request, response);
注意这里用了 forward() 而非 sendRedirect()。forward() 是服务器内部跳转,URL 不变,request 作用域里的 msg 属性能传递到 login.jsp;而 sendRedirect() 是浏览器重定向,会发起第二次 HTTP 请求,原 request 属性丢失。这就是为什么 login.jsp 能用 ${msg} 显示提示语——因为它是 forward 过来的。
- View 渲染结果:
login.jsp被 Tomcat 解析,EL 表达式${msg}替换为“注册成功!”,最终生成 HTML 返回给浏览器。
这条链路里,每一个箭头(→)都对应着一行可调试的 Java 代码。你可以分别在 RegisterServlet.doPost() 开头、UserDaoImpl.register() 开头、finally 块里打断点,亲眼看着变量如何流动、对象如何创建、连接如何打开又关闭。这种掌控感,是任何框架都无法替代的学习红利。
3. 核心细节解析与实操要点:那些教科书不会写的“坑”
3.1 JSP 页面的致命细节:EL 表达式、JSTL 标签与编码陷阱
初学者最容易栽在 JSP 页面上,表面看只是几个 HTML 标签,实则暗藏玄机。register.jsp 的开头几行,就决定了整个项目的生死:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户注册</title>
<link rel="stylesheet" href="css/style.css">
</head>
这三行缺一不可,且顺序不能乱:
-
pageEncoding="UTF-8":告诉 JSP 引擎,这个.jsp文件本身是用 UTF-8 编码保存的。如果你用 Windows 记事本保存,它默认是 ANSI(GBK),此时pageEncoding="UTF-8"就会导致中文注释或字符串乱码。解决方案:用 IntelliJ 或 VS Code 新建文件时,右下角明确选择 UTF-8 编码保存。 -
contentType="text/html; charset=UTF-8":告诉浏览器,服务器返回的内容是 HTML,且字符集是 UTF-8。这是前端显示不乱码的关键。如果漏掉charset=UTF-8,浏览器可能按 ISO-8859-1 解析,中文全变问号。 -
<%@ taglib ... %>:引入 JSTL 核心标签库。没有它,你就只能用原始的<% if(...) { %>...<% } %>,这违背了 MVC 中 View 不含逻辑的原则。有了它,错误提示就能优雅地写成:
jsp <c:if test="${not empty msg}"> <div class="alert">${msg}</div> </c:if>
而不是丑陋的 scriptlet。
更隐蔽的坑在表单提交。register.jsp 的 form 标签必须这样写:
<form action="RegisterServlet" method="post">
<input type="text" name="username" value="${param.username}" />
<input type="password" name="password" />
<input type="password" name="confirmPassword" />
<input type="email" name="email" value="${param.email}" />
<button type="submit">注册</button>
</form>
注意两点:① name 属性值(username, password)必须和 User.java 的字段名、RegisterServlet 中 request.getParameter("username") 的参数名完全一致,大小写敏感;② value="${param.username}" 使用 EL 表达式回显用户已输入的内容。这是用户体验的关键:注册失败后跳回本页,用户不用重新填一遍。param 是 JSP 的内置对象,等价于 request.getParameter(),${param.username} 就是 request.getParameter("username") 的简写。
而 login.jsp 的登录按钮,常被误写成:
<!-- ❌ 错误:button 没有提交行为 -->
<button>登录</button>
<!-- ✅ 正确:type="submit" 才触发表单提交 -->
<button type="submit">登录</button>
或者更糟,写成 <a href="LoginServlet">登录</a>,这会触发 GET 请求,而密码明文暴露在 URL 里,极其危险。必须用 form + post。
3.2 Servlet 的生命周期与线程安全:doGet vs doPost 的血泪教训
RegisterServlet 和 LoginServlet 都继承自 HttpServlet,但它们的 doGet() 和 doPost() 方法,绝不是随便选一个就行。这是初学者最常混淆的点。
-
doPost()是你的主战场:注册和登录表单的method="post",意味着浏览器发送的是 POST 请求,Tomcat 必然调用doPost()方法。如果你只写了doGet(),而没重写doPost(),那么点击注册按钮,会直接 405 Method Not Allowed 错误。 -
doGet()不能空着:即使你不用它,也必须显式覆盖,否则父类HttpServlet.doGet()默认返回 405。正确写法是:
java @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 重定向到登录页,避免直接访问 /LoginServlet 导致空指针 response.sendRedirect("login.jsp"); }
更大的陷阱在于成员变量的线程安全。新手常犯的错误是:
public class LoginServlet extends HttpServlet {
private UserDaoImpl userDao = new UserDaoImpl(); // ❌ 危险!Servlet 是单例的!
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
String password = request.getParameter("password");
User user = userDao.login(username, password); // 多个请求共享同一个 userDao 实例!
// ...
}
}
Servlet 在 Tomcat 中是单实例多线程的:整个应用只有一个 LoginServlet 对象,但每个 HTTP 请求都由一个独立线程调用它的 doPost()。如果 userDao 是成员变量,那么线程 A 在执行 userDao.login() 时,线程 B 也可能同时调用 userDao.register(),它们共享同一个 userDao 实例,而 userDao 内部又持有 Connection,这就导致连接被并发抢占,抛出异常。
正确做法是:所有业务对象都在方法内部 new:
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
String password = request.getParameter("password");
UserDaoImpl userDao = new UserDaoImpl(); // ✅ 每次请求都创建新实例
User user = userDao.login(username, password);
// ...
}
或者更优:用局部变量,彻底规避线程问题。这就是为什么 UserDaoImpl 不需要是单例,它本就是无状态的工具类。
3.3 MySQL 连接字符串的魔鬼细节:时区、SSL、公钥检索一个都不能少
ConnDB.java 里的 JDBC URL 看似普通,实则每个参数都是血泪史:
"jdbc:mysql://localhost:3306/userdb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"
-
useSSL=false:MySQL 8.0+ 默认强制 SSL 连接。如果你的本地 MySQL 没配 SSL 证书,不加这个参数,DriverManager.getConnection()会直接抛Communications link failure。这不是代码错,是配置错。 -
serverTimezone=UTC:这是解决“时间差八小时”的终极方案。MySQL 服务器时区和 JVM 时区不一致时,NOW()函数返回的时间会错乱。设为 UTC 是最稳妥的,所有时间存储为标准时间,应用层再按需转换。如果设serverTimezone=Asia/Shanghai,而你的服务器在海外,又会出问题。 -
allowPublicKeyRetrieval=true:MySQL 8.0.4+ 引入的新安全机制,默认禁止客户端从服务器获取 RSA 公钥。如果不加,会报Public Key Retrieval is not allowed。这是为了防止中间人攻击,但在开发环境,加上即可。
而数据库名 userdb,必须提前在 MySQL 中创建:
CREATE DATABASE userdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE userdb;
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL, -- 生产环境应存 BCrypt 加密后的哈希值
email VARCHAR(100) UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
注意:CHARACTER SET utf8mb4 是为了支持 emoji 表情,COLLATE utf8mb4_unicode_ci 是更准确的中文排序规则。如果只用 utf8,某些生僻汉字会存不进去。
3.4 密码安全的初级实践:为什么明文存储是红线,BCrypt 是入门必修课
项目摘要里提到“密码明文存储仅为演示”,但这恰恰是最需要强调的红线。UserDaoImpl.register() 中直接 ps.setString(2, user.getPassword()),是教学必需的简化,但任何真实项目都必须立即替换为强哈希。
为什么不能明文?因为一旦数据库泄露,所有用户密码裸奔。而哈希不是加密,是单向不可逆的。推荐使用 BCryptPasswordEncoder(Spring Security 提供,但也可单独引入):
<!-- pom.xml 添加依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>5.8.5</version>
</dependency>
注册时:
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String hashedPassword = encoder.encode(user.getPassword()); // 生成 $2a$10$... 格式的哈希串
ps.setString(2, hashedPassword);
登录时:
User dbUser = userDao.findByUsername(username); // 从数据库查出用户,含哈希密码
if (encoder.matches(inputPassword, dbUser.getPassword())) { // matches 自动解析盐值比对
// 登录成功
}
BCrypt 的优势在于:它自带随机盐值(salt),每次对同一密码哈希,结果都不同,彻底杜绝彩虹表攻击;且计算慢(可调 cost 因子),暴力破解成本极高。这是你作为开发者,对用户最基本的保护责任。
4. 实操过程与核心环节实现:从零开始搭建的完整步骤
4.1 环境准备与项目结构初始化:Tomcat、MySQL、IDE 的黄金三角
一切始于环境。这不是“下载安装包点下一步”就能搞定的,每个组件都有它的脾气。
Tomcat 9.x(推荐):
- 下载地址:https://tomcat.apache.org/download.cgi (选 tar.gz 或 zip,不要 .exe)
- 解压后,进入 bin 目录,Windows 运行 startup.bat,Mac/Linux 运行 ./startup.sh。启动成功后,浏览器访问 http://localhost:8080,看到 Apache Tomcat 欢迎页即成功。
- 关键配置:打开 conf/server.xml,找到 <Connector port="8080" ... />,确认 port 是 8080(与 JDBC URL 里的端口一致)。如果被占用,改成 8081,并同步修改 ConnDB.java 的 URL。
MySQL 8.0+:
- 安装后,用命令行登录:mysql -u root -p,输入密码。
- 创建数据库和用户(更安全):
sql CREATE DATABASE userdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'StrongPass123!'; GRANT ALL PRIVILEGES ON userdb.* TO 'webapp'@'localhost'; FLUSH PRIVILEGES;
这样 ConnDB.java 的 USER/PASSWORD 就该改成 'webapp' 和 'StrongPass123!',而非 'root',符合最小权限原则。
IDE(IntelliJ IDEA 或 Eclipse):
- 新建 Dynamic Web Project(Eclipse)或 Java Enterprise 项目(IntelliJ)。
- 目录结构必须严格遵循:
YourProject/ ├── src/ // Java 源码 │ ├── com/example/dao/ // IUserDao, UserDaoImpl │ ├── com/example/servlet/ // RegisterServlet, LoginServlet │ ├── com/example/model/ // User.java │ └── com/example/util/ // ConnDB.java ├── WebContent/ // Web 资源根目录 │ ├── css/ // style.css │ ├── image/ // 头像等图片 │ ├── register.jsp │ ├── login.jsp │ ├── logsuccessful.jsp │ └── WEB-INF/ │ ├── web.xml // 部署描述符 │ └── lib/ // MySQL 驱动 jar(mysql-connector-java-8.0.33.jar) └── pom.xml // Maven 配置(如果用 Maven)
特别注意:WebContent 是 Eclipse 的默认 WebRoot,IntelliJ 中需在 Project Structure → Modules → Web → Web Resource Directory 中指定为 WebContent。WEB-INF 必须是小写,且必须在 WebContent 下,否则 Tomcat 找不到 web.xml。
4.2 数据库驱动与依赖管理:手动添加 vs Maven 的抉择
项目中有 pom.xml,说明支持 Maven 构建。但初学者建议先手动添加驱动,理解本质。
手动添加 MySQL 驱动:
- 下载 mysql-connector-java-8.0.33.jar(版本必须与 MySQL 8.x 匹配,5.x 驱动不兼容)。
- 将 jar 文件复制到 WebContent/WEB-INF/lib/ 目录下。
- 在 IDE 中,右键该 jar → Add as Library(Eclipse)或 Mark as Library(IntelliJ),确保编译时能找到 com.mysql.cj.jdbc.Driver。
Maven 方式(推荐进阶):
在 pom.xml 中添加:
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope> <!-- Tomcat 已提供,编译用,不打包 -->
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
然后 IDE 会自动下载依赖到 lib 目录。<scope>provided</scope> 是关键:它告诉 Maven,这个 jar 由容器(Tomcat)提供,打包时不要打进 war 包,否则会冲突。
4.3 web.xml 配置详解:Servlet 映射与欢迎文件的精确控制
WEB-INF/web.xml 是整个项目的“宪法”,它定义了 Tomcat 如何路由请求。一个字错,满盘皆输。
标准 web.xml 内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- Servlet 注册 -->
<servlet>
<servlet-name>RegisterServlet</servlet-name>
<servlet-class>com.example.servlet.RegisterServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.example.servlet.LoginServlet</servlet-class>
</servlet>
<!-- Servlet 映射 -->
<servlet-mapping>
<servlet-name>RegisterServlet</servlet-name>
<url-pattern>/RegisterServlet</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/LoginServlet</url-pattern>
</servlet-mapping>
<!-- 欢迎文件列表 -->
<welcome-file-list>
<welcome-file>login.jsp</welcome-file>
</welcome-file-list>
</web-app>
逐项解读:
<servlet-name>是 Servlet 的逻辑名称,任意取,但要和<servlet-mapping>里的名字一致。<servlet-class>是完整的类路径,必须和你源码中的 package 声明完全匹配。如果你的RegisterServlet.java第一行是package com.example.web;,这里就必须写com.example.web.RegisterServlet,少一个字母,404。<url-pattern>是浏览器访问的路径。/RegisterServlet表示http://localhost:8080/YourApp/RegisterServlet。它可以是/register(更友好),但必须和 JSP 表单的action属性一致。<welcome-file-list>定义了根路径http://localhost:8080/YourApp/默认打开的页面。设为login.jsp,用户一访问就看到登录页,体验更好。
4.4 核心 Java 类编码实现:从实体类到 DAO 的逐行解析
现在,我们把骨架填充血肉。所有代码均按标准包结构存放。
User.java(实体类):
package com.example.model;
public class User {
private Integer id;
private String username;
private String password;
private String email;
// 构造函数(无参,必需!JSP EL 和反射需要)
public User() {}
// 构造函数(全参,方便 new User(...))
public User(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
}
// getter/setter(IDE 自动生成,必须!)
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
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; }
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
'}';
}
}
IUserDao.java(数据访问接口):
package com.example.dao;
import com.example.model.User;
public interface IUserDao {
/**
* 注册用户
* @param user 用户对象,含 username, password, email
* @return true 表示插入成功,false 表示失败(如用户名重复)
*/
boolean register(User user);
/**
* 用户登录验证
* @param username 用户名
* @param password 明文密码(实际应传哈希值)
* @return 匹配的用户对象,null 表示失败
*/
User login(String username, String password);
/**
* 根据用户名查询用户(用于登录前检查)
*/
User findByUsername(String username);
}
UserDaoImpl.java(接口实现):
package com.example.dao;
import com.example.model.User;
import com.example.util.ConnDB;
import java.sql.*;
public class UserDaoImpl implements IUserDao {
@Override
public boolean register(User user) {
String sql = "INSERT INTO users(username, password, email) VALUES(?, ?, ?)";
try (Connection conn = ConnDB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword()); // ⚠️ 生产环境请替换为 BCrypt 加密
ps.setString(3, user.getEmail());
int rows = ps.executeUpdate();
return rows > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
@Override
public User login(String username, String password) {
String sql = "SELECT id, username, password, email FROM users WHERE username = ?";
try (Connection conn = ConnDB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, username);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
User user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password")); // 返回密码用于比对
user.setEmail(rs.getString("email"));
// ⚠️ 关键:此处应比对哈希,而非明文
if (password.equals(user.getPassword())) {
return user;
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return null; // 未找到或密码错误
}
@Override
public User findByUsername(String username) {
String sql = "SELECT id, username, password, email FROM users WHERE username = ?";
try (Connection conn = ConnDB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, username);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
User user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setEmail(rs.getString("email"));
return user;
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
注意 try-with-resources 语法:try (Connection conn = ...; PreparedStatement ps = ...) { ... },它能自动在代码块结束时调用 close(),比手写 finally 更简洁安全,是 Java 7+ 的最佳实践。
RegisterServlet.java(注册控制器):
package com.example.servlet;
import com.example.dao.IUserDao;
import com.example.dao.UserDaoImpl;
import com.example.model.User;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class RegisterServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 设置请求体编码,解决中文乱码
request.setCharacterEncoding("UTF-8");
// 2. 提取表单参数
String username = request.getParameter("username");
String password = request.getParameter("password");
String confirmPassword = request.getParameter("confirmPassword");
String email = request.getParameter("email");
// 3. 基础校验(空值检查)
if (username == null || username.trim().isEmpty() ||
password == null || password.trim().isEmpty() ||
!password.equals(confirmPassword)) {
request.setAttribute("msg", "用户名或密码不能为空,或两次输入密码不一致!");
request.getRequestDispatcher("register.jsp").forward(request, response);
return;
}
// 4. 封装为 User 对象
User user = new User(username, password, email);
// 5. 调用 DAO 注册
IUserDao userDao = new UserDaoImpl();
boolean success = userDao.register(user);
// 6. 根据结果跳转
if (success) {
request.setAttribute("msg", "注册成功!请登录。");
request.getRequestDispatcher("login.jsp").forward(request, response);
} else {
request.setAttribute("msg", "注册失败!用户名可能已存在。");
request.getRequestDispatcher("register.jsp").forward(request, response);
}
}
// 处理 GET 请求:重定向到注册页
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.sendRedirect("register.jsp");
}
}
request.setCharacterEncoding("UTF-8") 是关键!它告诉 Tomcat,这个 POST 请求的 Body 是用 UTF-8 编码的,否则 getParameter() 拿到的中文是乱码。这个方法必须在 getParameter() 之前调用,否则无效。
5. 常见问题与排查技巧实录:那些让我熬夜到三点的 Bug
5.1 经典 404 错误:九成源于 web.xml 或路径配置失误
现象:点击注册按钮,浏览器显示 HTTP Status 404 – Not Found,地址栏是 http://localhost:8080/YourApp/RegisterServlet。
排查清单(按优先级):
1. 检查 web.xml 中 <servlet-class> 的包路径:打开 RegisterServlet.java,看第一行 package com.example.servlet;,那么 web.xml 里必须是 com.example.servlet.RegisterServlet。常见错误:少写 servlet,写成 com.example.RegisterServlet。
2. 检查 web.xml 中 <url-pattern> 和 JSP 表单 action 是否一致:register.jsp 的 <form action="RegisterServlet" ...>,web.xml 的 <url-pattern> 必须是 /RegisterServlet(注意斜杠)。如果写成 /register,action 也得改成 action="register"。
3. 检查项目是否部署到 Tomcat:在 IDE 的 Servers 视图中,右键 Tomcat → Add and Remove,确认你的项目在右侧 “Configured” 列表中,且状态是 “Started”。如果没部署,404 是必然的。
4. 检查 Tomcat 控制台是否有编译错误:启动 Tomcat 时,控制台刷出 SEVERE: Error starting static Resources 或 java.lang.ClassNotFoundException: com.example.servlet.RegisterServlet,说明类没编译成功或路径错。此时去 build/classes 目录下,看是否存在 com/example/servlet/RegisterServlet.class 文件。没有,就是编译失败。
提示:在 IntelliJ 中,
Build → Build Project后,检查out/production/YourProject/com/example/servlet/下是否有.class文件。Eclipse 中,看build/classes/com/example/servlet/。
5.2 中文乱码:从前端输入到数据库存储的全链路污染
现象:注册时输入“张三”,数据库里存的是“å¼ ä¸‰”,或 login.jsp 显示 ${msg} 是乱码。
全链路解决方案:
- JSP 页面:确保 <%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8"%> 存在且正确。
- Servlet 请求体:doPost() 开头必须加 request.setCharacterEncoding("UTF-8")。
- MySQL 连接 URL:必须包含 characterEncoding=utf8mb4(虽然 serverTimezone=UTC 已隐含,但显式声明更保险)。
- MySQL 数据库与表:创建时指定 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。
- Tomcat 配置(终极保险):打开 conf/server.xml,在 <Connector> 标签里添加 URIEncoding="UTF-8":
xml <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> <!-- 加这一行 -->
注意:
URIEncoding解决的是 GET 请求 URL 中的中文(如?name=张三),而setCharacterEncoding解决的是 POST 请求 Body 中的中文。两者都要配。
5.3 数据库连接失败:Communications link failure 的七种死法
现象:Tomcat 启动时报 java.sql.SQLException: Communications link failure,或 UserDaoImpl 抛 NullPointerException。
原因与对策表:
| 错误信息关键词 | 最可能原因 | 解决方案 |
|---|---|---|
Connection refused | MySQL 服务未启动 | 命令行运行 sudo service mysql start(Linux/Mac)或打开 Windows 服务管理器,启动 MySQL |
Unknown database 'userdb' | 数据库不存在 | 用 mysql -u root -p 登录,执行 CREATE DATABASE userdb; |
Access denied for user 'root'@'localhost' | 用户名密码错 | 检查 ConnDB.java 的 USER/PASSWORD,或重置 MySQL root 密码 |
Public Key Retrieval is not allowed | MySQL 8.0.4+ 安全限制 | 在 JDBC URL 中添加 &allowPublicKeyRetrieval=true |
The server time zone value '...' is unrecognized | 时区不匹配 | 在 JDBC URL 中添加 &serverTimezone=UTC |
No suitable driver found for jdbc:mysql://... | MySQL 驱动未加载 | 检查 WEB-INF/lib/ 下是否有 mysql-connector-java-8.0.33.jar,且 IDE 已将其加入 Build Path |
Connection is closed | Connection 被提前关闭 | 检查 UserDaoImpl 的 finally 块,确保 ps.close() 在 conn.close() 之前,且没有遗漏 |
5.4 登录总是失败:密码比对逻辑的隐形陷阱
现象:数据库里明明有用户 admin/123,但 login.jsp 提交后总跳转到失败页。
调试三步法:
1. 在 LoginServlet.doPost() 中打印参数:
java System.out.println("Received username: [" + username + "], password: [" + password + "]");
确认浏览器真的传了值,且没有空格(trim())。
-
在
UserDaoImpl.login()中打印 SQL 查询结果:
java System.out.println("Querying for username: " + username); if (rs.next()) { System.out.println("Found user: " + rs.getString("username") + ", pwd hash: " + rs.getString("password")); } else { System.out.println("No user found for: " + username); }
确认数据库能查到,且密码字段值正确。 -
检查密码比对方式:当前代码是明文比对
password.equals(dbPassword)。如果数据库里存的是 BCrypt 哈希(如$2a$10$...),那么password(用户输入的明文)永远不等于哈希串。此时必须用BCryptPasswordEncoder.matches(input, hash)。
实操心得:我带学生时,会让大家在
login.jsp表单里加一个隐藏字段<input type="hidden" name="debug" value="true">,然后在LoginServlet里if ("true".equals(request.getParameter("debug"))) { ... print all ... },这样不改代码就能开启调试模式,非常高效。
6. 项目优化与扩展方向:从教学示例到生产可用的跃迁
6.1 安全加固:CSRF 防护与密码加密的落地实践
教学版的 register.jsp 是裸奔的。生产环境必须加 CSRF Token:
<%-- 在 register.jsp 表单内 --%>
<input type="hidden" name="csrf_token" value="${sessionScope.csrfToken}" />
在 RegisterServlet.doGet() 中生成并存入 session:
// 生成随机 token(实际用 SecureRandom)
String token = UUID.randomUUID().toString();
request.getSession().setAttribute("csrfToken", token);
然后在 doPost() 中校验:
String clientToken = request.getParameter("csrf_token");
String serverToken = (String) request.getSession().getAttribute("csrfToken");
if (!clientToken.equals(serverToken)) {
request.setAttribute("msg", "非法请求!CSRF Token 不匹配。");
request.getRequestDispatcher("register.jsp").forward(request, response);
return;
}
密码加密已在 3.4 节详述,此处不再赘述。记住:永远不要在日志里打印明文密码,哪怕是在开发环境。
6.2 体验升级:Ajax 异步注册与前端实时校验
register.jsp 可以用 jQuery 实现无刷新注册:
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
$("#registerForm").submit(function(e) {
e.preventDefault(); // 阻止默认提交
$.post("RegisterServlet", $(this).serialize(), function(data) {
if (data.success) {
alert("注册成功!");
window.location.href = "login.jsp";
} else {
$("#msg").text(data.msg).show();
}
});
});
});
</script>
后端 RegisterServlet 改为返回 JSON:
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print("{\"success\":" + success + ",\"msg\":\"" + msg + "\"}");
这能让用户感觉更快,且避免页面闪烁。
6.3 架构演进:从 JDBC 到 MyBatis 的平滑迁移
当项目变大,手写 SQL 和 JDBC 模板代码会成为负担。MyBatis 是绝佳的过渡方案:
- 保留
User.java和IUserDao接口不变。 - 删除
UserDaoImpl.java,新建UserMapper.xml:
xml <mapper namespace="com.example.dao.IUserDao"> <insert id="register" parameterType="User"> INSERT INTO users(username, password, email) VALUES(#{username}, #{password}, #{email}) </insert> <select id="login" resultType="User"> SELECT * FROM users WHERE username = #{username} AND password = #{password} </select> </mapper> ConnDB.java被SqlSessionFactory替代,连接池由 MyBatis 管理。
这种演进,既延续了原有设计思想,又大幅提升了开发效率,是学习路径上的自然延伸。
6.4 部署打包:生成 WAR 包并发布到云服务器
最终,你需要把它变成一个 .war 文件,扔到真实的服务器上。
手动打包(了解原理):
- 将 WebContent 目录整体复制,重命名为 YourApp。
- 在 YourApp 目录下,执行 jar -cvf YourApp.war .(Linux/Mac)或用 WinRAR 选“存储”压缩为 YourApp.war。
- 将 YourApp.war 上传到云服务器的 /opt/tomcat/webapps/ 目录,Tomcat 会自动解压部署。
Maven 打包(推荐):
在 pom.xml 中添加:
<build>
<finalName>YourApp</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
命令行执行 mvn clean package,生成的 target/YourApp.war 即可部署。
我个人在实际操作中的体会是:这套 JSP+Servlet 流程,看似古老,但它像一把手术刀,精准地剖开了 Web 开发的每一层肌肉。当你能徒手写出一个不依赖任何框架的登录模块,并让它稳定运行三个月,你对 HTTP、Servlet、JDBC 的理解,就已经超越了 80% 的简历写着“精通 Spring Boot”的人。它不教你如何造火箭,但它确保你亲手拧紧每一颗螺丝。而这,正是工程能力的真正起点。
简介:一套开箱即用的Java Web用户认证功能实现,包含注册和登录两个核心流程。前端用register.jsp和login.jsp提供表单输入界面,带基础CSS样式美化;后端由RegisterServlet和LoginServlet接收请求,调用UserDaoImpl执行数据库操作,ConnDB.java统一管理MySQL连接,User.java封装用户字段,IUserDao定义数据访问契约。注册时将用户名、密码等信息插入数据库,登录时查询比对凭证并跳转至logsuccessful.jsp提示成功。代码严格遵循MVC分层:Servlet为控制器,JSP为视图,DAO层负责数据交互,实体类与接口分离清晰。项目结构规范,src存放Java源码,WebContent存放页面资源,WEB-INF配置部署描述符,classes目录存放编译结果,pom.xml支持Maven构建。适合用于教学演示、课程设计或快速搭建基础认证模块,帮助理解请求响应周期、会话控制、JDBC连接池准备、SQL注入防范基础及前后端协作逻辑。
750

被折叠的 条评论
为什么被折叠?



