JSP+Servlet+MySQL实现的用户注册登录完整流程示例

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的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.jsplogin.jsplogsuccessful.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(控制器)层:由 RegisterServletLoginServlet 组成。它们是整个流程的“交通警察”,只做四件事:① 接收请求(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 请求流转路径:从点击按钮到页面跳转,每一步都在你掌控中

整个流程不是黑盒,而是可以逐帧拆解的动画。我们以注册为例,走一遍完整链路:

  1. 浏览器发起请求:用户在 register.jsp 填写表单,点击“注册”按钮。表单 method=”post”,action=”RegisterServlet”。此时浏览器生成一个 HTTP POST 请求,Body 里是 username=abc&password=123&email=abc@163.com,发往 http://localhost:8080/YourApp/RegisterServlet

  2. Tomcat 路由分发:Tomcat 收到请求,根据 WEB-INF/web.xml 中的 <servlet-mapping> 配置,将 /RegisterServlet 路径匹配到 RegisterServlet 类,并调用其 doPost(HttpServletRequest request, HttpServletResponse response) 方法。

  3. Controller 提取参数RegisterServlet.doPost() 里,request.getParameter("username") 从请求 Body 中解析出 “abc”,同理拿到密码和邮箱,封装成 User user = new User(username, password, email)

  4. Controller 调用业务层new UserDaoImpl().register(user) 被调用。此时控制权移交 DAO 层。

  5. DAO 层执行数据库操作UserDaoImpl.register() 调用 ConnDB.getConnection() 获取新连接,用 PreparedStatement 安全插入数据。执行成功返回 true,失败返回 false。

  6. Controller 决策跳转RegisterServlet 根据 DAO 返回值,决定后续动作:
    - 如果 success == truerequest.setAttribute("msg", "注册成功!"); request.getRequestDispatcher("login.jsp").forward(request, response);
    - 如果 success == falserequest.setAttribute("msg", "注册失败,请重试!"); request.getRequestDispatcher("register.jsp").forward(request, response);

注意这里用了 forward() 而非 sendRedirect()forward() 是服务器内部跳转,URL 不变,request 作用域里的 msg 属性能传递到 login.jsp;而 sendRedirect() 是浏览器重定向,会发起第二次 HTTP 请求,原 request 属性丢失。这就是为什么 login.jsp 能用 ${msg} 显示提示语——因为它是 forward 过来的。

  1. 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 的字段名、RegisterServletrequest.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 的血泪教训

RegisterServletLoginServlet 都继承自 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.gzzip,不要 .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 中指定为 WebContentWEB-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 Resourcesjava.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,或 UserDaoImplNullPointerException

原因与对策表

错误信息关键词最可能原因解决方案
Connection refusedMySQL 服务未启动命令行运行 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 allowedMySQL 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 closedConnection 被提前关闭检查 UserDaoImplfinally 块,确保 ps.close()conn.close() 之前,且没有遗漏

5.4 登录总是失败:密码比对逻辑的隐形陷阱

现象:数据库里明明有用户 admin/123,但 login.jsp 提交后总跳转到失败页。

调试三步法
1. LoginServlet.doPost() 中打印参数
java System.out.println("Received username: [" + username + "], password: [" + password + "]");
确认浏览器真的传了值,且没有空格(trim())。

  1. 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); }
    确认数据库能查到,且密码字段值正确。

  2. 检查密码比对方式:当前代码是明文比对 password.equals(dbPassword)。如果数据库里存的是 BCrypt 哈希(如 $2a$10$...),那么 password(用户输入的明文)永远不等于哈希串。此时必须用 BCryptPasswordEncoder.matches(input, hash)

实操心得:我带学生时,会让大家在 login.jsp 表单里加一个隐藏字段 <input type="hidden" name="debug" value="true">,然后在 LoginServletif ("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.javaIUserDao 接口不变。
  • 删除 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.javaSqlSessionFactory 替代,连接池由 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”的人。它不教你如何造火箭,但它确保你亲手拧紧每一颗螺丝。而这,正是工程能力的真正起点。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的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注入防范基础及前后端协作逻辑。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值