JavaWeb登录注册实例:验证码、BCrypt加密与部署全解析
简介面向Java Web初学者的登录注册案例解析文档以完整可运行的实例演示带验证码的用户登录与注册流程。文档从需求分析入手逐步讲解登录页面与注册页面的界面设计、关系数据库中用户名、密码与编号字段的设计并在注册功能中通过主键唯一约束保证用户名不可重复。技术栈覆盖Servlet、JSP、MySQL、JDBCTemplate、Druid连接池、BeanUtils及Tomcat部署通过代码片段展示验证码生成与校验、登录成功后的重定向与用户名回显、Druid配置类等关键实现同时也说明了连接池对系统性能与可靠性的提升作用。资源包为单个PDF文件压缩包整体仅148KB内容精炼而系统完整。目前已由4793人学习下载适合正在学习Java Web基础开发、需要参考完整项目搭建思路的读者按文中步骤即可理解从页面设计到服务器部署的全过程也便于课程设计或毕业设计时快速复用。文档按章节组织代码与说明易对照查阅。1. 登录注册加验证码JavaWeb简易项目为什么值得亲手写一遍学完 Servlet、JSP 和 JDBC 之后最缺的是一套能把三样东西串起来的完整案例。这个「JavaWeb 简单用户登录注册实例代码有验证码」正好卡在这个需求上页面 请求处理 数据库读写 会话管理 验证码一条链路全走通。验证码是这里最值得抠的细节——它不是为了好看而是为了拦住脚本批量撞库和重复提交在真实系统里属于成本低、效果明显的防线。适合刚学完 JavaWeb 基础、想在 IDEA 里把第一个完整项目跑起来的人也适合做课程设计需要一套「能演示、能截图、能讲清楚」代码的人。别急着找现成源码抄先把这个项目拆成环境、建表、验证码、登录四个部分每部分都弄明白之后再去看黑马那套完整案例或者网上各种 JavaWeb 项目你会发现自己真能看懂别人在写什么了。2. 搭建能跑起来的 JavaWeb 骨架从 IDEA 配置到 Tomcat 部署2.1 环境选型JDK、Tomcat、MySQL 的版本搭配做这类登录注册项目版本搭配要保守。JDK 8 是最稳的选择Tomcat 8.5 或 9.0 都行MySQL 5.7 最省心。你可能会看到网上有人用 JDK 11 Tomcat 10但 Tomcat 10 把包名从javax.servlet改成了jakarta.servlet很多老教程里的代码直接复制会报ClassNotFoundException。新手阶段没必要跟这个坑较劲JDK 8 Tomcat 8.5 MySQL 5.7 的搭配能找到的参考案例最多出了问题也最好搜。数据库驱动方面MySQL 5.7 配mysql-connector-java 5.1.49如果你非要用 MySQL 8那必须换com.mysql.cj.jdbc.Driver连接串里还得加上serverTimezoneAsia/Shanghai否则会报时区错误。这两条是我每次搭环境都会先确认的版本不匹配的问题往往比业务代码更磨人。2.2 新建 Web 项目用 Maven 还是手工建目录如果你在用 IDEA新建项目时直接选 Maven 骨架org.apache.maven.archetypes:maven-archetype-webapp这套骨架自带src/main/webapp目录和web.xml省去手工建目录的麻烦。用 Maven 的核心好处只有一个依赖版本不会乱。Servlet API、JSP API、MySQL 驱动、连接池全部写进pom.xml换机器重新导入不会出现缺 jar 包的问题。pom.xml里的最小依赖集是这样dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjsp-api/artifactId version2.0/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency /dependenciesprovided作用域很关键Tomcat 自带的 Servlet 和 JSP 实现已经存在打包时不要把这两份重复打进去否则部署时会冲突。MySQL 驱动则必须打进包里因为 Tomcat 的 lib 目录里没有它。2.3 跑通最小 Servlet先确认环境没问题再写业务代码新项目最容易出现的问题不是代码而是 IDEA 里 Tomcat 没配好。建议先写一个什么都不干的 Servlet确认浏览器能访问到再开始写正题。在web.xml里注册 Servlet 的老做法虽然繁琐但能看到完整的映射机制servlet servlet-nameHelloServlet/servlet-name servlet-classcom.example.servlet.HelloServlet/servlet-class /servlet servlet-mapping servlet-nameHelloServlet/servlet-name url-pattern/hello/url-pattern /servlet-mapping对应的 Java 类继承HttpServlet重写doGet方法输出一行文本。这里要理解一件事url-pattern决定浏览器地址栏里访问的路径servlet-class决定这个路径对应哪个类。很多初学项目跑不起来不是代码错而是 Tomcat 部署时 Artifact 没配好访问 404 后完全不知道去哪排查。先在 IDEA 的 Run Configuration 里确认 Deployment 标签页有war exploded包Application context 是/还是/项目名这两个值直接影响你最终访问的 URL 前缀。跑通这个最小的 Servlet你的 JavaWeb 环境就算立住了。整个过程如果项目名里带中文Tomcat 控制台会输出乱码这是编码问题后面说中文数据的时候一起处理。3. 用户表设计与注册功能从建库到密码加密落库3.1 用户表设计字段、字符集与唯一索引登录注册项目的数据表极简但设计上有三个硬性要求用户名必须唯一、密码不能存明文、时间字段留作审计。常见的建表语句长这样CREATE DATABASE IF NOT EXISTS user_db DEFAULT CHARACTER SET utf8mb4; USE user_db; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 加密后的密码, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱可空, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 COMMENT用户表;utf8mb4不是可选项。如果你用默认的latin1或utf8中文用户名和带 emoji 的邮箱会直接报错或者存成乱码。用户名上的唯一索引是为了在数据库层面兜底防止应用层并发时两条一样的用户名同时插入成功。密码字段长度留VARCHAR(100)是因为 BCrypt 加密后的字符串长度超过传统 32 位 MD5如果预留 45 位后期换算法会很狼狈。这个表结构是我见过最多的标准写法黑马的 JavaWeb 案例里也差不多的套路字段再少不能少了时间戳排查问题全靠它。3.2 注册页面与表单校验前端校验只是用户体验注册页面的作用是把用户输入送到后端前端的required、maxlength这些属性只负责提示不能当作安全措施。一个干净的注册表单核心部分是这样的form action/register methodpost label用户名/label input typetext nameusername required maxlength20 / label密码/label input typepassword namepassword required maxlength20 / label确认密码/label input typepassword nameconfirmPassword required / label验证码/label input typetext namecaptcha required maxlength4 / img src/captcha idcaptchaImg title看不清点击刷新 / button typesubmit注册/button /form一个最容易忽略的点密码框和确认密码框的比对必须在后端再验一次。前端 JS 比对只是让用户体验好一点攻击者完全可以绕过页面直接 POST 请求。我在写这类项目时习惯把前端校验理解为「少发一次错误请求」的优化手段后端才是真正做判断的地方。3.3 注册 Servlet参数校验、唯一性检查与 BCrypt 加密注册接口的业务逻辑有四步收参数、校验、查重、落库。校验的意思是用户名和密码不能为空密码和确认密码一致验证码不能为空。查重和落库走 JDBC加密用 BCrypt。完整代码package com.example.servlet; import com.example.util.DBUtil; import com.example.util.PasswordUtil; 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 javax.servlet.http.HttpSession; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; 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 confirmPassword request.getParameter(confirmPassword); String captcha request.getParameter(captcha); // 1. 后端必须重复做的参数校验 if (username null || username.trim().isEmpty() || password null || password.trim().isEmpty()) { request.setAttribute(msg, 用户名和密码不能为空); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } if (!password.equals(confirmPassword)) { request.setAttribute(msg, 两次密码不一致); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } // 2. 校验验证码从 Session 里取校验后立即移除 HttpSession session request.getSession(); String sessionCaptcha (String) session.getAttribute(captcha); if (sessionCaptcha null || !sessionCaptcha.equalsIgnoreCase(captcha)) { request.setAttribute(msg, 验证码错误); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } session.removeAttribute(captcha); // 3. 检查用户名是否已存在 String checkSql SELECT id FROM t_user WHERE username ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { request.setAttribute(msg, 用户名已存在); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } } } catch (SQLException e) { throw new RuntimeException(查询用户失败, e); } // 4. BCrypt 加密后插入 String hashed BCrypt.hashpw(password, BCrypt.gensalt(12)); String insertSql INSERT INTO t_user (username, password) VALUES (?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, username); ps.setString(2, hashed); ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(插入用户失败, e); } response.sendRedirect(/login.jsp); } }这里有两个值得记住的参数。第一BCrypt.gensalt(12)的 12 是加密轮数轮数越高越安全但越慢12 是性能和安全的常见平衡点。第二session.removeAttribute(captcha)这行不能省验证码必须是一次性的用过就销毁否则同一个验证码能反复提交验证码就白做了。3.4 DBUtil 与连接管理try-with-resources 是底线上面代码里的DBUtil.getConnection()是用一个静态方法返回数据库连接。最简单的写法是直接在工具类里加载驱动并创建连接用 ThreadLocal 或者连接池都可以。这里给一个能用的基础版package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/user_db?useSSLfalsecharacterEncodingUTF-8; private static final String USER root; private static final String PASSWORD your_password; static { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }连接串里的characterEncodingUTF-8是中文不乱码的关键。DriverManager.getConnection每次创建物理连接开发阶段够用但后期并发上来一定要换连接池。网上搜「JavaWeb 完整案例」能看到别人项目里集成 Druid 或 HikariCP 的做法那是后面要做的优化不是现在卡壳的重心。现阶段最需要注意的是所有Connection、PreparedStatement、ResultSet必须关闭上面的代码全部用了 try-with-resources这是底线手工在 finally 里关闭容易漏一旦连接泄漏数据库到几百个连接就拒绝服务了。4. 图片验证码生成与登录校验核心流程的实现与边界4.1 验证码生成 ServletBufferedImage 与 Graphics2D 画图验证码的核心是「服务端生成图片、服务端保存答案、客户端填写、服务端比对」这条链路。答案存在 Session 里图片只负责展示给人类看。生成验证码的 Servletpackage com.example.servlet; import javax.imageio.ImageIO; 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.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Random; public class CaptchaServlet extends HttpServlet { private static final int WIDTH 120; private static final int HEIGHT 40; private static final char[] CHARS ABCDEFGHJKLMNPQRSTUVWXYZ23456789.toCharArray(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { BufferedImage image new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); Graphics2D g image.createGraphics(); Random random new Random(); // 背景填充 g.setColor(new Color(245, 245, 245)); g.fillRect(0, 0, WIDTH, HEIGHT); // 生成 4 位验证码文本 StringBuilder code new StringBuilder(); for (int i 0; i 4; i) { char c CHARS[random.nextInt(CHARS.length)]; code.append(c); g.setColor(new Color(random.nextInt(150), random.nextInt(150), random.nextInt(150))); g.setFont(new Font(Arial, Font.BOLD, 28 random.nextInt(6))); g.drawString(String.valueOf(c), 20 i * 24, 28 random.nextInt(8)); } // 干扰线 for (int i 0; i 8; i) { g.setColor(new Color(random.nextInt(200), random.nextInt(200), random.nextInt(200))); g.drawLine(random.nextInt(WIDTH), random.nextInt(HEIGHT), random.nextInt(WIDTH), random.nextInt(HEIGHT)); } HttpSession session request.getSession(); session.setAttribute(captcha, code.toString()); g.dispose(); response.setContentType(image/png); response.setHeader(Cache-Control, no-store, no-cache, must-revalidate); ImageIO.write(image, png, response.getOutputStream()); } }CHARS字符串里刻意去掉了I、O、1、0这些容易混淆的字符这是验证码设计的一个细节减少用户输错的概率。干扰线数量 8 条是常用的平衡点再多图片就不好认了再少脚本容易识别。字体大小随机和字符位置随机都是为了增加 OCR 识别的难度但不要做得太夸张否则人类也看不清就失去意义了。Session 的存活时间需要注意Tomcat 默认 Session 超时时间是 30 分钟用户打开页面半天不操作验证码仍然有效这其实不符合高安全要求。给验证码单独加过期时间戳是后面要做的优化点。4.2 前端嵌入验证码img 标签、缓存问题与点击刷新前端页面在表单里通过img src/captcha加载验证码图片点击图片刷新是常规操作实现方式是用 JS 给图片换一个查询参数绕过浏览器缓存。不换参数的话第二次点击看到的还是同一张图img src/captcha idcaptchaImg alt验证码 onclickthis.src/captcha?t new Date().getTime() stylecursor: pointer; /关键在new Date().getTime()每次点击生成不同的时间戳浏览器会认为请求的 URL 变了于是重新请求服务器拿到一张新验证码。后端response.setHeader(Cache-Control, no-store)也是配合这件事双重保险。验证码输入框和图片放在同一行能减少误操作。这个输入框要注意两点maxlength必须和后端生成的长度一致这里就是 4输入法不能开启中文模式否则用户会打出全角字符导致比对失败。这些问题都不是大坑但在真实使用里很影响体验。4.3 登录 Servlet验证码比对、查询用户、密码校验登录流程和注册流程在前半段几乎一样比对验证码、根据用户名查用户、用 BCrypt 校验密码。完整代码package com.example.servlet; import com.example.util.DBUtil; import com.example.util.PasswordUtil; 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 javax.servlet.http.HttpSession; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; 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); String captcha request.getParameter(captcha); // 先校验证码再校验账号 HttpSession session request.getSession(); String sessionCaptcha (String) session.getAttribute(captcha); if (sessionCaptcha null || !sessionCaptcha.equalsIgnoreCase(captcha)) { request.setAttribute(msg, 验证码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } session.removeAttribute(captcha); // 查用户 String sql SELECT id, username, password 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()) { String hashed rs.getString(password); if (BCrypt.checkpw(password, hashed)) { // 登录成功把用户信息放进 Session session.setAttribute(loginUser, rs.getString(username)); session.setAttribute(userId, rs.getInt(id)); response.sendRedirect(/welcome.jsp); return; } } request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } catch (SQLException e) { throw new RuntimeException(登录查询失败, e); } } }两个关键决策一是先验验证码再查数据库验证码不对就直接返回这能挡住大量无效的数据库查询二是查不到用户和密码错误返回同一个提示「用户名或密码错误」这是安全惯例避免攻击者通过提示信息枚举哪些用户名注册过。BCrypt.checkpw这个方法会把明文密码和存储的哈希做比对它内部会处理好盐值的提取不需要自己保存盐。这就是 BCrypt 比 MD5 高明的地方MD5 相同的密码哈希一样BCrypt 相同密码每次哈希结果都不一样。4.4 登录后的会话管理与注销Session 的生命周期登录成功后用户信息放 Session之后的页面从 Session 取。退出登录就是把 Session 里的用户信息移除通常还会调用session.invalidate()把整个 Session 销毁。这两个操作的区别是removeAttribute只删指定属性invalidate销毁整个会话连验证码、购物车之类的一起清掉。退出登录用invalidate()更合适protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(); session.invalidate(); response.sendRedirect(/login.jsp); }Session 本身还有个容易被忽略的行为浏览器关闭后Session 对应的 Cookie 还在不在。默认情况下 SessionID 存在浏览器的内存 Cookie 里关闭浏览器就没了服务端的 Session 对象要等超时时间到才销毁。所以「关浏览器等于退出登录」只是假象只要 SessionID 泄漏别人还能用。生产上要设置HttpSession超时时间web.xml 里加一行配置session-config session-timeout30/session-timeout /session-config单位是分钟。这个东西在真实项目里必须显式配置不要依赖 Tomcat 默认值。5. 登录注册开发的 5 个典型翻车点现象、原因与解法这个项目简单归简单但几乎每个初学的人都会在同样几个地方卡住。以下五条是我反复见过的踩坑记录每一条都是「现象 → 原因 → 解决」的套路。5.1 验证码图片显示不出来404、缓存与路径问题现象页面加载后验证码位置是一张破碎的图片或者永远显示第一张验证码。原因分三种第一种/captcha路径没有匹配到 Servlet检查 web.xml 中url-pattern和表单 img 标签的地址是否一致如果你的项目部署路径带上下文比如/项目名/captcha页面要用${pageContext.request.contextPath}/captcha才能拿到正确地址。第二种Tomcat 或浏览器缓存了旧的验证码图片没有走服务端。第三种web.xml里漏配了 Servlet 映射。解决先用浏览器直接访问http://localhost:8080/项目名/captcha能出图就是前端路径问题不能出图就检查控制台报错缓存问题用Cache-Control: no-store和 URL 加时间戳双重解决。在 IDEA 里改完代码要重启 TomcatJSP 修改会自动编译但 Servlet 修改不重启不生效。5.2 验证码明明输对了却不通过大小写、Session 覆盖与编码问题现象用户反复确认输入的字符和图上一致后端却一直提示验证码错误。原因有几个第一验证码生成时 Session 里的值被覆盖了。最典型的场景是用户在多个标签页分别打开了登录页后打开的页面会生成新的验证码存进同一个 Session先打开的页面提交的验证码对应的是旧值。第二比对时用了equals而不是equalsIgnoreCase用户认为大小写无所谓但程序不认。第三输入法全角字符问题用户输入的是全角程序里的A是半角。解决比对时统一转成大写或用equalsIgnoreCase弱化大小写问题全角问题在前端用oninputthis.valuethis.value.replace(/[\uFF01-\uFF5E]/g, function(c){return String.fromCharCode(c.charCodeAt(0)-0xFEE0);})转换或者在输入框加styletext-transform: uppercase;。多标签页的 Session 覆盖问题比较麻烦可以在生成验证码时先检查 Session 里有没有旧值有的话先删除再放新值至少保证最后一个打开的页面能用。5.3 注册中文用户名变成问号连接串、页面编码与数据库字符集三层问题现象数据库中username字段存的是???或å¼ ä¸‰这种乱码。原因JSP 页面编码、Servlet 请求解码、数据库连接、MySQL 表字符集这四个环节任何一层不一致就会乱码。最常见的是漏了request.setCharacterEncoding(UTF-8)POST 请求的参数中文就乱了其次是 JDBC 连接串里没有characterEncodingUTF-8。解决按下列清单检查JSP 顶部% page contentTypetext/html;charsetUTF-8 %、Servlet 里request.setCharacterEncoding(UTF-8)、JDBC URL 加characterEncodingUTF-8、数据库建表语句用utf8mb4。四层都对了中文就不会出问题。如果项目用了 Spring 之外的纯 Servlet并且 Tomcat 版本较新还需要注意 GET 请求的 URIEncoding。5.4 验证码可以反复使用一次性校验缺失导致重放现象同一个验证码第一次提交失败后不刷新图片直接改密码再提交竟然能通过。原因后端比对验证码时只做了「值相等」的判断没有在判断成功后销毁 Session 里的验证码。攻击者拿到一个有效验证码可以反复提交撞密码。解决注册和登录 Servlet 在校验通过后必须执行session.removeAttribute(captcha)。这是硬性要求不是技巧。另外在生成验证码的地方加一个时间戳字段存到 Session 里比对时顺便检查是否超过 5 分钟超过就算过期。图片上甚至可以直接印上当前时间让用户看到验证码的有效期是一目了然的。很多网上的实例代码没写这一步这是安全漏洞不要学。5.5 密码存储用明文或 MD5管理员能看到密码是最大的隐患现象数据库表里password字段就是用户设置的原始密码或者是一串 32 位的 MD5 哈希。原因初学阶段用的教材和博客可能只写了最简单实现没讲安全。MD5 虽然不属于明文但彩虹表攻击和暴力破解已经能让绝大多数弱密码在秒级内被还原。解决改成 BCrypt。引入jbcrypt依赖后在注册时hashpw登录时checkpw两个方法改完数据库里存的哈希每行都不一样。如果你看到网上案例用的是 MD5也不要直接抄这个项目需要用到的加密方式就是 BCrypt。迁移成本很低但意义很大这是从「作业代码」走向「可用系统」的第一步。6. 把简单实例向生产方向收一收验证方法、SQL 注入检查与会话加固一个「简单实例」写完不算完要花半小时做三件事验证登录注册链路是通的、检查 SQL 注入点、确认会话管理没有明显漏洞。下面这些内容是我在每次做完这类项目之后必做的一套检查你可以当 checklist 用。第一件事是验证完整的注册登录链路注册一个新用户观察数据库里多出来的记录确认密码字段是$2a$开头的 BCrypt 哈希然后用这个账号登录确认能跳到欢迎页退出登录后直接访问欢迎页的 URL应该被重定向回登录页。欢迎页需要在web.xml里加一层拦截器配置最简单的方式是用 Servlet Filter 的实现看 Session 里有没有loginUser没有就重定向到登录页。这一条是登录系统的底线少了它用户退出后还能通过 URL 直访绕过认证。第二件事是检查 PreparedStatement 的使用。全项目搜索所有 SQL 语句只要看到字符串拼接 SQL 的地方就是注入点。登录场景最常见的注入样例是 OR 11如果你用了PreparedStatement的?占位符这个注入直接就失效了。这个项目里的checkSql和insertSql全部是参数化查询没有拼接问题但如果你自己加过其他查询务必检查一遍。第三件事是给验证码加上过期时间。现在 Session 里存的只是验证码字符串可以改成用一个小对象存字符串和时间戳校验时先看时间戳是否超过 5 分钟。这个改动不是必须的但对理解 Session 的用法有帮助。我的做法是把 Session 里存的值改成一段 JSON 或者一个CaptchaVO校验时先解析再比较。最后一个习惯换一台没有装 IDEA 的电脑用命令行mvn clean package打一个 war 包手动丢进 Tomcat 的webapps目录启动。这一步能验证你的项目没有依赖 IDEA 的隐藏配置。很多人项目在 IDEA 里跑得好好的一部署到服务器就不行就是 Artifact 配置里带了本地路径依赖。能手动部署成功这个项目才算真正完成。我在第一次写这个登录注册案例时把验证码的答案直接写成了固定字符串「1234」觉得上线前再改就行。结果后来忘了改测试时所有同事都能用同一个验证码登录幸好只是内网系统。这个教训让我后来每次写验证码相关的代码都先问一句「答案是不是真的存在服务端了」。希望这些经验能帮你少走一段弯路做出来的项目既跑得通也拿得出手。本文还有配套的精品资源点击获取

