SpringBoot中使用AOP对用户登入和登出进行记录

SpringBoot中使用AOP对用户登入和登出进行记录

1、什么是AOP?

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程

是Spring的核心内容之一,另一个是IoC(控制反转),AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

2、测试步骤

  1. 引入需要的依赖
  2. 建立数据库
  3. 对应数据库字段新建pojo实体类
  4. 针对数据库的增删改查的需求新建Mapper接口
  5. 配置application.yaml文件
  6. Service对增删改查的数据进行进一步封装
  7. Controller里编写/login/logout接口
  8. 配置aop对控制器里的接口进行增强处理,完成向日志表里插入日志操作
  9. 配置拦截器,避免用户未登入而进行操作

3、步骤

(1)引入需要的依赖

  • aop:SpringAOP核心依赖

    		<dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-aopartifactId>
            dependency>
    
  • thymeleaf:模板引擎

            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-thymeleafartifactId>
            dependency>
    
  • web:web项目核心依赖

            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
  • mybatis-springboot:mybatis与SpringBoot整合核心依赖

            <dependency>
                <groupId>org.mybatis.spring.bootgroupId>
                <artifactId>mybatis-spring-boot-starterartifactId>
                <version>2.1.3version>
            dependency>
    
  • mysql:mysql数据库驱动

            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <scope>runtimescope>
            dependency>
    
  • lombok,因为我需要直接用lombok插件使用注解完成pojo类

            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <optional>trueoptional>
            dependency>
    

(2)数据库

user表,存放用户基本信息:

SpringBoot中使用AOP对用户登入和登出进行记录_第1张图片

userLog表,存放用户日志信息(登入和登出):

SpringBoot中使用AOP对用户登入和登出进行记录_第2张图片

(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请求

    在这里插入图片描述

    • 登入时间会记录进用户日志表

      在这里插入图片描述

    • 控制台也会打印相应通知

      SpringBoot中使用AOP对用户登入和登出进行记录_第3张图片

  • 登出

    登出我用的是a标签,发起logout请求
    在这里插入图片描述

    • 登出时间也会记录进用户日志表
      在这里插入图片描述
    • 控制台也会打印相应通知
      SpringBoot中使用AOP对用户登入和登出进行记录_第4张图片

5、文件目录架构

SpringBoot中使用AOP对用户登入和登出进行记录_第5张图片

你可能感兴趣的:(JavaEE,spring,aop,spring,boot)