
简介面向JavaWeb初学者的登录注册页面完整代码示例主要解决Web开发中用户账号创建与身份验证的基础搭建问题可快速形成带登录、注册功能的项目雏形。示例以index.jsp作为统一入口分别向LoginServlet和RegisterServlet提交用户名与密码Servlet解析请求参数后进行简单校验再通过sendRedirect将结果导向success.jsp或failure.jsp完整演示了从表单采集、服务端处理到页面跳转的核心流程。登录示例默认使用admin/admin作为预设账号便于直接跑通验证注册逻辑则检查用户名和密码是否为空并在实际开发中建议改为数据库校验、密码哈希存储及防SQL注入处理。包内为单个PDF文档压缩包大小约797KB包含JSP表单、两个Servlet类以及成功与失败页面的完整代码片段结构清晰适合课程设计或起步项目参考。已有2380人学习下载适合刚开始接触Servlet与JSP的开发者理解Web应用前后端交互方式也可在此基础上继续扩展数据库持久化、密码加密、注册查重、验证码等安全功能逐步完善为可实际使用的用户管理系统。1. javaweb登录注册页面完整代码一套能跑通前端、后端和数据库的样板项目如果你正在搜 javaweb登录注册页面完整代码大概率是课程设计或第一个完整小项目卡在了“代码零零散散拼不起来”这一步HTML 会写Java 基础也学过但表单提交给谁、Servlet 怎么拿参数、密码该不该存明文、为什么连数据库一直报错这些单独搜出来的文章互相矛盾。我要讲的是一个人能独立跑通的最小完整方案Tomcat 8.5 Servlet 3.1 JSP MySQL 8.0包含建表 SQL、页面、DAO、Servlet、IDEA 启动配置和坑位清单。适合刚学完 JavaWeb 语法、第一次做完整登录注册模块的人也适合想拿一套干净样板改造成自己项目的实习生。这套东西不是为了炫技而是让你今天就能在浏览器里看到自己的系统跑起来。2. 先立住技术栈为什么 ServletJSPMySQL 仍是登录注册首选的完整方案很多新手一上来就想用 Spring Boot因为它配置少、启动快。但如果你连请求怎么到后端、Session 怎么存都还没见过用 Spring Boot 写登录注册只会让你把“框架黑匣子”当成“知识黑匣子”。Servlet JSP MySQL 的好处是每一层都在你能打开的文件里请求路径、参数、数据库连接都看得见摸得着。等这套跑通再换 Spring Boot 时你才知道它在帮你省什么。2.1 三个选型理由包教会、能写进简历、能本地跑通第一个理由是“链路短”。浏览器提交表单Tomcat 按注解或 web.xml 找到对应 ServletServlet 里取参数、调 DAODAO 查 MySQL结果放回 request再 forward 到 JSP 渲染。这个链路没有中间件、没有 AOP、没有自动装配出了问题你顺着调用栈就能定位。第二个理由是“能写进简历”。登录注册虽然基础但它能展示你对 Session 生命周期、密码哈希、SQL 注入防护、字符编码处理的理解。这些恰好是很多 Java 后端岗位面试会追问的点。你用一个 Servlet 项目说清楚“我是怎么处理重复注册的”比说“我调了一个 Spring Security 过滤器”更有说服力。第三个理由是“本地能跑通”。只要你装了 JDK 8、IDEA、Tomcat 8.5 和 MySQL不需要额外装 Redis、MQ 这类重型组件整个工程不超过十个文件。对于一台配置普通的笔记本来说这套方案是负担最低的。2.2 先建 Maven war 工程pom.xml 与目录结构我用 Maven 管理依赖不是因为项目有多复杂而是为了让 MySQL 驱动和 JSTL 的版本不靠手动拷 jar。下面这个 pom.xml 是跑通这套登录注册的基础配置。project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.demo/groupId artifactIdlogin-demo/artifactId version1.0.0/version packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.30/version /dependency /dependencies build finalNamelogin-demo/finalName /build /projectjavax.servlet-api 的 scope 必须写成 provided因为 Tomcat 自己带了 Servlet 容器实现。如果漏了部署时会出现类冲突或者“已经加载的类不能再次加载”的报错。JSTL 依赖用于在 JSP 里做条件判断虽然登录注册页面用脚本也能写但加上 JSTL 会让页面干净很多。MySQL 驱动用 8.x对应 MySQL 8.0 数据库。下面是推荐的目录结构src/main/ ├── java/com/demo/ │ ├── dao/UserDao.java │ ├── util/DBUtil.java │ ├── util/PasswordUtil.java │ └── servlet/LoginServlet.java │ └── servlet/RegisterServlet.java │ └── servlet/LogoutServlet.java ├── resources/db.sql └── webapp/ ├── css/style.css ├── login.jsp ├── register.jsp ├── index.jsp └── WEB-INF/web.xmlweb.xml 里只配置一个 welcome-file指向 login.jsp这样访问http://localhost:8080/login-demo/时能直接看到登录页。Servlet 用 WebServlet 注解注册不需要在 web.xml 里再写一遍映射。2.3 数据库表用户名要唯一密码只存 Hash登录注册的数据表不需要设计得太复杂但有两个底线用户名唯一密码不能明文存储。下面的 SQL 直接放进 MySQL 执行即可。CREATE DATABASE IF NOT EXISTS login_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE login_demo; CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(64) NOT NULL, salt VARCHAR(32) NOT NULL, nick_name VARCHAR(50) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB;username 字段加 UNIQUE 是数据库层面的最后一道防线。即使你的 Java 代码没做重复判断数据库也会拒绝重复用户名DAO 层能通过 SQLException 的 1062 错误码识别出来。password_hash 存的是加了盐之后的 SHA-256 摘要长度固定 64 位十六进制字符。salt 字段存每个用户独立的随机盐。这里要强调一点表单里传过来的原始密码永远不要落库。3. 页面这一层JSP登录注册页面完整代码该长什么样很多教程只给 HTML 不给 JSP导致新手不知道怎么把后端错误提示显示到页面上。我的做法是登录页和注册页都用 JSP但页面里不放 Java 业务逻辑只在需要回显的地方用少量脚本或 EL 表达式。3.1 注册页前端校验不是摆设注册页需要校验用户名长度、密码长度、两次密码是否一致。前端校验的意义是省一次网络请求而不是替代后端校验。后端 Servlet 里必须再校验一遍因为用户可以绕过浏览器直接构造请求。% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 % !DOCTYPE html html head meta charsetUTF-8 title注册/title link relstylesheet href${pageContext.request.contextPath}/css/style.css /head body div classform-box h2注册新账号/h2 form idregForm action${pageContext.request.contextPath}/register methodpost div label用户名/label input typetext nameusername required minlength3 maxlength20 /div div label密码/label input typepassword namepassword idpassword required minlength6 /div div label确认密码/label input typepassword nameconfirm idconfirm required /div div label昵称/label input typetext namenickName maxlength50 /div button typesubmit注册/button % if (request.getAttribute(error) ! null) { % p classerror% request.getAttribute(error) %/p % } % /form p已有账号a href${pageContext.request.contextPath}/login.jsp去登录/a/p /div script document.getElementById(regForm).addEventListener(submit, function (e) { var password document.getElementById(password).value; var confirm document.getElementById(confirm).value; if (password ! confirm) { alert(两次密码不一致); e.preventDefault(); } }); /script /body /html这里最关键的是表单 action 用了${pageContext.request.contextPath}。如果项目部署在/login-demo下这个表达式会渲染成/login-demo/register。很多新手直接写action/register结果浏览器请求发到了http://localhost:8080/register少了一层 context pathTomcat 自然返回 404。前端 required 和 minlength 能拦住大部分误操作但它是浏览器提示不是安全边界。3.2 登录页错误提示和“上次输入的用户名”别丢登录失败时最差的做法是直接弹 alert刷新就没了。我习惯把错误信息和用户名放进 request attribute由 Servlet forward 回 login.jsp。这样页面能保留用户输入同时显示具体失败原因。% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 % % String error request.getAttribute(error) null ? : (String) request.getAttribute(error); String username request.getAttribute(username) null ? : (String) request.getAttribute(username); % !DOCTYPE html html head meta charsetUTF-8 title登录/title link relstylesheet href${pageContext.request.contextPath}/css/style.css /head body div classform-box h2登录/h2 form action${pageContext.request.contextPath}/login methodpost div label用户名/label input typetext nameusername value% username % /div div label密码/label input typepassword namepassword /div button typesubmit登录/button % if (!error.isEmpty()) { % p classerror% error %/p % } % /form p没有账号a href${pageContext.request.contextPath}/register.jsp去注册/a/p /div /body /htmllogin.jsp 和 register.jsp 的区别在于登录成功后会重定向到 index.jsp而不是 forward。原因很简单如果登录成功后 forward 到 index用户按 F5 刷新时会再次提交 POST 表单可能造成重复登录。重定向是让浏览器重新发起一个 GET 请求这样刷新页面不会重放登录动作。4. 后端核心从JDBC到Servlet的登录注册完整代码怎么串起来页面只是外壳真正决定这套代码能不能用的是数据库连接、密码处理、Servlet 三层逻辑。我按工具类、DAO、Servlet 的顺序来写每段代码都可以直接放进你的工程里。4.1 DBUtil连接别每写一次 Class.forName 一次最简单也最常见的错误是在每个 DAO 方法里写一遍Class.forName(com.mysql.jdbc.Driver)。驱动加载一次就够了而且 8.x 驱动的类名是com.mysql.cj.jdbc.Driver写旧的com.mysql.jdbc.Driver会直接抛 ClassNotFoundException。package com.demo.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/login_demo ?useUnicodetruecharacterEncodingutf8 useSSLfalseallowPublicKeyRetrievaltrue serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { 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) { } } }URL 参数里的 characterEncoding 决定传给 MySQL 的字符串编码serverTimezone 必须写成 Asia/Shanghai否则 MySQL 8 会报时区错误。allowPublicKeyRetrievaltrue 是配合 MySQL 8 默认的 caching_sha2_password 认证插件不加会出现 Public Key Retrieval is not allowed 的报错。这里用的是单连接管理适合学习生产环境建议换连接池比如 HikariCP。4.2 PasswordUtil用盐哈希别把明文写进数据库密码不能直接存也不能只做一次 MD5因为彩虹表能反查。最稳妥的轻量做法是给每个用户生成独立随机盐再把“盐 密码”做 SHA-256 摘要。下面是可直接使用的工具类。package com.demo.util; import java.security.MessageDigest; import java.security.SecureRandom; public class PasswordUtil { private static final char[] HEX 0123456789abcdef.toCharArray(); public static String randomSalt() { byte[] bytes new byte[8]; new SecureRandom().nextBytes(bytes); return toHex(bytes); } public static String hash(String rawPassword, String salt) throws Exception { String text salt # rawPassword; MessageDigest md MessageDigest.getInstance(SHA-256); return toHex(md.digest(text.getBytes(UTF-8))); } public static boolean verify(String rawPassword, String salt, String expectedHash) throws Exception { return hash(rawPassword, salt).equalsIgnoreCase(expectedHash); } private static String toHex(byte[] bytes) { char[] out new char[bytes.length * 2]; for (int i 0; i bytes.length; i) { int v bytes[i] 0xff; out[i * 2] HEX[v 4]; out[i * 2 1] HEX[v 0x0f]; } return new String(out); } }randomSalt 用 SecureRandom 生成 8 个随机字节转成十六进制字符串后长度是 16 位。hash 方法把盐和密码拼起来做摘要verify 方法重新算一遍再比对。注意这里没有把盐拼在密码后面而是用#分隔避免“abc”和“a#bc”产生歧义。这种方案能防住“单表密码一致”的批量暴力猜解但 SHA-256 迭代速度很快真正的高安全场景应该用 BCrypt 或 Argon2课程设计和部门内部系统用这套足够。4.3 UserDaoPreparedStatement 是底线写 DAO 时最容易翻车的是用字符串拼接 SQL。用户名里带个单引号就能把语句拆了这是 SQL 注入的基本姿势。PreparedStatement 的?占位符会把参数当纯数据传给 MySQL这才是正确做法。package com.demo.dao; import com.demo.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class UserDao { public User findByUsername(String username) { String sql SELECT id, username, password_hash, salt, nick_name, create_time FROM t_user WHERE username ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getLong(id)); user.setUsername(rs.getString(username)); user.setPasswordHash(rs.getString(password_hash)); user.setSalt(rs.getString(salt)); user.setNickName(rs.getString(nick_name)); user.setCreateTime(rs.getTimestamp(create_time).toLocalDateTime()); return user; } } } catch (SQLException e) { throw new RuntimeException(查询用户失败, e); } return null; } public boolean insertUser(String username, String passwordHash, String salt, String nickName) { String sql INSERT INTO t_user(username, password_hash, salt, nick_name) VALUES (?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, passwordHash); ps.setString(3, salt); ps.setString(4, nickName); return ps.executeUpdate() 1; } catch (SQLException e) { if (e.getErrorCode() 1062) { return false; } throw new RuntimeException(插入用户失败, e); } } }User 是实体类字段和 t_user 表一一对应getter/setter 按 IDEA 的 Generate 快捷键生成即可这里不占篇幅。注意 findByUsername 无论是否为空都要用LIMIT 1或保证 username 唯一否则查到多行会抛异常。insertUser 里捕获 1062 重复键错误是应对并发注册的关键即使两个请求同时通过 findByUsername 检查数据库也会保证只有一个插入成功。4.4 Servlet 三层分支注册、登录、退出登录注册 Servlet 要做的事很明确收参数、校验、查重、算盐哈希、插入、跳转。失败时用 forward 回注册页把错误信息带回成功用 sendRedirect 去登录页避免重复提交。package com.demo.servlet; import com.demo.dao.UserDao; import com.demo.entity.User; import com.demo.util.PasswordUtil; 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 { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); String confirm request.getParameter(confirm); String nickName request.getParameter(nickName); if (username null || username.trim().length() 3) { request.setAttribute(error, 用户名至少 3 位); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } if (password null || password.length() 6) { request.setAttribute(error, 密码至少 6 位); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } if (!password.equals(confirm)) { request.setAttribute(error, 两次密码不一致); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } UserDao userDao new UserDao(); User exist userDao.findByUsername(username.trim()); if (exist ! null) { request.setAttribute(error, 用户名已存在); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } try { String salt PasswordUtil.randomSalt(); String hash PasswordUtil.hash(password, salt); boolean ok userDao.insertUser(username.trim(), hash, salt, nickName); if (ok) { response.sendRedirect(request.getContextPath() /login.jsp); } else { request.setAttribute(error, 用户名已存在); request.getRequestDispatcher(/register.jsp).forward(request, response); } } catch (Exception e) { throw new ServletException(注册失败, e); } } }登录 Servlet 的职责是校验用户名密码成功后把用户标识放进 Session。Session 是服务器端的不是 Cookie但 Tomcat 会通过一个叫 JSESSIONID 的 Cookie 告诉浏览器会话编号。package com.demo.servlet; import com.demo.dao.UserDao; import com.demo.entity.User; import com.demo.util.PasswordUtil; 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 javax.servlet.http.HttpSession; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); UserDao userDao new UserDao(); User user userDao.findByUsername(username); boolean ok false; if (user ! null) { try { ok PasswordUtil.verify(password, user.getSalt(), user.getPasswordHash()); } catch (Exception e) { throw new ServletException(密码校验失败, e); } } if (ok) { HttpSession session request.getSession(true); session.setAttribute(userId, user.getId()); session.setAttribute(loginUser, user.getNickName() null || user.getNickName().isEmpty() ? user.getUsername() : user.getNickName()); session.setMaxInactiveInterval(30 * 60); response.sendRedirect(request.getContextPath() /index.jsp); } else { request.setAttribute(error, 用户名或密码错误); request.setAttribute(username, username); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }退出登录的代码更短拿到 Sessioninvalidate然后重定向到登录页。WebServlet(/logout) public class LogoutServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(false); if (session ! null) { session.invalidate(); } response.sendRedirect(request.getContextPath() /login.jsp); } }Session 的默认超时是 30 分钟setMaxInactiveInterval只是把单位从秒转成可读的 1800 秒。这里有个细节登录成功后用request.getSession(true)如果当前请求没有 SessionTomcat 会创建一个如果已经有了就复用旧的。退出时用getSession(false)避免强制创建一个不存在的 Session。5. 登录注册页面常见问题排查中文乱码、404 和数据库连接三座大山我见过太多人卡在“代码看起来没问题但就是跑不通”的状态。下面这几类问题不是玄学每一类都有固定的原因和解决办法。5.1 中文乱码从页面到数据库四个地方都要盯现象注册页面填“张三”数据库里存成“å¼ ä¸‰”或者页面显示“????”。原因浏览器发出的 POST 请求体是 UTF-8 编码但 Tomcat 默认用 ISO-8859-1 解码JSP 页面没有声明 UTF-8JDBC URL 没带 characterEncoding表不是 utf8mb4。这四级里任何一级断了中文就会乱。解决每条 doPost 第一行写request.setCharacterEncoding(UTF-8)页面头部写contentTypetext/html; charsetUTF-8和pageEncodingUTF-8JDBC URL 里加useUnicodetruecharacterEncodingutf8建库时指定 utf8mb4。如果 GET 请求中文也乱还要在 Tomcat 的 Connector 配置里加URIEncodingUTF-8但这套登录注册的业务里查询参数基本不用中文POST 处理好了就够。5.2 表单提交到后端但请求 404八成是路径少了 context path现象浏览器地址栏输入http://localhost:8080/login.jsp能打开但点注册按钮后跳到一个 404 页面。原因表单 action 写成了/register。当前项目的 context path 是/login-demoTomcat 收到/register会去当前应用里找但 Servlet 实际地址是/login-demo/register。解决所有表单、链接都写${pageContext.request.contextPath}/xxx不要硬编码/xxx。这样不管部署的 context path 是/login-demo还是根路径/都能自动适配。同理Servlet 里 response.sendRedirect 也要拼 request.getContextPath()。5.3 IDEA运行javaweb项目配置Artifact、Context root 和 war 包三件事现象IDEA 里配置了 Tomcat点运行后浏览器打开http://localhost:8080/login-demo/login.jsp但 IDEA 控制台报Error during artifact deployment. See server log for details或者页面一直 404。原因Tomcat 运行配置里的 Deployment 没加 artifact或者加了但 Application context 和实际访问路径不一致。新手经常只配置 Server 标签页忘了 Deployment 标签页Tomcat 启动时根本没东西可部署。解决按这个顺序检查先让 Maven 把项目编译成 war在 IDEA 右侧 Maven 面板执行clean package再到 Run 菜单打开 Edit Configurations点左上角加号选 Tomcat Server → Local然后在 Deployment 标签页点加号选 Artifact类型是login-demo:war exploded最后把 Application context 写成/login-demo点击 Apply。这里还有一个容易忽略的坑一定要在 Before launch 区域确认Build Artifacts存在否则你改了 Java 代码IDEA 可能还在跑旧 class。5.4 MySQL 8 报 Public Key Retrieval is not allowed现象项目启动后第一次查询就抛java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed。原因MySQL 8.0 默认使用 caching_sha2_password 认证插件客户端连接时需要向服务器请求公钥进行密码加密。JDBC 驱动出于安全考虑默认不允许这种公钥检索。解决在 JDBC URL 里追加allowPublicKeyRetrievaltrue同时加上useSSLfalse完整地址我在 4.1 的 DBUtil 里已经写出来了。如果已经加了仍然报 Access denied那就是 MySQL 账号密码或库名不一致。排查时直接在 IDEA 的 Database 面板测一次连接能连上再跑项目能省掉一半纠结。6. 给登录注册页面完整代码加一把锁过滤器与本地验收流程到了这一步你会意识到一个问题登录成功后访问 index.jsp 是没有拦的别人直接输 URL 也能打开。所以最后要加一个过滤器把没有登录标识的请求重定向到登录页。6.1 AuthFilter 把未登录请求拦在登录页package com.demo.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 AuthFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String path req.getRequestURI().substring(req.getContextPath().length()); if (path.startsWith(/login) || path.startsWith(/register) || path.startsWith(/css) || path.endsWith(.js)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); if (session null || session.getAttribute(userId) null) { req.setAttribute(error, 请先登录); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } chain.doFilter(request, response); } }过滤器把公开资源放行把受保护页面拦住。这里要特别注意静态资源也要放行否则浏览器请求 style.css 也会被转到登录页页面样式全丢。filter 里的判断基于 Session只要用户点退出、Session 过期访问受保护页面就会回到登录页。这是 Sky 层面最简单也最有效的拦截方案。6.2 用 curl 做一轮黑盒验证写完代码不要只在浏览器里点鼠标我习惯用 curl 跑一遍能快速确认 POST 参数、Session Cookie 是否真的生效。curl -i -X POST http://localhost:8080/login-demo/register \ --data usernametest1password123456confirm123456nickNametester curl -c cookie.txt -i -X POST http://localhost:8080/login-demo/login \ --data usernametest1password123456 curl -b cookie.txt -i http://localhost:8080/login-demo/index.jsp第一条命令注册成功后应该看到 302 重定向第二条命令如果能登录会在 cookie.txt 里留下 JSESSIONID第三条命令带着这个 Cookie 访问 index.jsp应该返回 200。如果第三条返回的是登录页说明 Session 没写进去或者被过滤器拦了。这个流程每天跑一遍比在浏览器里肉眼确认可靠得多。我自己的习惯是每改一次代码就重启 Tomcat清掉浏览器缓存开一个无痕窗口走一遍注册、退出、登录、过期重定向。这套重复动作虽然枯燥但挡掉过我太多因为 Session 残留和页面缓存导致的“假 bug”。希望这份代码和这些排查思路能帮你把登录注册这个 JavaWeb 入门槛稳稳迈过去。本文还有配套的精品资源点击获取