服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 使用SpringAOP获取用户操作日志入库

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

2022-07-16 11:59SenKnight Java教程

这篇文章主要介绍了使用SpringAOP获取用户操作日志入库,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

SpringAOP获取用户操作日志入库

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

  • 注意aspectjweaver.jar包的版本,一般要1.6以上版本,否则会报错
  • 注意是否使用了双重代理,spring.xml中不需要配置切面类的<bean>,否则会出现切两次的情况
  • 注意返回的数据类型,如果是实体类需要获取实体类中每个属性的值,若该实体类中的某个属性也是实体类,需要再次循环获取该属性的实体类属性
  • 用递归的方法获得参数及参数内容
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
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开发操作日志,都是相同的套路,总结如下图:

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

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

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

...

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

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

原文链接:https://blog.csdn.net/java_doctor/article/details/81876375

延伸 · 阅读

精彩推荐