SpringBoot中使用AOP对用户登入和登出进行记录
1、什么是AOP?
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程
是Spring的核心内容之一,另一个是IoC(控制反转),AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
2、测试步骤
- 引入需要的依赖
- 建立数据库
- 对应数据库字段新建pojo实体类
- 针对数据库的增删改查的需求新建Mapper接口
- 配置application.yaml文件
- Service对增删改查的数据进行进一步封装
- Controller里编写
/login
和/logout
接口 - 配置aop对控制器里的接口进行增强处理,完成向日志表里插入日志操作
- 配置拦截器,避免用户未登入而进行操作
3、步骤
(1)引入需要的依赖
-
aop
:SpringAOP核心依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
-
thymeleaf
:模板引擎<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
-
web
:web项目核心依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
-
mybatis-springboot
:mybatis与SpringBoot整合核心依赖<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency>
-
mysql
:mysql数据库驱动<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
-
lombok
,因为我需要直接用lombok插件使用注解完成pojo类<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
(2)数据库
user表,存放用户基本信息:
userLog表,存放用户日志信息(登入和登出):
(3)pojo实体类
user.java:
@Data
@Component
@NoArgsConstructor
@AllArgsConstructor
public class User {
/**
* 用户编号
*/
private Integer uid;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 年龄
*/
private Integer age;
}
userLog.java:
@Data
@Component
@NoArgsConstructor
@AllArgsConstructor
public class UserLog {
/**
* 日志编号
*/
private Integer id;
/**
* 产生该日志的用户编号
*/
private Integer uid;
/**
* 登录的时间
*/
private Date loginTime;
/**
* 登出的时间
*/
private Date logoutTime;
}
(4)Mapper进行增删改查
由于只是登录用,我们只需要对表里的数据查询出来然后比对一下就可以了
UserMapper.java:
@Mapper
@Repository
public interface UserMapper {
/**
* 用于用户登陆时的验证
* @return 如果用户名和密码同时匹配则返回一个User对象
*/
User selectUserByUsername(User user);
}
UserLogMapper.java:
@Mapper
@Repository
public interface UserLogMapper {
/**
* 新增用户日志(新增用户编号uid,登入时间loginTime)
* @return
*/
int insertUserLog(UserLog userLog);
/**
* 修改用户日志(根据日志编号id修改登出时间logoutTime)
* @return
*/
int updateUserLog(UserLog userLog);
}
(5)配置application.yaml
需要配置Tomcat默认的端口号,配置Mapper映射地址,以及pojo的别名
#########################################################
#Tomcat容器(默认8080)
server:
port: 8080
#########################################################
#########################################################
#数据源
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/um?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password:
#########################################################
#########################################################
#thymeleaf模板引擎
thymeleaf:
cache: false
#########################################################
#########################################################
#Mybatis相关配置
mybatis:
type-aliases-package: cn.wqk.springbootaop.pojo
mapper-locations: mybatis/mapper/*Mapper.xml
#########################################################
(6)Service将增删改查出来的数据进一步封装
接口:
UserService.java:
@Service
public interface UserService {
//根据用户名查询是否存在该用户(shiro)
User selectUserByUsername(String username);
}
UserLogService.java:
我的逻辑是用户登录成功后直接根据用户的编号然后插入日志表里,日志的日志编号自增,登录的时候就插入用户登入时间,然后登出的时候就根据日志编号,直接将登出时间字段改为用户登出时间
@Service
public interface UserLogService {
//新增用户日志,成功后返回该日志的编号
int insertUserLog(int uid);
//修改用户日志
int updateUserLog(int id);
}
实现类:
UserServiceImpl.java:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private User user;
@Override
public User selectUserByUsername(String username) {
user.setUsername(username);
return userMapper.selectUserByUsername(user);
}
}
UserLogServiceImpl.java:
@Service
public class UserLogServiceImpl implements UserLogService {
@Autowired
private UserLogMapper userLogMapper;
@Autowired
private UserLog userLog;
@Override
public int insertUserLog(int uid) {
userLog.setUid(uid);
//获得当前系统时间
Timestamp loginTime = DateUtils.nowDateTime();
userLog.setLoginTime(loginTime);
userLogMapper.insertUserLog(userLog);
return userLog.getId();
}
@Override
public int updateUserLog(int id) {
userLog.setId(id);
//获得当前系统时间
Timestamp logoutTime = DateUtils.nowDateTime();
userLog.setLogoutTime(logoutTime);
return userLogMapper.updateUserLog(userLog);
}
}
因为Java自带的Date类型是java.util.date
无法直接存入MySQL数据库,所以我写了一个工具类,获取到系统的时间并且转换为MySQL能够存储的Timestamp类型
DateUtils.java:
public class DateUtils {
/**
* 获取系统时间并且转换为能存入MySQL的datetime的格式
* @return timestamp(能存入MySQL)
*/
public static Timestamp nowDateTime(){
//获取系统当前时间,格式:Tue Jun 23 19:57:59 CST 2020
Date date = new Date();
//获取系统当前时间的时间戳,格式:1592913479942
long dataTime = date.getTime();
//把时间戳转换为能够存入MySQL的datetime的时间格式
Timestamp timestamp = new Timestamp(dataTime);
return timestamp;
}
}
(7)在Controller在完成/login
和/logout
接口
login接口的作用就是从前端接收username和password参数,然后通过selectUserByUsername()方法查询到用户再根据接收到的密码进行匹配,匹配成功后则登录成功,并且把User对象存入session,还有status赋值为login存入session
logout接口就是先判断用户的登录状态status是否为login,是则允许退出并且清除seesion,否则不进行任何实际操作
@Controller
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public String login(@RequestParam("username")String username,
@RequestParam("password")String password,
HttpSession session){
User user = userService.selectUserByUsername(username);
if (password.equals(user.getPassword())){//登录成功
//将user对象存入session里
session.setAttribute("user",user);
//将status状态设置为login登录并且存入session
session.setAttribute("status","login");
return "index";
}else {//登录失败
return "redirect:/toLogin";
}
}
@RequestMapping("/logout")
@ResponseBody
public String logout(HttpSession session){
String status = (String) session.getAttribute("status");
if (status.equals("login")){//退出成功
session.invalidate();
return "logout,success!";
}else {//退出失败
return "logout,failure!";
}
}
}
(8)配置AOP,增强接口,并且完成记录用户日志
LoginAop登录切面,记录用户登录时间:
配置LoginAop切面,切点为UserController下的login()方法,该切面的逻辑是通过前置通知得到用户请求login接口传入的参数1:username,2:password,3:session,其中再通过session得到存入的User对象和status状态,再在环绕通知里首先判断status的状态,用户登录成功后,得到User对象里的uid再通过UserLogService插入到用户日志信息表里,并且将该条日志的编号作为返回值存到session里
@Aspect
@Component
public class LoginAop {
@Autowired
private UserLogServiceImpl userLogService;
//提高作用域
private Object proceed;
private String username;
private String password;
private HttpSession session;
//定义切点为controller包下的UserController类里的login()方法
@Pointcut("execution(* cn.wqk.springbootaop.controller.UserController.login(..))")
public void pointCut(){}
//前置通知,在前置通知里一般是给变量赋值
@Before("pointCut()")
public void before(JoinPoint joinPoint){
System.out.println("前置通知-----------------------");
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] args=joinPoint.getArgs();
System.out.println("类:"+className);
System.out.println("方法:"+methodName);
System.out.println("传入参数:");
for (int i=0;i<args.length;i++){
System.out.println("参数"+(i+1)+":"+args[i]);
}
//将第一个参数赋值给username
username=(String) args[0];
//将第二个参数赋值给password
password=(String) args[1];
//第三个参数赋值给session,让我们能够从session中取到uid
session=(HttpSession)args[2];
System.out.println("前置通知完--------------------");
}
//环绕通知
/**
* 环绕通知:
* proceed为执行方法后返回的值
*/
@Around("pointCut()")
public Object Around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕通知:---------------------");
//获得方法执行后的返回值
proceed = pjp.proceed();
//System.out.println("执行的方法后的返回值:"+proceed+"");
String status = (String) session.getAttribute("status");
if (status.equals("login")){
User user = (User) session.getAttribute("user");
Integer uid = user.getUid();
System.out.println(uid);
int logId = userLogService.insertUserLog(uid);
session.setAttribute("logId",logId);
}
System.out.println("环绕通知完--------------------");
return proceed;
}
}
LogoutAop登出切面,更新用户登出时间:
配置LogoutAop切面,切点是UserController下的logout()方法,该切面的逻辑是通过前置通知得到session,但是有一点需要特别注意,因为我在logout接口里配置了session.invalidate()方法,所以必须在前置通知里就把session拿到,然后才能拿到session里存的status和logId,所以环绕通知只是进行一个善后操作,通过logId获取到对应的日志编号,然后将其登出时间修改为系统当前时间
@Aspect
@Component
public class LogoutAop {
@Autowired
private UserLogServiceImpl userLogService;
//提高作用域
private Object proceed;
private HttpSession session;
private int logId;
private String status;
@Pointcut("execution(* cn.wqk.springbootaop.controller.UserController.logout(..))")
public void pointCut(){}
//前置通知
@Before("pointCut()")
public void before(JoinPoint joinPoint){
System.out.println("前置通知-----------------------");
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] args=joinPoint.getArgs();
System.out.println("类:"+className);
System.out.println("方法:"+methodName);
System.out.println("传入参数:");
for (int i=0;i<args.length;i++){
System.out.println("参数"+(i+1)+":"+args[i]);
}
//第一个参数赋值给session,让我们能够从session中取到uid
session=(HttpSession)args[0];
//从session中获得uid
logId = (int) session.getAttribute("logId");
//从session中获得status
status = (String) session.getAttribute("status");
System.out.println("前置通知完--------------------");
}
@Around("pointCut()")
public Object Around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕通知:---------------------");
//获得方法执行后的返回值
proceed = pjp.proceed();
if (status.equals("login")) {
System.out.println("logId:"+logId);
userLogService.updateUserLog(logId);
}
/*if (proceed.equals("logout,success!")){
System.out.println("logId:"+logId);
userLogService.updateUserLog(logId);
}*/
System.out.println("执行的方法后的返回值:"+proceed+"");
System.out.println("环绕通知完--------------------");
return proceed;
}
}
(9)、配置Interceptor拦截器,避免用户未登入然后进行登出操作
拦截器的逻辑就是拿到session里的user对象,如果没有user对象代表用户登录失败或者未登录,则返回false就是拦截住,否则返回true,放行。
@Component
public class LoginInterceptor implements HandlerInterceptor {
//访问接口之前调用,返回true放行,返回false拦截住
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("LoginInterceptor.preHandle()前-----------------------");
//获得session
HttpSession session = request.getSession();
//获得session里的user对象
User user = (User) session.getAttribute("user");
System.out.println("LoginInterceptor.preHandle()后-----------------------");
if (user==null){
return false;
}else {
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
}
因为配置了拦截器,所以还需要自定WebConfig并且注册拦截器,过滤/**的请求,不过滤/login请求
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
//配置资源过滤,比如js、css、html
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
}
//配置拦截器,写好的拦截器需要到这里注册才行
@Override
public void addInterceptors(InterceptorRegistry registry) {
//过滤/**下的所有请求,不过滤/login,/,/toLogin请求
registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/login","/","toLogin");
}
}
4、测试运行
-
登录
登录我是用的一个form表单发起
/login
请求-
登入时间会记录进用户日志表
-
控制台也会打印相应通知
-
-
登出
登出我用的是
a
标签,发起logout
请求
- 登出时间也会记录进用户日志表
- 控制台也会打印相应通知
- 登出时间也会记录进用户日志表