news 2026/10/8 8:32:26

JavaWeb原生登录注册实战:Servlet+JSP+JDBC完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb原生登录注册实战:Servlet+JSP+JDBC完整实现

简介:本资源是一套基于JavaWeb技术栈实现的完整登录与注册系统,面向Java初学者及Web开发入门学习者,聚焦JSP页面开发、Servlet后端逻辑处理与MySQL数据库交互三大核心能力训练。压缩包共34个文件,包含7个Java源码文件(含Servlet与DAO类)、7个编译后的class文件、3个JSP页面(登录页、注册页、成功跳转页)、1个SQL建表脚本、2个XML配置文件(web.xml与数据库连接配置)以及必要的properties、JS、PNG等辅助资源,整体大小为3.56MB,结构清晰,符合标准JavaWeb项目组织规范。已有3049人学习下载,适合用于课堂实训、课程设计或自学实践。读者可直接导入Eclipse/MyEclipse运行,获得可执行的前后端联动Demo,涵盖表单验证、Session会话管理、JDBC参数化查询防SQL注入、密码明文存储警示等关键细节,附带完整目录结构与基础安全提示,是理解传统JavaWeb三层架构落地的典型参考案例。

1. 为什么一个“登录及注册”功能,会让 JavaWeb 新手在部署后反复刷新页面却始终跳转不到主页?

这不是一个玩具 Demo——它是你第一个真正暴露在公网请求下的业务入口。用户输入账号密码的那一刻,HTTP 请求就已携带 Session ID、Cookie、CSRF Token、Referer、User-Agent 等十余个隐性字段;后端要同步校验密码强度、邮箱格式、手机号唯一性、验证码时效、数据库连接池状态、事务隔离级别,还要防暴力撞库、防 SQL 注入、防 XSS 提交、防重复注册;前端则卡在表单序列化方式(application/x-www-form-urlencoded还是application/json?)、AJAX 跨域响应头(Access-Control-Allow-Origin是否带 Cookie)、重定向逻辑(response.sendRedirect()和RequestDispatcher.forward()的语义差异)上寸步难行。我见过太多人把web.xml里<welcome-file-list>配错路径,或在LoginServlet中忘了调用session.setAttribute("user", user),结果登录成功却仍显示未登录态——表面是“功能没做出来”,实则是 HTTP 协议、Servlet 生命周期、JDBC 连接管理、MVC 分层职责这四座大山还没翻过第一座。本文不讲 Spring Security 或 Shiro 框架封装,只用原生 JavaWeb(Servlet + JSP + JDBC + Tomcat),从零写出可上线、可调试、可审计的登录注册模块:所有代码可直接粘贴进 IDEA 运行,所有配置项标注 Tomcat 9/10 兼容性,所有坑位附带curl -v抓包验证命令和日志定位路径。


2. 用 Servlet + JSP 搭建最小可运行登录注册骨架:三步完成请求路由与页面流转

JavaWeb 登录注册不是写两个 HTML 表单再连个数据库那么简单。它本质是一套状态驱动的 HTTP 会话协议实现:用户首次访问/login.jsp是 GET 请求(展示表单),提交时是 POST 请求(携带凭证),服务端验证后需建立 Session 并重定向到主页,而后续所有受保护资源都必须校验该 Session 是否有效。这个流程里,任何一环断开——比如 POST 请求被 Tomcat 拦截、重定向地址写成相对路径、JSP 中 EL 表达式未启用——都会导致“点登录按钮没反应”或“登录成功却回到登录页”的玄学问题。下面用最简结构跑通全流程,不引入任何框架,只依赖 JDK 8+、Tomcat 9.0.83+、MySQL 5.7+。

2.1 创建标准 Web Application 结构并配置 web.xml(Tomcat 9 兼容写法)

JavaWeb 项目必须遵循 Servlet 规范定义的目录结构,否则 Tomcat 根本不会加载你的 Servlet。重点不是“能不能跑”,而是“Tomcat 能不能识别你的意图”。以下是经实测兼容 Tomcat 9.0.83 和 10.1.15 的最小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"> <!-- 设置默认首页,注意路径以 / 开头,且必须存在 --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- 登录处理 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 --> <servlet> <servlet-name>RegisterServlet</servlet-name> <servlet-class>com.example.web.RegisterServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>RegisterServlet</servlet-name> <url-pattern>/register</url-pattern> </servlet-mapping> <!-- 登出处理 Servlet --> <servlet> <servlet-name>LogoutServlet</servlet-name> <servlet-class>com.example.web.LogoutServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LogoutServlet</servlet-name> <url-pattern>/logout</url-pattern> </servlet-mapping> </web-app>

