通过一个模拟情景来进行讲解:项目打算增加一个日志审计功能,对所有的功能接口做一个事件记录,保存相关的请求地址、接口地址、用户账号信息、用户代理信息等。
我们对该功能做一个大致的想法:通过自定义的注解作为aop的开启条件,在该注解修饰的方法/接口下切入,通过AOP的环绕通知特性,环绕前置获取到请求信息作为记录,环绕后置请求成功后将事件访问持久化保存下来。
自定义注解用来作为事件的启用,具体的注释已经放在代码上了,仅仅是一个注解的声明,功能的实现大多还是在AOP中实现的。
// 注解的生命周期 3种:source:源码上 class:字节码上 runtime:运行时 source < calss < runtime
@Retention(RetentionPolicy.RUNTIME)
// 注解可用范围:类 、 方法、 参数...... 详细范围可见ElementType中的枚举类
@Target({ElementType.METHOD})
// 元注解 文档显示
@Documented
// 注解自动继承
// 父子类上,如果父类上标注的 某个注解上带@Inherited的注解 ,那么子类也可以拥有该注解。
// 如果接口上 某个注解上带@Inherited的注解,实现类则不会拥有该接口的这个注解
@Inherited
// @interface 注解的声明方式
public @interface AuditRecord {
/**
* 注解的方法声明,在使用注解时必须传入值 可以用default "" 就无需传值了。
* 如果只有一个可以在注解上省略 value="**" 多个必须指定字段名和值的对应关系 例:@XXX(value="**", value2="***")
*
* @return
*/
String value();
// String value() default "";
}
在通过指定的切点进入切面生成代理对象,对注释所修饰的方法上进行环绕通知,在请求前获取相关请求信息,请求完成后将事件记录到数据库中持久化。几乎每行代码都有注释,有问题评论区讨论。
// 声明切面
@Aspect
// 配置注解
@Component
// 日志
@Slf4j
public class AuditRecordAspect {
/**
* 注解@Pointcut:声明一个切点。@Pointcut包含多个表达式,@annotation是其中一种
* 注解@annotation:当执行的方法上拥有指定的注解时生效。
* 理解为当某个方法上拥有@AuditRecord注解时,会进入controllerAspect()方法
*/
@Pointcut("@annotation(com.example.web.config.AuditRecord)")
private void controllerAspect() {
}
@Resource
private UserService userService;
/**
* 注解@Around("controllerAspect()")指的是在该方法之前通知
*
* @param joinPoint
* @return
* @throws Throwable
*/
@ResponseBody
@Around("controllerAspect()")
public Object doRecord(ProceedingJoinPoint joinPoint) throws Throwable {
// 环绕前通知,接口请求前触发 也在before通知前触发
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 1. getDeclaredAnnotations和getAnnotations方法指的是获取修饰在 类/方法 上的注解
// 2. getDeclaredAnnotations得到的是当前成员所有的注解,不包括继承的。而getAnnotations得到的是包括继承的所有注解。
// 3. getAnnotation(XXX.class) 指的是获取XXX类型的注解,如果没有返回的是null
AuditRecord annotation = method.getAnnotation(AuditRecord.class);
// 获取注释参数
String eventName = auditRecord.eventName();
// 通过请求获取客户端ip地址
HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
String clientIP = getIpAdrress(request);
// 获取当前用户登录的认证对象(项目中Security定制化的登录认证对象)
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Map<String, Object> userAuthentication = null;
if (authentication != null) {
userAuthentication = new HashMap<>();
userAuthentication.put("name", authentication.getName());
userAuthentication.put("principal", authentication);
// 业务通过id查询出登录的账户信息
User user = userService.selectByPrimaryKey(authentication.getId());
userAuthentication.put("details", user);
}
HttpServletRequest httpServletRequest = HttpContextUtil.getHttpServletRequest();
String eventSource = httpServletRequest.getHeader("Referer");
if (StrUtil.isBlank(eventSource)) {
eventSource = httpServletRequest.getHeader("origin");
}
// 请求参数名称数组
String[] argNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
// 请求参数value数组
Object[] args = joinPoint.getArgs();
cn.hutool.json.JSONArray arrayValues = JSONUtil.parseArray(JSONUtil.toJsonStr(args));
cn.hutool.json.JSONObject argMap = JSONUtil.createObj();
for (int i = 0; i < argNames.length; i++) {
argMap.put(argNames[i], arrayValues.get(i));
}
String requestUri = httpServletRequest.getRequestURI();
String queryString = httpServletRequest.getQueryString();
Map<String, String[]> parameterMap = httpServletRequest.getParameterMap();
JSONObject requestParameters = new JSONObject();
requestParameters.put("queryString", queryString);
requestParameters.put("parameterMap", parameterMap);
requestParameters.put("args", argMap);
// 执行接口功能,返回response
Object result = joinPoint.proceed();
// 环绕后通知,接口请求成功后触发 也在after前触发 用来记录事件
ActionEvent actionEvent = new ActionEvent()
.setRequestId(IdUtil.fastUUID())
.setRequestUri(requestUri)
.setEventName(eventName)
.setEventTime(LocalDateTime.now())
.setEventSource(eventSource)
.setSourceIpAddress(clientIP)
.setRequestParameters(requestParameters)
.setUserAgent(ServletUtil.getHeaderIgnoreCase(getHttpServletRequest(), "User-Agent"))
// 记录接口请求结果
if (result instanceof ResponseMessage) {
actionEvent.setResponseBody(JSONUtil.toBean(JSONUtil.toJsonStr(result), JSONObject.class));
if (!((ResponseMessage<?>) result).getSuccess()) {
actionEvent.setErrorCode(String.valueOf(((ResponseMessage<?>) result).getCode()));
actionEvent.setErrorMessage(((ResponseMessage<?>) result).getMsg());
}
}
// 对象持久化
ThreadUtil.execute(() -> {
actionEventService.insertSelective(actionEvent);
});
return result;
}
/**
* 获取Ip地址方法
*
* @param request
* @return
*/
private static String getIpAdrress(HttpServletRequest request) {
String iPAddress = request.getHeader("X-Forwarded-For");
if (StringUtils.isNotEmpty(iPAddress) && !"unKnown".equalsIgnoreCase(iPAddress)) {
//多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = iPAddress.indexOf(",");
if (index != -1) {
return iPAddress.substring(0, index);
} else {
return iPAddress;
}
}
iPAddress = request.getHeader("X-Real-IP");
if (StringUtils.isNotEmpty(iPAddress) && !"unKnown".equalsIgnoreCase(iPAddress)) {
return iPAddress;
}
if (StringUtils.isBlank(iPAddress) || "unknown".equalsIgnoreCase(iPAddress)) {
iPAddress = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(iPAddress) || "unknown".equalsIgnoreCase(iPAddress)) {
iPAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(iPAddress) || "unknown".equalsIgnoreCase(iPAddress)) {
iPAddress = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isBlank(iPAddress) || "unknown".equalsIgnoreCase(iPAddress)) {
iPAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isBlank(iPAddress) || "unknown".equalsIgnoreCase(iPAddress)) {
iPAddress = request.getRemoteAddr();
}
if("127.0.0.1".equals(iPAddress) || "0:0:0:0:0:0:0:1".equals(iPAddress)){
//根据网卡取本机配置的IP
InetAddress inet=null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
iPAddress= inet.getHostAddress();
}
return iPAddress;
}
}