JavaWeb日记本系统毕业设计完整项目(JSP+MySQL+Servlet)实战
简介:JavaWeb日记本系统是一个基于JSP和Servlet技术的动态Web应用,结合MySQL数据库实现用户注册、登录、日记创建、编辑与安全管理等功能。系统采用JavaWeb经典架构,涵盖前端展示、后端逻辑处理与数据库交互全流程,支持富文本编辑、数据加密及防SQL注入等安全机制。配套论文详细阐述系统设计思路与实现过程,附带完整源码与SQL脚本,适合用于毕业设计与JavaWeb技术学习的综合实践项目。 
1. JavaWeb日记本系统整体架构设计
JavaWeb日记本系统采用典型的B/S三层架构模式,即表现层(JSP)、控制层(Servlet)与数据持久层(MySQL),通过HTTP协议实现浏览器与服务器之间的交互。系统遵循MVC设计思想,将页面展示、请求处理与数据操作分离,提升代码可维护性与模块化程度。前端使用JSP结合EL表达式和JSTL标签库动态渲染数据,Servlet作为控制器接收用户请求并调用对应Service业务逻辑,最终通过JDBC访问MySQL数据库完成数据持久化。项目结构清晰,符合标准JavaWeb工程规范,包含 webapp 资源目录、 java 源码包、配置文件及SQL脚本,支持快速部署与二次开发,适用于毕业设计中对Web全流程开发的综合实践需求。
2. JSP动态网页技术应用与实现
JavaServer Pages(JSP)作为JavaWeb开发中的核心表现层技术,承担着将服务器端数据动态渲染至前端页面的关键职责。在日记本系统中,JSP不仅用于展示用户信息、日记列表和操作界面,还通过内置对象、EL表达式与JSTL标签库的协同作用,实现了逻辑与视图的有效分离。随着现代Web应用对响应速度与用户体验要求的提升,合理使用JSP并结合HTML/CSS/JS进行深度整合,成为提升系统可维护性与交互体验的重要手段。本章将从语法基础出发,深入剖析JSP在实际项目中的应用场景,并探讨性能优化策略及前后端协作开发的最佳实践。
2.1 JSP基础语法与内置对象
JSP技术本质上是Servlet的一种简化形式,允许开发者以类HTML的方式编写动态网页,其最终会被容器编译为Servlet类执行。理解JSP的基础语法结构及其九大内置对象,是掌握其高效应用的前提。这些元素共同构成了JSP处理请求、生成响应的核心机制。
2.1.1 JSP页面元素构成(指令、脚本、动作标签)
JSP页面由三类基本元素组成: 指令元素(Directive Elements) 、 脚本元素(Scripting Elements) 和 动作元素(Action Tags) 。它们分别负责页面配置、嵌入Java代码以及控制运行时行为。
指令元素
指令用于告知JSP引擎如何处理当前页面,常见的有 page 、 include 和 taglib 三种。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ include file="header.jsp" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
page指令设置页面属性,如内容类型、编码格式、是否启用Session等;include指令静态包含其他JSP文件,在编译期合并,提高模块复用;taglib引入外部标签库,例如JSTL,避免直接书写Java代码。
逻辑分析 :上述代码片段展示了典型的JSP页面头部声明。
contentType确保浏览器正确解析中文字符;language="java"明确脚本语言类型;include实现页眉复用,减少重复代码;而taglib则为后续使用<c:forEach>等标签做准备。
脚本元素
包含三种形式:声明、表达式和脚本段落。
<%! int count = 0; %> <!-- 声明成员变量 -->
<p>当前访问次数:<%= ++count %> </p> <!-- 表达式输出 -->
<%
String name = request.getParameter("name");
if (name != null) {
out.println("欢迎 " + name);
}
%> <!-- 脚本片段执行Java逻辑 -->
<%! %>定义类级变量或方法;<%= %>输出表达式结果;<% %>插入任意Java语句。
参数说明 :
request.getParameter()获取URL参数,out是JSP内置的JspWriter对象,用于向客户端输出内容。该段代码演示了简单的用户问候功能,但存在安全隐患(未过滤XSS),应在生产环境中配合过滤器处理。
动作标签
动作标签在请求时动态执行,常见如 <jsp:include> 、 <jsp:forward> 、 <jsp:useBean> 等。
<jsp:include page="menu.jsp" />
<jsp:forward page="error.jsp">
<jsp:param name="msg" value="登录失败" />
</jsp:forward>
jsp:include动态包含资源,支持参数传递;jsp:forward实现请求转发,不改变浏览器地址栏。
| 标签 | 作用 | 执行时机 |
|---|---|---|
<%@ include %> |
静态包含 | 编译期 |
<jsp:include> |
动态包含 | 请求期 |
<jsp:forward> |
页面跳转 | 请求期 |
graph TD
A[JSP页面] --> B{包含方式}
B --> C[<%@ include file="..." %>\n静态合并]
B --> D[<jsp:include page="..." />\n动态调用]
C --> E[所有内容合并成一个Servlet]
D --> F[分别生成Servlet,运行时调用]
流程图解析 :静态包含在编译阶段完成文件合并,适用于固定布局组件(如页头、页脚);动态包含则保留独立性,适合可变内容区域。选择不当可能导致重复定义错误或性能损耗。
2.1.2 九大内置对象详解(request、response、session等)
JSP自动创建九个隐含对象,无需显式声明即可使用,极大简化了开发流程。
| 内置对象 | 类型 | 作用范围 | 描述 |
|---|---|---|---|
request |
HttpServletRequest | Request | 封装客户端请求数据 |
response |
HttpServletResponse | Page | 控制响应输出 |
session |
HttpSession | Session | 维护用户会话状态 |
application |
ServletContext | Application | 全局共享数据 |
out |
JspWriter | Page | 输出内容到客户端 |
pageContext |
PageContext | Page | 提供页面上下文访问 |
config |
ServletConfig | Page | 获取Servlet初始化参数 |
page |
Object (this) | Page | 当前JSP实例引用 |
exception |
Throwable | Page | 异常处理页面专用 |
实际应用示例:用户登录状态判断
<%
String username = (String) session.getAttribute("username");
if (username == null) {
response.sendRedirect("login.jsp");
return;
}
%>
<h3>欢迎你,<%= username %></h3>
逐行解读 :
- 第1行:尝试从session中读取已登录用户名;
- 第2-4行:若为空,重定向至登录页,阻止非法访问;
- 第6行:安全输出用户名(应进一步过滤XSS);扩展建议 :此逻辑更适合移至Filter中统一拦截,避免每个JSP重复编写。
对象作用域对比分析
// 设置不同作用域的数据
pageContext.setAttribute("pageVar", "仅本页可见");
request.setAttribute("reqVar", "一次请求内有效");
session.setAttribute("sessVar", "用户会话期间有效");
application.setAttribute("appVar", "全局共享");
flowchart LR
subgraph Scope_Hierarchy
direction TB
A[page] --> B[request]
B --> C[session]
C --> D[application]
end
style A fill:#f9f,stroke:#333
style B fill:#ff9,stroke:#333
style C fill:#9ff,stroke:#333
style D fill:#9f9,stroke:#333
流程图说明 :作用域从小到大依次增强。
page生命周期最短,随页面结束销毁;application伴随整个Web应用启动至关闭,常用于缓存全局配置。
2.1.3 EL表达式与JSTL标签库的结合使用
Expression Language(EL)与JSTL(JSP Standard Tag Library)的组合,显著提升了JSP的可读性和安全性,推荐替代传统Java脚本。
EL表达式语法
EL用于访问作用域中的数据:
<p>用户名:${sessionScope.username}</p>
<p>请求参数:${param.email}</p>
<p>集合长度:${fn:length(diaryList)}</p>
${}语法提取值;sessionScope指定作用域;param访问请求参数;fn:是JSTL函数前缀,需提前导入。
JSTL核心标签应用
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<c:if test="${not empty diaryList}">
<ul>
<c:forEach items="${diaryList}" var="diary" varStatus="status">
<li>
[${status.index + 1}]
<strong>${diary.title}</strong>
(${fn:substring(diary.createTime, 0, 10)})
</li>
</c:forEach>
</ul>
</c:if>
<c:otherwise>
<p>暂无日记记录。</p>
</c:otherwise>
代码逻辑分析 :
-<c:if>判断列表非空;
-<c:forEach>遍历日记集合,var表示当前项,varStatus提供索引信息;
-${fn:substring}截取日期部分,提升显示简洁性;
-<c:otherwise>处理空数据场景。优势说明 :完全脱离Java脚本,结构清晰,易于前端人员协作维护。
| 使用方式 | 可读性 | 安全性 | 维护成本 |
|---|---|---|---|
| Java Scriptlet | 差 | 低(易XSS) | 高 |
| EL + JSTL | 优 | 高(自动转义) | 低 |
推荐在所有展示型页面中全面采用EL+JSTL方案,仅在复杂逻辑处辅以少量脚本。
2.2 JSP在日记本系统中的实际应用场景
JSP在日记本系统的多个关键界面中发挥重要作用,涵盖用户认证、内容展示与个性化呈现等方面。通过合理的标签组织与数据绑定,实现了高度动态化的前端体验。
2.2.1 用户登录界面的动态数据绑定
登录页需接收用户输入并反馈错误信息,典型实现如下:
<form action="LoginServlet" method="post">
<input type="text" name="username" placeholder="请输入用户名"
value="${param.username}" />
<input type="password" name="password" placeholder="请输入密码" />
<c:if test="${not empty errorMsg}">
<div class="error">${errorMsg}</div>
</c:if>
<button type="submit">登录</button>
</form>
逻辑分析 :
-value="${param.username}"保留上次输入,改善用户体验;
-${errorMsg}来自Servlet设置的request属性,提示“账号不存在”等;
- 使用POST提交防止密码暴露于URL。
错误消息来源示例(Servlet中设置)
request.setAttribute("errorMsg", "用户名或密码错误");
RequestDispatcher rd = request.getRequestDispatcher("login.jsp");
rd.forward(request, response);
参数说明:
setAttribute()将数据注入request作用域,forward()保持请求链路,使JSP能访问该属性。
2.2.2 日记列表页的循环输出与分页展示
面对大量日记记录,必须实现分页机制以避免页面卡顿。
<div class="diary-list">
<c:forEach items="${diaryPage.list}" var="diary">
<article>
<h4><a href="DiaryViewServlet?id=${diary.id}">${diary.title}</a></h4>
<p>${fn:substring(diary.content, 0, 100)}...</p>
<small>发布于:${diary.createTime}</small>
</article>
</c:forEach>
</div>
<!-- 分页导航 -->
<div class="pagination">
<c:if test="${diaryPage.currentPage > 1}">
<a href="?page=${diaryPage.currentPage - 1}">上一页</a>
</c:if>
<span>第 ${diaryPage.currentPage} 页 / 共 ${diaryPage.totalPages} 页</span>
<c:if test="${diaryPage.currentPage < diaryPage.totalPages}">
<a href="?page=${diaryPage.currentPage + 1}">下一页</a>
</c:if>
</div>
参数说明 :
-diaryPage是封装了分页信息的JavaBean,包含list、currentPage、totalPages等字段;
-fn:substring限制摘要长度,防止过长文本影响布局;
- 分页链接通过URL参数传递页码,由Servlet解析后查询对应数据。
分页SQL构建(DAO层参考)
SELECT id, title, content, create_time
FROM t_diary
WHERE user_id = ? AND is_deleted = 0
ORDER BY create_time DESC
LIMIT ?, ?
参数:第一个
?为用户ID,第二、三个?分别为起始行偏移量((page-1)*size)和每页条数。
2.2.3 个人中心信息的条件判断显示
根据用户角色或状态差异化展示内容:
<div class="profile">
<h2>个人中心</h2>
<p>邮箱:${user.email}</p>
<c:choose>
<c:when test="${user.vipLevel > 0}">
<p style="color: gold;">VIP等级:${user.vipLevel}</p>
</c:when>
<c:otherwise>
<p><a href="upgrade_vip.jsp">升级为VIP会员</a></p>
</c:otherwise>
</c:choose>
<c:if test="${user.loginCount > 10}">
<p>🎖️ 您已是资深用户!</p>
</c:if>
</div>
逻辑分析 :
-<c:choose>实现多分支判断;
-${user.xxx}通过EL访问JavaBean属性;
- 条件渲染增强用户体验,激励功能转化。
2.3 JSP页面性能优化策略
尽管JSP已被Thymeleaf、Freemarker等模板引擎逐步取代,但在遗留系统或教学项目中仍广泛使用。采取有效优化措施可显著提升响应效率。
2.3.1 减少Java脚本嵌入,提升可读性
过度使用 <% %> 会导致代码混乱且难以调试。应优先使用EL+JSTL。
✅ 推荐做法 :
<c:set var="isAdmin" value="${sessionScope.role == 'admin'}"/>
<c:if test="${isAdmin}">
<a href="admin/dashboard.jsp">进入后台</a>
</c:if>
❌ 不推荐做法 :
<%
String role = (String)session.getAttribute("role");
if ("admin".equals(role)) {
%>
<a href="admin/dashboard.jsp">进入后台</a>
<%
}
%>
前者逻辑清晰,后者混合HTML与Java,易出错且不利于维护。
2.3.2 合理利用include指令复用公共组件(头部、侧栏)
使用 <%@ include %> 统一管理公共模块:
<!-- header.jsp -->
<header>
<nav>
<a href="index.jsp">首页</a>
<a href="diary_list.jsp">我的日记</a>
<span>欢迎 ${username}</span>
</nav>
</header>
<!-- diary_list.jsp -->
<%@ include file="header.jsp" %>
<main>...日记内容...</main>
<%@ include file="footer.jsp" %>
优点:修改一次即可全局生效;缺点:无法传参,灵活性差。对于需要动态内容的场景,建议改用
<jsp:include>。
2.3.3 缓存机制引入减少重复渲染开销
可通过Filter或应用级缓存预加载频繁访问的内容。
// 在Filter中缓存热门日记
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
ServletContext context = filterConfig.getServletContext();
List<Diary> hotDiaries = (List<Diary>) context.getAttribute("hotDiaries");
if (hotDiaries == null) {
hotDiaries = diaryService.getTopViews(5);
context.setAttribute("hotDiaries", hotDiaries);
}
chain.doFilter(req, res);
}
<!-- sidebar.jsp -->
<h4>热门日记</h4>
<ul>
<c:forEach items="${applicationScope.hotDiaries}" var="d">
<li><a href="view?id=${d.id}">${d.title}</a></li>
</c:forEach>
</ul>
利用
application作用域实现内存缓存,降低数据库压力。
2.4 JSP与HTML/CSS/JS协同开发实践
现代Web开发强调前后端协作,JSP虽属服务端模板,但仍需与前端技术深度融合。
2.4.1 响应式布局在JSP中的实现方式
集成Bootstrap框架实现自适应设计:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<div class="container-fluid">
<div class="row">
<div class="col-md-3 d-none d-md-block">侧边栏</div>
<div class="col-md-9">
<jsp:include page="diary_content.jsp"/>
</div>
</div>
</div>
使用
col-md-*实现栅格布局,d-none d-md-block控制小屏隐藏侧栏。
2.4.2 JavaScript异步提交表单提升用户体验
结合Ajax实现无刷新提交:
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<form id="diaryForm">
<input type="text" name="title" required />
<textarea name="content" required></textarea>
<button type="submit">保存日记</button>
</form>
<script>
$("#diaryForm").submit(function(e){
e.preventDefault();
$.post("DiaryAddServlet", $(this).serialize(), function(res){
if(res.success){
alert("保存成功!");
location.href = "diary_list.jsp";
} else {
alert("保存失败:" + res.msg);
}
}, "json");
});
</script>
逻辑分析 :
-e.preventDefault()阻止默认提交;
-$(this).serialize()序列化表单数据;
-$.post发送AJAX请求;
- 返回JSON格式{success: true, msg: "ok"};后端Servlet需设置
response.setContentType("application/json")并输出JSON字符串。
示例响应输出(Servlet中)
response.setContentType("application/json;charset=UTF-8");
PrintWriter pw = response.getWriter();
pw.write("{\"success\":true,\"msg\":\"保存成功\"}");
pw.flush();
此模式显著提升交互流畅度,是现代JSP应用的标准实践方向。
3. Servlet请求处理与业务逻辑控制
在JavaWeb日记本系统中,Servlet作为MVC架构中的 控制层核心组件 ,承担着接收HTTP请求、解析参数、调用业务逻辑、转发响应结果的关键职责。其本质是运行在Web服务器上的Java类,通过实现 javax.servlet.Servlet 接口或继承 HttpServlet 抽象类来响应客户端的请求。与JSP主要负责视图渲染不同,Servlet专注于流程调度和数据处理,是连接前端页面与后端服务的“中枢神经”。深入理解Servlet的工作机制,不仅有助于构建高效稳定的Web应用,还能为后续引入Spring MVC等现代框架打下坚实基础。
3.1 Servlet生命周期与核心方法解析
Servlet的执行过程并非简单的函数调用,而是一个具有明确阶段划分的生命周期模型。该模型由Web容器(如Tomcat)管理,确保资源的合理分配与释放。整个生命周期可分为三个关键阶段:初始化、服务处理和销毁。每个阶段对应一个核心方法—— init() 、 service() 和 destroy() ,它们共同构成了Servlet运行时的行为骨架。
3.1.1 init()、service()、destroy()执行流程分析
当Web应用首次被部署或第一个请求到达某个Servlet时,容器会创建该Servlet的实例,并立即调用其 init(ServletConfig config) 方法。此方法在整个生命周期中 仅执行一次 ,通常用于加载配置文件、建立数据库连接池、初始化缓存等耗时操作。由于这些操作无需重复进行,将其放在 init() 中可显著提升性能。
public class DiaryInitServlet extends HttpServlet {
private DataSource dataSource;
@Override
public void init() throws ServletException {
try {
// 初始化Druid连接池
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:mysql://localhost:3306/diary_db");
druidDataSource.setUsername("root");
druidDataSource.setPassword("password");
druidDataSource.setInitialSize(5);
druidDataSource.setMaxActive(20);
this.dataSource = druidDataSource;
getServletContext().setAttribute("dataSource", dataSource);
} catch (Exception e) {
throw new ServletException("Failed to initialize data source", e);
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setAttribute("msg", "System initialized successfully.");
req.getRequestDispatcher("/index.jsp").forward(req, resp);
}
@Override
public void destroy() {
if (dataSource instanceof DruidDataSource) {
((DruidDataSource) dataSource).close(); // 释放连接池资源
}
}
}
代码逻辑逐行解读:
- 第4行:重写init()方法,在Servlet实例化后自动执行。
- 第7-15行:配置Druid连接池参数并设置全局数据源,存储到ServletContext作用域中供其他组件共享使用。
- 第29行:destroy()方法中主动关闭连接池,防止内存泄漏,体现良好的资源管理习惯。
这一设计模式体现了典型的“懒加载+预初始化”思想:避免每次请求都重建资源,同时保证系统启动时完成必要准备。以下流程图展示了完整的生命周期流转:
graph TD
A[客户端发起请求] --> B{Servlet是否已加载?}
B -- 否 --> C[容器创建Servlet实例]
C --> D[调用init()方法初始化]
D --> E[进入就绪状态]
B -- 是 --> E
E --> F[调用service()处理请求]
F --> G[根据HTTP方法分发doGet/doPost]
G --> H[生成响应返回客户端]
H --> I{应用停止或卸载?}
I -- 是 --> J[调用destroy()释放资源]
J --> K[实例被GC回收]
从图中可见, init() 和 destroy() 分别位于生命周期的两端,中间的 service() 则可能被多次调用。这种设计使得Servlet具备高并发处理能力的同时保持低开销。
此外,值得注意的是, 默认情况下Servlet是单例模式运行的 ,即多个线程共享同一个实例。因此在编写代码时必须注意线程安全问题,例如避免在类成员变量中保存用户特定数据。
| 方法 | 执行次数 | 触发时机 | 典型用途 |
|---|---|---|---|
init() |
1次 | 实例化后立即执行 | 资源初始化、配置读取 |
service() |
N次 | 每次HTTP请求到来时 | 请求分发、业务逻辑调度 |
destroy() |
1次 | Web应用停止或重新加载时 | 关闭连接、清理临时文件、注销监听 |
该表格总结了三大方法的核心特性,帮助开发者在实际开发中做出合理的设计决策。
3.1.2 doGet()与doPost()方法的选择与区别
尽管 service() 是总入口,但在大多数场景下开发者更常直接重写 doGet() 和 doPost() 方法。二者最根本的区别在于所支持的HTTP请求类型及其语义含义。
doGet() 用于处理GET请求,特点是:
- 参数附加在URL后(查询字符串),长度受限(一般不超过2KB)
- 可被浏览器缓存、收藏、历史记录保存
- 幂等性良好,适合获取数据而非修改状态
doPost() 用于处理POST请求,特点包括:
- 参数封装在请求体中,理论上无长度限制
- 不会被缓存,安全性更高
- 适用于提交敏感信息或大量数据
以日记本系统的登录功能为例:
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 直接跳转至登录页,不涉及数据提交
request.getRequestDispatcher("/WEB-INF/pages/login.jsp").forward(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
UserService userService = new UserServiceImpl();
User user = userService.login(username, password);
if (user != null) {
request.getSession().setAttribute("currentUser", user);
response.sendRedirect(request.getContextPath() + "/diary/list");
} else {
request.setAttribute("error", "用户名或密码错误!");
request.getRequestDispatcher("/WEB-INF/pages/login.jsp").forward(request, response);
}
}
}
参数说明与逻辑分析:
-@WebServlet("/login"):注解方式映射URL路径,替代web.xml配置。
-doGet()仅做页面跳转,符合RESTful原则中“获取资源”的语义。
-doPost()处理表单提交,调用UserService完成认证,成功则写入Session并重定向,失败则带回错误信息重新渲染页面。
- 使用sendRedirect()实现重定向,避免刷新导致重复提交;而forward()用于内部转发,保留请求上下文。
选择正确的HTTP方法不仅是技术实现问题,更是系统设计规范性的体现。如下表所示:
| 特性 | GET | POST |
|---|---|---|
| 数据传输位置 | URL参数 | 请求体 |
| 安全性 | 低(明文暴露) | 较高(不可见) |
| 缓存支持 | 支持 | 不支持 |
| 幂等性 | 是 | 否 |
| 典型应用场景 | 查询列表、查看详情 | 登录、注册、发布内容 |
实践中应严格遵守“ 安全操作用GET,变更操作用POST ”的原则,这不仅能提升用户体验,也为未来API扩展提供一致性保障。
3.2 在日记本系统中构建控制器链路
在真实项目中,单一Servlet难以支撑复杂业务流。需要通过合理的控制器链路设计,将用户请求精准路由到对应的处理模块,并在各层之间高效传递数据。这一过程涉及请求接收、业务委托、视图跳转等多个环节,构成完整的MVC闭环。
3.2.1 LoginServlet处理用户认证请求
用户登录是系统安全的第一道防线。 LoginServlet 不仅要完成身份验证,还需维护会话状态、处理异常分支,并与前端形成良好的交互反馈机制。
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private UserService userService = new UserServiceImpl();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String username = req.getParameter("username").trim();
String password = req.getParameter("password");
// 参数校验
if (username.isEmpty() || password == null || password.isEmpty()) {
req.setAttribute("error", "请输入完整的登录信息!");
req.getRequestDispatcher("/WEB-INF/pages/login.jsp").forward(req, resp);
return;
}
try {
User user = userService.login(username, password);
if (user != null) {
HttpSession session = req.getSession();
session.setAttribute("currentUser", user);
session.setMaxInactiveInterval(30 * 60); // 30分钟超时
String redirect = req.getParameter("redirect");
resp.sendRedirect(redirect != null ? redirect : "/diary/list");
} else {
req.setAttribute("error", "账户名或密码错误!");
req.getRequestDispatcher("/WEB-INF/pages/login.jsp").forward(req, resp);
}
} catch (ServiceException e) {
log("Login failed for user: " + username, e);
req.setAttribute("error", "系统繁忙,请稍后再试。");
req.getRequestDispatcher("/WEB-INF/pages/login.jsp").forward(req, resp);
}
}
}
代码解析:
- 第8行:从请求中提取用户名和密码,注意trim()去除空格以防误判。
- 第12-16行:前置校验,防止空值攻击或无效输入。
- 第20行:调用UserService.login()执行认证逻辑,此处已解耦具体实现。
- 第23行:成功登录后创建Session并设置最大非活动时间,增强安全性。
- 第26行:支持跳转来源追踪(如未登录访问编辑页后自动回跳),提升体验。
- 第31行:捕获自定义业务异常,统一降级处理,避免敏感信息泄露。
该Servlet体现了典型的“ 防御式编程 ”思想:对所有外部输入进行校验,对异常情况进行兜底处理,确保系统稳定可靠。
3.2.2 DiaryAddServlet接收富文本内容并转发至服务层
日记添加功能面临更大的挑战:需处理包含HTML标签的富文本内容、图片上传路径、时间戳记录等。 DiaryAddServlet 作为入口,需协调多个组件协同工作。
@WebServlet("/diary/add")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024, // 1MB
maxFileSize = 1024 * 1024 * 5, // 5MB
maxRequestSize = 1024 * 1024 * 10 // 10MB
)
public class DiaryAddServlet extends HttpServlet {
private DiaryService diaryService = new DiaryServiceImpl();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
String title = req.getParameter("title");
String content = req.getParameter("content");
User currentUser = (User) req.getSession().getAttribute("currentUser");
if (currentUser == null) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "请先登录");
return;
}
Diary diary = new Diary();
diary.setTitle(title);
diary.setContent(content);
diary.setUserId(currentUser.getId());
diary.setCreateTime(new Timestamp(System.currentTimeMillis()));
diary.setUpdateTime(diary.getCreateTime());
try {
boolean success = diaryService.addDiary(diary);
if (success) {
resp.sendRedirect("/diary/list?status=created");
} else {
req.setAttribute("error", "保存失败,请重试");
req.getRequestDispatcher("/WEB-INF/pages/diary/edit.jsp").forward(req, resp);
}
} catch (Exception e) {
log("Failed to add diary", e);
req.setAttribute("error", "系统异常:" + e.getMessage());
req.getRequestDispatcher("/WEB-INF/pages/diary/edit.jsp").forward(req, resp);
}
}
}
关键点说明:
-@MultipartConfig:启用文件上传支持,限定单个文件及整体请求大小。
-setCharacterEncoding("UTF-8"):显式设置编码,防止中文乱码。
- Session中获取当前用户ID,实现权限绑定。
- 构造Diary对象并填充元数据(创建/更新时间)。
- 成功后使用重定向防止刷新重复提交,失败则保留原数据以便修改。
3.2.3 使用RequestDispatcher实现页面跳转与数据传递
在Servlet内部跳转时, RequestDispatcher 提供了两种模式: forward() 和 include() 。前者完全移交控制权,后者则允许组合多个片段。
// 示例:组合头部、主体、尾部三部分生成完整页面
RequestDispatcher header = req.getRequestDispatcher("/common/header.jsp");
RequestDispatcher body = req.getRequestDispatcher("/pages/diary/detail.jsp");
RequestDispatcher footer = req.getRequestDispatcher("/common/footer.jsp");
resp.setContentType("text/html;charset=UTF-8");
header.include(req, resp);
body.include(req, resp);
footer.include(req, resp);
这种方式实现了模板化布局,减少重复代码。对比 forward() 与 include() 的行为差异:
| 特性 | forward() | include() |
|---|---|---|
| 响应状态码 | 继承原始设置 | 可多次设置,最终以最后一次为准 |
| 请求属性共享 | 是 | 是 |
| URL地址栏变化 | 否(服务器内部跳转) | 否 |
| 是否终止后续输出 | 是 | 否,可继续写入 |
| 典型用途 | 错误处理、权限拦截 | 页面组件复用 |
结合过滤器(Filter)机制,可进一步实现统一的字符编码、登录检查等功能,形成完整的请求处理管道。
sequenceDiagram
participant Browser
participant LoginServlet
participant UserService
participant Database
Browser->>LoginServlet: POST /login
activate LoginServlet
LoginServlet->>UserService: login(username, password)
activate UserService
UserService->>Database: SELECT * FROM t_user WHERE ...
activate Database
Database-->>UserService: 返回用户记录
deactivate Database
UserService-->>LoginServlet: 返回User对象或null
deactivate UserService
alt 登录成功
LoginServlet->>Browser: 302 Redirect to /diary/list
else 失败
LoginServlet->>Browser: 200 OK + 错误提示页面
end
deactivate LoginServlet
该序列图清晰地展示了从用户提交到数据库查询再到响应返回的完整链路,体现了分层协作的设计理念。
3.3 多模块请求映射与路径管理
随着功能增多,URL映射管理成为维护性的重要考量因素。传统 web.xml 配置虽稳定但繁琐,而注解驱动的方式则更加灵活直观。
3.3.1 web.xml中servlet-mapping配置规范
早期JavaEE标准要求在 WEB-INF/web.xml 中显式声明所有Servlet及其映射关系:
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.example.web.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/auth/login</url-pattern>
</servlet-mapping>
优点是集中管理、便于审查;缺点是冗长且易出错。尤其当项目包含数十个Servlet时,维护成本急剧上升。
3.3.2 注解方式(@WebServlet)替代XML配置提升灵活性
Servlet 3.0起支持 @WebServlet 注解,极大简化了配置:
@WebServlet({
"/diary/add",
"/note/create"
})
public class DiaryAddServlet extends HttpServlet { ... }
支持多路径映射、通配符匹配(如 /api/* )、精确优先级规则等高级特性。同时可通过IDE直接导航到对应类,提升开发效率。
| 配置方式 | 可读性 | 修改便捷性 | 环境适配性 | 推荐程度 |
|---|---|---|---|---|
| web.xml | 中 | 低 | 高 | ★★☆☆☆ |
| @WebServlet | 高 | 高 | 中 | ★★★★★ |
现代开发中强烈推荐使用注解方式,尤其在微服务或快速迭代项目中优势明显。
3.4 业务逻辑封装与Service层设计
将核心业务逻辑从Servlet中剥离出来,形成独立的Service层,是实现松耦合、高内聚的关键步骤。
3.4.1 UserService与DiaryService接口定义
采用面向接口编程,定义清晰契约:
public interface UserService {
User login(String username, String password) throws ServiceException;
boolean register(User user) throws DuplicateUsernameException;
}
public interface DiaryService {
List<Diary> getDiariesByUserId(int userId);
boolean addDiary(Diary diary);
boolean updateDiary(Diary diary);
boolean deleteDiary(int id);
}
接口隔离关注点,便于单元测试和Mock模拟。
3.4.2 实现类中调用DAO完成具体操作
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();
@Override
public User login(String username, String password) {
String hashedPassword = HashUtil.md5(password + "SALT_2024");
return userDao.findUserByUsernameAndPassword(username, hashedPassword);
}
}
DAO层专注数据存取,Service层专注事务编排,形成清晰的责任边界。
3.4.3 异常统一捕获与错误页面跳转机制
通过 try-catch 捕获业务异常,并转发至通用错误页:
req.setAttribute("errorMessage", "操作失败:" + e.getMessage());
req.getRequestDispatcher("/error.jsp").forward(req, resp);
或利用 web.xml 配置全局异常处理器:
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/error.jsp</location>
</error-page>
实现统一的用户体验和日志追踪能力。
综上所述,Servlet不仅是技术组件,更是架构思想的载体。通过科学设计其生命周期使用、请求链路组织、路径管理和分层解耦,才能构建出健壮、可维护的JavaWeb应用。
4. MySQL数据库设计与表结构实现
在JavaWeb日记本系统的开发中,数据库作为整个系统的核心存储层,承担着用户信息、日记内容以及各类操作记录的持久化任务。一个合理、高效且具备扩展性的数据库设计方案,不仅能够提升系统整体性能,还能为后续的功能迭代和数据维护提供坚实基础。本章节将围绕MySQL数据库的设计原则与实际应用展开深入探讨,从需求分析到ER模型构建,再到表结构优化与SQL脚本部署,全面解析如何为日记本系统打造稳定可靠的数据支撑体系。
4.1 数据库需求分析与ER模型构建
在进行数据库设计之前,必须首先明确系统的业务需求,并据此抽象出核心实体及其相互关系。对于日记本系统而言,主要涉及两个关键实体: 用户(User) 和 日记(Diary) 。用户是系统的使用主体,负责注册、登录并创建自己的日记内容;而日记则是用户行为的结果产物,包含标题、正文、创建时间等元数据。通过对这两个实体的属性提取与关联建模,可以形成初步的ER(Entity-Relationship)模型,为后续的表结构设计打下理论基础。
4.1.1 用户表(t_user)字段设计(id、username、password_hash等)
用户表 t_user 是系统权限控制和身份认证的基础,其字段设计需兼顾安全性、可扩展性与规范性。以下是该表的核心字段定义:
| 字段名 | 类型 | 是否主键 | 是否非空 | 默认值 | 说明 |
|---|---|---|---|---|---|
| id | BIGINT UNSIGNED | 是 | 是 | 自增 | 用户唯一标识,主键自增 |
| username | VARCHAR(50) | 否 | 是 | 无 | 登录用户名,唯一索引 |
| password_hash | CHAR(64) | 否 | 是 | 无 | SHA-256加密后的密码哈希值 |
| salt | CHAR(32) | 否 | 是 | 无 | 密码加盐随机字符串 |
| VARCHAR(100) | 否 | 否 | NULL | 邮箱地址,可用于找回密码 | |
| create_time | DATETIME | 否 | 是 | CURRENT_TIMESTAMP | 账户创建时间 |
| update_time | DATETIME | 否 | 是 | CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 最后更新时间 |
| is_deleted | TINYINT(1) | 否 | 是 | 0 | 软删除标记:0-正常,1-已删除 |
该设计遵循最小冗余原则,避免重复字段。其中 password_hash 使用SHA-256或BCrypt算法配合独立盐值(salt)存储,杜绝明文密码风险; is_deleted 字段用于实现软删除机制,保障数据可追溯性。此外,对 username 建立唯一索引,防止重名注册。
CREATE TABLE t_user (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password_hash CHAR(64) NOT NULL COMMENT '密码哈希',
salt CHAR(32) NOT NULL COMMENT '加盐值',
email VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除: 0-否, 1-是'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
代码逻辑逐行解读:
- 第1行:声明创建表语句,表名为
t_user。- 第2~8行:定义各字段类型及约束条件,
AUTO_INCREMENT实现主键自动增长,UNIQUE确保用户名唯一。- 第9~10行:设置默认时间和自动更新机制,利用MySQL内置功能减少Java层处理负担。
- 第11行:添加软删除标志位,便于后期恢复或审计。
ENGINE=InnoDB支持事务与外键,CHARSET=utf8mb4兼容中文及emoji字符。
4.1.2 日记表(t_diary)结构规划(title、content、create_time、update_time)
日记表 t_diary 存储用户撰写的全部日记内容,是系统中最频繁读写的数据表之一。考虑到富文本内容可能较大,应合理选择字段类型以平衡存储效率与查询性能。
| 字段名 | 类型 | 是否主键 | 是否非空 | 默认值 | 说明 |
|---|---|---|---|---|---|
| id | BIGINT UNSIGNED | 是 | 是 | 自增 | 日记ID |
| user_id | BIGINT UNSIGNED | 否 | 是 | 无 | 外键关联t_user.id |
| title | VARCHAR(200) | 否 | 是 | 无 | 日记标题 |
| content | LONGTEXT | 否 | 是 | 无 | 富文本内容,支持HTML |
| create_time | DATETIME | 否 | 是 | CURRENT_TIMESTAMP | 创建时间 |
| update_time | DATETIME | 否 | 是 | CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 修改时间 |
| is_public | TINYINT(1) | 否 | 是 | 0 | 是否公开:0-私密,1-公开 |
| view_count | INT UNSIGNED | 否 | 是 | 0 | 浏览次数统计 |
CREATE TABLE t_diary (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL COMMENT '所属用户ID',
title VARCHAR(200) NOT NULL COMMENT '日记标题',
content LONGTEXT NOT NULL COMMENT '日记内容(富文本)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
is_public TINYINT(1) DEFAULT 0 COMMENT '是否公开: 0-私密, 1-公开',
view_count INT UNSIGNED DEFAULT 0 COMMENT '浏览次数',
FOREIGN KEY (user_id) REFERENCES t_user(id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
代码逻辑逐行解读:
- 第7行:通过
FOREIGN KEY明确建立与t_user的一对多关系,确保数据一致性。ON DELETE CASCADE表示当用户被删除时,其所有日记也一并清除,符合业务逻辑。LONGTEXT类型支持最大约4GB文本内容,适合富文本编辑器输出的HTML结构。view_count字段用于未来实现热度排行功能,初始设为0。
4.1.3 表间关系设定与外键约束合理性探讨
在ER模型中,用户与日记之间是一对多的关系,即一个用户可撰写多篇日记,每篇日记仅归属于一个用户。这种关系通过外键 user_id 在 t_diary 中引用 t_user.id 来实现。外键的存在不仅能保证参照完整性,还可借助数据库层面的级联操作简化业务逻辑。
erDiagram
t_user ||--o{ t_diary : "has"
t_user {
BIGINT id PK
VARCHAR username
CHAR password_hash
DATETIME create_time
}
t_diary {
BIGINT id PK
BIGINT user_id FK
VARCHAR title
LONGTEXT content
DATETIME create_time
INT view_count
}
上述Mermaid流程图清晰展示了实体之间的“一对多”关系。
t_user为主实体,t_diary为从实体,箭头方向表示归属关系。外键user_id不仅是连接桥梁,也是权限校验的关键依据——在查询某条日记时,必须验证当前登录用户的ID是否与其user_id匹配,从而实现数据隔离。
尽管外键增强了数据一致性,但在高并发场景下也可能带来锁竞争问题。因此,在大型分布式系统中常选择应用层维护关系。但对于本中小型项目而言,启用外键更有利于降低开发复杂度,提升数据安全性。
4.2 高效表结构设计原则应用
数据库性能的优劣往往取决于初期的表结构设计是否科学。即使后期通过索引优化或分库分表手段补救,也无法完全弥补结构性缺陷。因此,在设计阶段就应遵循规范化、字段选型合理、索引策略得当等基本原则,确保系统具备良好的可伸缩性和响应能力。
4.2.1 字段类型选择(VARCHAR vs TEXT,DATETIME精度控制)
字段类型的选取直接影响存储空间占用与查询效率。例如,在 t_diary 中,若将 title 定义为 TEXT 类型,则会浪费大量内存资源,因为标题通常不超过200字符。相反,使用 VARCHAR(200) 更为合适,既能满足长度需求,又可在内存中高效处理。
同样地, content 字段若采用 TEXT ,最大仅支持65KB,不足以容纳长篇幅富文本内容。因此选用 LONGTEXT 可支持高达4GB的内容存储,虽代价是增加I/O开销,但这是必要权衡。
关于时间字段,MySQL提供了多种时间类型:
| 类型 | 精度 | 范围 | 是否支持毫秒 |
|---|---|---|---|
| DATE | 天 | 1000-9999年 | 否 |
| DATETIME | 秒 | 1000-9999年 | 否(v5.6前) |
| DATETIME(3) | 毫秒 | 同上 | 是 |
| TIMESTAMP | 秒 | 1970-2038年 | 否 |
本系统采用 DATETIME 类型,默认精确到秒即可满足日记记录的时间粒度要求。若未来需要记录操作日志的毫秒级时间戳,可升级为 DATETIME(3) 。同时,使用 DEFAULT CURRENT_TIMESTAMP 和 ON UPDATE CURRENT_TIMESTAMP 特性,无需在Java代码中手动赋值,减少出错概率。
4.2.2 主键自增与索引建立提升查询效率
主键设计方面,统一采用 BIGINT UNSIGNED AUTO_INCREMENT 类型,支持最多约184亿条记录,远超普通Web应用生命周期内的数据量。相较于UUID主键,自增ID具有插入快、聚集索引连续的优点,特别适合以时间为序展示日记列表的场景。
为提高常用查询效率,应在高频检索字段上建立适当索引:
-- 在用户名上建立唯一索引(已由UNIQUE约束隐式创建)
ALTER TABLE t_user ADD UNIQUE INDEX idx_username (username);
-- 在日记表上为user_id建立普通索引,加速按用户查询
ALTER TABLE t_diary ADD INDEX idx_user_id (user_id);
-- 为公开日记创建复合索引,支持按时间排序+过滤
ALTER TABLE t_diary ADD INDEX idx_public_time (is_public, create_time DESC);
参数说明:
idx_username:用于登录时快速查找用户,避免全表扫描。idx_user_id:当用户查看“我的日记”时,根据user_id快速定位所有记录。idx_public_time:支持首页“热门公开日记”按时间倒序展示,复合索引可覆盖WHERE和ORDER BY条件,显著提升性能。
需要注意的是,索引并非越多越好。每增加一个索引都会拖慢写入速度,并占用额外磁盘空间。因此应结合实际查询模式,只对关键路径建立索引。
4.2.3 软删除标记(is_deleted)代替物理删除保障数据安全
传统做法中,删除操作直接执行 DELETE FROM t_diary WHERE id=? ,导致数据永久丢失。而在真实业务环境中,误删、法律合规、数据分析等需求要求我们保留历史痕迹。为此,引入软删除机制成为最佳实践。
通过添加 is_deleted TINYINT(1) 字段,默认为0表示有效,设置为1表示已删除。查询时始终附加条件 AND is_deleted = 0 ,使已删数据不再显示。后台管理界面则可通过切换状态实现“回收站”功能。
-- 查询未删除的日记
SELECT * FROM t_diary
WHERE user_id = ? AND is_deleted = 0
ORDER BY create_time DESC;
-- 标记删除(非物理删除)
UPDATE t_diary SET is_deleted = 1 WHERE id = ? AND user_id = ?;
此方式实现了逻辑隔离,同时保留了数据完整性。若将来支持“彻底清理”功能,可定期归档并物理删除
is_deleted=1且超过30天的数据,做到空间回收与安全留存的平衡。
4.3 SQL脚本编写与初始化部署
完成表结构设计后,需将其固化为可重复执行的SQL脚本,以便团队协作、持续集成与生产环境一键部署。
4.3.1 创建数据库与字符集设定(UTF-8支持中文)
为确保中文内容正确存储,必须显式指定字符集为 utf8mb4 ,兼容标准UTF-8并支持Emoji表情符号。
-- 创建数据库
CREATE DATABASE IF NOT EXISTS diary_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用数据库
USE diary_system;
-- 执行建表语句(省略)
-- ...
utf8mb4_unicode_ci排序规则基于Unicode标准,比utf8mb4_general_ci更准确,尤其适用于多语言混合环境。
4.3.2 插入测试数据用于功能验证
为方便前后端联调,预先插入若干测试数据:
-- 插入测试用户
INSERT INTO t_user (username, password_hash, salt, email) VALUES
('alice', 'e7f6c011776e8db7cd330b54174fd76f', 'salty123', 'alice@example.com'),
('bob', 'd7ebedc2a82d2be84e56989d9e6099dda0263826', 'pepper456', 'bob@example.com');
-- 插入测试日记
INSERT INTO t_diary (user_id, title, content, is_public) VALUES
(1, '第一天开始写日记', '<p>今天天气很好,我决定开始记录生活点滴。</p>', 1),
(2, '工作心得', '<p>本周完成了项目重构,收获颇丰。</p>', 0);
这些数据可用于前端页面渲染、分页测试、权限校验等功能验证。
4.3.3 导出完整schema.sql供一键部署使用
最终将所有DDL语句整合为 schema.sql 文件,内容包括:
- 创建数据库
- 建表语句
- 索引定义
- 初始数据插入
开发者只需运行:
mysql -u root -p < schema.sql
即可完成环境初始化,极大提升部署效率。
4.4 JDBC连接池配置与DAO模式实现
数据库访问效率直接影响系统响应速度。直接使用原始JDBC连接会导致频繁创建销毁连接,造成资源浪费。为此,引入Druid连接池进行统一管理,并封装通用DAO基类,提升代码复用性与安全性。
4.4.1 使用Druid连接池提高数据库访问性能
Druid是阿里巴巴开源的高性能数据库连接池,具备监控、防御SQL注入、日志追踪等企业级特性。
<!-- pom.xml 引入Druid依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
配置文件 druid.properties :
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/diary_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4
username=root
password=your_password
initialSize=5
minIdle=5
maxActive=20
maxWait=60000
timeBetweenEvictionRunsMillis=60000
minEvictableIdleTimeMillis=300000
validationQuery=SELECT 1
testWhileIdle=true
testOnBorrow=false
testOnReturn=false
poolPreparedStatements=true
maxPoolPreparedStatementPerConnectionSize=20
参数说明:
initialSize: 初始化连接数maxActive: 最大活跃连接数maxWait: 获取连接最大等待时间(毫秒)validationQuery: 用于检测连接有效性的SQLpoolPreparedStatements: 开启预编译语句缓存,提升批量操作性能
4.4.2 BaseDao抽象类封装通用CRUD操作
创建抽象基类 BaseDao<T> ,封装基本的增删改查方法:
public abstract class BaseDao<T> {
protected QueryRunner queryRunner = new QueryRunner(DruidUtils.getDataSource());
public int insert(String sql, Object... params) throws SQLException {
return queryRunner.update(sql, params);
}
public int delete(String sql, Object... params) throws SQLException {
return queryRunner.update(sql, params);
}
public int update(String sql, Object... params) throws SQLException {
return queryRunner.update(sql, params);
}
public T findOne(String sql, ResultSetHandler<T> rsh, Object... params) throws SQLException {
return queryRunner.query(sql, rsh, params);
}
public List<T> findAll(String sql, ResultSetHandler<List<T>> rsh, Object... params) throws SQLException {
return queryRunner.query(sql, rsh, params);
}
}
使用Apache Commons DbUtils简化JDBC模板代码,避免手动关闭资源。
QueryRunner线程安全,可作为实例变量复用。
4.4.3 PreparedStatement防止SQL注入攻击
所有动态查询均使用 PreparedStatement 绑定参数,从根本上杜绝SQL注入风险:
String sql = "SELECT * FROM t_user WHERE username = ? AND is_deleted = 0";
User user = findOne(sql, new BeanHandler<>(User.class), username);
占位符
?由数据库驱动自动转义,即便输入' OR '1'='1也不会破坏语法结构,确保系统安全。
综上所述,通过科学的数据库设计、合理的索引策略、安全的访问机制,JavaWeb日记本系统构建了一个稳健、高效、可维护的数据底层架构,为上层业务功能提供了强有力的支撑。
5. 用户管理模块开发(注册、登录、密码找回)
在现代Web应用系统中,用户管理模块是整个系统的安全基石与核心入口。一个健壮的用户管理体系不仅需要实现基础的注册、登录和密码找回功能,还需兼顾安全性、可用性与可扩展性。本章围绕JavaWeb日记本系统的用户管理模块展开深度剖析,从前后端协同设计到服务层逻辑封装,再到安全机制落地,全面阐述如何构建一套高可用的身份认证体系。
该模块作为系统的第一道防线,直接影响用户体验与数据资产的安全边界。尤其在高校毕业设计类项目中,虽然不追求工业级复杂度,但仍需体现对关键业务流程的理解和技术选型的合理性。因此,在本模块开发过程中,我们遵循MVC架构原则,将前端交互、Servlet控制、Service业务处理与DAO持久化操作进行分层解耦,并引入过滤器、加密算法、会话跟踪等关键技术手段,确保系统具备基本的安全防护能力。
5.1 用户注册功能全流程实现
用户注册是系统用户生命周期的起点,其设计不仅要保证信息采集的完整性,还需防止恶意注册行为并保障敏感数据存储安全。完整的注册流程包括前端表单验证、后端唯一性校验、密码加密存储等多个环节,任何一个环节疏漏都可能导致系统面临安全风险或数据一致性问题。
5.1.1 前端表单校验与Ajax实时提示
为了提升用户体验,避免频繁提交失败带来的挫败感,前端必须实施多层次输入校验。采用HTML5原生属性结合JavaScript脚本进行初步验证,再通过Ajax异步请求实现用户名重复性的实时反馈。
<form id="registerForm" action="RegisterServlet" method="post">
<label>用户名:</label>
<input type="text" id="username" name="username" required minlength="3" maxlength="20"/>
<span id="usernameTip"></span><br/>
<label>密码:</label>
<input type="password" id="password" name="password" required pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,}"/>
<br/>
<label>邮箱:</label>
<input type="email" name="email" required/>
<br/>
<button type="submit">注册</button>
</form>
<script>
document.getElementById('username').onblur = function() {
const username = this.value;
if (username.length < 3) return;
fetch('CheckUsernameServlet?username=' + encodeURIComponent(username))
.then(response => response.json())
.then(data => {
const tip = document.getElementById('usernameTip');
if (data.exists) {
tip.textContent = '用户名已存在';
tip.style.color = 'red';
} else {
tip.textContent = '用户名可用';
tip.style.color = 'green';
}
});
};
</script>
代码逻辑逐行分析:
- 第1~9行:定义注册表单结构,使用
required强制非空校验,minlength和maxlength限制用户名长度。 pattern正则表达式要求密码至少包含大小写字母和数字,长度不少于6位,增强初始强度。- JavaScript部分绑定
onblur事件,在用户离开用户名输入框时触发检查。 - 使用
fetch()发起GET请求至CheckUsernameServlet,携带URL编码后的用户名参数。 - 接收JSON响应,根据
exists字段动态更新提示文字颜色与内容,绿色表示可用,红色表示冲突。
| 校验类型 | 实现方式 | 目的 |
|---|---|---|
| 客户端静态校验 | HTML5 属性(required等) | 减少无效提交 |
| 客户端动态校验 | JavaScript + 正则表达式 | 提升密码安全性 |
| 异步唯一性校验 | Ajax 请求后端接口 | 避免重复用户名,优化体验 |
sequenceDiagram
participant U as 用户
participant F as 前端页面
participant S as CheckUsernameServlet
participant D as 数据库
U->>F: 输入用户名并失焦
F->>S: 发起Ajax GET请求(/CheckUsername?username=xxx)
S->>D: 执行SELECT COUNT(*) FROM t_user WHERE username=?
D-->>S: 返回结果(0或1)
S-->>F: 返回JSON {exists: true/false}
F->>U: 显示“可用”或“已存在”
该流程图清晰展示了从用户操作到数据库查询的完整链路,体现了前后端分离思想下的轻量级交互模式。通过异步校验,显著减少了服务器负载与用户等待时间。
5.1.2 后端唯一性检查(用户名重复判断)
尽管前端进行了校验,但所有关键业务规则必须在服务端再次确认,防止绕过前端直接调用API的攻击行为。 CheckUsernameServlet 负责接收Ajax请求并返回JSON格式判断结果。
@WebServlet("/CheckUsernameServlet")
public class CheckUsernameServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
if (username == null || username.trim().isEmpty()) {
response.setStatus(400);
return;
}
UserService userService = new UserService();
boolean exists = userService.usernameExists(username);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
out.print("{\"exists\":" + exists + "}");
out.flush();
}
}
参数说明:
- request.getParameter("username") :获取URL中的查询参数。
- UserService.usernameExists() :调用业务层方法执行数据库查询。
- 设置响应头为 application/json ,确保浏览器正确解析。
逻辑分析:
- 第6~8行:对输入做空值校验,防御非法请求。
- 第10行:实例化 UserService 对象,调用其封装的方法完成逻辑判断。
- 第13~17行:构造标准JSON响应体,供前端解析使用。
对应的DAO层查询语句如下:
SELECT COUNT(*) FROM t_user WHERE username = ? AND is_deleted = 0;
此处加入 is_deleted = 0 条件是为了支持软删除场景下仍能准确判断活跃用户是否存在,避免已被“删除”的旧账户影响新用户注册。
5.1.3 密码加密存储(MD5加盐处理)
明文存储密码属于严重安全漏洞。即使是在教学项目中,也应模拟真实环境的最佳实践——使用不可逆哈希算法配合随机盐值(salt)进行加密。
public class PasswordEncoder {
private static final String SALT = "diary_salt_2024"; // 可配置为配置文件读取
public static String encode(String password) {
String saltedPassword = password + SALT;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hashBytes = md.digest(saltedPassword.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 algorithm not available", e);
}
}
}
参数说明:
- SALT :固定盐值,增加彩虹表破解难度;生产环境建议使用每用户独立盐值。
- MessageDigest.getInstance("MD5") :获取MD5摘要算法实例。
- digest() :执行哈希运算,返回字节数组。
- String.format("%02x", b) :将每个字节转为两位十六进制字符串,补零对齐。
逻辑分析:
- 第6行:将原始密码与预设盐值拼接,形成带盐密码。
- 第7~11行:调用JDK内置的 MessageDigest 类完成MD5计算。
- 最终返回32位小写十六进制字符串,如: e10adc3949ba59abbe56e057f20f883e 。
注册成功时,DAO层插入语句示例如下:
String sql = "INSERT INTO t_user (username, password_hash, email, create_time) VALUES (?, ?, ?, NOW())";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
pstmt.setString(1, user.getUsername());
pstmt.setString(2, PasswordEncoder.encode(user.getPassword()));
pstmt.setString(3, user.getEmail());
pstmt.executeUpdate();
}
此方式虽非当前最先进(推荐使用BCrypt或PBKDF2),但在JavaWeb教学环境中足以展示密码保护的基本理念。
5.2 登录认证机制深度实现
登录是用户进入系统的通行证,其实现质量直接决定系统的安全性与稳定性。本节重点介绍基于Session的状态保持、Filter权限拦截以及“记住我”功能的Cookie实现方案。
5.2.1 Session跟踪用户登录状态
HTTP协议本身无状态,需借助Session机制维持用户会话。用户成功登录后,服务器创建HttpSession对象并将用户信息绑定其中,后续请求通过JSESSIONID识别身份。
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
UserService userService = new UserService();
User user = userService.login(username, PasswordEncoder.encode(password));
if (user != null) {
HttpSession session = request.getSession();
session.setAttribute("loginUser", user); // 绑定用户对象
session.setMaxInactiveInterval(30 * 60); // 30分钟超时
String redirect = request.getParameter("redirect");
response.sendRedirect(redirect != null ? redirect : "index.jsp");
} else {
request.setAttribute("error", "用户名或密码错误");
request.getRequestDispatcher("login.jsp").forward(request, response);
}
}
}
参数说明:
- request.getSession() :若不存在则新建,否则返回已有Session。
- setAttribute("loginUser", user) :将用户POJO存入Session,供其他页面访问。
- setMaxInactiveInterval(1800) :设置Session最大空闲时间(秒),防止长期占用内存。
前端可通过EL表达式判断是否已登录:
<c:if test="${empty loginUser}">
<a href="login.jsp">登录</a>
</c:if>
<c:if test="${not empty loginUser}">
欢迎您,${loginUser.username}
</c:if>
5.2.2 过滤器(Filter)拦截未授权访问
对于需要登录才能访问的资源(如日记编辑页),应统一通过 Filter 进行权限控制,避免在每个Servlet中重复编写校验逻辑。
@WebFilter("/secure/*")
public class AuthFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("loginUser") == null) {
response.sendRedirect("../login.jsp?redirect=" + request.getRequestURI());
return;
}
chain.doFilter(request, response);
}
}
流程图说明:
graph TD
A[客户端请求 /secure/diaryEdit] --> B{Filter拦截}
B --> C[获取Session]
C --> D{Session存在且含loginUser?}
D -- 是 --> E[放行,继续处理]
D -- 否 --> F[重定向至登录页]
F --> G[附带回跳地址]
该过滤器注册路径为 /secure/* ,所有匹配该前缀的请求都将被检查。若未登录,则跳转至登录页并携带原始URL作为回跳参数,提升用户体验。
5.2.3 记住我功能基于Cookie的安全实现
“记住我”功能允许用户关闭浏览器后仍保持登录状态,通常通过加密Cookie实现。
// 登录成功后,若勾选“remember-me”
if ("on".equals(request.getParameter("rememberMe"))) {
String token = UUID.randomUUID().toString();
Cookie cookie = new Cookie("rememberToken", token);
cookie.setPath("/");
cookie.setMaxAge(7 * 24 * 3600); // 7天
response.addCookie(cookie);
// 将token与用户关联存入数据库(t_remember_me_tokens)
userService.saveRememberMeToken(user.getId(), token);
}
下次访问时由Filter自动识别:
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie c : cookies) {
if ("rememberToken".equals(c.getName())) {
String token = c.getValue();
User user = userService.findUserByToken(token);
if (user != null) {
request.getSession(true).setAttribute("loginUser", user);
break;
}
}
}
}
此机制需配合定期清理过期Token任务,防止无限期有效带来的安全隐患。
5.3 密码找回与安全性平衡设计
当用户遗忘密码时,系统应提供安全可控的恢复途径。本节介绍基于邮箱验证码+Token链接的有效期控制机制,并引入操作日志增强审计能力。
5.3.1 邮箱验证码发送模拟流程
由于真实SMTP集成较为复杂,教学项目中常采用模拟方式生成验证码并暂存于内存或缓存。
Map<String, String> verificationStore = new ConcurrentHashMap<>(); // 模拟存储
@PostMapping("/sendResetCode")
public void sendResetCode(HttpServletRequest request, HttpServletResponse response) {
String email = request.getParameter("email");
User user = userService.findByEmail(email);
if (user == null) {
writeJson(response, Map.of("success", false, "msg", "邮箱未注册"));
return;
}
String code = String.format("%06d", new Random().nextInt(1000000));
verificationStore.put(email, code);
// TODO: 实际项目中调用邮件服务发送code
logService.recordAction("SEND_RESET_CODE", email, request.getRemoteAddr());
writeJson(response, Map.of("success", true, "msg", "验证码已发送"));
}
5.3.2 重置链接有效期控制(Token+过期时间戳)
更安全的方式是生成一次性Token并设定有效期:
public class ResetToken {
private String token;
private int userId;
private long expireTime;
// getter/setter
}
// 生成Token
String token = UUID.randomUUID().toString();
long expire = System.currentTimeMillis() + 30 * 60 * 1000; // 30分钟后过期
resetTokenService.store(token, userId, expire);
String resetLink = "http://localhost:8080/reset-password?token=" + token;
点击链接后验证有效性:
ResetToken rt = resetTokenService.findByToken(token);
if (rt == null || rt.getExpireTime() < System.currentTimeMillis()) {
showError("链接已失效");
} else {
showResetForm(token);
}
5.3.3 操作日志记录增强审计能力
所有敏感操作均应记录日志,便于追踪异常行为:
CREATE TABLE t_operation_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
action VARCHAR(50) NOT NULL, -- SEND_RESET_CODE, PASSWORD_CHANGED
target VARCHAR(100), -- 邮箱或用户名
ip_address VARCHAR(45),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
Java记录示例:
logService.recordAction("PASSWORD_RESET_SUCCESS", user.getUsername(), request.getRemoteAddr());
此举虽增加少量开销,却极大提升了系统的可观测性与合规性水平。
6. 日记创建与富文本编辑功能实现
6.1 富文本编辑器集成(如KindEditor或UEditor)
在现代Web应用中,用户对内容的排版需求日益增强。为提升用户体验,JavaWeb日记本系统引入了 KindEditor 作为前端富文本编辑器,支持文字加粗、图片插入、超链接、列表等常见功能。
6.1.1 编辑器引入与基本配置初始化
首先将KindEditor资源文件放入 webapp/js/kindeditor 目录下,并通过JSP页面引入核心JS:
<script src="${pageContext.request.contextPath}/js/kindeditor/kindeditor-all.js"></script>
<script src="${pageContext.request.contextPath}/js/kindeditor/lang/zh-CN.js"></script>
<textarea id="diaryContent" name="content" style="width:100%;height:400px;"></textarea>
<script>
KindEditor.ready(function(K) {
window.editor = K.create('#diaryContent', {
uploadJson: '${pageContext.request.contextPath}/UploadServlet',
fileManagerJson: '${pageContext.request.contextPath}/FileManagerServlet',
allowFileManager: true,
afterCreate: function() {
this.sync();
},
afterBlur: function() {
this.sync(); // 失去焦点时同步数据到textarea
},
items: [
'bold', 'italic', 'underline', 'image', 'link', 'emoticons', 'code', 'fullscreen'
]
});
});
</script>
参数说明:
-uploadJson: 指定图片上传接口路径;
-fileManagerJson: 文件管理服务地址;
-allowFileManager: 是否启用服务器端文件浏览;
-items: 工具栏按钮集合;
-this.sync(): 确保编辑内容实时同步至原始<textarea>,便于表单提交。
6.1.2 图片上传接口对接与路径回显
后端需提供一个 UploadServlet 处理图片上传请求。以下为关键代码片段:
@WebServlet("/UploadServlet")
public class UploadServlet extends HttpServlet {
private static final String UPLOAD_DIR = "uploads";
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String appPath = request.getServletContext().getRealPath("");
String savePath = appPath + File.separator + UPLOAD_DIR;
File fileSaveDir = new File(savePath);
if (!fileSaveDir.exists()) fileSaveDir.mkdirs();
try {
Collection<Part> parts = request.getParts();
for (Part part : parts) {
if ("imgFile".equals(part.getName())) { // KindEditor默认字段名
String fileName = extractFileName(part);
InputStream is = part.getInputStream();
Files.copy(is, Paths.get(savePath + File.separator + fileName),
StandardCopyOption.REPLACE_EXISTING);
// 返回JSON格式结果供编辑器识别
JSONObject json = new JSONObject();
json.put("error", 0);
json.put("url", request.getContextPath() + "/" + UPLOAD_DIR + "/" + fileName);
response.setContentType("application/json");
response.getWriter().print(json.toString());
}
}
} catch (Exception e) {
JSONObject json = new JSONObject();
json.put("error", 1);
json.put("message", "上传失败:" + e.getMessage());
response.getWriter().print(json.toString());
}
}
private String extractFileName(Part part) {
String contentDisp = part.getHeader("content-disposition");
for (String s : contentDisp.split(";")) {
if (s.trim().startsWith("filename")) {
return s.substring(s.indexOf("=") + 2, s.length() - 1);
}
}
return "";
}
}
该接口接收文件流并保存至服务器指定目录,返回包含访问URL的JSON响应,KindEditor自动将其插入编辑区域。
6.1.3 内容过滤XSS恶意脚本的净化处理
由于富文本可能携带JavaScript脚本,直接存储存在XSS风险。采用OWASP Java HTML Sanitizer进行内容净化:
<!-- pom.xml -->
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20211018.1</version>
</dependency>
使用示例:
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
public class XssFilterUtil {
private static final PolicyFactory policy = new HtmlPolicyBuilder()
.allowElements("p", "br", "strong", "em", "u", "img", "ol", "ul", "li")
.allowUrlProtocols("http", "https")
.allowAttributes("src").onElements("img")
.allowStyling()
.toFactory();
public static String sanitize(String input) {
return policy.sanitize(input);
}
}
在保存日记前调用 XssFilterUtil.sanitize(content) ,可有效阻止 <script> 等危险标签注入。
6.2 日记增删改查全功能闭环开发
系统实现了完整的CRUD操作链路,确保用户能流畅地管理个人日记。
6.2.1 添加日记时自动记录创建时间戳
MySQL表结构设计如下:
CREATE TABLE t_diary (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
user_id BIGINT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
is_deleted TINYINT DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES t_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
利用 DEFAULT CURRENT_TIMESTAMP 机制,在插入时不显式传入时间即可自动生成。
6.2.2 修改日记触发更新时间字段变更
当执行UPDATE语句时,MySQL会自动更新 update_time 字段,无需Java层干预:
UPDATE t_diary SET title=?, content=? WHERE id=? AND user_id=?
此特性由 ON UPDATE CURRENT_TIMESTAMP 保障,简化业务逻辑。
6.2.3 分页查询避免大数据量加载阻塞
面对大量日记数据,采用分页机制提升性能。以下是DAO层实现:
public List<Diary> findByUserId(Long userId, int offset, int pageSize) {
String sql = "SELECT id, title, content, create_time, update_time " +
"FROM t_diary WHERE user_id = ? AND is_deleted = 0 " +
"ORDER BY create_time DESC LIMIT ?, ?";
return queryForList(sql, Diary.class, userId, offset, pageSize);
}
前端配合JSP+EL表达式展示:
<c:forEach items="${diaryList}" var="diary">
<div class="diary-item">
<h4>${diary.title}</h4>
<small>创建于:<fmt:formatDate value="${diary.createTime}" pattern="yyyy-MM-dd HH:mm"/></small>
<p>${fn:substring(diary.content, 0, 100)}...</p>
</div>
</c:forEach>
<!-- 分页导航 -->
<div class="pagination">
<c:if test="${currentPage > 1}">
<a href="?page=${currentPage - 1}">上一页</a>
</c:if>
<span>第 ${currentPage} 页,共 ${totalPages} 页</span>
<c:if test="${currentPage < totalPages}">
<a href="?page=${currentPage + 1}">下一页</a>
</c:if>
</div>
| 当前页 | 总页数 | 每页条数 | 记录总数 | 偏移量(offset) |
|---|---|---|---|---|
| 1 | 5 | 10 | 47 | 0 |
| 2 | 5 | 10 | 47 | 10 |
| 3 | 5 | 10 | 47 | 20 |
| 4 | 5 | 10 | 47 | 30 |
| 5 | 5 | 10 | 47 | 40 |
| 6 | 5 | 10 | 47 | 50 → 越界 |
| 3 | 8 | 5 | 38 | 10 |
| 7 | 8 | 5 | 38 | 30 |
| 8 | 8 | 5 | 38 | 35 |
| 9 | 8 | 5 | 38 | 40 → 截断 |
注:offset = (page - 1) * pageSize,SQL中LIMIT生效前应校验合法性。
6.3 时间戳自动记录机制与展示优化
6.3.1 利用MySQL DEFAULT CURRENT_TIMESTAMP自动填充
已在建表语句中设定:
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
这一设计减少Java代码中手动设值的冗余,提高一致性。
6.3.2 Java中Date与Timestamp类型转换处理
实体类映射建议使用 java.util.Date 统一处理:
public class Diary {
private Long id;
private String title;
private String content;
private Date createTime;
private Date updateTime;
// getter/setter...
}
数据库查询结果可通过BeanUtils或手动封装完成类型映射。
6.3.3 前端格式化输出“几秒前/几分钟前”人性化显示
编写JavaScript工具函数实现相对时间计算:
function timeAgo(dateStr) {
const now = new Date();
const past = new Date(dateStr);
const diffMs = now - past;
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHour / 24);
if (diffSec < 10) return "刚刚";
if (diffSec < 60) return `${diffSec}秒前`;
if (diffMin < 60) return `${diffMin}分钟前`;
if (diffHour < 24) return `${diffHour}小时前`;
if (diffDay < 7) return `${diffDay}天前`;
return past.toLocaleDateString();
}
// 使用示例
document.getElementById("createTime").textContent = timeAgo("${diary.createTime}");
6.4 系统安全性设计综合防护措施
6.4.1 防SQL注入:预编译语句全面覆盖
所有数据库操作均使用 PreparedStatement ,杜绝拼接SQL:
String sql = "SELECT * FROM t_user WHERE username = ? AND password_hash = ?";
User user = queryForObject(sql, User.class, username, hashedPassword);
6.4.2 防XSS攻击:输入输出双重过滤
- 输入侧:使用OWASP Sanitizer清洗富文本;
- 输出侧:JSP中使用
<c:out>标签自动转义:
<c:out value="${diary.title}" />
6.4.3 敏感操作增加二次确认与权限校验
删除日记前弹出确认框:
<a href="DeleteServlet?id=${diary.id}"
onclick="return confirm('确定要删除这篇日记吗?')">删除</a>
后端验证归属权:
Diary diary = diaryService.findById(diaryId);
if (!diary.getUserId().equals(sessionUser.getId())) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权操作他人日记");
return;
}
sequenceDiagram
participant User
participant Browser
participant Servlet
participant Service
participant DAO
participant DB
User->>Browser: 点击“删除”
Browser->>Servlet: GET /DeleteServlet?id=123
Servlet->>Service: deleteDiary(123, userId)
Service->>DAO: findById(123)
DAO->>DB: SELECT * FROM t_diary WHERE id=123
DB-->>DAO: 返回日记对象
DAO-->>Service: Diary
alt 用户匹配
Service->>DAO: softDelete(id)
DAO->>DB: UPDATE is_deleted=1
DB-->>DAO: OK
DAO-->>Service: 成功
Service-->>Servlet: 删除成功
Servlet-->>Browser: 重定向到列表页
else 用户不匹配
Service-->>Servlet: 抛出权限异常
Servlet-->>Browser: 返回403错误
end
简介:JavaWeb日记本系统是一个基于JSP和Servlet技术的动态Web应用,结合MySQL数据库实现用户注册、登录、日记创建、编辑与安全管理等功能。系统采用JavaWeb经典架构,涵盖前端展示、后端逻辑处理与数据库交互全流程,支持富文本编辑、数据加密及防SQL注入等安全机制。配套论文详细阐述系统设计思路与实现过程,附带完整源码与SQL脚本,适合用于毕业设计与JavaWeb技术学习的综合实践项目。
更多推荐




所有评论(0)