
简介一份面向Web开发初学者的JSP与SQL Server登录注册示例程序包集中演示用户身份验证的完整实现流程。内容涵盖数据库用户表设计、密码处理、JDBC连接、表单提交与数据校验、登录后的Session会话跟踪等环节可在Eclipse或IntelliJ IDEA中直接导入运行适合课程设计或入门实战。压缩包共46个文件以28个JSP页面为主包含页面展示与业务逻辑另有XML配置、Java源码、JAR驱动及class文件用于工程配置和数据库访问支撑。整包仅581KB轻量易用目录层级清楚便于按模块查找对应文件。截至目前已有835人学习使用能帮助读者快速理清JSP与SQL Server集成时的分层思路并为后续Web项目提供可复用的登录注册代码与异常处理参考。1. JSP SQL Server 登录注册老技术栈为什么仍是课程设计与小系统的首选“JSP sqlserver 登录注册.zip”这类文件在网盘和代码分享站里常年排在前列。别看 JSP 是上世纪九十年代末的技术在 Java 课程设计、毕业设计和中小企业内部管理系统里它依然是出现频率最高的组合之一SQL Server 负责数据存储JSP 负责页面渲染Servlet 处理请求整个链路简单直接没有 Spring Boot 那套自动配置的黑匣子所有逻辑都能肉眼看到。如果你正面临“必须在一周内交一个能跑通的登录注册模块”或者“拿到别人打包的源码却跑不起来”的处境这篇文章就是把这条路踩平给你看。先说结论这个技术栈能解决的问题非常明确——用户注册、登录验证、会话保持、个人信息展示。它的边界也很明确不适合高并发、不适合复杂权限、不适合前后端分离。但作为理解“请求到底经过谁、数据到底存哪里”的入门骨架它仍然干净得让人舒服。下面从环境选型开始一路讲到代码实现和最常见的翻车现场最后给你一套能直接搬进自己项目的落地姿势。2. 环境与版本选型先解决“能不能跑起来”再谈“写得好不好”2.1 JDK 和 Tomcat老版本组合反而最省事JSP 项目对运行环境的要求不算高但版本组合错了报错信息能让人绕半天。最常见的坑是拿 JDK 17 配 Tomcat 10跑老项目直接报java.lang.NoClassDefFoundError或者Servlet包名冲突。原因很简单——Tomcat 10 开始把javax.servlet迁移成了jakarta.servlet而老项目的代码里全写的是javax。我一般会这样选JDK 8 Tomcat 8.5 或 Tomcat 9。这两个版本都还在使用javax.servlet命名空间能和绝大多数 JSP 登录注册源码无缝衔接。如果你手头拿到的是更早的压缩包里面可能连 Maven 都不是而是直接把 jar 扔在WEB-INF/lib下的传统.war结构那 JDK 8 Tomcat 8.5 更是最稳妥的落点。Tomcat 8.5 的下载和安装不需要过多解释解压后配置好CATALINA_HOME再把项目拷到webapps目录下即可。SQL Server 版本同理。2008、2012、2014、2016、2019、2022 都支持 JDBC 连接但做本地开发我建议用 SQL Server 2012 或 2014 的 Express 版——Express 免费安装包小功能上对于学习登录注册完全够用。版本太新的 SQL Server 2019/2022 在安装时经常卡在“无法找到数据库引擎启动句柄”上后文避坑章会专门拆这个问题。SQL Server 2012 下载安装时注意选择“具有高级服务的 Express”版本它带了图形化管理工具对新手友好得多。2.2 JDBC 驱动mssql-jdbc 和 jtds 到底选哪个连接 SQL Server 的 JDBC 驱动主要有两派微软官方mssql-jdbc和开源的jtds。两者都能完成连接行为差异有时让老手也挠头。微软官方驱动推荐用在生产环境因为它一直在维护支持 SQL Server 2019 以后的新特性jtds 的优势是轻量、兼容旧数据库版本但项目已基本停更。对于登录注册这种简单场景选微软官方驱动就够了版本选 7.4.1.jre8 或 8.2.2.jre8。如果你的项目是 Maven 结构在pom.xml里加依赖dependency groupIdcom.microsoft.sqlserver/groupId artifactIdmssql-jdbc/artifactId version8.2.2.jre8/version scoperuntime/scope /dependency如果项目是传统的WEB-INF/lib结构直接把mssql-jdbc-8.2.2.jre8.jar拷进lib目录。这里有个细节Maven 项目里如果把 scope 写成providedTomcat 运行时不会把 jar 打包进去启动时就会报ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver。这个错在后面的避坑清单里会再出现先记住一点驱动 jar 必须出现在运行时的 classpath 上。2.3 连接串参数URL 写法决定你能不能连上SQL Server 的 JDBC 连接串比 MySQL 略讲究一些常见写法jdbc.drivercom.microsoft.sqlserver.jdbc.SQLServerDriver jdbc.urljdbc:sqlserver://localhost:1433;DatabaseNameuser_db;encryptfalse;trustServerCertificatetrue jdbc.usernamesa jdbc.password123456参数说明localhost:1433是 SQL Server 默认实例的默认监听端口DatabaseName指定要连的库名encryptfalse;trustServerCertificatetrue这两项在高版本驱动里尤其重要——微软驱动从 7.0 开始默认对连接加密而自签名证书会触发校验失败不关掉加密就会在建立连接时报 SSL 错误。很多“能 ping 通数据库却连不上”的场景八成就是栽在这里。另外注意jdbc:sqlserver://前缀不能写错少了sqlserver会直接报Connection refused。3. 建库建表与数据库连接代码T-SQL 没那么吓人3.1 用户表设计千万别把表名起成 user登录注册模块的核心数据表很简单但表名有个大坑在 SQL Server 里USER是一个保留关键字如果你直接执行CREATE TABLE user (...)会立刻报语法错误。我见过不少新手在“为什么我的表建不出来”上卡了一下午。正确做法是给表起名user_info或者sys_user如果非要用user就必须写成[user]。这里用user_info更省心。T-SQL 建表脚本CREATE DATABASE user_db; GO USE user_db; GO CREATE TABLE user_info ( id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(64) NOT NULL, salt VARCHAR(16) NOT NULL, email NVARCHAR(100) NULL, real_name NVARCHAR(50) NULL, create_time DATETIME NOT NULL DEFAULT GETDATE(), last_login_time DATETIME NULL ); GO逻辑说明IDENTITY(1,1)是 SQL Server 的自增列等价于 MySQL 的AUTO_INCREMENT不需要手动管理主键NVARCHAR比VARCHAR更适合存中文因为 JSP 页面传来的用户名很可能是 UTF-8 编码的中文NVARCHAR按 Unicode 存储不吃亏UNIQUE约束保证用户名不重复——这一步比你在代码里先查再插要可靠得多。参数说明password_hash用VARCHAR(64)是因为后面会用 SHA-256 算法输出固定 64 位十六进制字符salt字段必须是VARCHAR(16)或更大哪怕是固定长度 8 位随机盐也建议单独存下来否则密码验证时无从比对。GETDATE()是 SQL Server 取当前时间的函数比从前端传时间字段靠谱避免客户端时钟不准带来的问题。3.2 数据库连接封装一个 DBUtil 类跑遍全项目JSP 项目里没有 Spring 管理数据源通常用一个工具类封装连接和外理。写连接工具类时注意两点一是用Class.forName注册驱动二是把连接参数写在.properties文件里而不是硬编码。硬编码的后果是——数据库密码改了一个字符你得重新编译整个项目。package com.demo.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(jdbc.properties)) { Properties props new Properties(); props.load(in); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); Class.forName(props.getProperty(jdbc.driver)); } catch (Exception e) { throw new ExceptionInInitializerError(数据库初始化失败: e.getMessage()); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(url, username, password); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) {} } } } }逻辑说明静态代码块在类加载时执行一次完成驱动注册和配置读取getConnection()每次调用都返回一个物理连接——这种写法虽然不高效但胜在简单适合登录注册这种低频操作。关闭资源用可变参数AutoCloseable...一次调用就能把Connection、PreparedStatement、ResultSet全部关掉不会漏。参数说明getResourceAsStream(jdbc.properties)要求配置文件在src/main/resources目录下如果项目是普通 Web 工程放在src目录即可编译后会出现在WEB-INF/classes下。顺带一提如果你在连接串里把encrypt写成true这一步就会迎来第一个报错所以jdbc.properties里的参数要和 2.3 节保持一致。4. 登录注册核心代码实现Servlet 处理请求JSP 只负责显示4.1 注册流程从表单到数据库中间加一道密码处理注册页的流程是用户填写用户名、密码、邮箱 → JSP 表单提交到RegisterServlet→ Servlet 读取参数 → 密码加盐哈希 → DAO 层插入数据库 → 跳转到登录页。这里最容易被抄袭作业的代码写崩的点是密码处理——直接把明文存进数据库。这不是“会不会被黑”的问题而是当你写完这个项目交给老师或甲方时任何人打开 SQL Server 看一眼表就能读出所有用户密码这种交付方式很丢人。注册 Servlet 的代码骨架package com.demo.servlet; import com.demo.dao.UserDao; import com.demo.entity.User; 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; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Random; WebServlet(/register) public class RegisterServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); String email req.getParameter(email); if (username null || username.trim().isEmpty() || password null || password.isEmpty()) { req.setAttribute(msg, 用户名和密码不能为空); req.getRequestDispatcher(/register.jsp).forward(req, resp); return; } // 生成随机盐 String salt generateSalt(); String passwordHash sha256(password, salt); User user new User(); user.setUsername(username.trim()); user.setPasswordHash(passwordHash); user.setSalt(salt); user.setEmail(email); boolean ok userDao.register(user); if (ok) { resp.sendRedirect(login.jsp?registered1); } else { req.setAttribute(msg, 用户名已存在); req.getRequestDispatcher(/register.jsp).forward(req, resp); } } private String generateSalt() { Random r new Random(); StringBuilder sb new StringBuilder(8); String chars abcdefghijklmnopqrstuvwxyz0123456789; for (int i 0; i 8; i) { sb.append(chars.charAt(r.nextInt(chars.length()))); } return sb.toString(); } private String sha256(String password, String salt) { try { MessageDigest md MessageDigest.getInstance(SHA-256); md.update((password salt).getBytes(UTF-8)); byte[] digest md.digest(); StringBuilder hex new StringBuilder(); for (byte b : digest) { hex.append(String.format(%02x, b)); } return hex.toString(); } catch (NoSuchAlgorithmException | IOException e) { throw new RuntimeException(e); } } }逻辑说明WebServlet(/register)是 Servlet 3.0 的注解式路由省去了web.xml里到处写映射的麻烦。req.setCharacterEncoding(UTF-8)必须放在读取参数之前否则中文用户名会乱码。盐值生成用 8 位随机字符每次注册都不同这样即使用户相同密码哈希结果也不相同。String.format(%02x, b)把字节转为两位十六进制注意负数时补位不能丢。参数说明forward是请求转发地址栏不变登录失败时回显错误消息用它sendRedirect是重定向注册成功后跳登录页用重定向可以避免刷新页面时重复提交表单。两类跳转的方向不能搞反否则会出现“注册成功了但页面没变化”的困扰。4.2 登录流程Session 就是那个“记住我是谁”的凭据登录相对于注册只多两步查询用户是否存在、把用户信息放进Session。查询时要注意密码比对方式——绝不是把数据库中存的password_hash取出来跟用户输入的密码直接比较而是把用户输入的密码加上该用户自己的盐再做一次 SHA-256然后比较两个哈希字符串是否相等。package com.demo.dao; import com.demo.entity.User; import com.demo.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; public class UserDao { public User findByUsername(String username) { String sql SELECT id, username, password_hash, salt, email, real_name FROM user_info 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 u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setPasswordHash(rs.getString(password_hash)); u.setSalt(rs.getString(salt)); u.setEmail(rs.getString(email)); u.setRealName(rs.getString(real_name)); return u; } } } catch (Exception e) { e.printStackTrace(); } return null; } public boolean register(User user) { String sql INSERT INTO user_info (username, password_hash, salt, email) VALUES (?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPasswordHash()); ps.setString(3, user.getSalt()); ps.setString(4, user.getEmail()); return ps.executeUpdate() 0; } catch (Exception e) { e.printStackTrace(); return false; } } }这段 DAO 代码对新手有两个值得抄的习惯一是全程使用PreparedStatement参数用?占位从根源上杜绝 SQL 注入——网上流传的很多登录代码用Statement.executeQuery(SELECT * FROM user WHERE username name )这就是经典的万能密码注入入口二是每个方法都用 try-with-resourcesConnection、PreparedStatement、ResultSet自动关闭不用写一堆 finally。登录 Servlet 里的核心判断逻辑// 登录验证——比对哈希而不是比对明文 User user userDao.findByUsername(username); if (user null) { req.setAttribute(msg, 用户不存在); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } String inputHash sha256(password, user.getSalt()); if (inputHash.equals(user.getPasswordHash())) { // 密码通过放 Session req.getSession().setAttribute(currentUser, user); // 可顺带更新 last_login_time resp.sendRedirect(profile.jsp); } else { req.setAttribute(msg, 密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); }参数说明sha256(password, user.getSalt())里的第二个参数不是新生成的盐而是数据库里那个用户自己的盐两次注册同一个密码得到的哈希一定不同但登录时把存储的盐拿出来重算结果一定能对齐。req.getSession()如果会话不存在会自动创建setAttribute(currentUser, user)把整个用户对象塞进 Session后续 JSP 页面要展示个人信息直接session.getAttribute(currentUser)即可这就是“登录后显示欢迎信息”的最小实现。4.3 JSP 页面登录表单和数据回显的写法JSP 页面的难点通常不在标签而在于如何把错误消息和服务端数据渲染出来。一个不重造轮子的login.jsp表单长得是这样% page contentTypetext/html;charsetUTF-8 languagejava % html head title用户登录/title /head body % String msg (String) request.getAttribute(msg); if (msg ! null) { out.println(div stylecolor:red msg /div); } % form actionlogin methodpost 用户名input typetext nameusername requiredbr 密码input typepassword namepassword requiredbr button typesubmit登录/button /form a hrefregister.jsp没有账号去注册/a /body /html逻辑说明request.getAttribute(msg)拿到的是 Servletforward时塞进去的错误消息写在表单上方可以让用户看到“密码错误”的反馈“required”是 HTML5 表单自带的非空校验能挡住一部分把空字符串提交到后端的操作。字符编码方面page contentType指定了页面输出编码而输入编码由 Servlet 的setCharacterEncoding保证两个连带关系搞清后中文乱码基本消失。5. 避坑清单从安装失败到 sa 登录失败的六条血泪记录5.1 现象SQL Server 2019 安装报“无法找到数据库引擎启动句柄”这个报错在 SQL Server 2016、2017、2019 安装时都出现过在安装日志中往往伴随“数据库引擎服务无法启动”的字样。原因通常是两个一是安装时选择了命名实例而实例名称里包含特殊字符二是服务账户权限不足导致数据库引擎服务启动即崩溃。解决路径用默认实例MSSQLSERVER重新安装如果已经安装完进入 SQL Server 配置管理器把数据库引擎服务的登录身份改成Local System再手动启动服务。注意改完需要重启机器或在配置管理器中重启服务否则配置不生效。5.2 现象JDBC 连本机数据库报“通过端口 1433 连接失败”开发机上能打开 SSMS、却连不上十有八九是 TCP/IP 协议没启用。SQL Server 默认安装时TCP/IP 协议在配置管理器里是禁用的而 JDBC 驱动走的就是 TCP——这个矛盾让人排查起来格外烦躁。打开 SQL Server 配置管理器进入“SQL Server 网络配置”右键点击“TCP/IP”启用它再进入“协议”下的“IP 地址”页签把IPAll的TCP 端口改为 1433。改完后重启 SQL Server 服务在命令行执行netstat -an | findstr 1433确认端口在听。另外SQL Server Browser 服务最好一并启动否则通过实例名连接时解析不到端口。5.3 现象用户名密码都对登录却报“用户 sa 登录失败”这是新人最摸不着头脑的错。SSMS 里 sa 明明能登录换到 JDBC 就连不上。原因是 SQL Server 默认开启了 Windows 身份验证模式sa 账户被禁用或密码为空。用 Windows 身份验证登录 SSMS右键服务器属性安全性页签里改成“SQL Server 和 Windows 身份验证模式”然后在安全 → 登录名 → sa 上右键设置密码并取消“禁用”。改完这两处必须重启服务。重启后再看 JDBC 连接串里jdbc.usernamesa就通了。这种现象最常出现在拿网上的 .zip 源码跑本地环境时——源码里的jdbc.properties写的是 sa/123456而本机 sa 根本没开。5.4 现象页面中文全变问号数据库里也存的是问号中文乱码是 JSP 登录注册的老大难但根因就那么几个JSP 页面没有指定contentType编码、Servlet 读取参数前没调setCharacterEncoding、数据库表字段用的是VARCHAR而不是NVARCHAR。三步都检查到位后再确认数据库的排序规则是否支持简体中文。SQL Server 2012 以上默认排序规则通常是Chinese_PRC_CI_AS支持中文不需要额外改动。顺带一提如果response.getWriter()输出的 JSON 或页面内容也乱需要在 Servlet 里加上response.setContentType(text/html;charsetUTF-8)。5.5 现象代码和数据库都正常启动 Tomcat 报ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver驱动 jar 没进来是唯一的真相但“没进来”有三种可能一是 jar 放在src目录而不是WEB-INF/libMaven 项目里则可能是依赖 scope 写成了provided二是项目通过 IDEA 运行时Artifacts 设置漏掉了lib目录导致部署后的 WEB-INF/lib 是空的三是 jar 本身没有跟随项目一起 Export。解决手段先检查打包后的目录target/项目/WEB-INF/lib下有没有驱动文件没有就把它加进 Artifacts如果是 Eclipse右键项目 → Properties → Deployment Assembly添加Java Build Path Entries里的驱动 jar。5.6 现象两个 JSP 项目共用同一套jdbc.properties互相干扰这个坑很隐蔽两个项目分别部署在同一个 Tomcat 的不同应用下但配置了同一个数据库和同一个用户名密码。当你在项目 A 里改了密码项目 B 的 JDBC 连接也跟着失效。原因不是配置混了而是两台项目共用了同一个 SQL Server 登录账号 sa任何一方改密码都会波及另一方。解决的长期做法是为每个项目创建独立的数据库登录名用CREATE LOGIN app_user WITH PASSWORDxxx;然后在对应的数据库里CREATE USER并授予db_datareader和db_datawriter权限不让项目动 sa。这是生产环境的基本素养代码里也只需要把jdbc.username从 sa 换成专属账号即可。6. 让登录注册更像生产环境会话注销、SQL 注入补防与个人中心入口最后一个环节也是别人拿到你的代码后能多看两眼的细节把“能跑”升级成“能用”。首要是会话注销。登录后把用户放进 Session退出登录绝不能只是跳回登录页——Session 里的currentUser还活着别人按后退键照样能进个人页面。做法是增加一个LogoutServlet调用req.getSession().invalidate()使整个 Session 失效然后重定向到登录页。这里有个老手习惯用户点击退出时机很难说但至少要在index.jsp或profile.jsp的头几行判断session.getAttribute(currentUser) null就强制跳回login.jsp这种前置检查能挡住“直接输 URL 进后台”的访问路径。其次是 SQL 注入的兜底。DAO 层已经用了PreparedStatement主流程没问题但如果你在代码里还搜得到任何一处字符串拼 SQL 的地方——比如排序字段、表名动态传入——都要警惕。SQL Server 的注入危害比 MySQL 更直接xp_cmdshell能直接执行系统命令。一个朴素的习惯是凡是外界传来的字符串要么走参数化要么在入口处做白名单校验。登录模块里没有这种动态结构的需求就完全可以避免。最后是登录后的个人中心入口。这正好回应了“jsp个人信息展示页面”这个高频需求登录成功跳转到一个profile.jsp从 Session 里取用户对象展示用户名、邮箱、注册时间。不需要额外查数据库因为登录时已经查全了。如果想让页面更好看可以再加一个头像上传字段——但那就是下一个模块的事了。我的个人习惯是即使时间再紧也留一个显示last_login_time的位置这一个小字段能让使用方直观确认“登录确实记录到数据库了”排查登录问题时会省很多沟通成本。做 JSP SQL Server 登录注册这些年最大的教训是“不要动 sa 密码、不要关 Windows 服务、不要跳过 TCP/IP 协议”。这三条是我在帮人排错时重复最多次的操作也是本地环境最容易被误伤的地方。希望这篇笔记能让你从“找了半天 zip 却跑不起来”直接跳到“顺滑跑通并看懂每一段代码”。希望帮到你。本文还有配套的精品资源点击获取