☰
JSP+JavaBean+Servlet实战:从零搭建登录注册与留言板系统
2026/10/8 15:05:31 网站建设 项目流程

简介:一套基于JSP+JavaBean+Servlet的JavaWeb经典入门项目,采用简洁的MVC分层设计,面向初学JavaWeb的开发者,覆盖登录注册、留言板、分页三大功能模块。项目中model层由JavaBean封装数据库连接与增删改查方法,连接的webapp数据库未设置密码,view层使用JSP生成动态页面,controller层通过Servlet拦截请求、调用业务方法并完成页面转发,三层职责清晰,便于分段阅读与调试。压缩包共78个文件,包含14个Java源文件、14个JSP页面、14个class编译文件,以及CSS样式、XML与properties配置、SQL建库脚本、JAR依赖库等,足以支撑项目在IDE中直接导入运行,并借助SQL脚本快速重建数据库,压缩包整体仅1.86MB。目前已有1611人浏览学习。借助它可快速理解MVC各层如何协作、JDBC如何读写数据库、分页查询如何实现,以及留言板功能的设计思路,很适合作为JavaWeb课程设计或入门练习的系统性参考。

1. 为什么毕业设计和课程设计仍然在用 JSP+JavaBean+Servlet 写登录注册和留言板

“用 jsp + javabean + servlet 实现登录注册以及留言板”,这行字在题目里出现不是巧合,它是 Web 开发里最典型的“能用且够用”结构。不靠任何框架,Servlet 处理请求、JavaBean 封装和携带数据、JSP 负责把页面输出给浏览器,一次完整的请求-响应周期,全程不超过五个类。它解决的是:一个没有接触过企业级框架的学生,如何在两周内跑通一套带数据存储的真实系统。同时它也能回答一个本质问题——MVC 到底在代码里怎么落地。网上能搜到的 servlet demo 大多停留在打印一句 Hello World,和真实项目之间隔着一条数据库连接、一套页面渲染和一堆异常处理。这篇文章适合第一次做完整 Web 系统的初学者,也适合需要快速交付演示程序、但又不想引入 Spring 全家桶的从业者。

2. 先定架构再建表:这套组合里的数据到底怎么流

把 jsp、javabean、servlet 三个角色摆正,后面写代码才不会变成一坨 Servlet 里拼 HTML 的老式写法。我的建议是记住一句口诀:“浏览器只认识 JSP,Servlet 只做通信兵,JavaBean 只背数据包。”

2.1 MVC 分工与实际包结构

严格来说这是标准的 MVC 变体:JSP 是 View,Servlet 是 Controller,JavaBean 是 Model。一个请求进来后数据流的顺序是固定的:

  1. 浏览器把表单参数提交给 Servlet 的 doPost;
  2. Servlet 用 request.getParameter 取出参数;
  3. 参数封装进 JavaBean 对象;
  4. DAO 层用 JDBC 把 JavaBean 的数据写进 MySQL 或读出来;
  5. Servlet 把结果放进 request 或 session;
  6. 最后 forward 或 redirect 到某个 JSP,由 JSP 负责渲染。

很多初学同学喜欢把 SQL 直接写在 Servlet 里,把 for 循环写在 JSP 的<% %>里,这种写法不是不能跑,而是后面改需求时每改一次都像拆一次炸弹。我一般会按下面这个包结构组织工程,这也是课程设计里最容易被答辩老师接受的布局:

com.example.entity User.java / Message.java com.example.dao UserDao.java / MessageDao.java com.example.servlet LoginServlet / RegisterServlet / MessageServlet com.example.filter LoginFilter.java com.example.util DBUtil.java / MD5Util.java

entity 里就是纯 JavaBean,属性对应数据库字段;dao 里只放 JDBC 操作;servlet 里做参数接收、校验和页面跳转;util 放数据库连接和加密工具。如果你连 web.xml 都不熟,那就先别急着给 Servlet 配映射,直接用@WebServlet("/login")注解,省掉一批配置。前提是 Tomcat 7 以上,现在实验室里基本都满足。

