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

网友投稿 720 2022-11-24

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

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

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

SpringAOP获取用户操作日志入库

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

注意aspectjweaver.jar包的版本,一般要1.6以上版本,否则会报错

注意是否使用了双重代理,spring.xml中不需要配置切面类的,否则会出现切两次的情况

注意返回的数据类型,如果是实体类需要获取实体类中每个属性的值,若该实体类中的某个属性也是实体类,需要再次循环获取该属性的实体类属性

用递归的方法获得参数及参数内容

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 + "; ");

// 获取方法名

String methodName = joinPoint.getSignature().getName();

rs.append("方法名:" + methodName + "; ");

// 获取类的所有方法

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("]");

}

}

// 创建日志对象

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".equalsIgnoreCaseKWqYmhwaWe(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 list = (List) info;

for (Object obj: list) {

rs.append(" 集合内容" + i + "————");

//递归

userOptionContent(obj, rs);

rs.append("");

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(" " + 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、用户等共同的信息外,还需要记录很多个性化的东西,比如一次http://修改操作,一般来讲需要记录对象标识、修改前后的值等等。有的值甚至并不能从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变量中存入操作日志需要的信息

...

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:大学C语言实验(一.数据类型入门汇总)
下一篇:慕课软件工程(第一章.软件工程的起源和概念)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~