相关新闻

Fine语言带参数子线程:原理、传参方式与实操案例

Fine语言带参数子线程:原理、传参方式与实操案例

Fine语言在报表开发里用得很多,尤其涉及到数据抽取、批量计算、复杂报表渲染时,单线程跑起来经常让人等到怀疑人生。很多人在社区里问过“子线程怎么传参”“多线程能不能带参数跑”,其实这是个非常典型的场景:你有一个耗时的任务…

2026/10/7 12:30:29 阅读更多 →
UE5 Coop网络同步实战:从权威模型到状态协同

UE5 Coop网络同步实战:从权威模型到状态协同

1. 这不是“加个Replicated就完事”的问题:UE5网络同步的真实战场 很多人刚接触UE5网络开发时,第一反应是打开Actor的Replication选项,勾上“Replicated”,再把变量打上UPROPERTY(Replicated)标签——然后发现角色在客户端要么卡顿…

2026/10/7 12:30:29 阅读更多 →
8G显存16G内存也能跑本地大模型:Ollama+GGUF实战指南

8G显存16G内存也能跑本地大模型:Ollama+GGUF实战指南

很多人一听"8G显存、16G内存"就觉得跟本地大模型没什么关系,觉得那是48G甚至80G显存玩家才配碰的东西。我一开始也这么想,直到自己用一台老旧的RTX 3050 8G笔记本(内存刚好16G)把Llama 3、Qwen2.5这些模型稳稳跑起来之后…

