Spring AOP 实战安全日志安全日志是 AOP 最典型的应用场景之一。它需要记录“谁在什么时间、从哪里、做了什么操作、结果如何”。这些信息横跨所有业务方法用 AOP 统一处理最合适。一、需求分析安全日志通常要记录以下信息字段说明来源操作人当前登录用户SecurityContext / ThreadLocal操作时间方法执行时间LocalDateTime.now()操作类型登录、删除、修改等自定义注解操作模块用户管理、订单管理自定义注解请求方法类名 方法名JoinPoint请求参数方法入参JoinPoint.getArgs()请求 IP客户端 IPRequestContextHolder执行结果成功/失败返回值或异常耗时方法执行时间System.currentTimeMillis()二、整体设计自定义注解 SecurityLog → 标注在需要记录安全日志的方法上 → 切面拦截标注了 SecurityLog 的方法 → 收集操作人、IP、参数、结果等信息 → 异步写入日志表或日志文件三、自定义注解Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)DocumentedpublicinterfaceSecurityLog{/** 操作模块 */Stringmodule()default;/** 操作类型 */Stringtype()default;/** 操作描述 */Stringdescription()default;/** 是否记录请求参数 */booleansaveParams()defaulttrue;/** 是否记录返回值 */booleansaveResult()defaultfalse;}四、日志实体DatapublicclassSecurityLogEntity{privateLongid;/** 操作人 */privateStringoperator;/** 操作模块 */privateStringmodule;/** 操作类型 */privateStringtype;/** 操作描述 */privateStringdescription;/** 请求方法 */privateStringmethod;/** 请求参数 */privateStringparams;/** 返回值 */privateStringresult;/** 客户端 IP */privateStringip;/** 请求 URI */privateStringuri;/** 执行状态SUCCESS / FAIL */privateStringstatus;/** 异常信息 */privateStringerrorMsg;/** 耗时毫秒 */privateLongcostTime;/** 操作时间 */privateLocalDateTimecreateTime;}五、获取当前用户和 IPComponentpublicclassSecurityContextHolder{privatestaticfinalThreadLocalStringCURRENT_USERnewThreadLocal();publicstaticvoidsetCurrentUser(Stringusername){CURRENT_USER.set(username);}publicstaticStringgetCurrentUser(){StringuserCURRENT_USER.get();returnuser!null?user:anonymous;}publicstaticvoidclear(){CURRENT_USER.remove();}}获取 IP 的工具类publicclassIpUtils{publicstaticStringgetClientIp(){HttpServletRequestrequestgetRequest();if(requestnull)returnunknown;Stringiprequest.getHeader(X-Forwarded-For);if(isValid(ip))returnip.split(,)[0].trim();iprequest.getHeader(X-Real-IP);if(isValid(ip))returnip;iprequest.getHeader(Proxy-Client-IP);if(isValid(ip))returnip;returnrequest.getRemoteAddr();}privatestaticHttpServletRequestgetRequest(){RequestAttributesattrsRequestContextHolder.getRequestAttributes();if(attrsinstanceofServletRequestAttributes){return((ServletRequestAttributes)attrs).getRequest();}returnnull;}privatestaticbooleanisValid(Stringip){returnip!null!ip.isEmpty()!unknown.equalsIgnoreCase(ip);}}六、切面实现AspectComponentOrder(1)Slf4jpublicclassSecurityLogAspect{AutowiredprivateSecurityLogServicesecurityLogService;Pointcut(annotation(com.example.annotation.SecurityLog))publicvoidsecurityLogPointcut(){}Around(securityLogPointcut() annotation(securityLog))publicObjectaround(ProceedingJoinPointpjp,SecurityLogsecurityLog)throwsThrowable{// 1. 开始计时longstartSystem.currentTimeMillis();// 2. 构建日志实体SecurityLogEntitylogEntitynewSecurityLogEntity();logEntity.setOperator(SecurityContextHolder.getCurrentUser());logEntity.setModule(securityLog.module());logEntity.setType(securityLog.type());logEntity.setDescription(securityLog.description());logEntity.setMethod(getMethodName(pjp));logEntity.setIp(IpUtils.getClientIp());logEntity.setUri(getRequestUri());logEntity.setCreateTime(LocalDateTime.now());// 3. 记录请求参数if(securityLog.saveParams()){logEntity.setParams(serializeArgs(pjp.getArgs()));}// 4. 执行目标方法Objectresult;try{resultpjp.proceed();logEntity.setStatus(SUCCESS);if(securityLog.saveResult()){logEntity.setResult(serializeResult(result));}}catch(Throwablee){logEntity.setStatus(FAIL);logEntity.setErrorMsg(e.getMessage());throwe;}finally{logEntity.setCostTime(System.currentTimeMillis()-start);// 5. 异步保存日志securityLogService.saveAsync(logEntity);}returnresult;}privateStringgetMethodName(ProceedingJoinPointpjp){returnpjp.getSignature().getDeclaringTypeName().pjp.getSignature().getName();}privateStringgetRequestUri(){RequestAttributesattrsRequestContextHolder.getRequestAttributes();if(attrsinstanceofServletRequestAttributes){return((ServletRequestAttributes)attrs).getRequest().getRequestURI();}return;}privateStringserializeArgs(Object[]args){if(argsnull||args.length0)return;try{// 过滤掉 HttpServletRequest、HttpServletResponse 等不可序列化对象ListObjectfilteredArrays.stream(args).filter(arg-!(arginstanceofHttpServletRequest)).filter(arg-!(arginstanceofHttpServletResponse)).filter(arg-!(arginstanceofMultipartFile)).collect(Collectors.toList());returnnewObjectMapper().writeValueAsString(filtered);}catch(Exceptione){returnArrays.toString(args);}}privateStringserializeResult(Objectresult){if(resultnull)return;try{returnnewObjectMapper().writeValueAsString(result);}catch(Exceptione){returnresult.toString();}}}七、异步保存日志安全日志不应该阻塞业务方法用Async异步保存。ServiceSlf4jpublicclassSecurityLogService{AutowiredprivateSecurityLogMappersecurityLogMapper;Async(securityLogExecutor)publicvoidsaveAsync(SecurityLogEntitylogEntity){try{securityLogMapper.insert(logEntity);}catch(Exceptione){log.error(保存安全日志失败,e);}}}线程池配置ConfigurationEnableAsyncpublicclassAsyncConfig{Bean(securityLogExecutor)publicExecutorsecurityLogExecutor(){ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();executor.setCorePoolSize(2);executor.setMaxPoolSize(5);executor.setQueueCapacity(1000);executor.setThreadNamePrefix(security-log-);executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());executor.initialize();returnexecutor;}}八、业务方法使用RestControllerRequestMapping(/api/users)publicclassUserController{AutowiredprivateUserServiceuserService;SecurityLog(module用户管理,typeDELETE,description删除用户)DeleteMapping(/{id})publicResultdeleteUser(PathVariableLongid){userService.deleteById(id);returnResult.success();}SecurityLog(module用户管理,typeUPDATE,description修改用户角色)PutMapping(/{id}/role)publicResultupdateRole(PathVariableLongid,RequestParamStringrole){userService.updateRole(id,role);returnResult.success();}SecurityLog(module认证,typeLOGIN,description用户登录,saveParamsfalse)PostMapping(/login)publicResultlogin(RequestBodyLoginRequestrequest){returnuserService.login(request);}}saveParams false用于登录接口避免密码被记录到日志中。九、当前用户设置在登录拦截器或过滤器中设置当前用户ComponentpublicclassAuthInterceptorimplementsHandlerInterceptor{OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){Stringtokenrequest.getHeader(Authorization);if(token!null){StringusernameJwtUtils.getUsername(token);SecurityContextHolder.setCurrentUser(username);}returntrue;}OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){SecurityContextHolder.clear();}}十、日志表结构CREATETABLEt_security_log(idBIGINTAUTO_INCREMENTPRIMARYKEY,operatorVARCHAR(64)COMMENT操作人,moduleVARCHAR(64)COMMENT操作模块,typeVARCHAR(32)COMMENT操作类型,descriptionVARCHAR(255)COMMENT操作描述,methodVARCHAR(255)COMMENT请求方法,paramsTEXTCOMMENT请求参数,resultTEXTCOMMENT返回值,ipVARCHAR(64)COMMENT客户端IP,uriVARCHAR(255)COMMENT请求URI,statusVARCHAR(16)COMMENT执行状态,error_msgTEXTCOMMENT异常信息,cost_timeBIGINTCOMMENT耗时(ms),create_timeDATETIMECOMMENT操作时间,INDEXidx_operator(operator),INDEXidx_create_time(create_time))COMMENT安全日志表;十一、扩展敏感操作告警安全日志不仅能记录还能对敏感操作实时告警。AspectComponentOrder(2)Slf4jpublicclassSecurityAlertAspect{AutowiredprivateAlertServicealertService;AfterReturning(annotation(securityLog))publicvoidalert(JoinPointjp,SecurityLogsecurityLog){// 高风险操作触发告警if(isHighRisk(securityLog.type())){StringmessageString.format(【安全告警】用户 %s 在 %s 执行了 %s 操作,SecurityContextHolder.getCurrentUser(),IpUtils.getClientIp(),securityLog.description());alertService.send(message);}}privatebooleanisHighRisk(Stringtype){returnDELETE.equals(type)||GRANT.equals(type)||RESET_PASSWORD.equals(type);}}十二、注意事项1. 敏感信息脱敏密码、身份证号、银行卡号等不应记录到日志中。可以在序列化参数前做脱敏处理。privateStringdesensitize(Stringjson){// 将 password 字段的值替换为 ***returnjson.replaceAll(\password\\\s*:\\s*\[^\]*\,\password\:\***\);}2. 异步日志的上下文传递Async方法在新线程中执行ThreadLocal中的用户信息会丢失。解决方案在切面中提前把用户信息取出来作为参数传给异步方法。上面的代码已经这样做了。3. 日志量控制不是所有方法都需要记录安全日志。用SecurityLog精确标注需要记录的方法避免日志表被撑爆。4. 日志表分区安全日志数据量大按月分区或定期归档。5. 切面顺序安全日志切面应该在事务切面外层这样才能记录到事务提交前的方法调用。Order(1)保证它在外层。十三、总结环节实现标注自定义SecurityLog注解拦截Around切面拦截标注方法收集操作人、IP、参数、结果、耗时脱敏过滤密码等敏感字段保存Async异步写入数据库告警高风险操作实时通知顺序Order(1)放在事务切面外层安全日志是 AOP 最实用的场景之一。它把“记录操作日志”这个横切逻辑从业务方法中完全抽离业务代码只需要加一个SecurityLog注解剩下的事情由切面统一处理。结合异步保存、敏感信息脱敏、高风险告警可以构建一套完整的安全审计体系。