关键说明:

  • version="4.0"对应 Servlet 4.0 规范,Tomcat 9 默认支持,Tomcat 10 默认使用 Jakarta EE 命名空间(需改javax.servlet为jakarta.servlet),本文统一用 Tomcat 9 保持兼容性;
  • <url-pattern>/login</url-pattern>表示所有发往/login的请求(无论 GET/POST)均由LoginServlet处理,不是/login.jsp;
  • <welcome-file>必须指向真实存在的 JSP 文件,且该文件不能放在WEB-INF下(该目录下资源无法被客户端直接访问);
  • 若使用 Tomcat 10,请将web.xml中所有javax.servlet替换为jakarta.servlet,并修改 Java 类中的 import 语句(本文后续代码均按 Tomcat 9 编写,如需适配 Tomcat 10,仅需批量替换包名)。

2.2 编写 LoginServlet:GET 显示表单,POST 处理登录逻辑

LoginServlet必须同时处理两种 HTTP 方法:GET 用于返回登录页(避免用户直接访问/login时 404),POST 用于接收表单数据。这是新手最容易忽略的“双模式”设计。

package com.example.web; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.sql.*; public class LoginServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // GET 请求:直接转发到 login.jsp 页面 req.getRequestDispatcher("/login.jsp").forward(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置请求编码,防止中文乱码(尤其当 form 表单 enctype 为默认值时) req.setCharacterEncoding("UTF-8"); // 获取表单参数 String username = req.getParameter("username"); String password = req.getParameter("password"); // 校验非空(前端也应校验,但后端必须双重校验) if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { req.setAttribute("error", "用户名和密码不能为空"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } // 数据库查询(此处用硬编码演示,实际应抽离为 DAO 层) Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { Class.forName("com.mysql.cj.jdbc.Driver"); // MySQL 8+ 驱动类名 conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&serverTimezone=Asia/Shanghai", "root", "123456" ); // 使用 PreparedStatement 防止 SQL 注入 String sql = "SELECT id, username, password FROM users WHERE username = ? AND status = 1"; ps = conn.prepareStatement(sql); ps.setString(1, username); rs = ps.executeQuery(); if (rs.next()) { String dbPassword = rs.getString("password"); // 注意:生产环境必须使用 BCrypt 或 PBKDF2 加密比对,此处仅为演示明文存储(绝对不可用于生产!) if (password.equals(dbPassword)) { // 登录成功:创建 Session 并存入用户信息 HttpSession session = req.getSession(true); session.setAttribute("userId", rs.getLong("id")); session.setAttribute("username", username); session.setMaxInactiveInterval(30 * 60); // Session 30 分钟无操作失效 // 重定向到主页,避免表单重复提交(POST-Redirect-GET 模式) resp.sendRedirect(req.getContextPath() + "/home.jsp"); return; } } // 用户名或密码错误 req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } catch (Exception e) { e.printStackTrace(); req.setAttribute("error", "系统繁忙,请稍后再试"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } finally { // 关闭资源(顺序:ResultSet → PreparedStatement → Connection) try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (ps != null) ps.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } } }

逻辑说明与参数说明:

  • req.setCharacterEncoding("UTF-8")必须在getParameter()之前调用,否则中文参数会乱码;
  • req.getSession(true)表示如果当前请求没有 Session,则创建一个新的;false表示只获取已有 Session,不存在则返回 null;
  • session.setMaxInactiveInterval(1800)单位是秒,设为 0 表示永不过期(不推荐),设为负数表示永不超时(Tomcat 默认 -1);
  • resp.sendRedirect()是302 重定向,浏览器地址栏 URL 会变成/home.jsp,且后续请求不再携带原 POST 数据;而request.getRequestDispatcher().forward()是服务器内部转发,URL 不变,且能传递 request 属性(如error消息);
  • Class.forName()在 JDBC 4.0+ 已非必需(驱动自动注册),但显式调用更可控,尤其在某些 OSGi 或模块化环境中;
  • 数据库连接字符串中useSSL=false是开发环境简化配置,生产环境必须启用 SSL 并配置信任证书;serverTimezone=Asia/Shanghai解决 MySQL 时区与 Java 时区不一致导致的时间字段错误。

