使用SpringAOP获取用户操作日志入库

目录
  • SpringAOP获取用户操作日志入库
  • 记录操作日志的一般套路

SpringAOP获取用户操作日志入库

切service层中所有的方法,将有自定义注解的方法的操作日志入库,其中需要注意的几点:

  • 注意aspectjweaver.jar包的版本,一般要1.6以上版本,否则会报错
  • 注意是否使用了双重代理,spring.xml中不需要配置切面类的<bean>,否则会出现切两次的情况
  • 注意返回的数据类型,如果是实体类需要获取实体类中每个属性的值,若该实体类中的某个属性也是实体类,需要再次循环获取该属性的实体类属性
  • 用递归的方法获得参数及参数内容
package awb.aweb_soa.service.userOperationLog;
import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.Timestamp;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.sql.rowset.serial.SerialBlob;

import org.apache.commons.lang.WordUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import cn.com.agree.aweb.asapi.ASAPI;
import edm.aweb_soa.aweb_soa.base.user.UserOperationLogDO;
import awb.aweb_soa.aservice.app.DefaultUser;
import awb.aweb_soa.global.annotation.UserOperationType;

@Service
@Aspect
public class UserOperationLogAspect {
	@Autowired
	UserOperationLog userOperationLog;
	/**
	 * 业务逻辑方法切入点,切所有service层的方法
	 */
	@Pointcut("execution(* awb.aweb_soa.service..*(..))")
	public void serviceCall() {

	}
	/**
	 * 用户登录
	 */
	@Pointcut("execution(* awb.aweb_soa.aservice.app.LoginController.signIn(..))")
	public void logInCall() {

	}
	/**
	 * 退出登出切入点
	 */
	@Pointcut("execution(* awb.aweb_soa.aservice.app.DefaultUser.logout(..))")
	public void logOutCall() {

	}

	/**
	 * 操作日志(后置通知)
	 *
	 * @param joinPoint
	 * @param rtv
	 * @throws Throwable
	 */
	@AfterReturning(value = "serviceCall()", argNames = "rtv", returning = "rtv")
	public void doAfterReturning(JoinPoint joinPoint, Object rtv) throws Throwable {
		operationCall(joinPoint, rtv,"S");
	}
	/**
	 * 用户登录(后置通知)
	 *
	 * @param joinPoint
	 * @param rtv
	 * @throws Throwable
	 */
	@AfterReturning(value = "logInCall()", argNames = "rtv", returning = "rtv")
	public void doLoginReturning(JoinPoint joinPoint, Object rtv) throws Throwable {
		operationCall(joinPoint, rtv,"S");
	}

