
简介本资源是一个基于JavaWeb技术栈开发的在线答题平台完整项目源码面向高校计算机专业师生及Java初学者用于课程设计、毕业设计或Web开发实践教学。系统采用B/S架构包含学生端与教师端双角色功能学生可完成单次答题、管理个人信息及查看成绩分班结果教师端支持学院/专业/班级/试题/学生等基础数据的全量CRUD操作并提供多维度成绩统计图表。资源包共388个文件含50个核心Java业务类、41个JSP页面、38个JavaScript交互脚本、22个CSS样式文件及大量静态资源如PNG/GIF图标、Bootstrap/Layui前端库整体6.1MB结构清晰、模块划分明确便于理解MVC分层逻辑与前后端协作流程。目前已有62人学习下载适合掌握Servlet、JSP、MySQL及基础前端技术的学习者进行二次开发与功能扩展。1. 为什么一个“基于JavaWeb的在线答题平台”至今仍是校招面试官最爱拆解的练手项目不是因为它有多炫——没有实时音视频、不跑大模型、不接区块链恰恰相反它朴素得像块砖用最基础的ServletJSPMySQL搭起骨架却把Web开发里最硬的几块骨头全露了出来用户会话怎么管试卷怎么动态生成又防篡改答题时间怎么精准控制到秒级且不可绕过交卷后答案怎么比对、怎么防前端篡改提交这些事Spring Boot自动配置藏不住Vue组件封装也绕不开——它们就明晃晃卡在HTTP协议、Servlet生命周期、数据库事务隔离级别和浏览器缓存策略的交界处。我带过的实习生里80%栽在“本地能跑部署到Tomcat就404”剩下20%里一半卡在“学生改前端JS就能多拿10分”一半困在“并发50人交卷时数据库死锁”。这个.zip不是玩具它是用JavaWeb原生能力划出的一条分水岭跨过去才算真正摸到了企业级Web系统的脉门。适合刚学完JDBC和HTTP状态码、正琢磨“到底什么叫‘服务端渲染’”的开发者也适合想找回底层手感的熟手——毕竟所有高阶框架都是在这块砖上垒起来的。2. 从解压到可运行三步走通本地开发环境含Tomcat 9.0JDK 11最小兼容链这个.zip本质是一个传统JavaWeb工程不是Maven聚合项目也不是Spring Boot可执行jar。它依赖的是Servlet规范原生能力因此环境适配必须抠到版本细节。常见翻车点不是代码写错而是JDK和Tomcat的“代际错位”——比如用JDK 17跑Servlet 3.1规范的代码或用Tomcat 10加载web.xml里声明的Servlet 3.0类。我们按最稳路径来2.1 环境清单只列必须项删掉所有“可选”干扰项提示别碰IDEA的“自动配置Tomcat”按钮。它常默认选错Servlet版本且把webapp目录映射到错误路径。手动配一步到位。组件版本要求为什么必须这个范围验证命令JDK11LTS或8仅限老版Servlet 3.1要求JDK 8但JDK 17需Tomcat 10本项目web.xml声明version3.1Tomcat 10已升级到Servlet 4.0不兼容java -versionTomcat9.0.87推荐或9.0.x任意Tomcat 9是最后一个支持Servlet 3.1且默认启用web.xml解析的稳定分支Tomcat 10强制使用jakarta.*包名本项目仍用javax.servlet.*cat $CATALINA_HOME/RELEASE-NOTESIDEIDEA 2023.3 或 Eclipse 2023-09新版IDE对web.xml中servlet-class路径解析更准旧版可能把com.xxx.servlet.SubmitServlet误判为不存在创建新JavaWeb项目时检查“Servlet version”下拉框2.2 解压后第一件事修正web.xml里的绝对路径陷阱很多同学解压后直接导入IDE启动报404——问题不在代码而在WebContent/WEB-INF/web.xml里藏着一个被忽略的硬编码路径!-- WebContent/WEB-INF/web.xml 片段 -- servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/servlet/LoginServlet/url-pattern !-- ⚠️ 这里 -- /servlet-mapping这个/servlet/是早期教程遗留的“伪目录”实际项目中应统一为根路径映射。否则浏览器访问http://localhost:8080/login.jsp点击登录按钮提交到/servlet/LoginServlet而Tomcat根本没注册该路径。正确做法是删掉所有/servlet/前缀!-- 改为 -- servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern !-- ✅ 根路径映射 -- /servlet-mapping同理检查SubmitServlet、ScoreServlet等所有url-pattern全部改为/xxx格式。改完保存重启Tomcat——这是90%本地404的根源。2.3 数据库初始化用最简SQL绕过Hibernate配置黑洞项目自带db/online_exam.sql但直接执行常失败——因为脚本里混着ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci这种MySQL 8.0语法而很多本地MySQL是5.7。不要改MySQL版本改SQL脚本-- db/online_exam.sql 开头替换为删除collation降级charset CREATE DATABASE IF NOT EXISTS online_exam CHARACTER SET utf8mb4; USE online_exam; -- 所有建表语句中删掉 COLLATEutf8mb4_0900_ai_ci保留 ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;然后执行mysql -u root -p db/online_exam.sql最后检查src/jdbc.properties# src/jdbc.properties drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/online_exam?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue usernameroot passwordyour_password注意useSSLfalse是开发环境必需生产环境必须开serverTimezone必须显式指定否则JDBC连接抛The server time zone value XXX is unrecognized异常。3. 用户会话与权限控制为什么HttpSession不能只靠request.getSession()这个平台最易被忽略的致命设计是把“登录态”和“考试态”混在同一Session里。结果就是学生A登录后打开两个标签页一个做题、一个查答案系统无法区分哪个是“正在考试”的会话——导致交卷时数据错乱。真正的解法不是加更多if判断而是用Session属性的命名空间隔离。3.1 登录态用固定key绑定用户基础信息LoginServlet.java中成功验证密码后// LoginServlet.java HttpSession session request.getSession(true); session.setAttribute(user_id, userId); // ✅ 基础用户ID session.setAttribute(username, username); // ✅ 用户名用于页面显示 session.setAttribute(role, student); // ✅ 角色标识后续扩展教师用 // ❌ 不要放 exam_id、start_time 等考试相关字段3.2 考试态用动态key隔离并发考试会话关键在StartExamServlet.java——学生点击“开始考试”时不是续用原Session而是创建考试专属子会话// StartExamServlet.java HttpSession session request.getSession(); int examId Integer.parseInt(request.getParameter(exam_id)); long startTime System.currentTimeMillis(); // 生成唯一考试会话keyuser_id exam_id timestamp防重复 String examSessionKey exam_ session.getAttribute(user_id) _ examId; session.setAttribute(examSessionKey, new ExamContext(examId, startTime)); // 重定向到exam.jsp并透传key response.sendRedirect(exam.jsp?exam_key examSessionKey);ExamContext类定义public class ExamContext { public final int examId; public final long startTime; public final MapInteger, String answers new HashMap(); // 题目ID→答案 public ExamContext(int examId, long startTime) { this.examId examId; this.startTime startTime; } public boolean isTimeUp(int durationMinutes) { return System.currentTimeMillis() - startTime durationMinutes * 60 * 1000L; } }3.3 答题页exam.jsp用URL参数锁定当前考试上下文exam.jsp顶部必须先校验考试key% String examKey request.getParameter(exam_key); if (examKey null || !examKey.startsWith(exam_)) { response.sendRedirect(error.jsp?msg非法考试入口); return; } HttpSession session request.getSession(); ExamContext context (ExamContext) session.getAttribute(examKey); if (context null || context.isTimeUp(60)) { // 60分钟考试时长 response.sendRedirect(timeout.jsp?exam_key examKey); return; } %这样即使同一用户开10个标签页每个exam_key都指向独立ExamContext互不污染。交卷时SubmitServlet也通过exam_key取数据彻底规避并发冲突。4. 防作弊核心前端不可信所有校验必须落在服务端拦截器新手常犯的错在exam.jsp里用JS计时时间到自动submit或用disabledtrue禁用题目选项。这些全是纸糊防线——F12删掉JS、删掉disabled属性就能无限答题。真正的防作弊是让每次AJAX请求都携带时效性凭证并在服务端做三重校验。4.1 动态Token机制每次答题请求都需新鲜凭证修改exam.jsp中提交单题答案的AJAX// exam.jsp 中 submitAnswer() 函数 function submitAnswer(questionId, answer) { const examKey % request.getParameter(exam_key) %; fetch(answer.jsp, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded }, body: question_id${questionId}answer${answer}exam_key${examKey}token${generateToken(examKey)} }); } // 客户端生成Token简单哈希非加密 function generateToken(examKey) { return btoa(examKey Date.now().toString().slice(-6)); // 取时间戳后6位防重放 }4.2 服务端Token拦截器用Filter统一校验新建src/filter/ExamTokenFilter.javapublic class ExamTokenFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String examKey request.getParameter(exam_key); String token request.getParameter(token); if (examKey null || token null) { response.sendError(HttpServletResponse.SC_FORBIDDEN, Missing exam_key or token); return; } // 1. 校验exam_key是否存在 HttpSession session request.getSession(); ExamContext context (ExamContext) session.getAttribute(examKey); if (context null) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, Invalid exam session); return; } // 2. 校验token时效性10分钟内有效 try { String decoded new String(Base64.getDecoder().decode(token)); if (!decoded.startsWith(examKey)) { throw new IllegalArgumentException(Token mismatch); } String timePart decoded.substring(decoded.length() - 6); long clientTime Long.parseLong(timePart); if (Math.abs(System.currentTimeMillis() - clientTime * 1000L) 10 * 60 * 1000L) { throw new IllegalArgumentException(Token expired); } } catch (Exception e) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, Invalid token); return; } // 3. 校验是否超时 if (context.isTimeUp(60)) { response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, Exam time expired); return; } chain.doFilter(req, resp); } }web.xml中注册filter filter-nameExamTokenFilter/filter-name filter-classfilter.ExamTokenFilter/filter-class /filter filter-mapping filter-nameExamTokenFilter/filter-name url-pattern/answer.jsp/url-pattern /filter-mapping逻辑说明Token不是密码而是“一次一密”的会话快照。客户端生成时绑定exam_key和当前毫秒级时间戳后6位如1712345678901取8901服务端解码后验证三件事① Token开头必须是当前exam_key② 时间戳在10分钟窗口内③ 对应考试未超时。三者缺一不可绕过成本远高于前端JS篡改。5. 避坑指南那些让项目卡在上线前夜的5个血泪现场5.1 现象本地Tomcat运行正常部署到Linux服务器后所有JSP页面404原因Linux文件系统大小写敏感而Windows不敏感。项目中WebContent/WEB-INF/web.xml引用了/jsp/login.jsp但实际文件名为Login.jsp首字母大写。Windows下自动匹配Linux下严格区分。解决统一小写命名所有JSP文件login.jsp,exam.jsp,score.jsp并全局搜索web.xml和Servlet中所有.jsp路径确保大小写完全一致。5.2 现象学生交卷后成绩为0数据库score表里score字段全为NULL原因SubmitServlet.java中获取答案时用了request.getParameter(answer)但前端form里radio按钮的name是answer_123带题号而非统一answer。解决前端遍历所有题目生成name唯一值c:forEach items${questions} varq divQ${q.id}: ${q.content}/div input typeradio nameanswer_${q.id} valueA A input typeradio nameanswer_${q.id} valueB B !-- ... -- /c:forEach服务端用request.getParameterMap()遍历MapString, String[] paramMap request.getParameterMap(); for (Map.EntryString, String[] entry : paramMap.entrySet()) { if (entry.getKey().startsWith(answer_)) { int questionId Integer.parseInt(entry.getKey().substring(7)); String answer entry.getValue()[0]; context.answers.put(questionId, answer); } }5.3 现象MySQL插入中文乱码用户表里用户名显示为????原因三个环节漏设UTF-8① MySQL服务端字符集my.cnf中[mysqld]下缺character-set-serverutf8mb4② 数据库创建时未指定CHARACTER SET utf8mb4③ JDBC URL缺useUnicodetruecharacterEncodingUTF-8。解决三处同步修正my.cnf添加[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci重建数据库CREATE DATABASE online_exam CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;jdbc.properties中URL补全urljdbc:mysql://localhost:3306/online_exam?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseUnicodetruecharacterEncodingUTF-85.4 现象Tomcat启动时报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因MySQL 8.0驱动类名已从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver而项目代码仍用旧类名。解决两处修改src/jdbc.properties中drivercom.mysql.cj.jdbc.Driversrc/utils/DBUtil.java中Class.forName(com.mysql.cj.jdbc.Driver);同时确认lib/mysql-connector-java-8.0.33.jar已放入WebContent/WEB-INF/lib/5.5 现象考试倒计时在Chrome正常在Firefox中跳变或停止原因exam.jsp里用setTimeout递归调用倒计时函数但Firefox对setTimeout的最小间隔限制更严≥4ms且页面失焦时会降频。解决改用requestAnimationFrame替代setTimeoutlet countdown 3600; // 60分钟 function updateTimer() { const minutes Math.floor(countdown / 60); const seconds countdown % 60; document.getElementById(timer).innerText ${minutes}:${seconds 10 ? 0 : }${seconds}; if (countdown 0) { countdown--; requestAnimationFrame(updateTimer); // ✅ 浏览器原生帧率调度 } else { document.getElementById(submitBtn).click(); } } requestAnimationFrame(updateTimer);6. 进阶验证用Postman模拟真实攻击链检验你的防作弊是否真牢固写完代码不等于安全。我习惯用Postman发起三类攻击请求直接暴露逻辑漏洞——这比写单元测试更快定位问题。6.1 攻击场景1重放TokenReplay Attack步骤正常打开exam.jsp?exam_keyexam_123_456抓包记录answer.jsp的POST请求复制完整请求含exam_keyexam_123_456tokenxxxxxquestion_id1answerA修改question_id2保持原token不变发送10次查看数据库exam_context表或日志是否记录重复提交。预期结果服务端应拒绝第2次起的请求因Token中时间戳已过期Date.now()生成的token在10分钟后失效。若接受说明Token校验逻辑未生效。6.2 攻击场景2伪造exam_keySession Hijacking步骤构造请求POST /answer.jsp HTTP/1.1exam_keyexam_999_999tokenxxxxxquestion_id1answerAexam_999_999是不存在的key发送请求。预期结果返回HTTP 401 Unauthorized。若返回200说明ExamTokenFilter未拦截或session.getAttribute(examKey)未判空。6.3 攻击场景3暴力刷题Brute Force Answer步骤编写Python脚本循环提交所有题目答案import requests for qid in range(1, 51): # 假设有50题 data { exam_key: exam_123_456, token: generate_token(exam_123_456), # 用当前时间生成 question_id: qid, answer: A } requests.post(http://localhost:8080/answer.jsp, datadata)观察ExamContext.answers.size()是否超过题目总数。预期结果服务端应在context.answers.put()前加锁或校验题号唯一性避免同一题多次提交覆盖。若answersMap大小超过50说明缺少幂等校验。我的习惯每次功能迭代后必跑这三组Postman请求。不是为了证明代码完美而是确认“攻击成本”是否足够高——当重放需要每10秒换一次token、伪造key要先窃取真实session、刷题要逆向JS生成逻辑时我就知道这个JavaWeb平台的底子已经扛得住真实教室里的那点小聪明了。希望帮到你。本文还有配套的精品资源点击获取