经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring » 查看文章
使用SpringAOP获取用户操作日志入库
来源:jb51  时间:2021/11/8 17:18:59  对本文有异议

SpringAOP获取用户操作日志入库

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

  • 注意aspectjweaver.jar包的版本,一般要1.6以上版本,否则会报错
  • 注意是否使用了双重代理,spring.xml中不需要配置切面类的<bean>,否则会出现切两次的情况
  • 注意返回的数据类型,如果是实体类需要获取实体类中每个属性的值,若该实体类中的某个属性也是实体类,需要再次循环获取该属性的实体类属性
  • 用递归的方法获得参数及参数内容
  1. package awb.aweb_soa.service.userOperationLog;
  2. import java.io.IOException;
  3. import java.lang.reflect.Method;
  4. import java.sql.Timestamp;
  5. import java.util.List;
  6. import javax.servlet.http.HttpServletRequest;
  7. import javax.sql.rowset.serial.SerialBlob;
  8. import org.apache.commons.lang.WordUtils;
  9. import org.aspectj.lang.JoinPoint;
  10. import org.aspectj.lang.annotation.AfterReturning;
  11. import org.aspectj.lang.annotation.AfterThrowing;
  12. import org.aspectj.lang.annotation.Aspect;
  13. import org.aspectj.lang.annotation.Before;
  14. import org.aspectj.lang.annotation.Pointcut;
  15. import org.springframework.beans.factory.annotation.Autowired;
  16. import org.springframework.stereotype.Service;
  17. import org.springframework.web.context.request.RequestContextHolder;
  18. import org.springframework.web.context.request.ServletRequestAttributes;
  19. import cn.com.agree.aweb.asapi.ASAPI;
  20. import edm.aweb_soa.aweb_soa.base.user.UserOperationLogDO;
  21. import awb.aweb_soa.aservice.app.DefaultUser;
  22. import awb.aweb_soa.global.annotation.UserOperationType;
  23. @Service
  24. @Aspect
  25. public class UserOperationLogAspect {
  26. @Autowired
  27. UserOperationLog userOperationLog;
  28. /**
  29. * 业务逻辑方法切入点,切所有service层的方法
  30. */
  31. @Pointcut("execution(* awb.aweb_soa.service..*(..))")
  32. public void serviceCall() {
  33. }
  34. /**
  35. * 用户登录
  36. */
  37. @Pointcut("execution(* awb.aweb_soa.aservice.app.LoginController.signIn(..))")
  38. public void logInCall() {
  39. }
  40. /**
  41. * 退出登出切入点
  42. */
  43. @Pointcut("execution(* awb.aweb_soa.aservice.app.DefaultUser.logout(..))")
  44. public void logOutCall() {
  45. }
  46. /**
  47. * 操作日志(后置通知)
  48. *
  49. * @param joinPoint
  50. * @param rtv
  51. * @throws Throwable
  52. */
  53. @AfterReturning(value = "serviceCall()", argNames = "rtv", returning = "rtv")
  54. public void doAfterReturning(JoinPoint joinPoint, Object rtv) throws Throwable {
  55. operationCall(joinPoint, rtv,"S");
  56. }
  57. /**
  58. * 用户登录(后置通知)
  59. *
  60. * @param joinPoint
  61. * @param rtv
  62. * @throws Throwable
  63. */
  64. @AfterReturning(value = "logInCall()", argNames = "rtv", returning = "rtv")
  65. public void doLoginReturning(JoinPoint joinPoint, Object rtv) throws Throwable {
  66. operationCall(joinPoint, rtv,"S");
  67. }
  68. @Before(value = "logOutCall()")
  69. public void logoutCalls(JoinPoint joinPoint) throws Throwable {
  70. operationCall(joinPoint, null,"S");
  71. }
  72. /**
  73. * 操作日志(异常通知)
  74. *
  75. * @param joinPoint
  76. * @param e
  77. * @throws Throwable
  78. */
  79. @AfterThrowing(value = "serviceCall()", throwing="e")
  80. public void doAfterThrowing(JoinPoint joinPoint, Object e) throws Throwable {
  81. operationCall(joinPoint, e,"F");
  82. }
  83. /**
  84. * 获取用户操作日志详细信息
  85. *
  86. * @param joinPoint
  87. * @param rtv
  88. * @param status
  89. * @throws Throwable
  90. */
  91. private void operationCall(JoinPoint joinPoint, Object rtv,String status)
  92. throws Throwable {
  93. //获取当前用户
  94. DefaultUser currentUser = (DefaultUser) ASAPI.authenticator().getCurrentUser();
  95. String userName = null;
  96. if (currentUser != null) {
  97. //获取用户名
  98. userName = currentUser.getUsername();
  99. //获取用户ip地址
  100. HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
  101. .getRequestAttributes()).getRequest();
  102. String userIp = getIpAddress(request);
  103. // 拼接操作内容的字符串
  104. StringBuffer rs = new StringBuffer();
  105. // 获取类名
  106. String className = joinPoint.getTarget().getClass()
  107. .getCanonicalName();
  108. rs.append("类名:" + className + "; </br>");
  109. // 获取方法名
  110. String methodName = joinPoint.getSignature().getName();
  111. rs.append("方法名:" + methodName + "; </br>");
  112. // 获取类的所有方法
  113. Method[] methods = joinPoint.getTarget().getClass()
  114. .getDeclaredMethods();
  115. //创建变量用于存储注解返回的value值
  116. String operationType = "";
  117. for (Method method:methods) {
  118. String mName = method.getName();
  119. // 当切的方法和类中的方法相同时
  120. if (methodName.equals(mName)) {
  121. //获取方法的UserOperationType注解
  122. UserOperationType userOperationType =
  123. method.getAnnotation(UserOperationType.class);
  124. //如果方法存在UserOperationType注解时
  125. if (userOperationType!=null) {
  126. //获取注解的value值
  127. operationType = userOperationType.value();
  128. // 获取操作内容
  129. Object[] args = joinPoint.getArgs();
  130. int i = 1;
  131. if (args!=null&&args.length>0) {
  132. for (Object arg :args) {
  133. rs.append("[参数" + i + "======");
  134. userOptionContent(arg, rs);
  135. rs.append("]</br>");
  136. }
  137. }
  138. // 创建日志对象
  139. UserOperationLogDO log = new UserOperationLogDO();
  140. log.setLogId(ASAPI.randomizer().getRandomGUID());
  141. log.setUserCode(userName);
  142. log.setUserIP(userIp);
  143. log.setOperationDesc(new SerialBlob(rs.toString().getBytes("UTF-8")));
  144. log.setOperationType(operationType);
  145. log.setOperationTime(new Timestamp(System.currentTimeMillis()));
  146. log.setStatus(status);
  147. //日志对象入库
  148. userOperationLog.insertLog(log);
  149. }
  150. }
  151. }
  152. }
  153. }
  154. /**
  155. * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址;
  156. *
  157. * @param request
  158. * @return
  159. * @throws IOException
  160. */
  161. public final static String getIpAddress(HttpServletRequest request)
  162. throws IOException {
  163. // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
  164. String ip = request.getHeader("X-Forwarded-For");
  165. if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
  166. if (ip == null || ip.length() == 0
  167. || "unknown".equalsIgnoreCase(ip)) {
  168. ip = request.getHeader("Proxy-Client-IP");
  169. }
  170. if (ip == null || ip.length() == 0
  171. || "unknown".equalsIgnoreCase(ip)) {
  172. ip = request.getHeader("WL-Proxy-Client-IP");
  173. }
  174. if (ip == null || ip.length() == 0
  175. || "unknown".equalsIgnoreCase(ip)) {
  176. ip = request.getHeader("HTTP_CLIENT_IP");
  177. }
  178. if (ip == null || ip.length() == 0
  179. || "unknown".equalsIgnoreCase(ip)) {
  180. ip = request.getHeader("HTTP_X_FORWARDED_FOR");
  181. }
  182. if (ip == null || ip.length() == 0
  183. || "unknown".equalsIgnoreCase(ip)) {
  184. ip = request.getRemoteAddr();
  185. }
  186. } else if (ip.length() > 15) {
  187. String[] ips = ip.split(",");
  188. for (int index = 0; index < ips.length; index++) {
  189. String strIp = (String) ips[index];
  190. if (!("unknown".equalsIgnoreCase(strIp))) {
  191. ip = strIp;
  192. break;
  193. }
  194. }
  195. }
  196. return ip;
  197. }
  198. /**
  199. * 使用Java反射来获取被拦截方法(insert、update, delete)的参数值, 将参数值拼接为操作内容
  200. */
  201. @SuppressWarnings("unchecked")
  202. public StringBuffer userOptionContent(Object info, StringBuffer rs){
  203. String className = null;
  204. // 获取参数对象类型
  205. className = info.getClass().getName();
  206. className = className.substring(className.lastIndexOf(".") + 1);
  207. rs.append("类型:"+className+",");
  208. //参数对象类型不是实体类或者集合时,直接显示参数值
  209. if (className.equals("String")||className.equals("int")||className.equals("Date")
  210. ||className.equals("Timestamp")||className.equals("Integer")
  211. ||className.equals("B")||className.equals("Long")) {
  212. rs.append("值:(" + info + ")");
  213. }
  214. //参数类型是ArrayList集合,迭代里面的对象,并且递归
  215. if(className.equals("ArrayList")){
  216. int i = 1;
  217. //将参数对象转换成List集合
  218. List<Object> list = (List<Object>) info;
  219. for (Object obj: list) {
  220. rs.append("</br>&nbsp;集合内容" + i + "————");
  221. //递归
  222. userOptionContent(obj, rs);
  223. rs.append("</br>");
  224. i++;
  225. }
  226. //参数对象是实体类
  227. }else{
  228. // 获取对象的所有方法
  229. Method[] methods = info.getClass().getDeclaredMethods();
  230. //遍历对象中的所有方法是否是get方法
  231. for (Method method : methods) {
  232. //获取方法名字
  233. String methodName = method.getName();
  234. if (methodName.indexOf("get") == -1 || methodName.equals("getPassword")
  235. || methodName.equals("getBytes")|| methodName.equals("getChars")
  236. || methodName.equals("getLong") || methodName.equals("getInteger")
  237. || methodName.equals("getTime") || methodName.equals("getCalendarDate")
  238. || methodName.equals("getDay") || methodName.equals("getMinutes")
  239. || methodName.equals("getHours")|| methodName.equals("getSeconds")
  240. || methodName.equals("getYear") || methodName.equals("getTimezoneOffset")
  241. || methodName.equals("getDate") || methodName.equals("getJulianCalendar")
  242. || methodName.equals("getMillisOf") || methodName.equals("getCalendarSystem")
  243. || methodName.equals("getMonth")|| methodName.equals("getTimeImpl")
  244. || methodName.equals("getNanos")) {
  245. continue;
  246. }
  247. rs.append("</br>&nbsp;" + className + "——" + changeString(methodName) + ":");
  248. Object rsValue = null;
  249. try {
  250. // 调用get方法,获取返回值
  251. rsValue = method.invoke(info);
  252. userOptionContent(rsValue, rs);
  253. } catch (Exception e) {
  254. continue;
  255. }
  256. }
  257. }
  258. return rs;
  259. }
  260. //有get方法获得属性名
  261. public String changeString(String name){
  262. name = name.substring(3);
  263. name = WordUtils.uncapitalize(name);//首字符小写
  264. return name;
  265. }
  266. }

记录操作日志的一般套路

记录操作日志是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变量中存入操作日志需要的信息

...

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

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号