	@Before(value = "logOutCall()")
	public void logoutCalls(JoinPoint joinPoint) throws Throwable {
		operationCall(joinPoint, null,"S");
	}
	/**
	 * 操作日志(异常通知)
	 *
	 * @param joinPoint
	 * @param e
	 * @throws Throwable
	 */
	@AfterThrowing(value = "serviceCall()", throwing="e")
	public void doAfterThrowing(JoinPoint joinPoint, Object e) throws Throwable {
		operationCall(joinPoint, e,"F");
	}
	/**
	 * 获取用户操作日志详细信息
	 *
	 * @param joinPoint
	 * @param rtv
	 * @param status
	 * @throws Throwable
	 */
	private void operationCall(JoinPoint joinPoint, Object rtv,String status)
			throws Throwable {
		//获取当前用户
		DefaultUser currentUser = (DefaultUser) ASAPI.authenticator().getCurrentUser();
		String userName = null;
		if (currentUser != null) {
			//获取用户名
			userName = currentUser.getUsername();
			//获取用户ip地址
			HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
					.getRequestAttributes()).getRequest();
			String userIp = getIpAddress(request);

			// 拼接操作内容的字符串
			StringBuffer rs = new StringBuffer();

			// 获取类名
			String className = joinPoint.getTarget().getClass()
					.getCanonicalName();
			rs.append("类名:" + className + "; </br>");

			// 获取方法名
			String methodName = joinPoint.getSignature().getName();
			rs.append("方法名:" + methodName + "; </br>");

			// 获取类的所有方法
			Method[] methods = joinPoint.getTarget().getClass()
					.getDeclaredMethods();
			//创建变量用于存储注解返回的value值
			String operationType = "";
			for (Method method:methods) {
				String mName = method.getName();
				// 当切的方法和类中的方法相同时
				if (methodName.equals(mName)) {
					//获取方法的UserOperationType注解
					UserOperationType userOperationType =
							method.getAnnotation(UserOperationType.class);
					//如果方法存在UserOperationType注解时
					if (userOperationType!=null) {
						//获取注解的value值
						operationType = userOperationType.value();

						// 获取操作内容
						Object[] args = joinPoint.getArgs();
						int i = 1;
						if (args!=null&&args.length>0) {
							for (Object arg :args) {
								rs.append("[参数" + i + "======");
								userOptionContent(arg, rs);
								rs.append("]</br>");
							}
						}
						// 创建日志对象
						UserOperationLogDO log = new UserOperationLogDO();
						log.setLogId(ASAPI.randomizer().getRandomGUID());
						log.setUserCode(userName);
						log.setUserIP(userIp);
						log.setOperationDesc(new SerialBlob(rs.toString().getBytes("UTF-8")));
						log.setOperationType(operationType);
						log.setOperationTime(new Timestamp(System.currentTimeMillis()));
						log.setStatus(status);
						//日志对象入库
						userOperationLog.insertLog(log);
					}
				}
			}
		}
	}
	/**
	 * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址;
	 *
	 * @param request
	 * @return
	 * @throws IOException
	 */
	public final static String getIpAddress(HttpServletRequest request)
			throws IOException {
		// 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址

		String ip = request.getHeader("X-Forwarded-For");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			if (ip == null || ip.length() == 0
					|| "unknown".equalsIgnoreCase(ip)) {
				ip = request.getHeader("Proxy-Client-IP");
			}
			if (ip == null || ip.length() == 0
					|| "unknown".equalsIgnoreCase(ip)) {
				ip = request.getHeader("WL-Proxy-Client-IP");
			}
			if (ip == null || ip.length() == 0
					|| "unknown".equalsIgnoreCase(ip)) {
				ip = request.getHeader("HTTP_CLIENT_IP");
			}
			if (ip == null || ip.length() == 0
					|| "unknown".equalsIgnoreCase(ip)) {
				ip = request.getHeader("HTTP_X_FORWARDED_FOR");
			}
			if (ip == null || ip.length() == 0
					|| "unknown".equalsIgnoreCase(ip)) {
				ip = request.getRemoteAddr();
			}
		} else if (ip.length() > 15) {
			String[] ips = ip.split(",");
			for (int index = 0; index < ips.length; index++) {
				String strIp = (String) ips[index];
				if (!("unknown".equalsIgnoreCase(strIp))) {
					ip = strIp;
					break;
				}
			}
		}
		return ip;
	}
	/**
	 * 使用Java反射来获取被拦截方法(insert、update, delete)的参数值, 将参数值拼接为操作内容
	 */
	@SuppressWarnings("unchecked")
	public StringBuffer userOptionContent(Object info, StringBuffer rs){
		String className = null;
		// 获取参数对象类型
		className = info.getClass().getName();
		className = className.substring(className.lastIndexOf(".") + 1);
		rs.append("类型:"+className+",");

		//参数对象类型不是实体类或者集合时,直接显示参数值
		if (className.equals("String")||className.equals("int")||className.equals("Date")
				||className.equals("Timestamp")||className.equals("Integer")
				||className.equals("B")||className.equals("Long")) {
			rs.append("值:(" + info + ")");
		}

		//参数类型是ArrayList集合,迭代里面的对象,并且递归
		if(className.equals("ArrayList")){
			int i = 1;
			//将参数对象转换成List集合
			List<Object> list = (List<Object>) info;
			for (Object obj: list) {
				rs.append("</br>&nbsp;集合内容" + i + "————");
				//递归
				userOptionContent(obj, rs);
				rs.append("</br>");
				i++;
			}
		//参数对象是实体类
		}else{
			// 获取对象的所有方法
			Method[] methods = info.getClass().getDeclaredMethods();
			//遍历对象中的所有方法是否是get方法
			for (Method method : methods) {
				//获取方法名字
				String methodName = method.getName();
				if (methodName.indexOf("get") == -1 || methodName.equals("getPassword")
						|| methodName.equals("getBytes")|| methodName.equals("getChars")
						|| methodName.equals("getLong") || methodName.equals("getInteger")
						|| methodName.equals("getTime") || methodName.equals("getCalendarDate")
						|| methodName.equals("getDay")  || methodName.equals("getMinutes")
						|| methodName.equals("getHours")|| methodName.equals("getSeconds")
						|| methodName.equals("getYear") || methodName.equals("getTimezoneOffset")
						|| methodName.equals("getDate") || methodName.equals("getJulianCalendar")
						|| methodName.equals("getMillisOf") || methodName.equals("getCalendarSystem")
						|| methodName.equals("getMonth")|| methodName.equals("getTimeImpl")
						|| methodName.equals("getNanos")) {
					continue;
				}
				rs.append("</br>&nbsp;" + className + "——" + changeString(methodName) + ":");

				Object rsValue = null;
				try {
					// 调用get方法,获取返回值
					rsValue = method.invoke(info);
					userOptionContent(rsValue, rs);
				} catch (Exception e) {
					continue;
				}
			}
		}
		return rs;
	}
	//有get方法获得属性名
	public String changeString(String name){
		name = name.substring(3);
		name = WordUtils.uncapitalize(name);//首字符小写
		return name;
	}
}

