目录
一. 前言
二. 主要技术栈
三.项目搭建
3.1 数据库搭建
3.2 maven项目搭建
四、实现基本的CRUD操作
五、SpringAOP实现记录日志
六、SpringMVC拦截器interceptor拦截用户的权限
七、Bcrypt加密算法
八、总结
本项目的客户端是基于开源项目AdminLTE 和AdminLTE 黑马定制版进行编写。AdminLTE 是一个完全的响应式管理模板,基于Bootstrap3框架,高度可定制,易于使用。适合从小型移动设备到大型台式机的屏幕分辨率 。
本项目可实现用户登录与头像的上传,根据用户的角色与权限不同,操作不同的模块进行基本的CRUD操作。实现对用户的请求进行日志记录,对用户的权限进行监控与拦截,用户在没有登录的情况下,不能对后台进行访问操作,当点击菜单就会跳转到登录页面。只有用户登录成功,才能进行后台功能的操作。对用户的密码进行加密与解密,整合redis实现mybatis的二级缓存。
开发工具:jdk8,idea2022,navicat,redis,mysql8,maven仓库,firefox/chorm
打开navicat,创建名为ssm的数据库,导入准备好的sql脚本文件,数据库模型图如下所示,共11张表。用户表通过中间表与角色表相连,角色表通过中间表与权限表相连;订单表通过外键与订单负责人相连、通过中间表与旅客表进行相连。syslog为日志记录表,product为产品表。
打开idea新建maven工程,在pom文件中添加对应的依赖,具体的依赖如下所示。
4.0.0
com.jbz
ssm
1.0-SNAPSHOT
ssm
war
UTF-8
1.8
1.8
5.0.5.RELEASE
junit
junit
4.13.2
test
javax.servlet
javax.servlet-api
3.1.0
provided
javax.servlet.jsp
javax.servlet.jsp-api
2.2.1
provided
javax.servlet
jstl
1.1.2
taglibs
standard
1.1.2
mysql
mysql-connector-java
8.0.28
compile
com.alibaba
druid
1.2.15
org.springframework
spring-context
5.0.5.RELEASE
org.springframework
spring-core
5.0.5.RELEASE
org.springframework
spring-tx
5.0.5.RELEASE
compile
org.springframework
spring-beans
5.0.5.RELEASE
compile
org.springframework
spring-jdbc
5.0.5.RELEASE
compile
org.springframework
spring-test
5.0.5.RELEASE
org.aspectj
aspectjweaver
1.9.4
org.springframework
spring-webmvc
5.0.5.RELEASE
org.mybatis
mybatis
3.5.6
com.baomidou
mybatis-plus
3.4.3.4
org.mybatis
mybatis-spring
1.3.2
com.google.code.gson
gson
2.4
com.fasterxml.jackson.core
jackson-core
2.9.0
com.fasterxml.jackson.core
jackson-databind
2.9.0
com.fasterxml.jackson.core
jackson-annotations
2.9.0
com.github.pagehelper
pagehelper
5.1.2
commons-fileupload
commons-fileupload
1.2.2
commons-io
commons-io
2.4
org.springframework.data
spring-data-redis
1.4.0.RELEASE
redis.clients
jedis
2.4.2
org.springframework.security
spring-security-core
5.0.5.RELEASE
org.apache.tomcat.maven
tomcat7-maven-plugin
2.1
80
/
UTF-8
src/main/java
**/*.xml
false
src/main/resources
**/*.xml
*.xml
*.properties
添加完记得刷新一下,接下来在resource文件下添加jdbc.properties文件
添加SqlMapperConfig文件
添加spring-mvc.xml
添加applicationContext.xml
添加redis.properties
添加application-redis.xml
redis整合所需要的工具类
package com.jbz.utils;
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import redis.clients.jedis.exceptions.JedisConnectionException;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author: jbz
* @date: 2023/1/10
* @description:
* @version: 1.0
*/
public class RedisCache implements Cache {
private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);
private static JedisConnectionFactory jedisConnectionFactory;
private final String id;
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public RedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("require an ID");
}
logger.debug("RedisCache:id=" + id);
this.id = id;
}
@Override
public void clear() {
RedisConnection connection = null;
try {
connection = jedisConnectionFactory.getConnection();
connection.flushDb();
connection.flushAll();
} catch (JedisConnectionException e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.close();
}
}
}
public String getId() {
return this.id;
}
public Object getObject(Object key) {
Object result = null;
RedisConnection connection = null;
try {
connection = jedisConnectionFactory.getConnection();
RedisSerializer
package com.jbz.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
/**
* @author: jbz
* @date: 2023/1/10
* @description:
* @version: 1.0
*/
public class RedisCacheTransfer {
@Autowired
public void setJedisConnectionFactory(JedisConnectionFactory jedisConnectionFactory) {
RedisCache.setJedisConnectionFactory(jedisConnectionFactory);
}
}
在web.xml文件中配置过滤器、前端控制器、监听器
/pages/login.jsp
CharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
CharacterEncodingFilter
/*
DispatcherServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:*.xml
1
DispatcherServlet
/
default
*.js
*.css
/img/*
/plugins/*
org.springframework.web.context.request.RequestContextListener
接下来将准备好的静态资源放在webapp下,至此项目搭建完毕 。
首先在main文件下新建如图所示的包:
首先在domain包下新建Product实体类,实体类的字段要与数据库product表中的字段名、类型要一致。注意:由于我们要用到redis,因此实体类必须要实现序列化接口。
package com.jbz.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* @author: jbz
* @date: 2022/12/22
* @description:
* @version: 1.0
*/
public class Product implements Serializable {
//编号
private Integer id;
//产品编号
private String productNum;
//产品名称
private String productName;
//出发城市名称
private String cityName;
//出发时间
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date departureTime;
private String departureTimeStr;
//产品价格
private Double productPrice;
//产品详情描述
private String productDesc;
//产品状态 0:下架 1:上架
private Integer productStatus;
private String productStatusStr;
public Product() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getProductNum() {
return productNum;
}
public void setProductNum(String productNum) {
this.productNum = productNum;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getCityName() {
return cityName;
}
public void setCityName(String cityName) {
this.cityName = cityName;
}
public Date getDepartureTime() {
return departureTime;
}
public void setDepartureTime(Date departureTime) {
this.departureTime = departureTime;
}
public String getDepartureTimeStr() {
return departureTimeStr;
}
public void setDepartureTimeStr(String departureTimeStr) {
this.departureTimeStr = departureTimeStr;
}
public Double getProductPrice() {
return productPrice;
}
public void setProductPrice(Double productPrice) {
this.productPrice = productPrice;
}
public String getProductDesc() {
return productDesc;
}
public void setProductDesc(String productDesc) {
this.productDesc = productDesc;
}
public Integer getProductStatus() {
return productStatus;
}
public void setProductStatus(Integer productStatus) {
this.productStatus = productStatus;
}
public String getProductStatusStr() {
if (productStatus == 0) {
productStatusStr = "下架";
}else if(productStatus == 1){
productStatusStr = "上架";
}
return productStatusStr;
}
public void setProductStatusStr(String productStatusStr) {
this.productStatusStr = productStatusStr;
}
@Override
public String toString() {
return "Product{" +
"id='" + id + '\'' +
", productNum='" + productNum + '\'' +
", productName='" + productName + '\'' +
", cityName='" + cityName + '\'' +
", departureTime=" + departureTime +
", departureTimeStr='" + departureTimeStr + '\'' +
", productPrice=" + productPrice +
", productDesc='" + productDesc + '\'' +
", productStatus=" + productStatus +
", productStatusStr='" + productStatusStr + '\'' +
'}';
}
}
其次我们在数据持久层mapper包下新建ProductMapper接口,添加@Repoistory注解,用来操作数据库,封装一个方法用来条件查询所有的产品,在resources文件夹下新建mapper文件夹,新建ProductMapper.xml,文件名要与接口名一致。在namespace里通过全限定类名进行映射。select标签的id要与方法名一致,返回类型与方法名返回类型一致
接着我们在service层新建对应的接口与实现类,在实现类中添加@Service注解,通过依赖注入,将mapper层注入到ioc容器,通过mybatis分页插件进行分页,调用方法返回PageInfo对象。
在controller层新建ProductController,添加@Controller注解,和请求的一级路径@RequestMapping("/product"),将对应的service接口依赖注入进来,创建方法并添加二级路径@RequestMapping("/queryProductList"),具体代码如下:
@RequestMapping("/queryProductList")
public String queryProductList(@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "4") int pageSize,
@RequestParam(value = "productName", required = false) String productName, Model model) {
//调用service的查询产品列表的方法
PageInfo pageInfo = productService.queryAllProduct(pageNum, pageSize, productName);
//设置数据 保存容器中
model.addAttribute("pageInfo", pageInfo);
//返回给视图
return "product-list";
}
方法的参数为前端页面进行分页和查询所需要的请求参数 ,将查询出的PageInfo对象添加到Model中,并返回给视图(这里的视图的前缀和后缀已经在配置文件中进行配置过了,实际的路径应该为/pages/product-list.jsp)。
接下来我们在静态页面product-list.jsp页面中,通过jstl表达式用foreach标签进行遍历出来,在对应的侧边栏产品管理上添加跳转url路径:
${product.id}
${product.productNum}
${product.productName}
${product.cityName}
${product.productPrice}
${product.productDesc}
${product.productStatusStr}
最后运行项目,在点击控制台的链接,点击侧边栏的产品管理至如下图所示:
增删改的操作与上面类似,这里就不展开说了。
我们在AOP包下,新建LogAspect切面类,添加注解@Aspect声明当前类是一个切面类,添加@Pointcut注解配置通用切入点表达式。我们选择用环绕通知来进行日志的记录。环绕通知的特点是目标执行前后,都进行增强(控制目标方法执行),它的应用场景:日志、缓存、权限、性能监控、事务管理。具体代码如下所示。接着我们需要在applciationContext.xml文件中添加
package com.jbz.aop;
import com.jbz.constant.MessageConstant;
import com.jbz.domain.User;
import com.jbz.utils.RecordingLogUtils;
import com.jbz.utils.SetDataUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
/**
* @author: jbz
* @date: 2023/1/7
* @description: 记录日志的切面类(对用户的请求进行日志监控与记录)
* @version: 1.0
*/
@Component
@Aspect
public class LogAspect {
//依赖注入
@Autowired
private RecordingLogUtils recordingLogUtils;
@Autowired
private SetDataUtils setDataUtils;
//定义方法执行开始时间
private long startTime;
//定义请求路径url
private StringBuilder url;
//获取请求方法名
private String methodStr;
//定义ServletRequestAttributes对象
private static ServletRequestAttributes requestAttributes;
//通用切入点表达式
@Pointcut("execution (* com.jbz.controller.*.*(..))")
private void pt1() {
}
//环绕通知
@Around("pt1()")
public Object aroundLog(ProceedingJoinPoint proceedingJoinPoint) {
requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
//调用初始化方法
init(proceedingJoinPoint);
//设置返回值
Object obj = null;
//获取方法执行所需的参数
Object[] objs = proceedingJoinPoint.getArgs();
//获取当前用户的username
String userName = getUserName(objs[0].toString());
try {
//执行切入点的方法
obj = proceedingJoinPoint.proceed(objs);
} catch (Throwable e) {
setDataUtils.setExceptionCount(requestAttributes.getRequest().getSession());
recordingLogUtils.insertSysLog(userName, MessageConstant.EXECUTE_METHOD_EXCEPTION, url.toString(), 0, methodStr);
e.printStackTrace();
} finally {
//定义方法执行结束时间
long endTime = System.currentTimeMillis();
recordingLogUtils.insertSysLog(userName, MessageConstant.EXECUTE_METHOD_SUCCESS, url.toString(), (int) (endTime - startTime), methodStr);
}
return obj;
}
/**
* @author: jbz
* @description: 获得当前用户名
* @date: 2023/1/7 19:00
* @param: username
* @return: String
*/
public String getUserName(String username) {
//获取Session对象
HttpSession session = requestAttributes.getRequest().getSession();
//获取当前用户
User user = (User) session.getAttribute("user");
//判断当前用户是否为null,为null说明未登录
if (user == null) {
//在未登录情况下,用户进行操作会调用拦截器进行登录,此时将参数username返回
return username;
} else {
//不为null,说明当前用户处于登录状态,就将用户的username返回
return user.getUsername();
}
}
/**
* @author: jbz
* @description: 初始化参数
* @date: 2023/1/7 19:39
* @param: proceedingJoinPoint
* @return: void
*/
public void init(ProceedingJoinPoint proceedingJoinPoint) {
//获取HttpSession对象
HttpSession session = requestAttributes.getRequest().getSession();
setDataUtils.setVisitCount(session);
//给方法开始时间进行赋值
startTime = System.currentTimeMillis();
url = new StringBuilder();
//获取执行方法的类
Object target = proceedingJoinPoint.getTarget();
RequestMapping[] classRequest = target.getClass().getAnnotationsByType(RequestMapping.class);
//获取请求方法的一级路径
url.append(classRequest[0].value()[0]);
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
//获取请求方法
Method method = signature.getMethod();
//获取请求方法名
methodStr = "[类名]" + target.getClass().getName() + "[方法名]" + method.getName();
//获取请求方法的二级路径
RequestMapping[] methodRequest = method.getAnnotationsByType(RequestMapping.class);
url.append(methodRequest[0].value()[0]);
if(url.toString().equals("orders/add")){
setDataUtils.setOrderCount(session);
}
}
}
SpringMVC的拦截器类似于Servlet开发中的过滤器Filter,用于处理器进行预处理和后处理。将拦截器按一定的顺序联结成一条链,这条链称为拦截器链。在访问被拦截的方法或字段时,拦截器链的拦截器就会按其之前定义的顺序被调用。拦截器也是AOP思想的具体实现。拦截器和过滤器的区别如图所示:
拦截器的步骤:
首先我们先封装一个权限处理的工具类PermissionHandleUtils.java,该工具类用于判断请求的路径是否包含在用户所具有的权限中,并返回boolean值,随后我们创建PermissionInterceptor.java实现HanlerInterceptor接口,重写preHandle(),preHandle方法是在目标方法之前执行,是预处理。随后我们在spring-mvc文件中对拦截器进行配置,配置如下图所示。在拦截器中我们首先对用户的登录进行权限控制,用户在没有登录的情况下,不能对后台菜单进行访问操作,当用户点击菜单时,跳转至登录页面,只有在用户登录成功安置后才能进行后台功能的操作。当用户在登录状态时,通过权限处理的工具类对当前请求进行权限判断,只有满足要求,才能放行,否则就跳转至错误页面。在权限拦截的同时,还要添加日志到数据库中。
PermissionHandleUtils.java
package com.jbz.utils;
import com.jbz.domain.User;
import com.jbz.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
/**
* @author: jbz
* @date: 2023/1/9
* @description:
* @version: 1.0
*/
@Component
public class PermissionHandleUtils {
//依赖注入
@Autowired
private IUserService userService;
public Boolean judgePermissions(HttpServletRequest request,User user) {
//获取用户的具有权限请求路径
String[] permissionUrl = userService.queryUserPermissionsById(user.getId());
//获得当前请求路径
String requestURL = request.getRequestURI();
//进行判断
if (permissionUrl != null && permissionUrl.length != 0) {
List list = Arrays.asList(permissionUrl);
if (list.contains("/*")) {
return true;
} else if (list.contains(requestURL)) {
return true;
} else if (requestURL.equals("/favicon.ico")) {
return true;
} else return list.contains("/personal/*") && requestURL.contains("/personal/");
} else {
return false;
}
}
}
PermissionInterceptor.java
package com.jbz.interceptor;
import com.jbz.constant.MessageConstant;
import com.jbz.domain.User;
import com.jbz.utils.PermissionHandleUtils;
import com.jbz.utils.RecordingLogUtils;
import com.jbz.utils.SetDataUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* @author: jbz
* @date: 2023/1/9
* @description: 用户权限拦截器(对用户的请求进行权限的监控与拦截)
* @version: 1.0
*/
public class PermissionInterceptor implements HandlerInterceptor {
@Autowired
private PermissionHandleUtils permissionHandle;
@Autowired
private RecordingLogUtils recordingLog;
@Autowired
private SetDataUtils setDataUtil;
//在目标方法之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断用户是否登录
HttpSession session = request.getSession();
//从session中获取用户
User user = (User) session.getAttribute("user");
//如果用户为null或者用户名为空
if (user == null || "".equals(user.getUsername())) {
//还停留在登录页
response.sendRedirect(request.getContextPath() + "/pages/login.jsp");
return false;
} else {
Boolean flag = permissionHandle.judgePermissions(request, user);
if(flag){
//放行
return true;
}else {
setDataUtil.setPermissionCount(request.getSession());
//添加日志
recordingLog.insertSysLog(user.getUsername(), MessageConstant.EXECUTE_METHOD_FAIL,request.getRequestURI(),0,"方法已拦截");
//跳转至错误页面
response.sendRedirect(request.getContextPath() + "/pages/403.jsp");
return false;
}
}
}
}
首先我们需要导入spring-security的依赖,如下所示,接着封装一个工具类用于返回一个BCryptPasswordEncoder并注册进IOC容器
org.springframework.security
spring-security-core
5.0.5.RELEASE
@Component
public class MyPasswordEncoder {
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(10);
}
}
接着我们在servic业务层对应的实现类里依赖注入PasswordEncoder,在新增用户以及修改密码的时候,对密码进行加密,使用passwordEncoder.encode()对传入的密码进行加密,返回加密后的密码,将user对象的密码修改为加密后的密码,最后存入数据库中。在登录时进行密码解密与比对,同一个密码的加密结果也是不一样的,PasswordEncoder也有加盐处理,passwordEncoder.matches(password,encodePassword)对密码进行比对,返回boolean值。代码如下所示。
@Override
public void updatePersonalUser(User user) {
//对密码进行加密处理
String encodePassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encodePassword);
userMapper.updateUser(user);
}
@Override
public void addUser(User user, int[] roleIds) {
//对密码进行加密处理
String encodePassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encodePassword);
userMapper.insertUser(user);
userRoleMapper.insertUserRole(user.getId(), roleIds);
}
@Override
public User login(String username, String password) {
//调用userMapper的根据用户名查user的方法
User user = userMapper.queryUserByUsername(username);
//判断user是否为null
if (user != null) {
//判断密码是否一致
if (passwordEncoder.matches(password,user.getPassword())) {
return user;
}
}
return null;
}
项目源码地址:GitHub - JBZ0805/ssm
项目运行效果如下图所示,