目录
六、SpringMVC拦截器interceptor拦截用户的权限
一. 前言
本项目的客户端是基于开源项目AdminLTE 和AdminLTE 黑马定制版进行编写。AdminLTE 是一个完全的响应式管理模板,基于Bootstrap3框架,高度可定制,易于使用。适合从小型移动设备到大型台式机的屏幕分辨率 。
本项目可实现用户登录与头像的上传,根据用户的角色与权限不同,操作不同的模块进行基本的CRUD操作。实现对用户的请求进行日志记录,对用户的权限进行监控与拦截,用户在没有登录的情况下,不能对后台进行访问操作,当点击菜单就会跳转到登录页面。只有用户登录成功,才能进行后台功能的操作。对用户的密码进行加密与解密,整合redis实现mybatis的二级缓存。
二. 主要技术栈
- 前端页面:Bootstrap3框架,Jquery,JSP
- 后端:JavaEE,SSM框架,redis,spring-security,bcrypt加密算法
开发工具:jdk8,idea2022,navicat,redis,mysql8,maven仓库,firefox/chorm
三.项目搭建
3.1 数据库搭建
打开navicat,创建名为ssm的数据库,导入准备好的sql脚本文件,数据库模型图如下所示,共11张表。用户表通过中间表与角色表相连,角色表通过中间表与权限表相连;订单表通过外键与订单负责人相连、通过中间表与旅客表进行相连。syslog为日志记录表,product为产品表。
3.2 maven项目搭建
打开idea新建maven工程,在pom文件中添加对应的依赖,具体的依赖如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jbz</groupId>
<artifactId>ssm</artifactId>
<version>1.0-SNAPSHOT</version>
<name>ssm</name>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<!--Spring的使用版本-->
<spring.version>5.0.5.RELEASE</spring.version>
</properties>
<dependencies>
<!--junit4依赖坐标-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!--servlet坐标-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!--jsp坐标-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.2.1</version>
<scope>provided</scope>
</dependency>
<!--jstl坐标-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<scope>compile</scope>
</dependency>
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
<!--spring相关坐标-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.5.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.0.5.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.5.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--AOP的实现包-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<!--SpringMVC坐标-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--mybatis坐标-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<!--mybatis-plus依赖坐标-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.4.3.4</version>
</dependency>
<!--spring与mybatis对接依赖坐标-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.4</version>
</dependency>
<!--jackson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.0</version>
</dependency>
<!--分页坐标-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.2</version>
</dependency>
<!--文件上传的依赖坐标-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.4.0.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.4.2</version>
</dependency>
<!--spring-security依赖-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<!-- tomcat7的插件, 不同tomcat版本这个也不一样 -->
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.1</version>
<configuration>
<!-- 通过maven tomcat7:run运行项目时,访问项目的端口号 -->
<port>80</port>
<!-- 项目访问路径 本例:localhost:9090, 如果配置的aa, 则访问路径为localhost:9090/aa-->
<path>/</path>
<uriEncoding>UTF-8</uriEncoding>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<!-- 此配置不可缺,否则mybatis的Mapper.xml将会丢失 -->
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<!--指定资源的位置-->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>*.xml</include>
<include>*.properties</include>
</includes>
</resource>
</resources>
</build>
</project>
添加完记得刷新一下,接下来在resource文件下添加jdbc.properties文件
添加SqlMapperConfig文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 是否开启全局缓存 -->
<setting name="cacheEnabled" value="true" />
<!-- 查询时,关闭关联对象即时加载以提高性能 -->
<setting name="lazyLoadingEnabled" value="false"/>
<!-- 对于未知的SQL查询,允许返回不同的结果集 -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 设置关联对象加载的形态 -->
<setting name="aggressiveLazyLoading" value="true"/>
</settings>
<!--扫描使用注解的类所在的包-->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="param1" value="value1"/>
</plugin>
</plugins>
<mappers>
<package name="com.jbz.mapper"/>
</mappers>
</configuration>
添加spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd">
<!--1.配置组件扫描-->
<context:component-scan base-package="com.jbz"/>
<!--2.开启MVC注解驱动 自动加载三大组件和转换json-->
<mvc:annotation-driven/>
<!--3.配置视图解析器-->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--前缀-->
<property name="prefix" value="/pages/"/>
<!--后缀-->
<property name="suffix" value=".jsp"/>
</bean>
<!--4.开放资源的访问css、js、img等-->
<mvc:default-servlet-handler/>
<!--5.配置拦截器-->
<mvc:interceptors>
<mvc:interceptor>
<!--对哪些资源执行拦截操作-->
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/personal/login"/>
<!--指定拦截器的处理类-->
<bean class="com.jbz.interceptor.PermissionInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
<!--6.配置文件上传解析器-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--文件上传总大小-->
<property name="maxUploadSize" value="5242800"/>
<!--文件上传的编码格式-->
<property name="defaultEncoding" value="UTF-8"/>
<!--单个文件上传的大小-->
<property name="maxUploadSizePerFile" value="5242800"/>
</bean>
</beans>
添加applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--1.加载jdbc.properties文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--配置数据源对象及相关参数-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--3.配置SqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--设置数据源-->
<property name="dataSource" ref="dataSource"/>
<!--设置Mybatis核心配置文件 导入SqlMapConfig.xml-->
<property name="configLocation" value="classpath:SqlMapConfig.xml"/>
<!--指定mybatis的mapper映射文件的位置-->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<!--4.配置Mapper扫描的包-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--配置指定mapper扫描接口的包路径-->
<property name="basePackage" value="com.jbz.mapper"/>
</bean>
<!--5.开启动态代理-->
<aop:aspectj-autoproxy/>
<!--引入redis配置文件-->
<import resource="application-redis.xml"/>
</beans>
添加redis.properties
添加application-redis.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--配置redis-->
<!--引入redis参数 -->
<context:property-placeholder location="classpath:redis.properties" ignore-unresolvable="true" />
<!--设置数据池-->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大空闲数 -->
<property name="maxIdle" value="${redis.maxIdle}" />
<!-- 最大空闲数 -->
<property name="maxTotal" value="${redis.maxActive}" />
<!-- 返回连接时,检测连接是否成功 -->
<property name="testOnBorrow" value="${redis.testOnBorrow}" />
</bean>
<!--配置连接工厂-->
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<!-- IP地址 -->
<property name="hostName" value="${redis.host}" />
<!-- 端口号 -->
<property name="port" value="${redis.port}" />
<property name="poolConfig" ref="poolConfig" />
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
<!--针对各种数据选择序列化方式-->
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<!--开启事务 -->
<property name="enableTransactionSupport" value="true"/>
</bean>
<!-- 使用中间类解决RedisCache.jedisConnectionFactory的静态注入,从而使MyBatis实现第三方缓存 -->
<bean id="redisCacheTransfer" class="com.jbz.utils.RedisCacheTransfer">
<property name="jedisConnectionFactory" ref="connectionFactory"/>
</bean>
<!-- Redis缓存管理对象 -->
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg index="0" ref="redisTemplate" />
</bean>
</beans>
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<Object> serializer = new JdkSerializationRedisSerializer();
result = serializer.deserialize(connection.get(serializer.serialize(key)));
} catch (JedisConnectionException e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.close();
}
}
return result;
}
public ReadWriteLock getReadWriteLock() {
return this.readWriteLock;
}
public int getSize() {
int result = 0;
RedisConnection connection = null;
try {
connection = jedisConnectionFactory.getConnection();
result = Integer.parseInt(connection.dbSize().toString());
} catch (JedisConnectionException e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.close();
}
}
return result;
}
public void putObject(Object key, Object value) {
RedisConnection connection = null;
try {
connection = jedisConnectionFactory.getConnection();
RedisSerializer<Object> serializer = new JdkSerializationRedisSerializer();
connection.set(serializer.serialize(key), serializer.serialize(value));
} catch (JedisConnectionException e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.close();
}
}
}
public Object removeObject(Object key) {
RedisConnection connection = null;
Object result = null;
try {
connection = jedisConnectionFactory.getConnection();
RedisSerializer<Object> serializer = new JdkSerializationRedisSerializer();
result = connection.expire(serializer.serialize(key), 0);
} catch (JedisConnectionException e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.close();
}
}
return result;
}
public static void setJedisConnectionFactory(JedisConnectionFactory jedisConnectionFactory) {
RedisCache.jedisConnectionFactory = jedisConnectionFactory;
}
}
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文件中配置过滤器、前端控制器、监听器
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--设置首页-->
<welcome-file-list>
<welcome-file>/pages/login.jsp</welcome-file>
</welcome-file-list>
<!--解决乱码的过滤器-->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--配置前端控制器-->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:*.xml</param-value>
</init-param>
<!--
配置springMVC什么时候启动。参数必须是整数,
如果等于0或大于0,随着容器的启动而启动
如果小于0在第一次请求进来的时候启动
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--在spring-mvc.xml加入<mvc:default-servlet-handler/>后再添加如下-->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
<url-pattern>*.css</url-pattern>
<url-pattern>/img/*</url-pattern>
<url-pattern>/plugins/*</url-pattern>
</servlet-mapping>
<!--配置监听器-->
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
</web-app>
接下来将准备好的静态资源放在webapp下,至此项目搭建完毕 。
四、实现基本的CRUD操作
首先在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<Product> 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路径:
<c:forEach items="${pageInfo.list}" var="product">
<tr>
<td><label><input name="ids" type="checkbox" value="${product.id}"></label></td>
<td>${product.id}</td>
<td>${product.productNum}</td>
<td>${product.productName}</td>
<td>${product.cityName}</td>
<td><fmt:formatDate value="${product.departureTime}" pattern="yyyy-MM-dd"/></td>
<td>${product.productPrice}</td>
<td>${product.productDesc}</td>
<td>${product.productStatusStr}</td>
<td class="text-center">
<button type="button" class="btn bg-olive btn-xs"
onclick="location.href='${pageContext.request.contextPath}/product/queryProductById?id='+${product.id}">
编辑
</button>
<button type="button" class="btn bg-olive btn-xs"
onclick="delProduct(${product.id})">删除
</button>
</td>
</tr>
</c:forEach>
最后运行项目,在点击控制台的链接,点击侧边栏的产品管理至如下图所示:
增删改的操作与上面类似,这里就不展开说了。
五、SpringAOP实现记录日志
我们在AOP包下,新建LogAspect切面类,添加注解@Aspect声明当前类是一个切面类,添加@Pointcut注解配置通用切入点表达式。我们选择用环绕通知来进行日志的记录。环绕通知的特点是目标执行前后,都进行增强(控制目标方法执行),它的应用场景:日志、缓存、权限、性能监控、事务管理。具体代码如下所示。接着我们需要在applciationContext.xml文件中添加<aop:aspectj-autoproxy/>开启动态代理,当用户在页面发起请求时,该切面类会获取到当前用户的请求路径,请求的方法名。同时我们在此基础上获取当前用户的用户名,访问时间以及当前用户的ip地址,将这些数据封装到Syslog实体类中。最后在将Syslog实体类保存到数据库中。
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拦截器interceptor拦截用户的权限
SpringMVC的拦截器类似于Servlet开发中的过滤器Filter,用于处理器进行预处理和后处理。将拦截器按一定的顺序联结成一条链,这条链称为拦截器链。在访问被拦截的方法或字段时,拦截器链的拦截器就会按其之前定义的顺序被调用。拦截器也是AOP思想的具体实现。拦截器和过滤器的区别如图所示:
拦截器的步骤:
- 创建拦截器类实现HandlerInterceptor接口
- 配置拦截器
- 测试拦截器效果
首先我们先封装一个权限处理的工具类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<String> 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;
}
}
}
}
七、Bcrypt加密算法
首先我们需要导入spring-security的依赖,如下所示,接着封装一个工具类用于返回一个BCryptPasswordEncoder并注册进IOC容器
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
@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
项目运行效果如下图所示,