多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

JSP+SQL Server登录注册实战:从环境配置到安全编码避坑

JSP+SQL Server登录注册实战:从环境配置到安全编码避坑 简介面向JSP初学者与Web开发学习者的完整登录注册示例项目基于JSP和SQL Server实现用户身份验证。项目覆盖从数据库设计到前端页面提交的完整链路包括用户表唯一性约束、密码加密存储、JDBC驱动加载与连接管理、表单数据解析、必填项和密码复杂度校验、HttpSession会话跟踪以及防止SQL注入等安全实践并针对数据库连接失败、重复用户名等常见异常给出处理思路帮助读者透彻理解动态网站中用户认证的实现机制。压缩包共46个文件以28个JSP页面为主辅以XML配置文件、Java源码、JAR依赖库、txt说明文档等项目配套文件整体仅581KB小巧易用。目录结构清晰包含WebContent、src、WebRoot等标准模块支持在Eclipse或IntelliJ IDEA中直接导入运行方便对照源码逐段调试快速掌握JSPJDBCSQL Server整合开发与部署技巧。目前已有835人学习浏览适合课程设计、毕业设计或自学参考也可作为快速搭建用户系统的入门模板。1. 登录注册这个东西真正的坑在注册不在登录跑通一个 JSP SQL Server 的登录注册很多人以为难点在登录那一下的比对查询实际拆完这个项目你会发现登录只是一个 SELECT 语句加一个 session 的事真正藏坑的是注册——用户名重复检查、SQL 注入、中文乱码、驱动版本不匹配全都堆在这一小段流程里。这个 zip 我解压看过是典型的 MyEclipse 工程结构里面 src、WebRoot、WebContent 目录齐全数据库侧需要手动建一张用户表然后通过 JDBC 连 SQL Server。适合两类人一是刚学完 JSP 语法但不知道工程怎么组织、JDBC 怎么落地的学生二是被公司老项目“Eclipse Tomcat SQL Server”组合折磨过、想找一份最小可运行参照的开发者。接下来我按从解压到跑通的顺序把每一步的参数和坑位拆开讲。2. 先看懂工程目录MyEclipse 项目的结构与启动方式2.1 为什么这个项目自带 .myeclipse 和 .mymetadata解压后第一眼看到的是.myeclipse、.mymetadata、.project、.classpath这些文件。.myeclipse目录是 MyEclipse 的配置缓存.mymetadata记录的是 Web 模块的元数据——包括 WebRoot 路径、context root 名称、JDK 版本。这些文件在 Git 时代属于“不该提交”的东西但在教学示例里反而是有用的它们告诉你这个项目当初是在哪个 IDE 下创建的、部署名是什么。.classpath里能看到这个项目依赖了哪些库。把这个文件打开重点关注有没有 sqljdbc 相关 jar 的引用。我见过很多初学者把驱动 jar 拷进 WEB-INF/lib但.classpath里指向的是本地磁盘路径换一台机器后引用失效于是项目一导入就报一堆红叉。遇到这种情况不用慌张——把 jar 从本地真实路径拷贝到项目的 WebContent/WEB-INF/lib 下再在 Build Path 里重新添加就能解决。常见做法是用 Eclipse或 MyEclipse的 Import → Existing Projects into Workspace 导入整个目录然后把 WebRoot 设为 Web 根目录。如果是纯 Eclipse WTP 环境则把 WebContent 作为 Web 根目录。两个目录指向的其实是同一个应用的静态资源和 JSP 文件只是 IDE 预设不同。2.2 WebRoot 与 WebContent 双目录是怎么回事这个项目同时有 WebRoot 和 WebContent这是历史遗留问题。MyEclipse 老版本默认 Web 根目录是 WebRootEclipse WTP 默认是 WebContent。项目里两个目录内容基本一致实际部署时只认其中一个。我的建议是直接用 WebRoot 作为部署目录因为.mymetadata里定义的 webrootdir 多半指向它。如果你的 IDE 编译后没有把 class 输出到 WEB-INF/classesTomcat 启动会直接报 404 或者 500页面显示org.apache.jasper.JasperException: Unable to compile class for JSP。这属于部署路径问题不是代码问题。具体操作按下面这组检查来做# 在项目根目录下查看 .mymetadata 里定义的根路径 cat .mymetadata # 确认 WEB-INF/classes 是否存在没有就手动建 mkdir -p WebRoot/WEB-INF/classes # 确认 WEB-INF/lib 下是否有 JDBC 驱动 ls -la WebRoot/WEB-INF/lib/这段命令里cat .mymetadata能看到类似webrootdirWebRoot/webrootdir的配置这就是判断依据。mkdir -p是强制建目录如果 class 输出目录不存在编译后的 Servlet 根本没地方落。lib目录检查是为了确认 sqljdbc 系列驱动在不在驱动缺失时 Tomcat 启动不会报错但第一个 JDBC 连接请求必然抛ClassNotFoundException。2.3 部署名与访问路径的对应关系.mymetadata里还有一个关键属性contextroot。假设它等于rlzy那么部署到 Tomcat 后访问地址是http://localhost:8080/rlzy/index.jsp。如果你改成 ROOT就直接http://localhost:8080/index.jsp。这里容易翻车很多人在 Eclipse 里改项目名结果 context root 没跟着变页面怎么刷新都是 404。改法是在项目属性里找到 Web Project Settings把 Context Root 改成你想要的路径然后重新发布。项目里所有 JSP 的跳转链接如果是绝对路径如/rlzy/login.jspcontext root 一旦变化这些链接全部失效。所以先确定 context root再决定页面里怎么写路径。3. 数据库侧准备建表脚本与 JDBC 连接参数3.1 用户表的最小设计用户表是登录注册的核心。这个项目里需要的字段不多但有几个约束必须建对。用户名要做唯一约束这是注册时判断“用户已存在”的数据库层保障。密码字段不要用 varchar 直接存明文——教学项目里可以简化但你在复现时至少要知道这一步是不安全的。在实际开发中密码存储常见做法是哈希加盐而不是加密。加密可逆数据库泄露等于明文泄露哈希不可逆攻击者拿到也只能撞库。这个项目作为入门示例没有做哈希但如果你要传到生产环境这一步必须补上。建表脚本如下CREATE TABLE [dbo].[users] ( [id] INT IDENTITY(1,1) PRIMARY KEY, [username] NVARCHAR(50) NOT NULL UNIQUE, [password] NVARCHAR(255) NOT NULL, [created_at] DATETIME DEFAULT GETDATE() );这段 SQL 里有几个参数值得解释。IDENTITY(1,1)是 SQL Server 的自增列起始值为 1步长为 1不需要手动插入 id。NVARCHAR而不是VARCHAR是因为 JSP 页面传过来的中文用户名如果用 VARCHAR 存储在非中文默认排序规则下会出现乱码或长度截断问题。UNIQUE约束直接写在列定义里比单独加索引更直观注册时插入重复用户名会抛异常应用层可以捕获这个异常来判断重名。3.2 SQL Server 与 JDBC 驱动匹配SQL Server 的 JDBC 驱动和数据库版本、JDK 版本三者必须对齐这是整个项目最容易踩的坑。sqljdbc4.jar 支持 JDK 1.6 及以上、SQL Server 2005 及以上sqljdbc41.jar 对应 JDK 1.7sqljdbc42.jar 对应 JDK 1.8 及以上。如果你的 Tomcat 跑在 JDK 1.8 上却用老项目里的 sqljdbc4.jar通常也能跑但某些新特性不支持建议直接换 sqljdbc42。连接字符串的写法也有讲究Class.forName(com.microsoft.sqlserver.jdbc.SQLServerDriver); String url jdbc:sqlserver://localhost:1433;databaseNamerlzy;encryptfalse;trustServerCertificatefalse; String user sa; String password your_password; Connection conn DriverManager.getConnection(url, user, password);encryptfalse这个参数要特别注意。SQL Server 2019 及以上版本默认强制加密连接而老驱动尝试连接时如果没显式关闭加密会报The driver could not establish a secure connection to SQL Server by using Secure Sockets Layer (SSL) encryption。你如果用的是 SQL Server 2019 或 2022要么在连接串里加encryptfalse要么在 SQL Server 配置管理器里把“强制加密”设为否。trustServerCertificatefalse是让客户端不盲目信任服务器证书内网开发环境可以设成 true 减少证书校验带来的麻烦。3.3 驱动放置位置与 ClassNotFoundException 的真相驱动 jar 放在 WebRoot/WEB-INF/lib 下是最稳妥的。有些人图省事把 jar 扔到 Tomcat 的 lib 目录里这样确实能让 Class.forName 找到类但多个项目共用同一个 Tomcat 时驱动版本互相污染A 项目用的 sqljdbc42 会被 B 项目里的老版本覆盖。放到 WEB-INF/lib 是隔离的每个 webapp 用自己的类加载器加载互不干扰。如果 Tomcat 启动后访问登录接口报ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver先看 lib 目录里 jar 是否存在再确认 jar 是不是损坏——有些下载站给的是 0 字节文件。还有一种情况IDE 发布时没有把 jar 同步到 Tomcat 的部署目录Eclipse 里经常出现“工程里能看到 jar但部署后没有”的问题。手动把项目重新发布一次勾选“Clean”选项强制全量拷贝。4. 登录与注册的完整链路复现从表单到 JDBC 再到会话4.1 前端表单的参数命名与提交方式登录注册页面的 form 表单决定了后端能拿到什么参数。看一下项目的 JSP 源码注意两个细节一是 input 的 name 属性值二是 form 的 action 路径。我用一个标准的注册表单做示例form actionRegisterServlet methodpost input typetext nameusername maxlength20 required / input typepassword namepassword maxlength20 required / input typepassword nameconfirmPassword maxlength20 required / button typesubmit注册/button /formactionRegisterServlet是相对路径浏览器会基于当前页面的 URL 目录去拼接请求地址。如果注册页面在/rlzy/register.jsp请求会发往/rlzy/RegisterServlet。这里有个隐藏坑用相对路径时如果 JSP 页面被转发forward到另一个目录下表单提交的 URL 会跟着变导致 404。我一般会在 JSP 里用${pageContext.request.contextPath}拼绝对路径比如action${pageContext.request.contextPath}/RegisterServlet这样无论页面怎么转发请求路径都不会错。4.2 注册 Servlet 的核心逻辑与参数检查顺序注册处理的关键不是 INSERT 语句本身而是查重和参数校验的顺序。顺序错了会出现“用户名已存在但覆盖了密码”或者数据库里写入空字符串的脏数据。protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username).trim(); String password request.getParameter(password); String confirm request.getParameter(confirmPassword); if (username.isEmpty() || password.isEmpty()) { response.sendRedirect(register.jsp?errorempty); return; } if (!password.equals(confirm)) { response.sendRedirect(register.jsp?errornotmatch); return; } try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement( SELECT COUNT(*) FROM users WHERE username?)) { ps.setString(1, username); ResultSet rs ps.executeQuery(); rs.next(); if (rs.getInt(1) 0) { response.sendRedirect(register.jsp?errorexists); return; } } catch (SQLException e) { e.printStackTrace(); response.sendRedirect(register.jsp?errordb); return; } try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement( INSERT INTO users(username, password) VALUES(?, ?))) { ps.setString(1, username); ps.setString(2, password); ps.executeUpdate(); response.sendRedirect(login.jsp?registered1); } catch (SQLException e) { e.printStackTrace(); response.sendRedirect(register.jsp?errordb); } }这段代码里有几个要点。setCharacterEncoding(UTF-8)必须放在读取任何参数之前否则 POST 提交的中文用户名在 request.getParameter 时就已经乱码了。PreparedStatement用?占位符替代字符串拼接这是防 SQL 注入的唯一正确写法——直接用SELECT * FROM users WHERE username username 拼 SQL 的话输入 OR 11就能绕过登录。try-with-resources 写法在 JDK 7 之后可用Connection 和 PreparedStatement 会自动关闭不用手动在 finally 里写一堆 close。查重用SELECT COUNT(*)而不是SELECT *数据量大时性能差异明显语义也更清晰。4.3 登录校验与 session 状态跟踪登录的流程比注册简单拿表单提交的用户名密码去数据库查查到就给 session 里放一个标记查不到就返回错误。但这里有个教科书里不讲的细节密码校验应该放在 SQL 里做还是先查用户名再在 Java 里比对我见过两种写法。第一种是SELECT * FROM users WHERE username? AND password?一条 SQL 完成校验。简单但数据库日志里会记录明文密码。第二种是先按用户名查出来在 Java 层比对密码哈希值——这是更安全的做法因为即使 SQL 日志泄露也不会暴露完整密码比对过程。这个教学项目里用的是第一种能跑通但你在生产环境做系统时应该改成第二种。session 的使用逻辑如下HttpSession session request.getSession(); session.setAttribute(username, username); session.setMaxInactiveInterval(30 * 60); response.sendRedirect(index.jsp);setMaxInactiveInterval(30 * 60)表示 session 30 分钟无操作后失效单位是秒。不设置的话Tomcat 默认是 30 分钟web.xml 里的 session-config 可以覆盖。退出登录时用session.invalidate()销毁整个 session而不是只用 removeAttribute——否则旧的 session id 还在攻击者可以尝试会话固定攻击。4.4 JSP 页面里怎么读登录状态页面头部通常会有一个判断登录了显示用户名和“退出”按钮没登录显示“登录/注册”链接。这个判断用 JSTL 标签比 Scriptlet 好看% taglib urihttp://java.sun.com/jsp/jstl/core prefixc % c:choose c:when test${not empty sessionScope.username} 欢迎${sessionScope.username} a hrefLogoutServlet退出/a /c:when c:otherwise a hreflogin.jsp登录/a | a hrefregister.jsp注册/a /c:otherwise /c:choosesessionScope.username是 EL 表达式等价于session.getAttribute(username)。如果页面上直接写% session.getAttribute(username) %也能用但混用 Scriptlet 和 EL 会让页面变得难维护。这里要留意如果项目里没引 jstl.jar这段c:choose会直接报错。教学项目里很多不用 JSTL而是直接在 JSP 里写 if 判断你的复现目标如果是“先跑起来”可以暂时保留原项目的写法后续重构时再替换。5. 避坑手册这个项目最容易翻车的五处位置5.1 驱动版本与 SQL Server 版本不匹配导致连接失败现象运行登录功能页面卡了几秒后报com.microsoft.sqlserver.jdbc.SQLServerException: 无法使用提供的值连接到数据库Tomcat 日志里能看到The driver could not establish a secure connection...。原因SQL Server 2019 之后强制加密连接老驱动不支持这种加密协商方式。或者驱动是 32 位/64 位不匹配。解决查看java -version确认 JDK 位数下载对应版本的 sqljdbc42.jar。连接字符串加encryptfalse;trustServerCertificatefalse。如果还不行在 SQL Server 配置管理器里把“MSSQLSERVER 的协议”下的“强制加密”设为否重启 SQL Server 服务。5.2 中文乱码页面显示问号或菱形现象注册成功后重新登录用户名里的中文变成???或者页面本身显示乱码。原因三层编码不一致。JSP 页面没设pageEncodingUTF-8POST 请求没设request.setCharacterEncoding(UTF-8)数据库表字段用了 VARCHAR 而非 NVARCHAR或者数据库排序规则不支持中文。解决逐层排查。第一层在 JSP 头部加% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%。第二层在 Servlet 的 doPost 第一行加 setCharacterEncoding。第三层建表时把字段改成 NVARCHAR。三层都改完再测试乱码必然消失。5.3 SQL Server 实例名导致的连接失败现象连接字符串写jdbc:sqlserver://localhost:1433;databaseNamerlzyTomcat 日志报Cannot connect to database但 SQL Server Management Studio 能正常连接。原因SQL Server 安装时如果不是默认实例MSSQLSERVER而是命名实例比如localhost\SQLEXPRESS默认端口不是 1433浏览器访问时实例名不会自动映射到端口。解决有两种做法。一是在连接串里带实例名jdbc:sqlserver://localhost\\SQLEXPRESS;databaseNamerlzy——注意 Java 字符串里反斜杠要转义成两个。二是用 SQL Server 配置管理器查一下实际监听端口直接在连接串里写端口号。我推荐第二种因为实例名在不同机器之间迁移时常变端口号更稳定。5.4 注册成功后跳转登录页但登录一直失败现象注册提示成功登录时无论输入什么密码都提示“用户名或密码错误”。原因注册时密码经过了一次处理比如哈希或截断但登录时没做同样处理两边的密码值不一致。或者注册密码字段长度不够密码被数据库截断存储。解决检查注册和登录两边的密码处理代码是否对称。如果注册时只是ps.setString(2, password)登录比对也是同一套逻辑就检查密码字段长度——VARCHAR(50) 存不下 60 位的哈希值会被截断。把字段改成 NVARCHAR(255) 是通用解法。5.5 Tomcat 热部署后 session 失效每次请求都要重新登录现象修改一个 JSP 文件后Eclipse 自动重新部署浏览器里的登录状态丢失要重新登录。原因Tomcat 热部署会重建 webapp 的 classloadersession 对象跟着销毁。这是正常现象不是代码 bug。解决开发阶段在 Tomcat 的 context.xml 里设置Manager pathname /禁用 session 持久化或者直接用session.setMaxInactiveInterval延长有效期。真正要注意的是不要在生产环境用热部署——改代码就重启 Tomcat别偷懒。6. 进阶一改把明文密码换成加盐哈希成本半小时如果你打算把这份教学项目改造成能拿得出手的作品第一件事就是把密码存储方式从明文改成加盐哈希。这个改造只涉及注册和登录两个 Servlet不碰前端页面。核心逻辑是三段式注册时生成随机盐把盐和密码拼起来做哈希把盐和哈希值一起存库登录时取出盐拼上用户输入的密码做同样的哈希比对结果。SecureRandom random new SecureRandom(); byte[] saltBytes new byte[16]; random.nextBytes(saltBytes); String salt Base64.getEncoder().encodeToString(saltBytes); String hashedPassword hashPassword(password, salt);哈希函数建议用 PBKDF2 而不是简单的 MD5 或 SHA-256因为后者没有加盐复杂度GPU 批量破解太快。JDK 自带PBKDF2WithHmacSHA1算法不用引第三方库public static String hashPassword(String password, String salt) throws NoSuchAlgorithmException, InvalidKeySpecException { PBEKeySpec spec new PBEKeySpec(password.toCharArray(), salt.getBytes(), 10000, 256); SecretKeyFactory factory SecretKeyFactory.getInstance(PBKDF2WithHmacSHA1); byte[] hash factory.generateSecret(spec).getEncoded(); return Base64.getEncoder().encodeToString(hash); }PBEKeySpec构造参数里的10000是迭代次数这个数字越大单次计算越慢暴力破解成本越高。OAuth 2.0 建议值是 10000 以上2024 年的基线推荐是 600000但教学项目用 10000 足够演示性能差异。256是派生密钥长度单位是 bit对应 32 字节。改完之后users 表结构也要微调——加一个salt字段原password字段长度从 50 改成 255。登录时的比对查询仍然是WHERE username?但不再用AND password?而是查出来后在 Java 层重新计算哈希对比。这半小时的改造能让你在讲项目时理直气壮地说“我考虑了安全问题”而不是被人一问就卡壳。从那以后我再看到任何教学项目的密码是明文存储都会下意识多问一句“注册和登录的校验逻辑是否对称”——这句话几乎能排查掉一半的登录疑难杂症。希望帮到你。本文还有配套的精品资源点击获取
返回列表