简介:这是一套面向Java Web初学者与课程设计者的完整超市订单管理系统实战项目,基于SSM(Spring+SpringMVC+MyBatis)框架、MySQL数据库与JSP动态页面技术构建,解决中小型超市日常订单、供应商及用户统一管理的业务需求。资源包共383个文件,含74个核心依赖jar包、40个JSP页面(实现登录、订单列表、表单提交等交互)、30个Java源码(如UserController、BillController、Provider等控制器与实体类)、38个XML配置文件(Spring与MyBatis映射)、46个JS脚本及36个PNG图标资源,整体压缩后26.2MB,结构清晰、模块职责分明。已有2568人学习下载,提供可直接部署运行的完整工程(含SQL建表语句、Tomcat适配配置及基础权限控制逻辑),涵盖订单增删改查、供应商维护、角色化权限管理及异常处理机制,代码规范、注释充分,适合用于Java Web课程实践、毕业设计参考或SSM框架入门项目复现。
1. 这不是又一个“Hello World”Demo:一个真实可跑的超市订单系统,它用SSM把Java Web开发的骨架全露出来了
你可能已经看过几十个“SSM整合教程”,但它们大多卡在spring-context.xml配完就戛然而止——没登录页、没真实SQL执行日志、没JSP里EL表达式怎么和MyBatis返回的List联动、更没人告诉你BillController.class重复写了两遍意味着什么。这个超市订单管理系统不是教学幻灯片,而是一个能直接部署到Tomcat 8.5+、连上MySQL 5.7+、打开浏览器就能增删查改供应商和订单的完整工程。它不炫技,不用Redis缓存、不接前端框架、不搞微服务拆分,但每一行代码都在回答一个实际问题:当用户点击“提交订单”按钮,SpringMVC如何把表单参数转成Bill对象?MyBatis怎么把<select>语句里的#{providerId}映射到Provider实体的id字段?JSP页面里${billList[0].productName}为什么能取到值,而${bill.productName}却报NullPointerException?如果你正卡在SSM三层架构的“胶水层”——即Controller怎么把Service结果塞进Model再交给View渲染——那这个项目就是为你拆解的。它适合两类人:刚学完Spring IoC但写不出完整CRUD的Java初学者,以及需要快速验证SSM老项目迁移可行性的运维/测试工程师。
2. SSM三层结构不是概念图,是四张必须对齐的配置表和三类必须重写的类
SSM框架的落地难点从来不在“会不会写注解”,而在配置文件、Mapper接口、Controller方法、JSP页面这四者之间的数据流向是否严格对齐。本系统用最朴素的方式暴露了这种对齐关系:所有Controller类名都带.class后缀(如UserController.class),这不是笔误,而是提醒你——这些类必须被Spring容器扫描到并实例化;所有实体类(User.class,Provider.class,Bill.class)的字段命名必须与MySQL表字段完全一致,否则MyBatis的resultMap自动映射会静默失败;所有JSP页面路径必须匹配@RequestMapping的value值,否则404错误不会告诉你缺了哪个斜杠。
2.1 Spring配置:applicationContext.xml里藏着事务控制的开关逻辑
Spring的核心作用是管理Bean生命周期和事务。本系统的applicationContext.xml中,关键配置集中在<tx:annotation-driven/>和<bean id="transactionManager">两处:
<!-- 数据源配置 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/supermarket?useSSL=false&serverTimezone=UTC&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <!-- MyBatis SqlSessionFactory配置 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <!-- 事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>注意:
url中的serverTimezone=UTC是MySQL 8.0+必需参数,漏掉会导致java.sql.SQLException: The server time zone value '...' is unrecognized;characterEncoding=utf8确保中文不乱码;mapperLocations指向src/main/resources/mapper/下的XML文件,路径错一位就会报Invalid bound statement (not found)。
事务生效的前提是:Service层方法必须加@Transactional注解,且该方法不能是private或final——因为Spring AOP代理机制依赖JDK动态代理或CGLIB,private方法无法被拦截。例如BillServiceImpl.java中:
@Service("billService") public class BillServiceImpl implements BillService { @Autowired private BillMapper billMapper; @Transactional // ← 此注解必须存在,且方法为public @Override public boolean addBill(Bill bill) { int rows = billMapper.insertBill(bill); if (rows > 0) { // 模拟业务逻辑:更新供应商库存 Provider provider = new Provider(); provider.setId(bill.getProviderId()); provider.setBalance(provider.getBalance() - bill.getTotalPrice()); // 实际需查库再更新 return providerMapper.updateProvider(provider) > 0; } return false; } }如果addBill()方法声明为private boolean addBill(...),即使加了@Transactional,事务也不会生效——Spring根本无法代理它。
2.2 MyBatis映射:XML里的<resultMap>比@Select注解更能暴露字段映射陷阱
本系统坚持使用XML方式定义SQL(src/main/resources/mapper/BillMapper.xml),因为它能清晰暴露字段映射的断点。比如Bill实体有providerId字段,而数据库表bill中对应列为providerid(小写),若不显式配置<resultMap>,MyBatis默认按驼峰规则转换(providerId→provider_id),但实际列名是providerid,就会导致providerId始终为null:
<!-- mapper/BillMapper.xml --> <mapper namespace="com.supermarket.dao.BillMapper"> <resultMap id="BillResultMap" type="com.supermarket.pojo.Bill"> <id property="id" column="id"/> <result property="billCode" column="billCode"/> <result property="productName" column="productName"/> <result property="providerId" column="providerid"/> <!-- ← 关键:column名必须与数据库列名完全一致 --> <result property="totalPrice" column="totalPrice"/> <result property="createdBy" column="createdBy"/> <result property="creationDate" column="creationDate"/> </resultMap> <select id="getBillList" resultMap="BillResultMap"> SELECT * FROM bill <where> <if test="productName != null and productName != ''"> AND productName LIKE CONCAT('%', #{productName}, '%') </if> <if test="providerId != null and providerId != 0"> AND providerid = #{providerId} </if> </where> ORDER BY creationDate DESC </select> </mapper><if>标签内的#{providerId}是预编译占位符,防止SQL注入;而column="providerid"必须与MySQL中DESC bill显示的实际列名(区分大小写)完全一致。若数据库列名为PROVIDERID(全大写),此处也必须写PROVIDERID,否则MyBatis无法将查询结果赋值给Bill.providerId。
2.3 Controller与JSP的数据桥接:Model.addAttribute()不是摆设,是ViewResolver找页面的唯一线索
SpringMVC的DispatcherServlet通过InternalResourceViewResolver将逻辑视图名解析为物理路径。BillController.class中:
@Controller @RequestMapping("/bill") public class BillController { @Autowired private BillService billService; @RequestMapping(value = "/list", method = RequestMethod.GET) public String getBillList(Model model, @RequestParam(value = "productName", required = false) String productName, @RequestParam(value = "providerId", required = false, defaultValue = "0") Integer providerId) { List<Bill> billList = billService.getBillList(productName, providerId); model.addAttribute("billList", billList); // ← 关键:key必须与JSP中${billList}一致 model.addAttribute("providerList", billService.getProviderList()); // 供下拉框选择 return "billlist"; // ← 返回逻辑视图名,ViewResolver自动拼成/WEB-INF/jsp/billlist.jsp } }return "billlist"触发InternalResourceViewResolver的解析规则:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean>最终定位到/WEB-INF/jsp/billlist.jsp。而JSP中${billList}能取到值,前提是model.addAttribute("billList", ...)的key字符串与EL表达式完全一致——大小写、下划线都不能错。若Controller写model.addAttribute("bill_list", ...),JSP写${billList},结果必为空列表。
2.4 JSP页面的EL与JSTL:${}不是万能的,它依赖PageContext的Scope链
billlist.jsp中展示订单列表:
<c:forEach items="${billList}" var="bill" varStatus="status"> <tr class="odd gradeX"> <td>${status.count}</td> <td>${bill.billCode}</td> <td>${bill.productName}</td> <td><fmt:formatNumber value="${bill.totalPrice}" pattern="#,##0.00"/></td> <td> <c:forEach items="${providerList}" var="p"> <c:if test="${p.id == bill.providerId}"> ${p.proName} </c:if> </c:forEach> </td> <td><fmt:formatDate value="${bill.creationDate}" pattern="yyyy-MM-dd HH:mm:ss"/></td> <td> <a href="${pageContext.request.contextPath}/bill/view/${bill.id}">查看</a> <a href="${pageContext.request.contextPath}/bill/delete/${bill.id}">删除</a> </td> </tr> </c:forEach>这里的关键细节:
<c:forEach>的items属性必须是List类型,若billList是null,JSTL会抛出javax.servlet.jsp.JspTagException: Don't know how to iterate over supplied items;${bill.providerId}取值成功,说明Bill实体的getProviderId()方法返回非null值,且providerId字段在数据库中不为NULL;<fmt:formatNumber>和<fmt:formatDate>需要在JSP顶部声明<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>,否则会报According to TLD or attribute directive in tag file, attribute value does not accept any expressions;${pageContext.request.contextPath}获取应用上下文路径(如/supermarket),避免硬编码/bill/view/...导致部署到子路径时404。
3. MySQL建表与索引:没有索引的WHERE条件,就是生产环境的定时炸弹
本系统数据库设计直指超市业务核心:bill(订单)、provider(供应商)、user(用户)三张主表,外加role(角色)表实现权限控制。建表语句不是照搬教科书,而是针对高频查询场景做了索引优化。
3.1 核心表结构与字符集强制统一
-- 创建数据库,显式指定字符集,避免乱码 CREATE DATABASE supermarket CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE supermarket; -- 用户表 CREATE TABLE `user` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `userCode` VARCHAR(15) NOT NULL COMMENT '用户编码', `userName` VARCHAR(15) NOT NULL COMMENT '用户名', `userPassword` VARCHAR(32) NOT NULL COMMENT '密码(MD5加密)', `gender` INT(10) DEFAULT NULL COMMENT '性别', `birthday` DATE DEFAULT NULL COMMENT '出生日期', `phone` VARCHAR(15) DEFAULT NULL COMMENT '电话', `address` VARCHAR(30) DEFAULT NULL COMMENT '地址', `userRole` INT(10) NOT NULL COMMENT '用户角色', `createdBy` INT(10) NOT NULL COMMENT '创建者', `creationDate` DATETIME NOT NULL COMMENT '创建时间', `modifyBy` INT(10) DEFAULT NULL COMMENT '更新者', `modifyDate` DATETIME DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_userCode` (`userCode`) USING BTREE, -- 用于登录查询 KEY `idx_userRole` (`userRole`) USING BTREE -- 用于角色权限过滤 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表 CREATE TABLE `bill` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `billCode` VARCHAR(20) NOT NULL COMMENT '订单编码', `productName` VARCHAR(20) NOT NULL COMMENT '商品名称', `productDesc` VARCHAR(200) DEFAULT NULL COMMENT '商品描述', `productUnit` VARCHAR(10) DEFAULT NULL COMMENT '商品单位', `productCount` DECIMAL(10,2) DEFAULT NULL COMMENT '商品数量', `totalPrice` DECIMAL(10,2) NOT NULL COMMENT '总金额', `isPayment` INT(10) DEFAULT '0' COMMENT '是否支付:0未支付,1已支付', `providerId` INT(11) NOT NULL COMMENT '供应商ID', `createdBy` INT(11) NOT NULL COMMENT '创建者', `creationDate` DATETIME NOT NULL COMMENT '创建时间', `modifyBy` INT(11) DEFAULT NULL COMMENT '更新者', `modifyDate` DATETIME DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_billCode` (`billCode`) USING BTREE, -- 精确查询订单号 KEY `idx_productName` (`productName`) USING BTREE, -- 模糊搜索商品名 KEY `idx_providerId` (`providerId`) USING BTREE, -- 关联查询供应商 KEY `idx_creationDate` (`creationDate`) USING BTREE -- 按时间排序/范围查询 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 供应商表 CREATE TABLE `provider` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `proCode` VARCHAR(20) NOT NULL COMMENT '供应商编码', `proName` VARCHAR(20) NOT NULL COMMENT '供应商名称', `proDesc` VARCHAR(200) DEFAULT NULL COMMENT '供应商描述', `proContact` VARCHAR(20) DEFAULT NULL COMMENT '联系人', `proPhone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话', `proAddress` VARCHAR(50) DEFAULT NULL COMMENT '地址', `proFax` VARCHAR(20) DEFAULT NULL COMMENT '传真', `createdBy` INT(11) NOT NULL COMMENT '创建者', `creationDate` DATETIME NOT NULL COMMENT '创建时间', `modifyBy` INT(11) DEFAULT NULL COMMENT '更新者', `modifyDate` DATETIME DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_proCode` (`proCode`) USING BTREE, -- 精确查询供应商编码 KEY `idx_proName` (`proName`) USING BTREE -- 模糊搜索供应商名称 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;提示:
utf8mb4是MySQL 5.5.3+推荐字符集,支持Emoji和四字节UTF-8字符;USING BTREE显式声明B+树索引,避免旧版本MySQL默认用哈希索引(不支持范围查询)。
3.2 索引失效的五个真实场景及修复方案
即使建了索引,查询仍可能全表扫描。以下是本系统SQL中必须规避的坑:
| 场景 | 错误示例 | 修复方案 | 原因 |
|---|---|---|---|
| LIKE前导% | WHERE productName LIKE '%手机%' | 改为WHERE productName LIKE '手机%'或用全文索引 | B+树索引无法从中间匹配 |
| 隐式类型转换 | WHERE providerId = '1'(providerId是INT) | WHERE providerId = 1 | 字符串转数字导致索引失效 |
| 函数操作字段 | WHERE DATE(creationDate) = '2024-01-01' | WHERE creationDate >= '2024-01-01' AND creationDate < '2024-01-02' | 函数使索引失效 |
| OR连接无索引字段 | WHERE providerId = 1 OR phone = '138...'(phone无索引) | 为phone字段加索引,或拆成UNION查询 | OR条件中任一字段无索引,整条WHERE失效 |
| 统计行数用COUNT(*) | SELECT COUNT(*) FROM bill WHERE productName LIKE '苹果%' | 添加复合索引KEY idx_productName_creationDate (productName, creationDate) | 单列索引无法覆盖排序需求 |
验证索引是否生效:在MySQL命令行执行EXPLAIN SELECT * FROM bill WHERE productName LIKE '苹果%';,观察key列是否显示索引名,rows是否远小于table总行数。
3.3 外键约束的取舍:为什么本系统用代码校验代替FOREIGN KEY
bill.providerId理论上应设为外键引用provider.id,但本系统选择在Service层做校验:
@Override public boolean addBill(Bill bill) { // 1. 校验供应商是否存在 Provider provider = providerMapper.getProviderById(bill.getProviderId()); if (provider == null) { throw new RuntimeException("供应商ID不存在:" + bill.getProviderId()); } // 2. 执行插入 return billMapper.insertBill(bill) > 0; }原因有三:
- 性能:外键约束在INSERT/UPDATE时触发额外查询,高并发下单条SQL延迟增加;
- 灵活性:供应商可能被软删除(
isDeleted=1),但外键约束只认物理存在; - 部署简化:某些云MySQL服务(如阿里云RDS)默认禁用外键,避免上线报错。
代价是开发者必须手动维护数据一致性——每次新增订单前,必须调用providerMapper.getProviderById()确认供应商有效。
4. Tomcat部署与常见启动失败诊断:从war包到404,每一步都有明确日志锚点
将项目打包为WAR部署到Tomcat,不是复制粘贴就完事。本系统在pom.xml中明确指定了Servlet版本和插件配置,确保生成的WAR符合Tomcat 8.5+规范。
4.1 Maven构建:pom.xml里的三个致命配置点
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>5.2.22.RELEASE</spring.version> <mybatis.version>3.4.6</mybatis.version> </properties> <dependencies> <!-- Servlet API 必须 provided,否则Tomcat会冲突 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> <!-- ← 关键:避免打包进WAR --> </dependency> <!-- JSP相关依赖 --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MyBatis-Spring整合包 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> <!-- ← Tomcat 7+支持无web.xml --> <warName>supermarket</warName> <!-- 生成supermarket.war --> </configuration> </plugin> </plugins> </build>注意:
<scope>provided</scope>表示Servlet API由Tomcat提供,Maven打包时不包含,否则会与Tomcat自带的servlet-api.jar冲突,启动时报java.lang.NoSuchMethodError: javax.servlet.http.HttpServletRequest.isAsyncStarted()。
4.2 Tomcat启动日志逐行解读:定位404/500/ClassNotFoundException的黄金路径
将supermarket.war放入$TOMCAT_HOME/webapps/后,启动Tomcat,关键日志位置:
catalina.out:主日志流,记录启动全过程;localhost.<date>.log:记录当前应用的Servlet初始化日志;manager.<date>.log:记录WAR部署状态。
典型失败场景与日志特征:
| 现象 | catalina.out关键日志 | 原因 | 解决方案 |
|---|---|---|---|
| 404所有页面 | INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [...] supermarket.warINFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [...] supermarket.war has finished in [X] ms | WAR部署成功,但web.xml缺失或DispatcherServlet未注册 | 检查src/main/webapp/WEB-INF/web.xml是否存在,或确认@WebServlet注解是否正确 |
| 500空指针 | SEVERE [http-nio-8080-exec-1] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [springmvc] in context with path [/supermarket] threw exceptionjava.lang.NullPointerException at com.supermarket.controller.BillController.getBillList(BillController.java:32) | Controller中Service注入失败 | 检查applicationContext.xml中<context:component-scan base-package="com.supermarket.service"/>是否覆盖Service包路径 |
| ClassNotFoundException | SEVERE [main] org.apache.catalina.startup.HostConfig.deployWAR Error waiting for multi-thread deployment of WAR filesjava.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver | MySQL驱动jar未放入$TOMCAT_HOME/lib/ | 将mysql-connector-java-8.0.28.jar复制到Tomcat的lib目录,而非仅放在项目WEB-INF/lib |
验证MySQL连接:在applicationContext.xml的<bean id="dataSource">配置后,添加以下代码测试连接:
<!-- 测试数据源 --> <bean id="testDataSource" class="org.springframework.jdbc.datasource.init.DataSourceInitializer"> <property name="dataSource" ref="dataSource"/> <property name="databasePopulator"> <bean class="org.springframework.jdbc.datasource.init.ResourceDatabasePopulator"> <property name="sqlScriptEncoding" value="UTF-8"/> <property name="scripts"> <list> <value>classpath:sql/init.sql</value> </list> </property> </bean> </property> </bean>并在src/main/resources/sql/init.sql中写SELECT 1;,启动时若报错,则证明数据库连接失败。
4.3 JSP编译路径与class文件定位:修改JSP后无需重启Tomcat的实操技巧
Tomcat运行时会将JSP编译为Servlet类,存放于$TOMCAT_HOME/work/Catalina/localhost/supermarket/org/apache/jsp/。例如billlist.jsp编译后生成billlist_jsp.java和billlist_jsp.class。
提示:修改JSP后,Tomcat默认会自动重新编译(需
<init-param><param-name>development</param-name><param-value>true</param-value></init-param>在web.xml中配置)。若发现修改不生效,可手动删除work目录下对应JSP的class文件,再刷新页面触发重编译。
验证JSP编译结果:访问http://localhost:8080/supermarket/bill/list后,检查work目录下是否有billlist_jsp.class,若有,说明JSP解析成功;若无,检查web.xml中<servlet-mapping>是否将*.jsp映射到JspServlet。
5. 权限控制与密码安全:RBAC模型在SSM中的最小可行实现
本系统权限控制不依赖Shiro或Spring Security,而是用最简RBAC(基于角色的访问控制)模型:user.userRole字段关联role.id,Controller中通过@SessionAttribute("userRole")获取当前角色,再用if-else判断操作权限。这种方式牺牲了扩展性,但胜在逻辑透明、调试直观。
5.1 角色表设计与登录态绑定
CREATE TABLE `role` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `roleName` VARCHAR(15) NOT NULL COMMENT '角色名称', `roleDesc` VARCHAR(30) DEFAULT NULL COMMENT '角色描述', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 初始化角色 INSERT INTO `role` VALUES (1,'管理员','拥有全部权限'),(2,'普通用户','仅能查看自己的订单');登录成功后,将用户角色存入Session:
// UserController.java @RequestMapping(value = "/login", method = RequestMethod.POST) public String login(@RequestParam String userCode, @RequestParam String userPassword, Model model, HttpSession session) { User user = userService.login(userCode, userPassword); if (user != null) { session.setAttribute("user", user); session.setAttribute("userRole", user.getUserRole()); // ← 关键:存入角色ID return "redirect:/bill/list"; } else { model.addAttribute("error", "用户名或密码错误"); return "login"; } }5.2 Controller层权限拦截:用@ModelAttribute预加载角色信息
在BaseController中定义通用方法,避免每个Controller重复写权限判断:
@Controller public class BaseController { @ModelAttribute public void addUserRole(Model model, HttpSession session) { User user = (User) session.getAttribute("user"); if (user != null) { model.addAttribute("userRole", user.getUserRole()); } } } @Controller @RequestMapping("/bill") public class BillController extends BaseController { // 继承BaseController @RequestMapping(value = "/list", method = RequestMethod.GET) public String getBillList(Model model, ...) { Integer userRole = (Integer) model.asMap().get("userRole"); if (userRole == null || userRole != 1) { // 只有管理员能看全部订单 // 普通用户只能看自己创建的订单 List<Bill> billList = billService.getBillListByCreatedBy(user.getId(), productName, providerId); model.addAttribute("billList", billList); } else { List<Bill> billList = billService.getBillList(productName, providerId); model.addAttribute("billList", billList); } return "billlist"; } }5.3 密码存储:MD5加盐不是银弹,但比明文强一万倍
用户密码不直接存明文,而是用MD5加固定盐值(salt)哈希:
// UserServiceImpl.java @Override public boolean saveUser(User user) { String salt = "supermarket_2024"; // 固定盐值,实际项目应为随机盐 String md5Password = DigestUtils.md5Hex(user.getUserPassword() + salt); user.setUserPassword(md5Password); return userDao.add(user) > 0; } @Override public User login(String userCode, String userPassword) { String salt = "supermarket_2024"; String md5Password = DigestUtils.md5Hex(userPassword + salt); return userDao.getLoginUser(userCode, md5Password); }注意:
DigestUtils.md5Hex()来自commons-codec库,需在pom.xml中添加:
<dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.15</version> </dependency>虽然MD5已被认为不安全(存在碰撞攻击),但在本教学系统中,它比明文存储更符合安全常识——真正的生产环境应使用BCryptPasswordEncoder或PBKDF2WithHmacSHA256。
5.4 JSP页面的权限裁剪:用JSTL<c:if>实现前端可见性控制
在billlist.jsp中,根据角色动态显示操作按钮:
<c:if test="${userRole == 1}"> <a href="${pageContext.request.contextPath}/bill/add">新增订单</a> </c:if> <td> <c:if test="${userRole == 1}"> <a href="${pageContext.request.contextPath}/bill/delete/${bill.id}">删除</a> </c:if> <a href="${pageContext.request.contextPath}/bill/view/${bill.id}">查看</a> </td>这种裁剪只是UI层隐藏,真正的权限控制必须在Controller或Service层完成——因为用户可以绕过JSP直接发DELETE请求。所以BillController.deleteBill()方法内必须再次校验:
@RequestMapping(value = "/delete/{id}", method = RequestMethod.GET) public String deleteBill(@PathVariable Integer id, Model model, HttpSession session) { Integer userRole = (Integer) session.getAttribute("userRole"); if (userRole != 1) { model.addAttribute("error", "权限不足"); return "error"; } billService.deleteBill(id); return "redirect:/bill/list"; }这才是RBAC的完整闭环:Session存角色 → Controller读角色 → Service执行校验 → JSP按角色渲染。
本文还有配套的精品资源,点击获取