简介:基于 JSP/Servlet/MySQL 技术栈的学生信息管理系统完整项目包,面向数据库课程设计、Java Web 初学与期末实训人群。系统覆盖用户登录注册、学生信息增删改查、成绩统计分析报表、数据可视化展示与权限分级管理模块,能够帮助读者理解 Servlet 处理请求、JSP 页面渲染、MySQL 数据持久化及前后端交互的核心流程,也可作为课设或毕设的改造基础。
压缩包共 268 个文件,大小约 10.25MB,核心为 51 个 Java 源文件、52 个 JSP 页面及对应 class 文件,另含 13 个依赖 jar 包、CSS/JS 前端资源、配置文件与数据库工作目录;同时附带说明文档和附赠资源,便于快速部署、运行与二次开发。
已有 35 人学习下载,适合需要完整可运行代码、数据库脚本和项目文档的开发者参考。
1. JSP+Servlet+MySQL 做学生信息管理系统,数据库课程设计为什么还选这套栈
每年做数据库课程设计,都有同学在技术栈上纠结半天。选 Spring Boot,评分老师会追问"JDBC 层在哪里";选纯 JDBC 控制台程序,又拿不出页面和交互。JSP+Servlet+MySQL 恰好卡在中间:MVC 结构能讲清楚,数据库设计和 SQL 能力是露在外面的,部署也只需要 Tomcat 加一个 MySQL 实例。这个学生信息管理系统正好覆盖课程设计最常见的评分点——用户注册登录、学生信息增删改查、成绩统计分析报表、ECharts 数据可视化,再加上权限分级管理。读懂这套代码,相当于把 Servlet 生命周期、Session 机制、JDBC 连接和聚合查询全部过了一遍。
2. MySQL 表结构与 JDBC 数据访问层:把增删改查的评分点做扎实
2.1 用户表、学生表、成绩表:三张核心表的字段设计
学生信息管理系统听起来功能多,落到数据库就是三张表:sys_user 管登录和权限,student 管学生档案,score 管成绩。很多课程设计会再加一张 course 表,但考虑到成绩报表基本按课程名分组,把 course_name 直接冗余在 score 表里更省事,评分时也更容易解释字段含义。
| 表名 | 承担职责 | 关键字段 | 与其他表关系 |
|---|---|---|---|
| sys_user | 登录认证与权限分级 | username、password、salt、role | 无外键 |
| student | 学生档案维护 | student_no、name、class_name、status | 被 score 引用 |
| score | 成绩明细与统计来源 | student_id、course_name、score | 外键指向 student(id) |
CREATE TABLE sys_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password CHAR(64) NOT NULL COMMENT 'SHA-256(salt+明文)', salt CHAR(16) NOT NULL COMMENT '16位随机盐', role TINYINT NOT NULL DEFAULT 2 COMMENT '0管理员 1教师 2学生', real_name VARCHAR(50) COMMENT '显示名', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 0 COMMENT '0未知 1男 2女', class_name VARCHAR(50) COMMENT '班级', major VARCHAR(50) COMMENT '专业', phone VARCHAR(20), email VARCHAR(100), enroll_date DATE COMMENT '入学日期', status TINYINT DEFAULT 1 COMMENT '1在读 0休学 2毕业', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; CREATE TABLE score ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL COMMENT '关联student.id', course_name VARCHAR(50) NOT NULL COMMENT '课程名', credit DECIMAL(3,1) DEFAULT 0.0 COMMENT '学分', score DECIMAL(5,2) NOT NULL COMMENT '百分制成绩', exam_date DATE, FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, UNIQUE KEY uk_stu_course (student_id, course_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩表';几个容易被扣分的点:字符集选 utf8mb4 而不是 utf8,否则插入 emoji 或生僻姓氏会报 Incorrect string value;学号和用户名必须加 UNIQUE,这是"数据完整性"的评分点;score 表加 (student_id, course_name) 联合唯一键,防止同一个人同一门课录两次。外键 ON DELETE CASCADE 的意思是删学生档案时成绩一并删除,避免孤儿数据,这比在 Service 层手动删两条更能体现数据库设计意识。
DECIMAL 类型专门用来存成绩和学分,不要用 FLOAT。FLOAT 是近似值,89.5 这种分数在浮点运算后会变成 89.49999,做统计报表时聚合结果对不上,评分老师用几条边界数据就能测出来。关于索引,联合唯一键本身就是一个索引,按学号精确查询走的是 student_no 的唯一索引;但 name LIKE '%张%' 这种前置模糊查询用不上索引,数据量上万后变慢是正常现象,课程设计阶段不需要做全文索引。
2.2 Druid 连接池与 db.properties:三个必调参数
JDBC 直连的写法在课程设计里也能跑,但每次请求都 new 一个 Connection,MySQL 默认的 wait_timeout 一超时,页面就开始报 Communications link failure。常见做法是用 Druid 连接池把连接复用起来,配置放在 src 根目录的 db.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/student_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=root jdbc.password=123456 jdbc.initialSize=5 jdbc.maxActive=20 jdbc.maxWait=60000 jdbc.validationQuery=SELECT 1三个必调参数:maxActive 控制最大连接数,课程设计并发量小,10 到 20 足够,开太大反而占 MySQL 的连接数;maxWait 是拿不到连接时的等待毫秒数,默认 -1 表示无限等,页面会卡死,设成 60000 至少能快速报错;serverTimezone=Asia/Shanghai 必加,MySQL 8.x 的驱动不写时区直接启动报错,这条在 mysql 安装配置教程里经常被单独拿出来说。characterEncoding=utf8 配合 JSP 页面编码一起用,能省掉一半中文乱码问题。
public class DruidUtil { private static DataSource dataSource; static { try (InputStream in = DruidUtil.class.getClassLoader().getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError("数据库连接池初始化失败"); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }druid-1.2.x 的 jar 包要放到 WEB-INF/lib 下,如果用 Maven 就引入 druid 和 mysql-connector-j 两个依赖。注意 mysql-connector-java 老版本驱动类是 com.mysql.jdbc.Driver,8.x 改成 com.mysql.cj.jdbc.Driver,配错的症状是启动时 ClassNotFoundException。
2.3 BaseDAO 抽取:PreparedStatement 封装与防注入
Servlet 里直接写 JDBC 代码也能交差,但数据库课程设计一般会被要求体现分层。常见做法是抽一个 BaseDAO,把连接、预编译、结果集映射封装起来,子类只写 SQL 和参数。
public abstract class BaseDAO { protected <T> List<T> queryList(String sql, RowMapper<T> mapper, Object... args) { try (Connection conn = DruidUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < args.length; i++) { ps.setObject(i + 1, args[i]); } try (ResultSet rs = ps.executeQuery()) { List<T> list = new ArrayList<>(); while (rs.next()) { list.add(mapper.map(rs)); } return list; } } catch (SQLException e) { throw new RuntimeException("SQL执行失败: " + sql, e); } } protected int update(String sql, Object... args) { try (Connection conn = DruidUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < args.length; i++) { ps.setObject(i + 1, args[i]); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("更新失败: " + sql, e); } } }这段代码有几个细节值得写进设计说明书:try-with-resources 保证 Connection、PreparedStatement、ResultSet 自动关闭,不写 finally 也不漏连接;全程用 PreparedStatement 的 ? 占位符拼参数,而不是字符串拼接 SQL,这是防 SQL 注入的第一道防线,答辩时经常被单独问。RowMapper 是函数式接口,子类用 Lambda 把每行 ResultSet 变成 Student 对象。
学生模块的 DAO 至少要有分页查询、按主键查、插入、更新、删除五个方法:
public class StudentDAO extends BaseDAO { public List<Student> findByPage(String keyword, int offset, int limit) { String sql = "SELECT * FROM student WHERE name LIKE ? OR student_no LIKE ? ORDER BY student_no LIMIT ?, ?"; String like = "%" + keyword + "%"; return queryList(sql, rs -> new Student(rs), like, like, offset, limit); } public int count(String keyword) { String sql = "SELECT COUNT(*) FROM student WHERE name LIKE ? OR student_no LIKE ?"; // 返回总条数,分页组件靠它算总页数 } public Student findById(int id) { /* SELECT * FROM student WHERE id = ? */ } public int insert(Student s) { /* INSERT ... */ } public int update(Student s) { /* UPDATE ... WHERE id = ? */ } public int deleteById(int id) { /* DELETE FROM student WHERE id = ? */ } }分页 SQL 用 LIMIT offset, limit,offset 是 (page-1)*pageSize,前端每传一次页码就重查一次,课程设计不需要做游标分页。keyword 里只匹配 name 和 student_no,不要给所有列都加 LIKE,否则查不出结果时用户会怀疑是 bug。
3. 登录注册与权限分级管理:盐值哈希、Session 与过滤器拦截
3.1 注册模块:密码加盐哈希,不能明文入库
注册功能最容易被忽略的是密码存储方式。直接把密码明文存进 sys_user,数据库一旦被导出来,所有账号裸奔,这在课程设计答辩里是硬伤。常见做法是 SHA-256 加盐:每个用户生成独立的随机盐,把盐拼在密码后面一起做哈希,库里只存盐和哈希结果。
public class PasswordUtil { public static String genSalt() { return UUID.randomUUID().toString().replace("-", "").substring(0, 16); } public static String hash(String salt, String rawPassword) { String input = salt + rawPassword; try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] bytes = md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }验证登录时,用库里查出来的 salt 重新算一遍哈希,和 password 字段比对。哈希结果定长 64 位十六进制字符,所以建表时 password 用 CHAR(64),salt 用 CHAR(16),字段长度和算法输出严格对应,这比统一 VARCHAR 更能体现设计意识。
注册 Servlet 里还要做两件事:一是先用 SELECT 查 username 是否已存在,存在就回注册页提示;二是两次输入密码的一致性校验前端做一遍、后端也要做一遍,因为前端校验可以被绕过。后端校验不通过直接转发回 register.jsp 并携带错误消息,不要跳转到成功页。
3.2 登录流程:BaseServlet 抽取与 Session 登录态
登录接口按 MVC 惯例放在 UserServlet 里,通过 action 参数分发:
@WebServlet("/user") public class UserServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("login".equals(action)) { doLogin(req, resp); } else if ("register".equals(action)) { doRegister(req, resp); } else { resp.sendError(404); } } }doLogin 的核心逻辑:从请求里取 username 和 password,调 UserDAO 按用户名查用户,查不到或哈希不匹配都统一提示"用户名或密码错误",不要分别提示,避免攻击者用注册接口做用户名枚举。登录成功后执行 req.getSession().setAttribute("loginUser", user),Session 默认超时是 30 分钟,可以在 web.xml 里调整:
<session-config> <session-timeout>60</session-timeout> </session-config>登录成功之后按角色分流:管理员进 /admin/index.jsp,教师进 /teacher/index.jsp,学生进 /student/index.jsp。注意要用 resp.sendRedirect 而不是 forward,否则刷新页面会重复提交登录请求。
3.3 权限分级管理:AuthFilter 按 URL 前缀拦截角色
权限分级管理在 JSP+Servlet 里的标准做法是 URL 前缀分区加 Filter 拦截。页面和 Servlet 按角色分目录,管理员访问 /admin/ 下的资源,教师只能访问 /teacher/,学生只能访问 /student/,公共登录注册页不设限制。
| 角色 | 可访问路径 | 操作范围 |
|---|---|---|
| 管理员 role=0 | 全部路径 | 用户管理、学生增删改查、成绩报表、数据可视化大屏 |
| 教师 role=1 | /teacher/、/public/ | 学生查询、成绩录入、统计报表 |
| 学生 role=2 | /student/、/public/ | 个人信息、个人成绩查询 |
@WebFilter("/*") public class AuthFilter implements Filter { private static final List<String> PUBLIC_URLS = Arrays.asList( "/login.jsp", "/register.jsp", "/user", "/css", "/js", "/images"); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI().substring(req.getContextPath().length()); if (isPublic(uri)) { chain.doFilter(request, response); return; } HttpSession session = req.getSession(false); User user = session == null ? null : (User) session.getAttribute("loginUser"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp?err=timeout"); return; } if (uri.startsWith("/admin/") && user.getRole() != 0) { resp.sendError(403); return; } if (uri.startsWith("/teacher/") && user.getRole() > 1) { resp.sendError(403); return; } chain.doFilter(request, response); } }三个要点:第一,req.getSession(false) 不会新建 Session,未登录用户拿到的 session 是 null,不会给每个请求都种 Session;第二,公开路径列表里必须有 /css 和 /js,否则登录页的样式表全被拦截,表现为"页面能打开但没样式";第三,拦截到未登录请求要 redirect 到登录页并带 err 参数,登录页根据 err 显示"会话超时"或"无权限"提示。权限部分写到这个程度,已经超出"能登录"的基本要求,答辩时可以讲清楚过滤器链的执行顺序。
4. 学生增删改查与成绩统计分析报表:动作分发与 GROUP BY 聚合
4.1 增删改查的动作分发与分页边界处理
学生信息模块是业务主体,用一个 StudentServlet 加 action 参数统一分发,比每个功能建一个 Servlet 更符合课程设计的代码量:
@WebServlet("/student/manage") public class StudentServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); switch (action) { case "list" -> listWithPage(req, resp); case "edit" -> showEditPage(req, resp); case "delete" -> delete(req, resp); default -> resp.sendError(404); } } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); switch (action) { case "insert" -> insert(req, resp); case "update" -> update(req, resp); default -> resp.sendError(404); } } }| action | 业务动作 | 请求方式 |
|---|---|---|
| list | 分页条件查询列表 | GET |
| edit | 按主键查单条去编辑页 | GET |
| delete | 按主键删除 | GET |
| insert | 新增学生 | POST |
| update | 修改学生 | POST |
分页列表是增删改查里最容易出错的环节。页面要显示三样东西:当前页数据、总条数、总页数。总条数用 COUNT(*) 单独查一次,不要用 List.size() 代替,因为 LIMIT 之后拿到的只是当前页的数据。
private void listWithPage(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page = 1; int pageSize = 10; try { page = Integer.parseInt(req.getParameter("page")); pageSize = Integer.parseInt(req.getParameter("pageSize")); } catch (NumberFormatException ignored) { } String keyword = req.getParameter("keyword"); if (keyword == null) keyword = ""; StudentDAO dao = new StudentDAO(); int total = dao.count(keyword); int totalPages = Math.max(1, (int) Math.ceil(total * 1.0 / pageSize)); page = Math.min(page, totalPages); List<Student> list = dao.findByPage(keyword, (page - 1) * pageSize, pageSize); req.setAttribute("studentList", list); req.setAttribute("page", page); req.setAttribute("totalPages", totalPages); req.setAttribute("keyword", keyword); req.getRequestDispatcher("/admin/student_list.jsp").forward(req, resp); }这里有个细节:page 不能只做解析,还要做边界收敛。用户手动把 URL 改成 ?page=99,要么返回空列表,要么 MySQL 报 LIMIT 溢出,正确做法是 page 超过总页数时收敛到总页数,小于 1 时回退到 1。类似的防御逻辑写在 Servlet 里而不是页面里,因为分页控件只是前端约束。
插入和更新的参数校验集中写在一个方法里,校验学号非空、姓名非空、学号长度不超过字段定义。学号重复时 MySQL 会抛重复键异常,要在 Servlet 里捕获 SQLIntegrityConstraintViolationException,转换成"学号已存在"的友好提示,不能把堆栈打到页面上。
4.2 成绩统计分析报表:GROUP BY 聚合与分数段行转列
成绩统计分析报表是整门课程设计的加分点,评分点在于 SQL 聚合能力。报表页需要三类数据:各课程平均分和及格率、最高最低分、分数段分布。一份 SQL 可以同时出前三项:
SELECT course_name, COUNT(*) AS total, ROUND(AVG(score), 2) AS avg_score, MAX(score) AS max_score, MIN(score) AS min_score, ROUND(SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS pass_rate FROM score GROUP BY course_name ORDER BY avg_score DESC;逐个说:AVG 是平均值,ROUND 保留两位小数防止出现 82.333333;SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) 是统计及格人数的标准写法,除以 COUNT() 再乘 100 就是及格率。COUNT() 统计的是分组内的行数,也就是选课人数,如果缺考的学生在成绩表里没有记录,及格率不会被拉低,这条要在设计文档里写明约定。
分数段分布用多段 CASE WHEN 做横向展开,也叫"行转列":
SELECT course_name, SUM(CASE WHEN score >= 90 THEN 1 ELSE 0 END) AS level_great, SUM(CASE WHEN score >= 80 AND score < 90 THEN 1 ELSE 0 END) AS level_good, SUM(CASE WHEN score >= 70 AND score < 80 THEN 1 ELSE 0 END) AS level_mid, SUM(CASE WHEN score >= 60 AND score < 70 THEN 1 ELSE 0 END) AS level_pass, SUM(CASE WHEN score < 60 THEN 1 ELSE 0 END) AS level_fail FROM score GROUP BY course_name;这种写法把分数段从行转换成列,前端图表直接拿五个字段当五个系列,不需要在 Java 里二次加工。很多同学习惯先在 Java 里把成绩全查出来再循环计数,数据量小也能跑,但 SQL 里能完成的事不要挪到应用层——"统计在哪里做的"是数据库课程设计答辩最常问的问题。如果觉得 SQL 太长,也可以把这段逻辑封装成 MySQL 存储过程,但课程设计建议保留在 DAO 层,方便现场演示和讲解。
4.3 报表数据转 JSON:ReportServlet 与 Gson 序列化
图表要的数据格式和 JSP 要的不一样,常见做法是单独写一个 ReportServlet,专门输出 JSON 给 AJAX 接口:
@WebServlet("/report/data") public class ReportDataServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String type = req.getParameter("type"); ReportDAO dao = new ReportDAO(); Object data; if ("course".equals(type)) { data = dao.courseSummary(); } else if ("level".equals(type)) { data = dao.scoreLevelDist(); } else { resp.sendError(400); return; } resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(new Gson().toJson(data)); } }resp.setContentType("application/json;charset=UTF-8") 这行不能省。不加 charset 时浏览器默认按 ISO-8859-1 解析,JSON 里的中文课程名会变成乱码,而且这种乱码在 DevTools 里看响应是正常的,只有渲染到图表上才暴露。Gson 序列化 List<Map<String, Object>> 是最省事的方案,返回结构直接对应 ECharts 的 xAxis 和 series 数据格式。
5. ECharts 数据可视化落地:报表装配、排错与答辩验证清单
5.1 ECharts 本地引入与图表装配
数据可视化展示通常指成绩统计的柱状图、饼图和折线图。ECharts 5.x 的 echarts.min.js 下载后放到 webapp/js 目录下,JSP 里用相对路径引用,避免硬编码端口:
<script src="${pageContext.request.contextPath}/js/echarts.min.js"></script> <script src="${pageContext.request.contextPath}/js/jquery.min.js"></script>页面里放一个 div 当图表容器,必须显式设置宽高,ECharts 初始化时容器宽高为 0 会渲染成空白。AJAX 请求上一章的 /report/data 接口,装配双轴图:
$.get(contextPath + "/report/data?type=course", function (data) { var chart = echarts.init(document.getElementById("courseChart")); chart.setOption({ tooltip: { trigger: 'axis' }, legend: { data: ['平均分', '及格率'] }, xAxis: { type: 'category', data: data.map(function (d) { return d.course_name; }) }, yAxis: [ { type: 'value', name: '平均分', max: 100 }, { type: 'value', name: '及格率', axisLabel: { formatter: '{value}%' } } ], series: [ { name: '平均分', type: 'bar', data: data.map(function (d) { return d.avg_score; }) }, { name: '及格率', type: 'line', yAxisIndex: 1, data: data.map(function (d) { return d.pass_rate; }) } ] }); });注意及格率在 SQL 里算出来是 0 到 100 的数值,不是 0 到 1 的小数,这里直接映射到右侧 y 轴。课程设计不需要做成实时刷新的大屏,柱状图加饼图放在报表页同一行,CSS Grid 两栏布局足够;导出图片用图表右上角自带的保存按钮即可。
5.2 三个必查排错点
第一,JSP 编译后的 class 文件位置。Tomcat 会把 JSP 翻译成 Java 再编译成 class,位置在 Tomcat 安装目录的 work/Catalina/localhost/项目名/org/apache/jsp 下。改了 JSP 但页面没变化,先看这里对应 .class 文件的时间戳,没更新说明 Tomcat 没重新编译;直接删掉 work 目录下该项目文件夹,重启 Tomcat 强制重新翻译。
第二,MySQL 连接报 The server time zone value。MySQL 8.x 默认时区和驱动要求不一致,解决方法是连接 URL 带 serverTimezone=Asia/Shanghai。如果带了还报错,检查 mysql-connector 版本,旧驱动对 JDBC 4.2 时区处理有兼容问题,升级到 mysql-connector-j 8.0.33 以上。
第三,中文乱码。乱码源头有三个:JSP 头部缺 <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>、Servlet 输出 JSON 没写 charset=UTF-8、MySQL 表或连接字符集不是 utf8mb4。按"页面、响应、数据库"三层顺序排查,大多数问题出在第一层。
5.3 答辩前的验证清单
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 未登录访问 | 直接访问 /admin/student_list.jsp | 跳转登录页 |
| 学生访问管理页 | 用 role=2 账号访问 /admin/ | 返回 403 |
| 学号重复 | 插入已存在学号 | 提示"学号已存在" |
| 成绩聚合 | 造 5 条成绩验证及格率 | 与手算结果一致 |
| SQL 注入尝试 | 搜索框输入' OR '1'='1 | 返回空结果或不报错 |
| 中文搜索 | 按姓名搜"张" | 结果正确且无乱码 |
权限边界测试值得单独强调:很多系统只在页面菜单层隐藏了入口,直接输入 URL 依然能访问。评分老师通常会点开"权限分级管理"这个功能点,验证学生账号能不能访问教师接口,所以过滤器层拦截是必须的,不能只靠前端藏按钮。答辩前用不同角色各登录一次,把表格里的场景手动过一遍,发现的问题记在纸上当场改,比对着文档空想要有效得多。
本文还有配套的精品资源,点击获取