简介:一份面向JavaWeb初学者的登录注册页面完整代码示例,涵盖JSP前端表单与Servlet后端处理逻辑,适合学习Web基础认证流程的开发者参考。资源仅含1个PDF文件,压缩包大小797KB,集中展示了index.jsp、LoginServlet、RegisterServlet以及success.jsp、failure.jsp等关键代码片段,方便对照阅读。该资源已有2380人学习浏览,适合作为入门阶段的手把手范例。示例演示了用户通过表单提交用户名和密码、Servlet获取参数并进行简单校验、成功或失败后重定向对应页面的完整过程;同时给出了RegisterServlet中扩展验证的思路,如检查用户名是否已存在、密码最小长度与复杂度等,并提醒实际项目还需引入数据库持久化、SQL注入防护、密码哈希加密等安全措施,帮助读者建立从demo到真实项目的进阶认识。
1. 这套 JavaWeb 登录注册代码:能跑通就是入门最快的路径
做 JavaWeb 开发绕不开登录注册,这几乎是每个初学者第一个要动手写的完整功能。这套代码示例把 Servlet + JSP 的请求处理链路完整串了起来:index.jsp提供登录和注册两个表单,LoginServlet和RegisterServlet分别处理 POST 请求,成功重定向到success.jsp,失败重定向到failure.jsp,代码量不大但五脏俱全。对刚学完 Servlet 理论、想在 IDEA 里跑通第一个 Web 项目的人来说,它是很好的起步素材;对已经写过业务代码的开发者,则可以把它当成一个检查自己基础是否扎实的对照样本。不过先说清楚,这套代码属于教学演示级别,离生产环境还差得远,硬编码的账号密码、明文传输、没有数据库持久化,这些都是在后续开发中必须替换掉的。
2. 表单提交与 Servlet 映射:先搞懂请求是怎么走到 doPost 的
2.1 index.jsp 里两个表单的关键差异
index.jsp是整套代码的入口,页面里同时放了登录和注册两个表单,它们都通过 POST 方法提交,但action指向不同的路径:登录表单提交到login,注册表单提交到register。这个设计很直观,两个表单共用同一个页面,但请求交给不同的 Servlet 处理,避免了在同一个 Servlet 里用分支判断去区分业务逻辑。
表单里的required属性值得注意,它是 HTML5 原生的非空校验,在浏览器端就会阻止空表单提交,能在到达服务器之前拦截掉一部分无效请求。但要注意,这只是前端体验层面的校验,不能作为安全手段——请求完全可以绕过浏览器直接构造,所以 Servlet 端必须再做一次参数校验,这也就是后面RegisterServlet里重新检查username和password是否为 null 的原因。
来看index.jsp的核心代码结构:
<h2>Login</h2> <form action="login" method="post"> <label for="username">Username:</label> <input type="text" id="username" name="username" required><br><br> <label for="password">Password:</label> <input type="password" id="password" name="password" required><br><br> <input type="submit" value="Login"> </form> <h2>Register</h2> <form action="register" method="post"> <label for="username">Username:</label> <input type="text" id="username" name="username" required><br><br> <label for="password">Password:</label> <input type="password" id="password" name="password" required><br><br> <input type="submit" value="Register"> </form>两个表单的字段名都叫username和password,这意味着两个 Servlet 里通过request.getParameter("username")取值的代码可以完全一致。但更推荐的实践是给注册表单额外加上确认密码、邮箱等字段,在后续章节里我会给出扩展思路。这里还有一个细节:<input type="password">只是让输入内容在界面上显示为圆点,数据本身还是明文传输的,所以 HTTPS 在生产环境是刚需。
2.2 Servlet 映射:web.xml 配置与注解两种方式的取舍
表单提交到login和register路径之后,需要让 Servlet 能接住这个请求,这一步依赖映射配置。老项目通常在web.xml里写<servlet-mapping>,新项目更推荐用@WebServlet注解,代码量少、就近维护。
@WebServlet("/login") public class LoginServlet extends HttpServlet { // doPost 方法实现 }@WebServlet("/login")的含义是把LoginServlet绑定到/login这个访问路径上,当浏览器或表单提交login请求时,Tomcat 就会调用这个类的doPost方法。要注意这里的路径是 URL 匹配模式,不是文件名,所以不需要写.jsp后缀。
如果走web.xml老方式,配置长这样:
<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.example.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>两种方式本质一样,但有几个容易翻车的区别:注解方式下类名变了映射就会失效,而web.xml方式下如果servlet-class写错包名,Tomcat 启动时就会报ClassNotFoundException。另外注意,同一个 URL 不能同时被注解和web.xml映射,否则容器会报冲突,启动阶段直接失败。
2.3 doGet 与 doPost:表单声明 method=post 后为什么还要处理 doGet
LoginServlet只重写了doPost方法,这在表单提交场景下没问题。但有一个常见情况会踩坑:用户直接在浏览器地址栏输入http://localhost:8080/your-app/login,这触发的是一次 GET 请求,而父类HttpServlet的doGet默认实现是返回 405 错误。页面会显示 "HTTP method GET is not supported by this URL",不懂的人会以为是代码写崩了。
实际开发中有个约定俗成的做法是同时重写doGet和doPost,让它们调用同一个处理逻辑:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 核心逻辑 }这样即使有人用 GET 方式访问,也能走同一套处理逻辑,不会出现 405。但要注意,如果你把 GET 请求也交给登录逻辑处理,敏感信息会出现在 URL 参数里(虽然我们现在的逻辑不依赖 URL 参数,但这是个坏习惯),所以更严谨的做法是只允许 POST,GET 请求直接重定向回登录页。
3. LoginServlet 用户认证:从硬编码账号到可扩展的校验逻辑
3.1 原始实现的局限性与改进方向
LoginServlet的原始代码非常简单,把用户提交的用户名密码直接和硬编码的"admin"/"admin"比较,相同就跳转success.jsp,不同就跳转failure.jsp。这段代码能跑通流程,但有两个明显问题:一是账号密码写死在代码里,每次改密码都要改代码重新编译;二是username.equals("admin")这句代码如果username是 null,会直接抛出NullPointerException。
原因是request.getParameter("username")在表单字段不存在或者请求里没有携带该参数时返回 null,此时调用null.equals("admin")就炸了。虽然 HTML 的required属性挡得住正常用户的空提交,但抓包工具构造的请求根本不带这个字段,所以代码层面必须防御。
改进后的登录校验逻辑:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); if (username == null || password == null || username.trim().isEmpty()) { response.sendRedirect("login.jsp?error=1"); return; } // 生产环境这里应该改为查数据库,比如从 user 表查记录 if ("admin".equals(username) && "admin".equals(password)) { HttpSession session = request.getSession(); session.setAttribute("username", username); response.sendRedirect("success.jsp"); } else { response.sendRedirect("failure.jsp?message=username or password incorrect"); } }这里把"admin".equals(username)写成常量在前、变量在后,是 Java 开发者防御 NPE 的经典写法,因为常量不是 null,equals 永远不会抛空指针。username.trim().isEmpty()用来过滤掉"全空格"这种看起来像有内容实际是空串的情况。做完这些基础防御之后,认证逻辑依然没有真正落地,因为它还在和写死的字符串比较。
3.2 认证流程设计:从 Servlet 到 Service 再到 DAO 的三层划分
当登录逻辑要接数据库时,就不能在 Servlet 里直接写 JDBC 代码了,否则doPost方法会膨胀到无法维护。常见的分层做法是 Servlet 只负责取参、调服务、做跳转,真正的业务逻辑放在 Service 层,数据库操作放在 DAO 层。以登录为例,Servlet 拿到参数后调用userService.login(username, password),Service 内部调userDao.findByUsername(username),拿到用户记录后比对密码哈希。
public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.login(username, password); if (user != null) { request.getSession().setAttribute("user", user); response.sendRedirect("success.jsp"); } else { response.sendRedirect("failure.jsp?message=bad credentials"); } } }UserService里要做的三件事:校验参数合法性、调用 DAO 查询用户、比对加密后的密码。任何一步失败都返回 null 或者抛出业务异常,Servlet 层不关心具体是"用户不存在"还是"密码错误",统一走失败跳转。从安全角度讲,这种模糊处理反而更好,因为它不会泄露账号是否存在的信息,避免攻击者通过"用户不存在"的提示批量探测有效账号。
这套代码作为一个教学示例,没有引入这些分层,但我建议你在自己的项目里至少把 DAO 单独拆出来。原因很简单:一旦数据库表结构变了,只需要改动 DAO 层,Servlet 和 JSP 完全不受影响。后面注册功能接入数据库之后,你会发现这个拆分几乎是必然的。
4. RegisterServlet 注册校验:空值、重名、密码复杂度三层把关
4.1 参数合法性检查与错误信息传递
RegisterServlet的原始代码只检查了username和password是否为空,这种力度显然不够。在实际注册场景中,至少要过三道关:非空校验、用户名唯一性校验、密码复杂度校验。先看第一道关怎么加固:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); if (username == null || password == null || username.trim().isEmpty() || password.trim().isEmpty()) { response.sendRedirect("failure.jsp?message=" + URLEncoder.encode("请填写完整的用户名和密码", "UTF-8")); return; } // 第二道关:用户名唯一性校验 if (usernameExists(username)) { response.sendRedirect("failure.jsp?message=" + URLEncoder.encode("用户名已存在", "UTF-8")); return; } // 第三道关:密码复杂度校验 if (!passwordMeetsCriteria(password)) { response.sendRedirect("failure.jsp?message=" + URLEncoder.encode("密码长度至少8位,且包含大小写字母和数字", "UTF-8")); return; } // 到这里说明校验全部通过,后续执行注册逻辑 response.sendRedirect("success.jsp"); }这里有一个容易被忽略的技术点:跳转 URL 里带中文参数必须做 URL 编码,否则浏览器解析会乱码,甚至某些容器直接报 400 错误。我用URLEncoder.encode(message, "UTF-8")把错误信息编码后再拼到 URL 里。
这个做法的另一种替代方案是用request.setAttribute("message", message)配合request.getRequestDispatcher("failure.jsp").forward(...)转发,这样中文参数不需要做 URL 编码,而且浏览器地址栏不会暴露错误信息。两者的区别在于:sendRedirect是重定向,浏览器会发起第二次请求,URL 会变;forward是服务器内部转发,URL 不变。在这个场景下我更推荐 forward,因为失败信息不需要被用户分享或收藏,留在服务器内部处理更干净。
4.2 密码复杂度校验的逐字符遍历实现
密码复杂度校验是注册功能的核心逻辑,前文代码已经展示过一个逐字符遍历的实现。拆开看一下它的逻辑:
private boolean passwordMeetsCriteria(String password) { if (password.length() < 8) { return false; } boolean hasUppercase = false; boolean hasLowercase = false; boolean hasDigit = false; for (char c : password.toCharArray()) { if (Character.isUpperCase(c)) { hasUppercase = true; } else if (Character.isLowerCase(c)) { hasLowercase = true; } else if (Character.isDigit(c)) { hasDigit = true; } } return hasUppercase && hasLowercase && hasDigit; }Character.isUpperCase、isLowerCase、isDigit三个方法分别判断字符类型,只要三类字符都出现过,校验就通过。这个实现没有检查特殊符号,也没有限制最大长度,属于基础规则。如果你想加特殊字符要求,加一个布尔变量,在else分支里判断!Character.isLetterOrDigit(c)即可。
我在实际项目中一般会把这套规则做成可配置的,用一个常量类统一管理:
public class PasswordPolicy { public static final int MIN_LENGTH = 8; public static final int MAX_LENGTH = 20; public static final boolean REQUIRE_SPECIAL_CHAR = true; public static final boolean REQUIRE_UPPERCASE = true; public static final boolean REQUIRE_DIGIT = true; }这样改策略不用翻业务代码,改常量就行。说到底,密码校验的目的是提高账号被暴力破解的成本,规则越严越安全,但也要考虑用户输入的挫败感,所以上限长度建议别卡太死,20 位足够。
4.3 用户名唯一性检查的 DAO 占位逻辑与后续接库方案
示例代码里usernameExists方法返回的是false占位逻辑,注释写着 "Placeholder logic",这意味着目前所有用户名都会被判定为"不存在"。在演示场景下效果就是任何人都能注册成功,但接上数据库之后这个方法的行为就完全不同了。
private boolean usernameExists(String username) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); String sql = "SELECT id FROM user WHERE username = ?"; ps = conn.prepareStatement(sql); ps.setString(1, username); rs = ps.executeQuery(); return rs.next(); // 有记录说明已存在 } catch (SQLException e) { e.printStackTrace(); return false; // 数据库异常时保守放行,让注册逻辑继续走 } finally { DBUtil.close(conn, ps, rs); } }这里必须要用PreparedStatement而不是拼 SQL 字符串,原因不多说了,SQL 注入是最经典的 Web 漏洞。?占位符配合setString把用户输入当作参数传给数据库,而不是拼进 SQL 语句里,从根上切断了注入路径。
占位逻辑里还有一个小细节:用户名校验和注册插入之间天然存在竞态条件——两个请求同时检查同一个用户名都不存在,然后同时插入,最终数据库里可能出现两条同名的记录。解决这个问题必须依靠数据库层面的唯一索引约束,在user表的username字段上建UNIQUE索引,插入时捕获DuplicateKeyException再返回"用户名已存在",这才是唯一可靠的方案。Java 代码里再小心也防不住并发。
5. 密码加密选型:为什么 SHA-256 只算及格,BCrypt 才适合生产
5.1 SHA-256 加密的代码实现与局限性
代码示例里给出了两种密码加密思路,一种是用 Java 标准库的MessageDigest实现 SHA-256,另一种是用 Spring Security 的BCryptPasswordEncoder。先看 SHA-256 实现:
private String encryptPassword(String password) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(password.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : hash) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("SHA-256 algorithm not available", e); } }这段代码的逻辑是:通过MessageDigest.getInstance("SHA-256")拿到算法实例,把密码字符串转成 UTF-8 字节数组喂进去,digest方法返回 32 字节的哈希值,最后遍历字节数组用String.format("%02x", b)格式化成十六进制字符串。这里%02x表示两位小写十六进制,不足两位补零,确保每个字节固定输出两位,避免哈希值长度不一致。
但 SHA-256 有个致命弱点:它是快速哈希算法,计算速度极快,攻击者可以用 GPU 集群每秒跑几十亿次。用户密码普遍存在规律性,一旦数据库泄露,彩虹表加暴力破解很快就能还原出弱密码。所以 SHA-256 只能算及格,离"安全存储密码"的要求还有距离。
5.2 BCrypt 接入方式:强哈希加盐的核心设计
BCrypt 的设计目标恰好是解决 SHA-256 的问题,它内置随机盐值、迭代成本可调,每次加密相同的密码都会产生不同的哈希结果,而且计算速度被刻意设计得很慢,让暴力破解的成本指数级上升。示例代码里用的是 Spring Security 的BCryptPasswordEncoder:
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encodedPassword = passwordEncoder.encode(password); // 存库时保存 encodedPassword,登录比对时调用 matches 方法BCryptPasswordEncoder默认强度是 10,代表 2 的 10 次方轮迭代,你可以根据服务器性能调大或调小。调大更安全但响应变慢,调小反之。我在项目里一般设 10 到 12,这个区间在普通服务器上单次加密耗时约 50 到 100 毫秒,用户感知不明显,但已经足够拖慢批量破解。
如果不想引入 Spring Secure 依赖,可以用jBCrypt这个轻量库,核心代码如下:
import org.mindrot.jbcrypt.BCrypt; // 注册时生成哈希 String hashed = BCrypt.hashpw(password, BCrypt.gensalt(12)); // 登录时校验 boolean matched = BCrypt.checkpw(password, storedHash);这里的gensalt(12)生成一个带成本因子的随机盐,hashpw把密码和盐合成最终哈希。校验时checkpw会从storedHash里自动提取盐和成本因子,不需要你额外存储盐值,这是 BCrypt 比"SHA-256 + 手动加盐"方便非常多的地方。
顺带说一句,MD5 千万不要用。网上有很多 MD5 加密的教程,但它的碰撞攻击和彩虹表都已经非常成熟,拿 MD5 存密码基本等于裸奔。如果项目里已经有 MD5 存量数据,建议在登录校验时做成"先验证 MD5 再自动升级为 BCrypt",逐步迁移。
5.3 密码加密方案对比与选型建议
把常见方案放在一起对比,选型思路会更清晰:
| 方案 | 是否加盐 | 计算速度 | 抗暴力破解 | 建议 |
|---|---|---|---|---|
| MD5 | 否 | 极快 | 极差 | 禁止使用 |
| SHA-256 | 手动加盐 | 快 | 差 | 不推荐,已有系统可临时用 |
| SHA-256 + 服务端加盐 | 是 | 快 | 一般 | 勉强可用 |
| BCrypt | 内置随机盐 | 慢(可控) | 强 | 优先推荐 |
| PBKDF2 | 内置盐 | 慢(可控) | 强 | 合规场景可选 |
选择加密方案的决策点只有一个:数据库泄露后,攻击者拿到哈希值反推明文的成本有多高。MD5 和 SHA-256 在 GPU 算力面前几乎不设防,BCrypt 和 PBKDF2 因为有成本参数控制,可以有效拖慢破解速度。所以哪怕你的项目再小,密码存储也建议直接上 BCrypt,这一行代码的改动带来的安全收益远大于那一点点性能损耗。
6. JavaWeb 登录注册常见问题排查:四个必踩的坑与解决方案
6.1 现象一:Tomcat 启动报 404,访问 login 路径找不到资源
原因排查方向很明确:Servlet 类没有正确编译到WEB-INF/classes目录,或者@WebServlet注解没被扫描到。解决方案是检查 IDEA 的项目结构,确认web.xml配置了metadata-complete="false"(或者干脆删掉这个属性),否则注解会被容器忽略;同时确认 Servlet 类上有@WebServlet("/login")注解且包名和实际路径一致。
一个实用的排查技巧是启动 Tomcat 时看日志里有没有Mapping servlet: 'LoginServlet' to [/login]这行输出,如果没有说明映射没生效,直接去查注解或配置文件,省得在页面上反复刷新浪费时间。
6.2 现象二:表单提交后中文用户名乱码
这是 JavaWeb 项目的经典坑。原因是 POST 请求的表单数据默认按 ISO-8859-1 解码,而页面是 UTF-8 编码,两边对不上就乱码。解决方式是在doPost方法最前面加一行:
request.setCharacterEncoding("UTF-8");注意这句话必须放在request.getParameter之前才有效,放在后面就晚了。还有一个隐蔽的坑:如果你的 Tomcat 版本比较老(8.0 以下),还必须在server.xml里给 Connector 加URIEncoding="UTF-8"才能处理 GET 请求的乱码,POST 请求的乱码只靠 Servlet 里的setCharacterEncoding就能解决。
6.3 现象三:登录成功后跳转到 success.jsp,刷新一下变成 404
原因在于用了sendRedirect做重定向,浏览器地址栏变成了http://localhost:8080/your-app/success.jsp,如果你把success.jsp放在了WEB-INF目录下,Tomcat 出于安全考虑禁止外部直接访问该目录下的资源,于是刷新就 404。解决方式有两种:要么把 JSP 放在webapp根目录下(不进WEB-INF),要么改用request.getRequestDispatcher("success.jsp").forward(request, response)做服务器内部转发。
我在项目里更倾向用 forward 处理成功页面,因为WEB-INF下可以防止用户绕过登录逻辑直接访问页面,这个结构本身就是一种安全手段,但代价是 URL 不变且刷新会重复提交表单。两个方案都有取舍,看你的页面需要哪种行为。
6.4 现象四:Tomcat 10 上运行报ClassNotFoundException: javax.servlet.*
这是从 Tomcat 9 升级到 Tomcat 10 最常见的兼容性问题。Tomcat 10 把 Servlet API 的包名从javax.servlet改成了jakarta.servlet,老代码里import javax.servlet.http.HttpServlet全部失效。解决方案有两种:一是把代码里所有javax.servlet替换为jakarta.servlet,同时确保项目构建依赖用的也是jakarta.servlet-api;二是把 Tomcat 降回 9.x,但这不是长久之计。IDE 的全局替换功能可以一键完成包名迁移,但替换后要重点检查web.xml头部的命名空间引用,web-app标签的xmlns也要同步改为jakarta命名空间。
7. JSP 页面的进阶用法:用 JSTL 和 EL 表达式改造跳转后的展示逻辑
7.1 在 success.jsp 中通过 session 显示当前登录用户
前文的登录逻辑已经用session.setAttribute("username", username)存了登录用户,但success.jsp里要把它显示出来就需要 JSP 的内置对象session配合 EL 表达式。
核心做法是这样:
<!DOCTYPE html> <html> <head> <title>Success</title> </head> <body> <h1>Welcome, ${sessionScope.username}!</h1> <p>You have successfully logged in.</p> <a href="logout">Logout</a> </body> </html>${sessionScope.username}是 EL 表达式的标准写法,sessionScope指定查找范围,username对应setAttribute时用的 key。这里有个细节值得注意:如果用<%= session.getAttribute("username") %>这种 scriptlet 写法也能实现,但页面里混入 Java 代码会让 JSP 变得难以维护,EL 表达式是更现代的做法。如果你传的是User对象,还可以直接点出属性,比如${sessionScope.user.username},比取单个字符串值方便得多。
7.2 用 JSTL 的 c:if 在 failure.jsp 中按错误类型显示不同提示
更进阶一点的用法是结合 JSTL 标签库,根据不同的错误类型展示不同的反馈。既然失败页面是通过sendRedirect("failure.jsp?message=...")带参数跳转的,那在 JSP 里通过${param.message}就能拿到这个值。
用c:if做条件展示:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <title>Registration Failed</title> </head> <body> <h1>Registration Failed</h1> <c:choose> <c:when test="${param.message == 'existed'}"> <p>Username already exists, please choose another one.</p> </c:when> <c:when test="${param.message == 'weakpassword'}"> <p>Password does not meet the requirements.</p> </c:when> <c:otherwise> <p>${param.message}</p> </c:otherwise> </c:choose> <a href="index.jsp">Back to Login/Register</a> </body> </html>这套写法的好处是:错误提示和业务逻辑解耦,Servlet 里只需要传一个简短的错误码(如existed或weakpassword),页面负责把错误码翻译成用户能看懂的文字。如果你在多个地方都要跳转到失败页,改提示文案不用动 Servlet,只改 JSP 即可。使用 JSTL 记得在pom.xml或 lib 目录里引入jstl-1.2.jar,很多新手卡在"页面直接展示 JSTL 源码"这个问题上,原因就是忘了加依赖。
7.3 从这套示例走向完整项目的最后一块拼图
把登录注册做成可以给别人用的系统,只靠 JSP 展示成功失败远远不够。后续要补的三件套是:用户表设计、Session 拦截器、验证码。用户表至少要有id、username、password_hash、created_at四个字段,username加唯一索引;Session 拦截器可以用一个LoginFilter实现,检查所有受保护页面的session.getAttribute("user")是否为 null;验证码推荐用kaptcha库,生成图片比手写字母数字组合靠谱得多。我自己的经验是:先把这套基础示例跑通,再按这个顺序逐步加功能,每一步都验证一次,比拿到完整项目直接抄要扎实得多。这也是我后来带项目时强制自己走的流程——先跑通最简路径,再逐层加固。希望这套代码能帮你把 JavaWeb 的登录注册这块基础补扎实。
本文还有配套的精品资源,点击获取