记录操作日志的一般套路

记录操作日志是web系统做安全审计和系统维护的重要手段,这里总结笔者在用java和python开发web系统过程中总结出来的、具有普遍意义的方法。

在java体系下,网络上搜索了一下,几乎一边倒的做法是用AOP,通过注解的方式记录操作日志,在此,笔者并不是很认同这种做法,原因如下:

  • AOP的应用场景是各种接口中可以抽象出普遍的行为,且切入点选择需要在各接口中比较统一。
  • 记录审计日志除了ip、用户等共同的信息外,还需要记录很多个性化的东西,比如一次修改操作,一般来讲需要记录对象标识、修改前后的值等等。有的值甚至并不能从request参数中直接获取,有可能需要一定的逻辑判断或者运算,使用AOP并不合适。
  • 当然,有人说AOP中也可以传递参数,这里且不说有些日志信息需要从request参数计算而来的问题,就是是可以直接获取,在注解中传递一大堆的参数也失去了AOP简单的好处。

当然这主要还是看需求,如果你的操作日志仅仅是需要记录ip、用户等与具体接口无关的信息,那就无所谓。

接下来记录操作日志就比较简单了,无非就是在接口返回之前记录一些操作信息,这些信息可能从request参数中获取,也可能用request参数经过一些运算获取,都无所谓,但是有一点需要注意,你得确保成功或者失败场景都有记录。

那么问题来了,现在的web框架,REST接口调用失败普遍的做法是业务往外抛异常,由一个“统一异常处理”模块来处理异常并构造返回体,Java的String Boot(ExceptionHandler)、Python的flask(装饰器里make_response)、pecan(hook)等莫不是如此。那么接口调用失败的时候如何记录审计日志呢?肯定不可能在业务每个抛异常的地方去记录,这太麻烦,解决方法当然是在前面说的这个“统一异常处理”模块去处理,那么记录的参数如何传递给这个模块呢?方法就是放在本地线程相关的变量里,java接口可以在入口处整理操作日志信息存放在ThreadLocal变量里,成功或者失败的时候设置一个status然后记录入库即可;python下,flask接口可以放在app_context的g里,pecan可以放在session里。另外如果是异步任务,还需要给任务写个回调来更新状态。

