JSP+Servlet+MySQL学生选课系统:多角色登录与权限控制实战解析
简介这是一套基于JSPServletMySQL开发的学生选课管理系统源码面向需要完成课程设计、毕业设计或学习传统JavaWeb开发的初学者。系统支持教师与学生双角色登录教师可管理学生、课程、选课信息并设置必修学分上下限学生可在线选课、查看已选课程和修改个人信息同时还能统计未修满最低学分的学生名单。资源包共105个文件包含25个Java类源码、25个class编译文件、15个JSP页面、19张效果图、SQL数据库脚本、论文文档及运行导入指导视频等压缩包大小约108.89MB从源码到部署说明一应俱全。目前已有4703人学习使用配套高清视频教程与导入文档能够帮助读者快速跑通项目并理解前后端交互逻辑适合作为JavaWeb项目实战参考。1. 为什么 2024 年了还在做 jspservletmysql 的学生选课系统如果你是在校生准备 Java Web 课程设计或者刚入行被分到一个老项目需要快速接手那我先说个结论jspservletmysql 这套组合技术上“老”但它是当前 Java Web 面试和课设里出现频率最高、最容易讲清楚架构逻辑的组合之一。学生选课管理系统正好落在它最舒适的业务范围——表格操作多、页面跳转多、权限边界清晰。多角色登录是这类系统里最容易踩坑的部分很多人的源码跑起来就卡在学生登录后直接访问老师页面、或者刷新一次就丢失登录状态。这篇就按我平时拿到这种源码后的完整处理流程来讲数据模型怎么拆、Servlet 里的角色分发怎么写、Filter 怎么拦、IDEA 里怎么从导入到跑通、以及几个我亲测容易翻车的细节。这套流程不只适用于这一个课设换个题目比如图书借阅或者会议室预约骨架完全不用动。2. 系统设计先落定数据模型与模块边界怎么拆2.1 四张核心表用户、学生、课程、选课的关系设计拿到源码第一步不是急着配 Tomcat而是先打开 SQL 文件看表结构。学生选课系统无论如何变体核心逃不开四张表用户表、学生表、课程表、选课表。多角色登录的“多角色”就体现在用户表里的一个 role 字段上。常见的角色设计有两种。第一种是建一张单独的用户表里面放 role 字段值为 admin、teacher、student第二种是学生表、教师表、管理员表分开登录时去不同表查。第二种看着“面向对象”实际在课设代码里会把你坑惨——登录逻辑要写三遍Session 里存的类型还不一致后面 Filter 做权限判断时得写一大堆 if。我一般建议用第一种把“登录验证”和“业务数据”解耦。下面是一份我常用的建表 SQL 精简版CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARSET utf8mb4; USE course_system; -- 用户表一个账号对应一个人角色区分身份 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50) NOT NULL, role TINYINT NOT NULL COMMENT 1-管理员 2-教师 3-学生, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 课程表谁教、在哪儿上、能选多少人 CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0, max_student INT DEFAULT 50, selected_count INT DEFAULT 0, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES t_user(id) ); -- 选课表学生选课记录联合唯一防重复选课 CREATE TABLE t_selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sel_student FOREIGN KEY (student_id) REFERENCES t_user(id), CONSTRAINT fk_sel_course FOREIGN KEY (course_id) REFERENCES t_course(id) );这段 SQL 有几个细节值得注意。password字段我故意用了 VARCHAR(64)因为哪怕课设用的还是明文存储也建议至少给将来留出 MD5 或 BCrypt 的升级空间。role用 TINYINT 而不是字符串是因为整数在 MySQL 索引和比较时开销小而且代码里用常量去对应不容易出现字符串大小写不匹配这种 Bug。选课表里的UNIQUE KEY uk_student_course是很多初学者会漏掉的它能在数据库层面直接挡住同一个学生对同一门课的重复选课光靠后端代码判断会有并发漏洞。selected_count字段属于典型的“空间换时间”设计显示在课程列表页时不需要去 count 选课表虽然会有并发下计数不准的隐患但对课设级系统够用了。2.2 角色边界与页面路由规划数据模型定了接下来要把“多角色登录”具体化成页面访问规则。这是拿到源码后最值得先画一张矩阵图的东西。我通常按“未登录、学生、教师、管理员”四个维度把每个 Servlet 或 JSP 页面按访问权限列一张表。/LoginServlet 匿名可访问 /index.jsp 匿名可访问登录页 /student/* 仅学生 /teacher/* 仅教师 /admin/* 仅管理员 /CommonLogoutServlet 登录后可访问这张矩阵的价值在于Filter 里做权限判断时规则是清晰可枚举的而不是一个 URL 一个 URL 去堆 if。很多源码的权限 Bug 就是出在这里比如某条选课 Servlet 路径写得过于宽泛学生顺着 URL 能直接打开教师的管理页面。JSP 页面按角色分目录存放是另一个重要约定。常见的做法是 WebRoot 下建 student、teacher、admin 三个目录各自只放自己角色的页面。它不解决权限问题——因为 JSP 本质上还是会编译成 Servlet 被直接访问——但在维护层面它让人一眼就能看懂这个页面的归属。真正拦住未授权访问的是后面要讲的 Filter。3. 多角色登录与权限控制日志走查一条链3.1 登录 Servlet一次请求里完成的验证与分发登录模块是整个系统里最核心的代码因为多角色登录的“多”字就体现在这里。一个简洁的 LoginServlet 应该只做三件事接收表单参数、查用户表验证账密、把用户信息塞进 Session 并跳转到对应角色首页。WebServlet(/LoginServlet) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); // 基础空值校验避免空指针直接打到 SQL 层 if (username null || username.trim().isEmpty() || password null || password.isEmpty()) { req.setAttribute(msg, 用户名或密码不能为空); req.getRequestDispatcher(/index.jsp).forward(req, resp); return; } UserDao dao new UserDao(); User user dao.findByUsernameAndPassword(username.trim(), password); if (user null) { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/index.jsp).forward(req, resp); return; } // 登录成功后把用户信息放进 Session后续页面全靠它识别身份 HttpSession session req.getSession(); session.setAttribute(currentUser, user); session.setAttribute(role, user.getRole()); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作自动过期 // 按角色分发到不同首页 switch (user.getRole()) { case 1: resp.sendRedirect(req.getContextPath() /admin/index.jsp); break; case 2: resp.sendRedirect(req.getContextPath() /teacher/index.jsp); break; case 3: resp.sendRedirect(req.getContextPath() /student/index.jsp); break; default: resp.sendRedirect(req.getContextPath() /index.jsp); } } }这段代码里最容易出问题的不是逻辑本身而是sendRedirect和forward的区别。我在很多源码里见过有同学在登录成功后又用forward转发到 index.jsp结果就是地址栏没变用户手动刷新时表单被重复提交。成功后的页面跳转一定要用重定向让浏览器发起一个新的 GET 请求。setMaxInactiveInterval(30 * 60)是个容易被忽略的参数它定义了 Session 的无操作过期时间。不设置的话Tomcat 默认 30 分钟看起来没问题但浏览器在课程设计演示现场经常一停就是大半天过十分钟去点下一页发现 Session 没了会非常尴尬。这个值我习惯统一在代码里显式指定服务端的存活和行为就可预期了。3.2 用 Filter 把拦截逻辑做成一处多角色系统最怕权限判断散落在各个 Servlet 和 JSP 里。比如在 student 的 JSP 页面顶部写一段检查 role 的 Java 脚本页面一多就漏。正确做法是写一个权限过滤器把 URL 前缀和角色映射集中管理。WebFilter(/*) public class AuthFilter implements Filter { // 无需登录即可访问的路径避免出现“登录页也需要登录”的死循环 private static final SetString ANONYMOUS_URLS new HashSet(Arrays.asList( /index.jsp, /LoginServlet, /css, /js, /images )); public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getRequestURI().substring(request.getContextPath().length()); // 静态资源和登录页直接放行 if (ANONYMOUS_URLS.contains(path) || path.equals(/)) { chain.doFilter(request, response); return; } HttpSession session request.getSession(false); if (session null || session.getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /index.jsp); return; } // 到这里说明已登录再按角色和路径前缀做二次校验 int role (int) session.getAttribute(role); if (path.startsWith(/admin/) role ! 1) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } if (path.startsWith(/teacher/) role ! 2) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } if (path.startsWith(/student/) role ! 3) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(request, response); } }用request.getSession(false)而不是req.getSession()是个非常实用的小技巧。前者在 Session 不存在时返回 null后者会强制创建一个新 Session导致未被拦截的匿名请求也会往 Session 表里写数据压测时会出现一堆空 Session。路径前缀的匹配逻辑写在这里是整个过滤器最薄的部分也是性价比最高的部分。只要项目里约定好业务 Servlet 必须有角色前缀这个 Filter 就能覆盖系统里百分之九十的越权访问。它对“直接输入 URL 绕过页面入口”这种攻击方式的防御非常有效因为 JSP 文件和 Servlet 一起被 Filter 管住了。3.3 登录会话与退出登录的处理退出登录只有三行代码但经常有人写错顺序。先清 Session 再跳转这是标准做法关键是不能只靠session.invalidate()了事最好把响应头里的缓存也设置一下防止用户按后退键看到登录前的页面。WebServlet(/LogoutServlet) public class LogoutServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(false); if (session ! null) { session.invalidate(); } // 防止浏览器后退访问已登录页面 resp.setHeader(Cache-Control, no-cache, no-store, must-revalidate); resp.sendRedirect(req.getContextPath() /index.jsp); } }关于课设里的越权问题我多说一句。Filter 拦截的是“未登录”和“角色不对”的情况但它不解决“学生 A 选了自己的课却修改了学生 B 的选课记录”这类横向越权。后者需要每个业务方法里再判断当前 Session 用户是否操作的是自己的数据。对课设系统来说做到 Filter 这一层已经能过关但点到这句话能让面试官觉得你思考到了纵深防御。4. 把源码跑起来IDEA 配置、建库与 Tomcat 部署全流程4.1 导入工程与 IntelliJ IDEA 配置检查拿到源码压缩包后不要急着双击打开。先看目录结构确认它是一个 Web 工程而不是普通的 Java 工程。判断标准很简单WEB-INF 目录下有没有 web.xml或者项目里有没有因 Maven 结构而存在的 pom.xml。老一代 jspservlet 项目的源码大多是 Eclipse 的.project结构直接用 IntelliJ IDEA 打开是打不开的。我一般这样做新建一个空项目然后把源码里的 src 目录变成 Sources Rootweb 目录识别成 Web Resources Directory再把 lib 下的 jar 包一个个加到 Libraries 里。这里的第一个坑就是 jar 包。很多所谓“绝无报错”的源码lib 目录下只有 mysql-connector-java 一个驱动包但代码里还引用了 JSTL 的标签库运行到 JSP 页面时直接给你抛javax.servlet.jsp.jstl.core.Config not found。如果你不想引入 Maven 去管理至少保证 lib 里有 mysql 驱动、jstl、standard 这三个 jar版本分别是 5.x 和 1.0.6 即可。4.2 建库建表与初始化测试数据SQL 文件通常在源码目录下叫db.sql或者course_system.sql。打开以后先不要直接执行用 CtrlF 查一下建表语句里有没有ENGINEInnoDB的指定。有时候原作者因为在 MySQL 5.5 上导出的表结构里可能混有DEFAULT CHARSETlatin1不改成 utf8mb4 的话后面你在页面上输入中文测试数据时全是问号或者乱码。执行 SQL 的完整流程我在命令行里操作比在 Navicat 里点按钮更能看清每一步的结果# 第一步登录 MySQL mysql -u root -p # 第二步执行建库建表脚本 source /path/to/course_system.sql; # 第三步检查表是否真的建出来了 USE course_system; SHOW TABLES; # 第四步查看用户表的数据确认初始账号密码是什么 SELECT id, username, real_name, role FROM t_user;执行完以后重点关注最后一步。很多源码的初始化数据里没有管理员账号或者密码是空的你要是不查等会儿登录页面输什么都是错误。遇到这种情况自己补一条 SQL 插进去INSERT INTO t_user(username, password, real_name, role) VALUES (admin, 123456, 系统管理员, 1);4.3 Tomcat 配置与数据库连接参数数据库连接参数通常写在src/db.properties或者DBUtil.java里面。打开它核对三样东西URL 里的数据库名、用户名、密码。最常见的错误是原作者本机密码是123456你的 MySQL 是root这一行不改系统永远连不上数据库。连接参数的正确写法是下面这个格式注意useSSLfalse和serverTimezoneAsia/Shanghai这两项在 MySQL 8.x 里必须带上不然控制台里全是 SSL 和时区警告jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的密码这里有一个版本陷阱要提醒你如果用 MySQL 8.x 的驱动 8.0.33 版本jdbc.driver应该写成com.mysql.cj.jdbc.Driver旧的com.mysql.jdbc.Driver虽然为了兼容性还能工作但控制台会打印一段很长的警告。如果你拿到的源码里是旧驱动类名同时数据库是 MySQL 8最稳的做法是升级 lib 下的驱动 jar然后把这个类名一起改掉。4.4 部署到 Tomcat 并确认启动无异常IDEA 里配置 Tomcat 的路径一般是右上角 Add Configuration - Tomcat Server - Local然后指定 Tomcat 的安装目录。有人会问为什么不直接用 Maven 的tomcat7-maven-plugin非 Maven 的老工程里没有 pom.xml反而增加工作量。部署完成后点击运行然后盯着 IDEA 底部的日志窗口。理论上应该看到一行Server startup in [xxx] milliseconds或Artifact is deployed successfully。访问地址不是项目根的 IP 加端口而是http://localhost:8080/项目名/。项目名在 Deployment 标签页里可以看到比如course_system_war_exploded太长了可以把它改成/courseURL 就变成http://localhost:8080/course/。这一步不做的话你拿着源码直接访问 localhost:8080 会看到 404因为根路径没有任何页面。想要快速判断系统是否健康不一定要先登进去。直接访问http://localhost:8080/course/LoginServlet如果被 Filter 重定向到index.jsp说明 Web 应用本身已经正常问题只可能在登录时的数据库连接上。这个判断顺序能帮你把“应用没起来”和“业务逻辑有 Bug”这两类问题干净地分开。5. 避坑记录从“报错一堆”到“亲测运行”的 5 个高频问题5.1 Java 版本与编译级别不一致导致的启动失败现象IDEA 导入源码后点击运行 Tomcat启动到一半报错堆栈里出现UnsupportedClassVersionError或者invalid source release: 1.8。原因源码里隐藏的.classpath文件或者Project Structure里残留的编译级别是 Java 8而本机安装的 JDK 是 17 或更高或者相反——本机是 JDK 8源码编译级别设成了 11。解决统一三处配置为同一个版本。在 File - Project Structure - Project 里把 SDK 和 Language Level 保持一致在 Modules - Sources 里确认 Language Level 同步最后在 Settings - Build Tools - Maven - Runner 里如果配置了 JRE也改成同一个版本。做完这三步后建议做一次 Clean而不是直接重新运行。5.2 Tomcat 启动时闪退日志里只有一行错误现象双击 Tomcat 启动后控制台窗口一闪而过什么都看不清。原因绝大多数情况是CATALINA_HOME环境变量配错了或者 Java 环境变量 JAVA_HOME 没配。Tomcat 启动脚本依赖 JAVA_HOME 去定位 java.exe找不到就直接退出。解决不要双击 Tomcat 目录下的startup.bat改在 IDEA 里启动。如果必须用命令行排查可以先执行catalina.bat run而不是startup.bat前者会保持窗口不关闭并把完整日志铺出来看到哪一行报错才能对症下药。5.3 登录后跳转的页面全部 404现象在登录页输入正确账号密码点击登录地址栏 URL 变成了http://localhost:8080/student/index.jsp但页面是 404。原因sendRedirect(req.getContextPath() /student/index.jsp)里的/student/index.jsp是按源码原作者的项目部署名生成的。如果源码里 JSP 页面是放在 WebRoot 根目录而不是 student 子目录下这个路径就找不到文件。解决打开 WebRoot 目录看实际目录结构确认是否真的有 student/teacher/admin 三个子目录。如果没有说明这套源码的角色区分靠的是页面内嵌的 Java 逻辑而不是目录隔离需要把 LoginServlet 里的跳转路径改成实际存在的文件路径。另外也要检查 Deployment 标签页里的 Application context 是不是和你访问的一致如果不一致getContextPath()返回的值就和你手动输入的 URL 不匹配。5.4 数据库连接池报错Access denied for user现象登录时提示登录失败或者说“系统繁忙稍后再试”Tomcat 日志里出现Access denied for user rootlocalhost (using password: YES)。原因99% 是db.properties里的密码写错了。剩下 1% 是 MySQL 的 root 账号不允许通过 JDBC 的 TCP 连接访问只允许本机 socket 登录。解决先去 properties 文件核对密码注意文件里如果密码后面带了空格也会报这个错——这是最隐蔽的情况。如果密码确实没错执行下面的 SQL 给 root 账号重新授权ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;MySQL 8 默认的认证插件是caching_sha2_password有些旧版本的 mysql-connector-java 驱动不支持这个插件连接时也会报错或者行为奇怪。上面的mysql_native_password做法是临时解决兼容性问题最直接的手段。5.5 页面上中文全是问号或者乱码现象课程名、教师姓名等中文数据显示成???或者页面上是各种看不懂的字符。原因乱码涉及三层编码任何一层不一致都会出问题。第一层是数据库编码建库时指定了 utf8mb4 但表还是 latin1第二层是 JDBC 连接 URL 里没带characterEncodingutf8第三层是 JSP 页面顶部没写pageEncodingUTF-8同时 Servlet 读参数前没setCharacterEncoding(UTF-8)。解决按照三层顺序逐一排查。先查表结构SHOW CREATE TABLE t_course;看表的 DEFAULT CHARSET是 latin1 就ALTER TABLE t_course CONVERT TO CHARACTER SET utf8mb4;。然后检查 JDBC URL对照前面 4.3 的写法改成标准格式。最后在所有 Servlet 的doGet/doPost开头加req.setCharacterEncoding(UTF-8);在 JSP 页面第一行加上% page contentTypetext/html;charsetUTF-8 languagejava %。这三层全对了乱码问题基本根治。这个排查思路也适用于任何 Java Web 项目不局限于选课系统。6. 验证不止能跑还要能守住底线再加一道防线实用技巧新的选课管理系统里如果只能看页面跳转我建议至少手动验证以下五个场景不用写自动化测试但每一条都必须真正点过一遍1. 学生选课成功后刷新页面选课列表不应出现重复记录 → 靠 t_selection 的唯一索引兜底 2. 学生退出登录后直接访问 /student/index.jsp → 应被 Filter 拦回登录页 3. 学生登录后手动访问 /admin/userManage.jsp → 应收到 403而不是看到用户列表 4. 课程选满selected_count max_student后学生再点选课 → 后端应给出明确提示 5. 两个不同角色同时登录同一个浏览器 → 后登录者挤掉前者Session 里永远只有一个用户第 5 条值得展开。当前我们的 Filter 依赖一个 Session 存当前用户这天然只能支持单角色单会话。如果演示时你开了两个浏览器窗口一个学生一个管理员那么后登录的那个会把先登录的覆盖掉。这个现象在课设答辩现场几乎必现轻则尴尬重则被追问“你连多用户并发的概念都没有”。常见改法有在 Session 里用 attribute 区分角色各自存一份或者在用户表里加一个 login_token 字段每次登录生成随机 token 存数据库同时响应给浏览器写入 cookie业务方法里以 token 为准判断当前用户。后者已经能触摸到传统单点登录的边缘作为扩展亮点写在结课报告里是加分的。除了验证还可以做一个实用性很强的小改造在请求进入业务 Servlet 之前用 Filter 统一往 request 里塞一个当前用户对象。在 JSP 里想显示“欢迎你张三”时不用每页都去 Session 里取和强转直接request.getAttribute(loginUser)就行。// 在原有 AuthFilter 的 doFilter 里最后放行前加一行 request.setAttribute(currentUser, session.getAttribute(currentUser));这样做的好处是后续新写的所有 JSP 和 Servlet都可以从一个统一的入口拿用户信息而不是自己从 Session 里取出来再强转。强转代码一旦在多个页面里复制粘贴哪天把User错写成Student编译期不报错运行期直接 ClassCastException。最后聊一下这套系统的价值判断。从学习角度讲jspservlet 这种“手写一切”的方式能帮你把 HTTP 请求生命周期、Servlet 加载顺序、Session 的运作机制、Filter 的链式调用这些概念全部过一遍从就业角度讲现在招聘要求里很少直接写 JSP但它作为理解 Spring MVC、Shiro 这些框架的映射基础价值仍然是不可替代的。希望我上面这些处理流程和踩坑记录能帮你少走几步弯路祝一次跑通。我从这套系统里最大的一个收获是做 Java Web 项目先把“一个点真正跑通”再做膨胀。选课系统的登录和角色权限这个点通了后面加什么功能都不会太难难的是你还没验证登录就急着去写课程管理模块结果两边互相干扰最后哪个都不确定是不是 bug排查起来非常头疼。这个习惯一直保留到现在写生产级代码都还在用。本文还有配套的精品资源点击获取