2.3 编写 RegisterServlet:插入用户前必须做唯一性校验与密码加密

注册流程比登录更复杂:不仅要插入新记录,还要确保用户名、邮箱、手机号全局唯一,且密码绝不能明文存储。以下代码实现基础校验 + BCrypt 加密(使用bcrypt库,Maven 依赖见后文):

package com.example.web; import org.mindrot.jbcrypt.BCrypt; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.*; 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 email = req.getParameter("email"); String phone = req.getParameter("phone"); String password = req.getParameter("password"); String confirmPassword = req.getParameter("confirmPassword"); // 基础校验 if (username == null || username.trim().isEmpty() || email == null || email.trim().isEmpty() || password == null || password.trim().isEmpty() || !password.equals(confirmPassword)) { req.setAttribute("error", "请完整填写信息,且两次输入密码需一致"); req.getRequestDispatcher("/register.jsp").forward(req, resp); return; } // 邮箱格式校验(简单正则,生产环境建议用 Apache Commons Validator) if (!email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) { req.setAttribute("error", "邮箱格式不正确"); req.getRequestDispatcher("/register.jsp").forward(req, resp); return; } Connection conn = null; PreparedStatement psCheck = null; PreparedStatement psInsert = null; try { Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&serverTimezone=Asia/Shanghai", "root", "123456" ); // 第一步:检查用户名是否已存在(使用 SELECT COUNT 而非 SELECT *,减少网络传输) String checkSql = "SELECT COUNT(*) FROM users WHERE username = ?"; psCheck = conn.prepareStatement(checkSql); psCheck.setString(1, username); ResultSet rs = psCheck.executeQuery(); rs.next(); if (rs.getInt(1) > 0) { req.setAttribute("error", "用户名已被注册"); req.getRequestDispatcher("/register.jsp").forward(req, resp); return; } // 第二步:BCrypt 加密密码(cost=12 是当前推荐强度,生成耗时约 200ms) String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt(12)); // 第三步:插入用户(status=1 表示启用状态) String insertSql = "INSERT INTO users (username, email, phone, password, status, create_time) VALUES (?, ?, ?, ?, 1, NOW())"; psInsert = conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS); psInsert.setString(1, username); psInsert.setString(2, email); psInsert.setString(3, phone); psInsert.setString(4, hashedPassword); int affectedRows = psInsert.executeUpdate(); if (affectedRows == 1) { // 获取自增主键(可选,用于后续逻辑) ResultSet generatedKeys = psInsert.getGeneratedKeys(); if (generatedKeys.next()) { long userId = generatedKeys.getLong(1); System.out.println("新用户注册成功,ID:" + userId); } // 注册成功,跳转到登录页 resp.sendRedirect(req.getContextPath() + "/login.jsp?registered=true"); } else { req.setAttribute("error", "注册失败,请重试"); req.getRequestDispatcher("/register.jsp").forward(req, resp); } } catch (SQLException e) { // 捕获唯一索引冲突异常(MySQL 错误码 1062) if (e.getSQLState().equals("23000")) { req.setAttribute("error", "用户名或邮箱已被占用"); } else { e.printStackTrace(); req.setAttribute("error", "系统繁忙,请稍后再试"); } req.getRequestDispatcher("/register.jsp").forward(req, resp); } catch (Exception e) { e.printStackTrace(); req.setAttribute("error", "系统繁忙,请稍后再试"); req.getRequestDispatcher("/register.jsp").forward(req, resp); } finally { try { if (psCheck != null) psCheck.close(); } catch (SQLException ignored) {} try { if (psInsert != null) psInsert.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } } }

关键细节说明:

  • BCrypt.hashpw(password, BCrypt.gensalt(12))中cost=12表示 2^12 次哈希迭代,平衡安全性与性能;值越大越安全但越慢,10~12 是当前推荐范围;
  • psInsert.executeUpdate()返回影响行数,1表示插入成功;Statement.RETURN_GENERATED_KEYS参数允许通过getGeneratedKeys()获取数据库自增 ID;
  • e.getSQLState().equals("23000")是 SQL 标准错误码,对应 MySQL 的Duplicate entry错误(如违反UNIQUE KEY约束),比解析e.getMessage()更可靠;
  • NOW()是 MySQL 函数,插入当前时间戳,比 Javanew Date()更精准(避免时区转换误差);
  • 生产环境应在users表中为username和email字段分别建立UNIQUE INDEX,数据库层面强制唯一性,应用层校验作为兜底。