可见,不管是用java还是python开发操作日志,都是相同的套路,总结如下图:

还有一点要注意,如果java接口是用的@Valid注解来进行参数校验,那么在校验失败时会抛出MethodArgumentNotValidException,问题在于,这个Valid发生在请求进入接口之前,也就是说,出现参数校验失败抛出MethodArgumentNotValidException的时候还没有进入接口里面的代码,自然也就没有往本地线程中记录操作日志需要的信息,那怎么办呢?方法就是在接口的请求入参中加一个BindingResult binding类型的参数,这个参数会截获参数校验的接口而不是抛出异常,然后在代码中(已经往线程上下文中写入了操作日志需要的信息以后的代码中)判断当binding中有错误,就抛出MethodArgumentNotValidException,此时就可以获取到操作日志需要的信息了,代码如下:

// 先往threadlocal变量中存入操作日志需要的信息

...

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • SpringAop实现操作日志记录

    前言 大家好,这里是经典鸡翅,今天给大家带来一篇基于SpringAop实现的操作日志记录的解决的方案.大家可能会说,切,操作日志记录这么简单的东西,老生常谈了.不! 网上的操作日志一般就是记录操作人,操作的描述,ip等.好一点的增加了修改的数据和执行时间.那么!我这篇有什么不同呢!今天这种不仅可以记录上方所说的一切,还增加记录了操作前的数据,错误的信息,堆栈信息等.正文开始~~~~~ 思路介绍 记录操作日志的操作前数据是需要思考的重点.我们以修改场景来作为探讨.当我们要完全记录数据的流向的时候,

  • Spring如何基于aop实现操作日志功能

    1. 在pom中添加所需依赖 创建一个springboot工程,添加所需要的依赖,持久化用的是mybatis <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--springboot aop依赖--> <dependency

  • 利用spring AOP记录用户操作日志的方法示例

    前言 最近项目已经开发完成,但发现需要加用户操作日志,如果返回去加也不太现实,所以使用springAOP来完成比较合适.下面来一起看看详细的介绍: 注解工具类: @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface LogAnnotation { String operateModelNm() default ""; String operateFuncNm() default

  • 详解基于SpringBoot使用AOP技术实现操作日志管理

    操作日志对于程序员或管理员而言,可以快速定位到系统中相关的操作,而对于操作日志的管理的实现不能对正常业务实现进行影响,否则即不满足单一原则,也会导致后续代码维护困难,因此我们考虑使用AOP切面技术来实现对日志管理的实现. 文章大致内容: 1.基本概念 2.基本应用 3.日志管理实战 对这几部分理解了,会对AOP的应用应该很轻松. 一.基本概念 项目 描述 Aspect(切面) 跨越多个类的关注点的模块化,切面是通知和切点的结合.通知和切点共同定义了切面的全部内容--它是什么,在何时和何处完成其功

  • 使用SpringAOP获取用户操作日志入库

    目录 SpringAOP获取用户操作日志入库 记录操作日志的一般套路 SpringAOP获取用户操作日志入库 切service层中所有的方法,将有自定义注解的方法的操作日志入库,其中需要注意的几点: 注意aspectjweaver.jar包的版本,一般要1.6以上版本,否则会报错 注意是否使用了双重代理,spring.xml中不需要配置切面类的<bean>,否则会出现切两次的情况 注意返回的数据类型,如果是实体类需要获取实体类中每个属性的值,若该实体类中的某个属性也是实体类,需要再次循环获取该

  • 使用Spring AOP实现用户操作日志功能

    目录 我使用Spring AOP实现了用户操作日志功能 需求分析 功能实现 1. 需要一张记录日志的 Log 表 导出的 sql 如下: 2.我使用的是 Spring Boot 所以需要引入 spring aop 的 starter 3.Log 实体类 4.ILog 注解 5.切面类 LogAspect 总结 我使用Spring AOP实现了用户操作日志功能 今天答辩完了,复盘了一下系统,发现还是有一些东西值得拿出来和大家分享一下. 需求分析 系统需要对用户的操作进行记录,方便未来溯源 首先想到

  • 使用SpringBoot AOP 记录操作日志、异常日志的过程

    平时我们在做项目时经常需要对一些重要功能操作记录日志,方便以后跟踪是谁在操作此功能:我们在操作某些功能时也有可能会发生异常,但是每次发生异常要定位原因我们都要到服务器去查询日志才能找到,而且也不能对发生的异常进行统计,从而改进我们的项目,要是能做个功能专门来记录操作日志和异常日志那就好了, 当然我们肯定有方法来做这件事情,而且也不会很难,我们可以在需要的方法中增加记录日志的代码,和在每个方法中增加记录异常的代码,最终把记录的日志存到数据库中.听起来好像很容易,但是我们做起来会发现,做这项工作很繁

  • PHP实现获取ip地址的5种方法,以及插入用户登录日志操作示例

    本文实例讲述了PHP实现获取ip地址的5种方法,以及插入用户登录日志操作.分享给大家供大家参考,具体如下: php 获取ip地址的5种方法,插入用户登录日志实例,推荐使用第二种方法 <?php //方法1: $ip = $_SERVER["REMOTE_ADDR"]; echo $ip; //方法2: $ip = ($_SERVER["HTTP_VIA"]) ? $_SERVER["HTTP_X_FORWARDED_FOR"] : $_SE

  • springAop实现权限管理数据校验操作日志的场景分析

    前言 作为一个写java的使用最多的轻量级框架莫过于spring,不管是老项目用到的springmvc,还是现在流行的springboot,都离不开spring的一些操作,我在面试的时候问的最多的spring的问题就是我们在平常的项目中使用spring最多的有哪几个点 在我看来无非就两个 spring的bean管理,说的高大上一点就是spring的ioc,di spring的AOP spring是一个很强大的轻量级框架,功能远不止这两点,但是我们用的最多的就是这两点. spring bean 管

  • 微信小程序实现获取用户信息并存入数据库操作示例

    本文实例讲述了微信小程序实现获取用户信息并存入数据库操作.分享给大家供大家参考,具体如下: 微信小程序获取用户信息简单,但是在存入自己服务器数据库的过程中研究了一天多的时间,并且网上搜索不到该资源,故发出来供大家参考. index.js Page({ data: { nickName: "微信账号登录", avatarUrl:"./user-unlogin.png", }, onLoad: function () { var that = this; var nic

  • 微信小程序学习笔记之登录API与获取用户信息操作图文详解

    本文实例讲述了微信小程序学习笔记之登录API与获取用户信息操作.分享给大家供大家参考,具体如下: 前面介绍了微信小程序跳转页面.传递参数获得数据,这里来分析一下登录API与获取用户信息操作方法. [小程序登录]wx.login() app.js: App({ onLaunch: function () { // 登录 wx.login({ success: function (res) { if (res.code) { //发起网络请求 wx.request({ url: 'https://w

  • bat文件与Vbs文件之间的常用操作(获取用户输入,执行VBS文件)

    bat文件 set /P StrInput="输入数字:" echo 输入的数字为%StrInput% set /P Flg="是否执行(y/n):" IF "%Flg%" equ "y" ( echo 执行命令 cscript abc.vbs "%StrInput%" ) 注意: 等于号(=)之间不能有空格,不然会出错. 判断值大小最好使用equ之类. 条件判断后的括号的有空格. VBS文件 获取外部参数

随机推荐