ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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 框架封装只用原生 JavaWebServlet 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.xmlTomcat 9 兼容写法JavaWeb 项目必须遵循 Servlet 规范定义的目录结构否则 Tomcat 根本不会加载你的 Servlet。重点不是“能不能跑”而是“Tomcat 能不能识别你的意图”。以下是经实测兼容 Tomcat 9.0.83 和 10.1.15 的最小web.xml?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- 设置默认首页注意路径以 / 开头且必须存在 -- welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list !-- 登录处理 Servlet -- servlet servlet-nameLoginServlet/servlet-name servlet-classcom.example.web.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping !-- 注册处理 Servlet -- servlet servlet-nameRegisterServlet/servlet-name servlet-classcom.example.web.RegisterServlet/servlet-class /servlet servlet-mapping servlet-nameRegisterServlet/servlet-name url-pattern/register/url-pattern /servlet-mapping !-- 登出处理 Servlet -- servlet servlet-nameLogoutServlet/servlet-name servlet-classcom.example.web.LogoutServlet/servlet-class /servlet servlet-mapping servlet-nameLogoutServlet/servlet-name url-pattern/logout/url-pattern /servlet-mapping /web-app关键说明version4.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.jspwelcome-file必须指向真实存在的 JSP 文件且该文件不能放在WEB-INF下该目录下资源无法被客户端直接访问若使用 Tomcat 10请将web.xml中所有javax.servlet替换为jakarta.servlet并修改 Java 类中的 import 语句本文后续代码均按 Tomcat 9 编写如需适配 Tomcat 10仅需批量替换包名。2.2 编写 LoginServletGET 显示表单POST 处理登录逻辑LoginServlet必须同时处理两种 HTTP 方法GET 用于返回登录页避免用户直接访问/login时 404POST 用于接收表单数据。这是新手最容易忽略的“双模式”设计。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?useSSLfalseserverTimezoneAsia/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不存在则返回 nullsession.setMaxInactiveInterval(1800)单位是秒设为 0 表示永不过期不推荐设为负数表示永不超时Tomcat 默认 -1resp.sendRedirect()是302 重定向浏览器地址栏 URL 会变成/home.jsp且后续请求不再携带原 POST 数据而request.getRequestDispatcher().forward()是服务器内部转发URL 不变且能传递 request 属性如error消息Class.forName()在 JDBC 4.0 已非必需驱动自动注册但显式调用更可控尤其在某些 OSGi 或模块化环境中数据库连接字符串中useSSLfalse是开发环境简化配置生产环境必须启用 SSL 并配置信任证书serverTimezoneAsia/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?useSSLfalseserverTimezoneAsia/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 加密密码cost12 是当前推荐强度生成耗时约 200ms String hashedPassword BCrypt.hashpw(password, BCrypt.gensalt(12)); // 第三步插入用户status1 表示启用状态 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?registeredtrue); } 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))中cost12表示 2^12 次哈希迭代平衡安全性与性能值越大越安全但越慢10~12 是当前推荐范围psInsert.executeUpdate()返回影响行数1表示插入成功Statement.RETURN_GENERATED_KEYS参数允许通过getGeneratedKeys()获取数据库自增 IDe.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 请求若都新建 Connection100 个用户同时登录就会耗尽连接。本节教你用 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户主表;字段设计理由VARCHAR(100)存 BCrypt 密文典型长度 60 字符留足余量status字段支持运营后台手动禁用账号比物理删除更安全failed_login_attemptslocked_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 version1.0 encodingUTF-8? Context !-- 全局 JNDI 数据源所有 WebApp 可共享 -- Resource namejdbc/javaweb_demo authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/javaweb_demo?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;allowPublicKeyRetrievaltrue usernameroot password123456 maxActive50 minIdle5 maxWait10000 testOnBorrowtrue validationQuerySELECT 1 validationInterval30000 timeBetweenEvictionRunsMillis30000 minEvictableIdleTimeMillis60000 removeAbandonedOnBorrowtrue removeAbandonedTimeout60 logAbandonedtrue/ /Context参数详解避坑核心maxActive50连接池最大活跃连接数必须小于 MySQL 的max_connections默认 151建议设为 30~50minIdle5空闲时保持的最小连接数避免冷启动延迟maxWait10000获取连接最大等待时间毫秒超时抛SQLException前端应捕获并提示“系统繁忙”testOnBorrowtruevalidationQuerySELECT 1每次从池中借连接前执行校验 SQL确保连接有效防数据库重启后连接失效timeBetweenEvictionRunsMillis30000每 30 秒运行一次空闲连接回收线程minEvictableIdleTimeMillis60000连接空闲超过 60 秒即被回收removeAbandonedOnBorrowtrue当连接被借用超时removeAbandonedTimeout仍未归还强制回收并记录日志防连接泄漏logAbandonedtrue记录被强制回收的连接堆栈定位泄漏源头如忘记close()allowPublicKeyRetrievaltrueMySQL 8.0 新增参数解决 RSA 密钥交换问题开发环境可加生产环境应配置 SSL。第二步在 WebApp 的META-INF/context.xml中声明资源引用局部绑定!-- src/main/webapp/META-INF/context.xml -- ?xml version1.0 encodingUTF-8? Context ResourceLink namejdbc/javaweb_demo globaljdbc/javaweb_demo typejavax.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 groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- MySQL JDBC Driver -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- BCrypt 加密 -- dependency groupIdorg.mindrot/groupId artifactIdjbcrypt/artifactId version0.4/version /dependency !-- JSTLJSP 页面需要 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies版本选择依据javax.servlet-api 4.0.1对应 Servlet 4.0兼容 Tomcat 9mysql-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-nameLoginCheckFilter/filter-name filter-classcom.example.filter.LoginCheckFilter/filter-class /filter filter-mapping filter-nameLoginCheckFilter/filter-name url-pattern/*/url-pattern !-- 拦截所有请求 -- /filter-mapping为什么用/*而不是//只匹配根路径/而/*匹配所有路径包括/home.jsp、/api/user、/css/style.cssFilter 的执行顺序由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类危险协议生产环境建议使用白名单域名或预定义路径映射如redirectprofile→/user/profile.jsp而非直接拼接。4.4 在 JSP 页面中显示登录态与用户信息用 EL 表达式 JSTL 标签home.jsp示例需引入 JSTL% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://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用户IDc:out value${sessionScope.userId}//p p最后登录时间c:out value${sessionScope.lastLoginTime}//p /div /c:if /body /htmlEL 表达式安全说明${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 hrefhttps://download.csdn.net/download/qq_42680327/11156049 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表