☰
JSP+SQL Server登录注册实战:从环境配置到安全编码避坑
2026/10/9 11:07:03 网站建设 项目流程

简介:面向JSP初学者与Web开发学习者的完整登录注册示例项目,基于JSP和SQL Server实现用户身份验证。项目覆盖从数据库设计到前端页面提交的完整链路,包括用户表唯一性约束、密码加密存储、JDBC驱动加载与连接管理、表单数据解析、必填项和密码复杂度校验、HttpSession会话跟踪,以及防止SQL注入等安全实践,并针对数据库连接失败、重复用户名等常见异常给出处理思路,帮助读者透彻理解动态网站中用户认证的实现机制。压缩包共46个文件,以28个JSP页面为主,辅以XML配置文件、Java源码、JAR依赖库、txt说明文档等项目配套文件,整体仅581KB,小巧易用。目录结构清晰,包含WebContent、src、WebRoot等标准模块,支持在Eclipse或IntelliJ IDEA中直接导入运行,方便对照源码逐段调试,快速掌握JSP+JDBC+SQL 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 根目录是 WebRoot,Eclipse WTP 默认是 WebContent。项目里两个目录内容基本一致,实际部署时只认其中一个。

我的建议是:直接用 WebRoot 作为部署目录,因为.mymetadata里定义的 webrootdir 多半指向它。如果你的 IDE 编译后没有把 class 输出到 WEB-INF/classes,Tomcat 启动会直接报 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能看到类似<webrootdir>WebRoot</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.jsp),context 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.7;sqljdbc42.jar 对应 JDK 1.8 及以上。如果你的 Tomcat 跑在 JDK 1.8 上,却用老项目里的 sqljdbc4.jar,通常也能跑,但某些新特性不支持,建议直接换 sqljdbc42。

连接字符串的写法也有讲究:

Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); String url = "jdbc:sqlserver://localhost:1433;databaseName=rlzy;encrypt=false;trustServerCertificate=false"; String user = "sa"; String password = "your_password"; Connection conn = DriverManager.getConnection(url, user, password);

encrypt=false这个参数要特别注意。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,要么在连接串里加encrypt=false,要么在 SQL Server 配置管理器里把“强制加密”设为否。trustServerCertificate=false是让客户端不盲目信任服务器证书,内网开发环境可以设成 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 action="RegisterServlet" method="post"> <input type="text" name="username" maxlength="20" required /> <input type="password" name="password" maxlength="20" required /> <input type="password" name="confirmPassword" maxlength="20" required /> <button type="submit">注册</button> </form>

action="RegisterServlet"是相对路径,浏览器会基于当前页面的 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?error=empty"); return; } if (!password.equals(confirm)) { response.sendRedirect("register.jsp?error=notmatch"); 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?error=exists"); return; } } catch (SQLException e) { e.printStackTrace(); response.sendRedirect("register.jsp?error=db"); 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?registered=1"); } catch (SQLException e) { e.printStackTrace(); response.sendRedirect("register.jsp?error=db"); } }

这段代码里有几个要点。setCharacterEncoding("UTF-8")必须放在读取任何参数之前,否则 POST 提交的中文用户名在 request.getParameter 时就已经乱码了。PreparedStatement用?占位符替代字符串拼接,这是防 SQL 注入的唯一正确写法——直接用"SELECT * FROM users WHERE username='" + username + "'"拼 SQL 的话,输入' OR '1'='1就能绕过登录。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 uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <c:choose> <c:when test="${not empty sessionScope.username}"> 欢迎,${sessionScope.username} <a href="LogoutServlet">退出</a> </c:when> <c:otherwise> <a href="login.jsp">登录</a> | <a href="register.jsp">注册</a> </c:otherwise> </c:choose>

sessionScope.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。连接字符串加encrypt=false;trustServerCertificate=false。如果还不行,在 SQL Server 配置管理器里把“MSSQLSERVER 的协议”下的“强制加密”设为否,重启 SQL Server 服务。

5.2 中文乱码:页面显示问号或菱形

现象:注册成功后重新登录,用户名里的中文变成???,或者页面本身显示乱码。

原因:三层编码不一致。JSP 页面没设pageEncoding="UTF-8",POST 请求没设request.setCharacterEncoding("UTF-8"),数据库表字段用了 VARCHAR 而非 NVARCHAR,或者数据库排序规则不支持中文。

解决:逐层排查。第一层在 JSP 头部加<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>。第二层在 Servlet 的 doPost 第一行加 setCharacterEncoding。第三层建表时把字段改成 NVARCHAR。三层都改完再测试,乱码必然消失。

5.3 SQL Server 实例名导致的连接失败

现象:连接字符串写jdbc:sqlserver://localhost:1433;databaseName=rlzy,Tomcat 日志报Cannot connect to database,但 SQL Server Management Studio 能正常连接。

原因:SQL Server 安装时如果不是默认实例(MSSQLSERVER),而是命名实例(比如localhost\SQLEXPRESS),默认端口不是 1433,浏览器访问时实例名不会自动映射到端口。

解决:有两种做法。一是在连接串里带实例名:jdbc:sqlserver://localhost\\SQLEXPRESS;databaseName=rlzy——注意 Java 字符串里反斜杠要转义成两个。二是用 SQL Server 配置管理器查一下实际监听端口,直接在连接串里写端口号。我推荐第二种,因为实例名在不同机器之间迁移时常变,端口号更稳定。

5.4 注册成功后跳转登录页,但登录一直失败

现象:注册提示成功,登录时无论输入什么密码都提示“用户名或密码错误”。

原因:注册时密码经过了一次处理(比如哈希或截断),但登录时没做同样处理,两边的密码值不一致。或者注册密码字段长度不够,密码被数据库截断存储。

解决:检查注册和登录两边的密码处理代码是否对称。如果注册时只是ps.setString(2, password),登录比对也是同一套逻辑,就检查密码字段长度——VARCHAR(50) 存不下 60 位的哈希值,会被截断。把字段改成 NVARCHAR(255) 是通用解法。

5.5 Tomcat 热部署后 session 失效,每次请求都要重新登录

现象:修改一个 JSP 文件后,Eclipse 自动重新部署,浏览器里的登录状态丢失,要重新登录。

原因:Tomcat 热部署会重建 webapp 的 classloader,session 对象跟着销毁。这是正常现象,不是代码 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 层重新计算哈希对比。

这半小时的改造能让你在讲项目时理直气壮地说“我考虑了安全问题”,而不是被人一问就卡壳。从那以后我再看到任何教学项目的密码是明文存储,都会下意识多问一句“注册和登录的校验逻辑是否对称”——这句话几乎能排查掉一半的登录疑难杂症。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询