2026/10/7 12:30:29 阅读更多 →

最新新闻

Agent-Reach:一类轻量级CLI工具的设计与实现

Agent-Reach:一类轻量级CLI工具的设计与实现

1. Agent-Reach 是什么:一个被误读的 CLI 工具命名陷阱“Agent-Reach”这个名称在当前技术社区里,正经历一场典型的语义漂移——它既不是某个广为人知的开源项目主仓库名,也不是主流模型厂商发布的官方 SDK 名称,更不是 PyPI 上注…

2026/10/7 13:05:08 阅读更多 →
Java多人联机飞机游戏:服务端权威模型与网络同步实战

Java多人联机飞机游戏:服务端权威模型与网络同步实战

简介:基于JAVA语言开发的多人联机飞机游戏客户端与服务器端设计源码包,面向Java游戏开发学习者和网络编程爱好者,帮助理解多人实时交互游戏的客户端/服务器架构与实现流程。项目包含客户端与服务器端两部分,客户端负责界面渲染、用…

2026/10/7 13:05:08 阅读更多 →
GroupMamba实战:分组状态空间模型在图像分类中的高效训练与部署

GroupMamba实战:分组状态空间模型在图像分类中的高效训练与部署

简介:这套面向图像分类与状态空间模型实战的资源包,以GroupMamba为核心,旨在为计算机视觉算法工程师和研究者提供一套可参考的工程实现,缓解SSM扩展到视觉任务时常见的大模型不稳定、显存效率低等问题。压缩包内共两千个文件&…

