使用SpringAOP獲取用戶操作日志入庫
SpringAOP獲取用戶操作日志入庫
切service層中所有的方法,將有自定義注解的方法的操作日志入庫,其中需要注意的幾點:
- 注意aspectjweaver.jar包的版本,一般要1.6以上版本,否則會報錯
- 注意是否使用了雙重代理,spring.xml中不需要配置切面類的<bean>,否則會出現切兩次的情況
- 注意返回的數據類型,如果是實體類需要獲取實體類中每個屬性的值,若該實體類中的某個屬性也是實體類,需要再次循環(huán)獲取該屬性的實體類屬性
- 用遞歸的方法獲得參數及參數內容
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;
/**
* 業(yè)務邏輯方法切入點,切所有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();
//創(chuàng)建變量用于存儲注解返回的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>");
}
}
// 創(chuàng)建日志對象
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> 集合內容" + 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> " + 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系統(tǒng)做安全審計和系統(tǒng)維護的重要手段,這里總結筆者在用java和python開發(fā)web系統(tǒng)過程中總結出來的、具有普遍意義的方法。
在java體系下,網絡上搜索了一下,幾乎一邊倒的做法是用AOP,通過注解的方式記錄操作日志,在此,筆者并不是很認同這種做法,原因如下:
- AOP的應用場景是各種接口中可以抽象出普遍的行為,且切入點選擇需要在各接口中比較統(tǒng)一。
- 記錄審計日志除了ip、用戶等共同的信息外,還需要記錄很多個性化的東西,比如一次修改操作,一般來講需要記錄對象標識、修改前后的值等等。有的值甚至并不能從request參數中直接獲取,有可能需要一定的邏輯判斷或者運算,使用AOP并不合適。
- 當然,有人說AOP中也可以傳遞參數,這里且不說有些日志信息需要從request參數計算而來的問題,就是是可以直接獲取,在注解中傳遞一大堆的參數也失去了AOP簡單的好處。
當然這主要還是看需求,如果你的操作日志僅僅是需要記錄ip、用戶等與具體接口無關的信息,那就無所謂。
接下來記錄操作日志就比較簡單了,無非就是在接口返回之前記錄一些操作信息,這些信息可能從request參數中獲取,也可能用request參數經過一些運算獲取,都無所謂,但是有一點需要注意,你得確保成功或者失敗場景都有記錄。
那么問題來了,現在的web框架,REST接口調用失敗普遍的做法是業(yè)務往外拋異常,由一個“統(tǒng)一異常處理”模塊來處理異常并構造返回體,Java的String Boot(ExceptionHandler)、Python的flask(裝飾器里make_response)、pecan(hook)等莫不是如此。那么接口調用失敗的時候如何記錄審計日志呢?肯定不可能在業(yè)務每個拋異常的地方去記錄,這太麻煩,解決方法當然是在前面說的這個“統(tǒng)一異常處理”模塊去處理,那么記錄的參數如何傳遞給這個模塊呢?方法就是放在本地線程相關的變量里,java接口可以在入口處整理操作日志信息存放在ThreadLocal變量里,成功或者失敗的時候設置一個status然后記錄入庫即可;python下,flask接口可以放在app_context的g里,pecan可以放在session里。另外如果是異步任務,還需要給任務寫個回調來更新狀態(tài)。
可見,不管是用java還是python開發(fā)操作日志,都是相同的套路,總結如下圖:

還有一點要注意,如果java接口是用的@Valid注解來進行參數校驗,那么在校驗失敗時會拋出MethodArgumentNotValidException,問題在于,這個Valid發(fā)生在請求進入接口之前,也就是說,出現參數校驗失敗拋出MethodArgumentNotValidException的時候還沒有進入接口里面的代碼,自然也就沒有往本地線程中記錄操作日志需要的信息,那怎么辦呢?方法就是在接口的請求入參中加一個BindingResult binding類型的參數,這個參數會截獲參數校驗的接口而不是拋出異常,然后在代碼中(已經往線程上下文中寫入了操作日志需要的信息以后的代碼中)判斷當binding中有錯誤,就拋出MethodArgumentNotValidException,此時就可以獲取到操作日志需要的信息了,代碼如下:
// 先往threadlocal變量中存入操作日志需要的信息
...

以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
詳談cxf和axis兩種框架下的webservice客戶端開發(fā)
這篇文章主要介紹了詳談cxf和axis兩種框架下的webservice客戶端開發(fā),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08