2.2 数据库表设计:用户表和留言表的字段与约束

留言板再简单,也必须有用户和留言两张表,这个设计是整篇文章的地基。直接执行下面这段 SQL:

CREATE DATABASE IF NOT EXISTS login_demo DEFAULT CHARSET utf8mb4; USE login_demo; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, salt CHAR(8) NOT NULL, email VARCHAR(120), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_message ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_id (user_id), CONSTRAINT fk_msg_user FOREIGN KEY (user_id) REFERENCES t_user (id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:t_user 的 password 长度设为 64 是为了容纳 MD5 摘要后的 32 位十六进制再加盐;如果只设 20 位,后面做加密时数据会写不进去,这是很多人忽略的字段约束问题。t_message 用外键关联 t_user,ON DELETE CASCADE表示删除用户时其留言一并删除,保证数据不会变成孤儿记录。字符集统一使用 utf8mb4 而不是 utf8,原因很简单:utf8 在 MySQL 里存不下 emoji 表情,留言内容里一旦出现表情符号,整条 insert 直接报错,这种报错还会把事务回滚,查半天才能发现是字符集问题。

参数说明:外键这行的CONSTRAINT fk_msg_user是约束名,可自定义;KEY idx_user_id是普通索引,目的是加快按用户查留言的速度。数据量没过万时索引体感不明显,但这是答辩时一个容易拿到分的点,写上不亏。

2.3 两个核心 JavaBean:User 和 Message 怎么写才不越界

JavaBean 在这一套组合里只做一件事:定义属性和对应的 getter/setter。不要在里面写数据库连接,也不要写业务判断,保持纯粹,后面 Servlet 里才好复用。

package com.example.entity; public class User { private int id; private String username; private String password; private String salt; private String email; private String createdAt; public User() {} public int getId() { return id; } public void setId(int 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 getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getCreatedAt() { return createdAt; } public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } }

Message 类的字段则是 id、userId、content、createTime,其中 createTime 因为是数据库自动生成,JavaBean 里对应的是 String 类型,从 ResultSet 取出来的是 java.sql.Timestamp,直接 toString 会得到一串带毫秒的格式,展示时做一次截断即可。为什么不建议在 JavaBean 里加一个showTime()方法?因为那是展示逻辑,属于 JSP 的职责范围。纯数据模型的好处是以后换用 MyBatis 或者 JSON 序列化时,这个类一行都不用改。

3. 登录和注册:表单提交、MD5、Session 和 Filter 一次讲透

登录注册是这套系统的门面,也是安全审查的重点。很多老代码直接用明文存密码,答辩时老师一打开数据库就能看到“123456”一排排放着,这个分基本别想拿。这里采用 MD5 加盐的方式,不加第三方依赖,纯 JDK 自带 MessageDigest 就能实现。

3.1 注册链路:从表单到数据库的六个步骤

注册页面的表单长什么样不重要,重要的是提交到哪个路径、Servlet 里做了什么。一个标准的注册流程包含六个步骤:接收参数、校验空值、检查用户名是否存在、密码加密、写入数据库、跳转登录页。

package com.example.servlet; import com.example.dao.UserDao; import com.example.entity.User; import com.example.util.MD5Util; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/register") public class RegisterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); String email = req.getParameter("email"); if (username == null || username.isEmpty() || password == null || password.isEmpty()) { req.setAttribute("error", "用户名和密码不能为空"); req.getRequestDispatcher("/register.jsp").forward(req, resp); return; } UserDao dao = new UserDao(); if (dao.findByUsername(username) != null) { req.setAttribute("error", "用户名已存在"); req.getRequestDispatcher("/register.jsp").forward(req, resp); return; } String salt = MD5Util.randomSalt(); User user = new User(); user.setUsername(username); user.setPassword(MD5Util.md5(password + salt)); user.setSalt(salt); user.setEmail(email); dao.insert(user); resp.sendRedirect(req.getContextPath() + "/login.jsp?registered=1"); } }

逻辑说明:所有参数先过一遍空值校验,任何一个为空就直接 forward 回注册页,同时把错误信息放进 request。检查用户名重复是在数据库层面用SELECT id FROM t_user WHERE username=?来做的,用 LIMIT 1 即可,这个查询是登录功能里最高频的操作,索引 uk_username 在这里生效。密码加密采用“原文加盐再做 MD5”的方式,盐是每次注册时随机生成的 8 位字符串,和 MD5 值一起入库。为什么要加盐而不是直接 md5(password)?因为相同的密码在不同用户身上会得到不同的摘要,即使数据库泄露,也无法通过彩虹表反查明文。

参数说明:@WebServlet("/register")里的斜杠是必须的,路径从应用根目录算起,这是 Servlet 3.0 注解写法,省掉了 web.xml 的一堆<servlet-mapping>配置。randomSalt 的实现可以用UUID.randomUUID().toString().substring(0, 8),这个方法在 util 包里维护,工具类放在 util 包而不是 servlet 包,是所有类的通用依赖。

3.2 登录校验与 Session 状态保持

登录成功后的核心是 Session。Session 里存什么、存多久、什么时候失效,直接决定整个系统的安全边界。登录成功后我只把 User 对象的 id 和 username 放进 Session,密码和盐一概不进,这是为了避免 JSP 页面里不小心把用户密码打印出来。

@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); UserDao dao = new UserDao(); User user = dao.findByUsername(username); if (user == null || !user.getPassword().equals(MD5Util.md5(password + user.getSalt()))) { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/message/list"); } }

逻辑说明:查询用户时把完整的 User 对象取出来,拿到盐后再对用户输入的密码做同样的 MD5 运算,和库里的值比对。这里有个细节:user.getPassword().equals(...)是拿数据库摘要和实时计算摘要比,而不是拿明文比,后者在日志里会暴露密码。登录成功后执行req.getSession(),如果当前请求没有关联的会话,容器会自动创建一个;有就用旧的,保证同一个浏览器多次请求拿到同一个 Session。Session 默认有效期在 Tomcat 里是 30 分钟,超过 30 分钟无操作会自动失效,这个时间可以在 web.xml 里通过<session-config>覆盖。跳转用重定向而不是 forward 的原因后面留言板章节会详细讲,核心就是防表单重复提交。

3.3 用 Filter 拦截未登录请求,避免整个站点裸奔

登录接口写完后,还要回答一个问题:用户不登录能不能直接访问留言列表页面?答案是不能,这需要 Filter 来完成。Filter 是 Java Web 里最原生的拦截器,不用引入 Spring Security 也能实现基本的页面保护。

package com.example.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebFilter("/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse resp = (HttpServletResponse) servletResponse; String path = req.getRequestURI().substring(req.getContextPath().length()); String contextPath = req.getContextPath(); if (path.startsWith("/login") || path.startsWith("/register") || path.equals("/index.jsp") || path.startsWith("/static/")) { chain.doFilter(servletRequest, servletResponse); return; } HttpSession session = req.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { resp.sendRedirect(contextPath + "/login.jsp"); return; } chain.doFilter(servletRequest, servletResponse); } }

逻辑说明:Filter 的拦截范围是/*,即所有请求先进过滤器再进 Servlet。白名单机制处理登录、注册、首页、静态资源这四类路径,其余所有路径都必须先验证 Session。关键点在req.getSession(false)——传 false 表示如果当前请求没有关联 Session 就返回 null,而不是自动创建一个新的,这样可以避免未登录用户每次访问都被迫生成一个无用的会话。未登录时重定向到登录页,重定向必须带 contextPath,否则在部署路径不是根目录的应用上会直接 404。静态资源放行的路径要与你的实际目录对应,如果 CSS 放在 webapp/css 下,就改成/css/,不要写错。

4. 留言板怎么写得清爽:列表循环、发留言、删除权限与分页

留言板是这个项目里功能最完整的部分,也是最能体现工程能力的一块。这里分享一个可复用的干净写法:JSP 只做展示和表单,Servlet 只做参数接收和跳转,DAO 只做增删查。

4.1 留言列表页的 JSP 渲染与个人信息展示

留言列表页其实就是一个最简单的 jsp 个人信息展示页面——每条留言展示用户名、发布时间、内容,附带一个删除入口。用一个 JSTL c:forEach 循环,配合 EL 表达式取值,这是老项目里最不容易出错的写法。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <div class="message-list"> <c:forEach items="${messageList}" var="msg"> <div class="message-item"> <p class="message-content">${msg.content}</p> <p class="message-meta"> 发布于 ${msg.createTime}, <c:if test="${sessionScope.loginUser.id == msg.userId}"> <a href="${pageContext.request.contextPath}/message/delete?id=${msg.id}">删除</a> </c:if> </p> </div> </c:forEach> </div>

逻辑说明:${messageList}是 Servlet 放进 request 里的集合,${msg.content}会自动调用 Message 类的 getContent 方法,这一整套 EL 表达式能正常工作需要 JSP 页面顶部引入 JSTL 标签库,并且在 pom.xml 或 WEB-INF/lib 里放 jstl 和 standard 两个 jar 包。删除入口用条件判断包住,只有当前登录用户是留言作者时才显示删除按钮,这个条件在页面上用 EL 从 sessionScope 里取 loginUser 的 id,和留言的作者 id 做比较,不需要写<% %>脚本片段。

这里有个容易忽略的点:JSP 页面里的msg.createTime如果是从数据库取出的 Timestamp,EL 表达式打印出来会是带毫秒的长字符串,看起来很不专业。处理办法是在 Message 类里加一个 getShowTime 方法,内部做日期格式化,在 EL 表达式里继续用msg.showTime取值,JavaBean 允许这种简化的只读属性存在,但不建议让 JSP 直接 new SimpleDateFormat 去格式化。

4.2 发留言为什么必须用重定向回列表页

发留言的表单提交到 MessageServlet,核心动作是 insert 成功后重定向列表页。这里有一个常见的翻车场景:用 forward 跳转列表页,用户按一下 F5,浏览器把刚才的表单请求原样重发一次,数据库里就多了一条一模一样的留言。解决方法是 Post/Redirect/Get 模式,简称 PRG。

@WebServlet("/message/add") public class MessageAddServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); User loginUser = (User) req.getSession().getAttribute("loginUser"); if (loginUser == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } String content = req.getParameter("content"); if (content == null || content.trim().isEmpty()) { req.setAttribute("error", "留言内容不能为空"); req.getRequestDispatcher("/message/list").forward(req, resp); return; } Message message = new Message(); message.setUserId(loginUser.getId()); message.setContent(content.trim()); MessageDao dao = new MessageDao(); dao.insert(message); resp.sendRedirect(req.getContextPath() + "/message/list"); } }

逻辑说明:PRG 的核心是让浏览器在提交完成之后收到一个 302 响应,浏览器随后自动发起一次 GET 请求去访问列表页。此时地址栏里已经变成/message/list,F5 刷新触发的是 GET 请求,不会再带上 POST 表单数据,重复提交问题从源头消失。留一句重要体验:留言成功后给用户一个视觉反馈,最简单的是在列表页顶部放一个一行提示“留言成功”,这个提示可以用一次性的 sessionAttribute 实现,读一次就清掉,否则刷新后提示还在。

参数说明:content.trim()是必要的,用户在文本框里打了空格也算空内容,这种边界条件在答辩演示时很容易被打出来。这里的空值校验和注册 Servlet 里的一致,保持全项目的校验风格统一,比每处写得五花八门更重要。

4.3 删除权限校验和分页参数:两个必调参数

删除留言的入口是上一小节那句删除链接,但前端藏住按钮不代表接口安全,Servlet 里必须再次校验登录状态和作者身份。因为任何人都可以手动在地址栏输入/message/delete?id=18发起删除请求,完全没有页面上的条件限制。

一个安全的删除逻辑实现如下:

@WebServlet("/message/delete") public class MessageDeleteServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { User loginUser = (User) req.getSession().getAttribute("loginUser"); if (loginUser == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } int id = Integer.parseInt(req.getParameter("id")); MessageDao dao = new MessageDao(); Message message = dao.findById(id); if (message == null) { resp.sendRedirect(req.getContextPath() + "/message/list"); return; } if (message.getUserId() != loginUser.getId()) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } dao.deleteById(id); resp.sendRedirect(req.getContextPath() + "/message/list"); } }

这样处理后,即使有人绕过页面直接调用接口,没有登录或不是留言作者都会被拦下。这里的用户 id 比较必须用 int 的基本类型判断,因为!=在自动拆箱时如果有一侧是 null 会抛空指针,取 session 里的 user 时先判空正是为了挡住这个异常。

数据量小的时候不需要分页,但留言板往往会越积越多。给列表页加一个最朴素的分页:pageNo 表示当前页,pageSize 固定为 5,用 SQL 的 LIMIT 和 count 查询配合。代码层面就是三句话:

int pageNo = 1; try { pageNo = Integer.parseInt(req.getParameter("page")); } catch (NumberFormatException ignore) {} int pageSize = 5; int total = messageDao.count(); int totalPages = (int) Math.ceil((double) total / pageSize); List<Message> list = messageDao.findPage(pageNo, pageSize); req.setAttribute("messageList", list); req.setAttribute("totalPages", totalPages); req.setAttribute("currentPage", pageNo);

对应的 DAO 里findPage执行 SQL 是SELECT * FROM t_message ORDER BY id DESC LIMIT ?, ?,第一个参数传(pageNo - 1) * pageSize,第二个传 pageSize。页码超出范围时,比如删掉最后一页最后一条数据后再点上一页,查出来是空列表,此时要在 Servlet 里把 pageNo 钳制到 1 和 totalPages 之间,不然空页面看起来像一个 bug。

5. 从乱码到404:这套代码最常见的五个坑

这一章是这套系统交付前必须过一遍的排错清单,每一条都是我实际踩过的,按“现象 → 原因 → 解决”来写,你会发现大部分诡异问题背后都是配置遗漏而不是代码逻辑错。

5.1 POST 中文全部变成问号

现象:注册用户名“张三”提交到数据库后变成“???”,或者 MySQL 客户端查出来是完整的,但 JSP 页面显示乱码。原因:请求参数编码、页面编码、数据库连接编码三处没有对齐。解决:在 LoginFilter 或每一个 Servlet 的 doPost 第一行调用req.setCharacterEncoding("UTF-8"),在 JDBC 连接 URL 里加characterEncoding=utf8,JSP 页面顶部声明pageEncoding="UTF-8"。三者齐了才能保证中文从浏览器到 MySQL 走一圈不变形。实际项目中最省事的做法是在 Filter 里对所有请求做 encode,而不在每个 Servlet 里重复写。

5.2 MySQL 8 下加载驱动直接 ClassNotFound

现象:程序一启动连数据库就抛ClassNotFoundException,但连接字符串和用户名密码全都检查过没错。原因:MySQL 驱动版本和写的老驱动类名不匹配。解决:MySQL 8 及以上版本的 JDBC 驱动类名是com.mysql.cj.jdbc.Driver,旧版是com.mysql.jdbc.Driver;同时连接 URL 里要追加serverTimezone=Asia/Shanghai或者serverTimezone=UTC,否则驱动会由于时区不明而拒绝连接。如果你的 pom.xml 引的是 mysql-connector-java 8.x,请直接使用新类名,旧类名在这代驱动里已经被移除,不存在兼容一说。

5.3 注册成功后 CSS 全部失效,页面裸奔

现象:登录页能正常访问时样式是好的,注册成功跳转后页面只有文字没有样式,按 F12 全是 404。原因:页面里用了相对路径引用 CSS,比如href="css/style.css",当 URL 从/login.jsp变成/message/list后,浏览器解析相对路径的基准目录变了。解决:在所有 JSP 头部写<base href="<%=request.getContextPath()%>/">,或在引用的 href 里完整地写${pageContext.request.contextPath}/css/style.css。这是老工程的老问题,养成全站写绝对路径的习惯可以避免这类问题。

5.4 F5 刷新一次存一条重复留言

现象:发布一条留言后按 F5,列表里出现两条同样的内容;如果是删除操作,刷新后还能看到“已删除”的记录。原因:表单 POST 提交后如果使用 forward 跳转结果页,地址栏仍是原来的 POST 地址,刷新即重放。解决:照第 4 章的做法,提交完成后调用resp.sendRedirect让浏览器跳转到 GET 请求的列表页。还有一个容易漏的细节:留言内容的 JS 校验挡住空提交没问题,但挡不住双击提交按钮——两个几乎同时到达的 POST 请求会各自通过检查,各插入一条。稳妥的做法是在前端加一次性禁用按钮标记,这个比后端去重简单得多。

5.5 登录成功后跳回列表页还是未登录

现象:重定向到/message/list时又被 Filter 踢回登录页,似乎 Session 没有生效。原因:Servlet 里是用request.getSession()存了用户,但 Filter 里用req.getSession(false)取不到对象——往往是因为登录成功后访问的不是同一个应用根,或者跳转到的地址跨了 contextPath。解决:跳转时统一使用req.getContextPath()拼路径,不要写死/login.jsp,否则部署在带应用名的路径下就会出这种问题。调试方法很简单:在 Filter 里临时打印req.getRequestURI()和req.getSession(false),一眼就能看出是跳转路径少了前缀还是 Filter 拦截范围判断失准。

5.6 留言里带 emoji 表情,整个 insert 报错

现象:用户在留言框里发了一个笑脸表情,提交后页面报 SQL 错误,而且提示信息里没有明显的字段名。原因:数据库表和连接 URL 的字符集是 utf8,而 utf8 在 MySQL 中只支持三字节的编码,emoji 是四字节。解决:建表时用DEFAULT CHARSET=utf8mb4,同时把连接 URL 中的characterEncoding=utf8改为characterEncoding=UTF-8,注意这里字节意义上的 utf8mb4 不是写到 URL 里的值,数据库连接层面统一成 UTF-8 即可。如果项目已经建完表,单独对表和字段执行ALTER TABLE t_message CONVERT TO CHARACTER SET utf8mb4;就能修好,不必删库重来。

6. 给登录补一个验证码:不引入框架的纯 Servlet 做法

登录接口如果没有任何验证码,会被脚本用撞库的方式刷用户名和密码,这个风险在你的应用部署到公网服务器时尤其明显。课程设计只要做到“有用即可”的验证码又不引第三方框架,最省事的方案是用 JDK 自带的 Graphics2D 绘制图片。核心逻辑只有三步:生成随机字符放进 Session;用图画出来输出成图片;登录时比对用户输入和 Session 里的值。关键点在于验证码要一次有效,校验成功后立即把 Session 里的验证码删掉,否则同一个验证码能被反复使用。

一个能用的简化实现是把固定长度字符画进 BufferedImage,再通过 ServletResponse 的输出流写回浏览器。要留意的是图片响应必须设置setContentType("image/jpeg")和禁止缓存的头,否则浏览器可能会把验证码图片缓存住,导致刷新也不换内容。图片坐标里的纵向偏移和字符间距都可以在生成图片的循环参数里调整,这也是搜索词里“jsp 图片如何对坐标定位”最常见的应用场景——验证码字符不是画在固定像素点,而是按字符宽度递增定位;如果你以后做头像裁剪,起始 x、y 和宽高四个值一存,后面 Java2D 裁剪的套路同源。

给登录页加上验证码后,LoginServlet 里要比对表单提交的code参数和 Session 里的验证码字符串,忽略大小写,比对后立刻session.removeAttribute("code")。不要小看这十来行代码,它在答辩时是一个拿得出手的加分段。我自己做这类老项目形成的习惯是:登录注册这条链路永远先从数据库表和 Filter 画起,再写 Servlet,最后补页面,这个顺序能避免一半的改稿返工;留言板哪怕数据量很小,分页和 PRG 也会先写上,因为演示现场永远是最容易按 F5 的场合。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询