2026/10/7 13:05:08 阅读更多 →
ABAP CDS Association 实战,从 Travel 与 Customer 关系建模到路径导航

ABAP CDS Association 实战,从 Travel 与 Customer 关系建模到路径导航

在 ABAP CDS 数据模型里看到 _Customer、_SalesOrder、_Supplier、_Product 这类以下划线开头的名称时,背后通常不是普通字段,而是一条 Association。 很多刚接触 CDS 的开发人员会把 Association 理解成一种写法更漂亮的 SQL Join。这样的理解只能解释一部分现象,却很难解…

2026/10/7 13:05:08 阅读更多 →
MOS管好坏判断的五大物理本质诀窍

MOS管好坏判断的五大物理本质诀窍

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:05:08 阅读更多 →
GEO实战:让AI在同城搜索中主动推荐你的本地生意

GEO实战:让AI在同城搜索中主动推荐你的本地生意

1. 同城流量新入口:AI回答里的那个"推荐位"正在决定生意去留在洛阳做本地品牌推广这几年,我遇到最明显的一个变化是:客户开口问的东西不一样了。以前上来就问抖音怎么投、百度排名怎么上,现在越来越多的老板会拿着手机问…

2026/10/7 13:04:07 阅读更多 →

日新闻

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:01:58 阅读更多 →
用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:02:00 阅读更多 →
芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:02:00 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 7:15:40 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 5:29:09 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 9:29:10 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 8:21:32 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 11:43:46 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 1:18:13 阅读更多 →