3. 数据库建表与 JDBC 连接池配置:让登录注册抗住 100 并发请求

一个没有连接池的 JavaWeb 应用,在 20 个并发用户下就会因Too many connections报错而瘫痪。MySQL 默认最大连接数为 151,每个 HTTP 请求若都新建 Connection,100 个用户同时登录就会耗尽连接。本节教你用 Tomcat 自带的 JNDI 数据源配置连接池,并给出生产级users表结构——它不是为了“能跑”,而是为了“能扛”。

3.1 创建 users 表:包含密码盐值、锁定机制、注册来源字段

CREATE DATABASE IF NOT EXISTS javaweb_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE javaweb_demo; CREATE TABLE `users` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名(登录用)', `email` VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱(用于找回密码)', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号(可为空)', `password` VARCHAR(100) NOT NULL COMMENT 'BCrypt 加密后的密码', `salt` VARCHAR(32) DEFAULT NULL COMMENT '旧版盐值(BCrypt 已内置,此字段可删除)', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-正常,0-禁用,2-待激活', `failed_login_attempts` TINYINT NOT NULL DEFAULT 0 COMMENT '连续失败登录次数(用于锁定)', `locked_until` DATETIME NULL DEFAULT NULL COMMENT '锁定截止时间', `last_login_time` DATETIME NULL DEFAULT NULL COMMENT '最后登录时间', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `source` ENUM('web', 'mobile', 'admin') NOT NULL DEFAULT 'web' COMMENT '注册来源', PRIMARY KEY (`id`), KEY `idx_username` (`username`), KEY `idx_email` (`email`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户主表';

字段设计理由:

  • VARCHAR(100)存 BCrypt 密文(典型长度 60 字符),留足余量;
  • status字段支持运营后台手动禁用账号,比物理删除更安全;
  • failed_login_attempts+locked_until实现账户锁定机制(5 次失败后锁定 15 分钟),防暴力破解;
  • source字段为未来多端(Web/App/小程序)统一用户体系预留;
  • 所有DATETIME字段使用CURRENT_TIMESTAMP和ON UPDATE,由数据库自动维护,避免 Java 时区偏差;
  • utf8mb4支持 Emoji 表情存储,utf8mb4_unicode_ci排序更符合中文习惯。

3.2 配置 Tomcat JNDI 数据源:复用连接、控制最大活跃数、自动回收空闲连接

直接在web.xml中配置 JDBC 连接是反模式——每次请求都新建 Connection,资源浪费且易超限。Tomcat 提供 JNDI 数据源,由容器统一管理连接池。配置分两步:

第一步:在$CATALINA_HOME/conf/context.xml中添加 Resource 定义(全局生效)

<!-- $CATALINA_HOME/conf/context.xml --> <?xml version="1.0" encoding="UTF-8"?> <Context> <!-- 全局 JNDI 数据源,所有 WebApp 可共享 --> <Resource name="jdbc/javaweb_demo" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true" username="root" password="123456" maxActive="50" minIdle="5" maxWait="10000" testOnBorrow="true" validationQuery="SELECT 1" validationInterval="30000" timeBetweenEvictionRunsMillis="30000" minEvictableIdleTimeMillis="60000" removeAbandonedOnBorrow="true" removeAbandonedTimeout="60" logAbandoned="true"/> </Context>

参数详解(避坑核心):

  • maxActive="50":连接池最大活跃连接数,必须小于 MySQL 的max_connections(默认 151),建议设为 30~50;
  • minIdle="5":空闲时保持的最小连接数,避免冷启动延迟;
  • maxWait="10000":获取连接最大等待时间(毫秒),超时抛SQLException,前端应捕获并提示“系统繁忙”;
  • testOnBorrow="true"+validationQuery="SELECT 1":每次从池中借连接前执行校验 SQL,确保连接有效(防数据库重启后连接失效);
  • timeBetweenEvictionRunsMillis="30000":每 30 秒运行一次空闲连接回收线程;
  • minEvictableIdleTimeMillis="60000":连接空闲超过 60 秒即被回收;
  • removeAbandonedOnBorrow="true":当连接被借用超时(removeAbandonedTimeout)仍未归还,强制回收并记录日志(防连接泄漏);
  • logAbandoned="true":记录被强制回收的连接堆栈,定位泄漏源头(如忘记close());
  • allowPublicKeyRetrieval=true:MySQL 8.0+ 新增参数,解决 RSA 密钥交换问题,开发环境可加,生产环境应配置 SSL。

第二步:在 WebApp 的META-INF/context.xml中声明资源引用(局部绑定)

<!-- src/main/webapp/META-INF/context.xml --> <?xml version="1.0" encoding="UTF-8"?> <Context> <ResourceLink name="jdbc/javaweb_demo" global="jdbc/javaweb_demo" type="javax.sql.DataSource"/> </Context>

第三步:在 Servlet 中通过 JNDI 查找数据源(替代 DriverManager)

// 在 LoginServlet 或 RegisterServlet 中替换原来的 DriverManager.getConnection() import javax.naming.Context; import javax.naming.InitialContext; import javax.sql.DataSource; import java.sql.Connection; // 获取 JNDI 数据源 Context initCtx = new InitialContext(); Context envCtx = (Context) initCtx.lookup("java:comp/env"); DataSource ds = (DataSource) envCtx.lookup("jdbc/javaweb_demo"); // 从连接池获取连接(自动复用,无需手动 close) Connection conn = ds.getConnection(); // ... 后续操作同前,但 finally 中只需关闭 conn(连接池会自动回收)

为什么必须用 JNDI?

  • 连接复用:100 个并发请求,最多只创建 50 个物理连接,其余排队复用;
  • 故障隔离:某个 WebApp 的连接泄漏不会影响其他 App;
  • 配置集中:数据库密码等敏感信息不在 WebApp 内,运维可独立修改;
  • 性能监控:Tomcat Manager 页面可实时查看连接池使用率、等待队列长度、泄漏连接数。

3.3 添加 Maven 依赖:BCrypt 加密与连接池必备库

在pom.xml中添加以下依赖(注意版本兼容性):

<dependencies> <!-- Servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- MySQL JDBC Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- BCrypt 加密 --> <dependency> <groupId>org.mindrot</groupId> <artifactId>jbcrypt</artifactId> <version>0.4</version> </dependency> <!-- JSTL(JSP 页面需要) --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>

版本选择依据:

  • javax.servlet-api 4.0.1对应 Servlet 4.0,兼容 Tomcat 9;
  • mysql-connector-java 8.0.33是 MySQL 官方推荐的稳定版,支持serverTimezone参数;
  • jbcrypt 0.4是轻量级、无依赖的 BCrypt 实现,hashpw()和checkpw()方法足够安全;
  • jstl 1.2提供<c:if>、<c:forEach>等标签,简化 JSP 逻辑(如判断 Session 是否存在)。

4. 登录状态校验与权限拦截:用 Filter 实现全站登录保护,拒绝未授权访问

登录成功只是开始,真正的安全在于每一次请求都校验身份。如果用户手动在浏览器地址栏输入/home.jsp,而此时 Session 已过期或根本未登录,页面仍会直接渲染——这是严重漏洞。必须用Filter在请求到达 Servlet/JSP 前统一拦截,根据 Session 状态决定放行或重定向。这不是“锦上添花”,而是生产环境的底线要求。

4.1 编写 LoginCheckFilter:拦截所有受保护路径,白名单放行静态资源

package com.example.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginCheckFilter implements Filter { // 定义不需要登录即可访问的路径(白名单) private static final String[] EXCLUDE_PATHS = { "/login.jsp", "/register.jsp", "/login", "/register", "/css/", "/js/", "/images/", "/favicon.ico" }; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String requestURI = req.getRequestURI(); String contextPath = req.getContextPath(); // 1. 判断是否为白名单路径(如登录页、静态资源) boolean isExcluded = false; for (String exclude : EXCLUDE_PATHS) { if (requestURI.startsWith(contextPath + exclude)) { isExcluded = true; break; } } if (isExcluded) { chain.doFilter(request, response); return; } // 2. 获取 Session,不创建新 Session(避免无意义 Session 泛滥) HttpSession session = req.getSession(false); // 3. 校验 Session 中是否存在用户标识 if (session == null || session.getAttribute("username") == null) { // 未登录,重定向到登录页,并携带原始请求路径(便于登录后跳回) String originalUrl = requestURI; if (req.getQueryString() != null) { originalUrl += "?" + req.getQueryString(); } resp.sendRedirect(req.getContextPath() + "/login.jsp?redirect=" + java.net.URLEncoder.encode(originalUrl, "UTF-8")); return; } // 4. 登录态有效,放行 chain.doFilter(request, response); } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }

关键设计点:

  • req.getSession(false):只获取已有 Session,绝不创建新 Session,避免爬虫或恶意请求触发大量空 Session 占用内存;
  • 白名单数组EXCLUDE_PATHS包含/css/、/js/等前缀,确保所有静态资源免检,提升性能;
  • redirect参数用于登录成功后跳转回原页面(如用户访问/order/list.jsp未登录,登录后应自动跳转至此),需在LoginServlet中读取并处理;
  • java.net.URLEncoder.encode()对原始 URL 编码,防止?、&等字符破坏重定向链接。

4.2 在 web.xml 中注册 Filter 并配置拦截路径

<!-- web.xml 中添加 --> <filter> <filter-name>LoginCheckFilter</filter-name> <filter-class>com.example.filter.LoginCheckFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginCheckFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 拦截所有请求 --> </filter-mapping>

为什么用/*而不是/?

  • /只匹配根路径/,而/*匹配所有路径(包括/home.jsp、/api/user、/css/style.css);
  • Filter 的执行顺序由<filter-mapping>在web.xml中的声明顺序决定,LoginCheckFilter应放在其他 Filter(如字符编码 Filter)之后,确保请求体已解码;
  • 若项目有多个 Filter(如日志 Filter、XSS 过滤 Filter),需按逻辑顺序排列:字符编码 → XSS 过滤 → 登录校验 → 业务处理。

4.3 在 LoginServlet 中支持 redirect 回跳:登录成功后跳转到原始请求页

修改LoginServlet的doPost方法末尾,增加对redirect参数的处理:

// 在登录成功分支中(即 password 校验通过后) String redirect = req.getParameter("redirect"); if (redirect != null && !redirect.trim().isEmpty()) { // 对 redirect URL 进行基础校验,防止开放重定向漏洞 if (redirect.startsWith(req.getContextPath()) && !redirect.contains("://") && !redirect.contains("@")) { resp.sendRedirect(redirect); return; } } // 默认跳转到主页 resp.sendRedirect(req.getContextPath() + "/home.jsp");

开放重定向防护要点:

  • redirect.startsWith(req.getContextPath())确保跳转地址在同一应用内;
  • !redirect.contains("://")过滤http://evil.com类绝对 URL;
  • !redirect.contains("@")过滤javascript:alert(1)或data:text/html,<script>...</script>类危险协议;
  • 生产环境建议使用白名单域名或预定义路径映射(如redirect=profile→/user/profile.jsp),而非直接拼接。

4.4 在 JSP 页面中显示登录态与用户信息:用 EL 表达式 + JSTL 标签

home.jsp示例(需引入 JSTL):

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>首页</title> </head> <body> <h1>欢迎来到首页</h1> <!-- 判断 Session 中是否有 username --> <c:choose> <c:when test="${not empty sessionScope.username}"> <p>你好,<strong>${sessionScope.username}</strong>!</p> <a href="${pageContext.request.contextPath}/logout">退出登录</a> </c:when> <c:otherwise> <p>请先 <a href="${pageContext.request.contextPath}/login.jsp">登录</a></p> </c:otherwise> </c:choose> <!-- 显示用户基本信息(可从数据库查,此处简化) --> <c:if test="${not empty sessionScope.username}"> <div> <h2>用户中心</h2> <p>用户ID:<c:out value="${sessionScope.userId}"/></p> <p>最后登录时间:<c:out value="${sessionScope.lastLoginTime}"/></p> </div> </c:if> </body> </html>

EL 表达式安全说明:

  • ${sessionScope.username}直接从 Session 中取值,比request.getSession().getAttribute("username")更简洁;
  • <c:out value="..."/>自动对输出内容进行 HTML 转义,防止 XSS(如用户名含<script>标签);
  • pageContext.request.contextPath获取应用上下文路径(如/myapp),确保链接在不同部署路径下均有效。

5. 避坑指南:JavaWeb 登录注册 5 个血泪经验,90% 的人栽在第 3 条

写完代码不等于功能可用。JavaWeb 登录注册是 HTTP 协议、Servlet 容器、数据库、浏览器缓存四者协同的结果,任何一个环节出错都会表现为“登录失败”“页面空白”“跳转异常”。以下是我在 12 个真实项目中踩过的坑,按发生频率排序,每条都附带curl验证命令和日志定位路径。

5.1 现象:登录表单提交后页面空白,浏览器控制台无报错,Tomcat 日志也无异常

原因:LoginServlet的doPost方法中,resp.sendRedirect()后未return,导致后续代码继续执行并尝试forward(),抛出IllegalStateException: Cannot forward after response has been committed异常。但该异常被 Tomcat 吞掉,不打印到日志,仅返回空响应。
解决:在所有sendRedirect()和forward()后必须加return,确保方法立即退出。验证命令:

# 模拟登录请求,观察响应头 curl -v - <p> <a href="https://download.csdn.net/download/qq_42680327/11156049" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 8:32:12

关于编码Agent的一点体会:为什么顶级AI编码Agent,永远只出自模型原厂? ---开源/生态厂商的底层宿命

使用各种Agent编码工具也有一段时间了&#xff0c; 谈一点体会。当下AI编码工具百花齐放&#xff0c;IDE插件、云端助手、开源Agent框架层出不穷&#xff0c;但所有开发者都有一个共同体感&#xff1a;真正能用、敢让它全自动接管工程、自主完成大型重构、长链路调试的顶级Agen…

作者头像 李华
网站建设 2026/10/8 8:29:21

WinForm TextBox 关键字智能提示:从数据源到下拉控件的完整实现

简介&#xff1a;这是一份面向 WinForm 开发者的 TextBox 关键字智能提示实现方案&#xff0c;针对项目中需要类似百度搜索框那样输入关键字后弹出下拉候选的需求。相比直接使用 ComboBox 与 TextBox 的 AutoCompleteMode 属性&#xff08;只能从首字符匹配、无法任意位置或多关…

作者头像 李华
网站建设 2026/10/8 8:28:35

MIMIC-IV医学影像报告分类:PyTorch与word2vec/Transformer实践

简介&#xff1a;面向MIMIC-IV英文影像报告分类任务&#xff0c;这份PyTorch实战资源包提供了从word2vec词向量训练到Transformer模型构建与评估的完整代码方案&#xff0c;适合具备一定Python基础、希望上手医学文本NLP项目的学习者或研究者。资源共20个文件&#xff0c;压缩包…

作者头像 李华
网站建设 2026/10/8 8:28:35

SpringBoot+JDBC直连MySQL:数据源配置、连接池调优与排错指南

简介&#xff1a;面向 Java 开发者的 Spring Boot 使用 JDBC 连接 MySQL 数据库整套解决方案&#xff0c;清晰定位传统 JDBC 方式下的数据访问场景&#xff0c;适合初学 Spring Boot 数据层搭建、需要快速跑通数据库连接的读者。资源包共 136 个文件&#xff0c;约 637MB&#…

作者头像 李华
网站建设 2026/10/8 8:28:14

OPC UA C#开发指南:从Server/Client示例到设备接入实战

简介&#xff1a;OPC UA C# 示例是一份面向工业自动化开发者的实战代码包&#xff0c;重点演示如何使用C#构建OPC UA客户端&#xff0c;实现与PLC的安全通信与实时数据采集&#xff0c;适合需要快速上手UA客户端开发的工程师学习。压缩包共135个文件&#xff0c;以cs源码为主&a…

作者头像 李华
网站建设 2026/10/8 8:26:33

PHP 基础语法入门指南

PHP&#xff08;全称 Hypertext Preprocessor&#xff09;是一种专为 Web 开发而生的开源脚本语言&#xff0c;其特点是简单易学&#xff0c;可以直接嵌入 HTML&#xff0c;且多平台通用&#xff0c;搭配 Apache/Nginx 服务器即可正常运行。本文主要讲述 PHP 语言的一些基础语法…

作者头像 李华