☰
Spring AOP 实战:安全日志
2026/10/11 1:45:22 网站建设 项目流程

Spring AOP 实战:安全日志

安全日志是 AOP 最典型的应用场景之一。它需要记录“谁在什么时间、从哪里、做了什么操作、结果如何”。这些信息横跨所有业务方法,用 AOP 统一处理最合适。

一、需求分析

安全日志通常要记录以下信息:

字段说明来源
操作人当前登录用户SecurityContext / ThreadLocal
操作时间方法执行时间LocalDateTime.now()
操作类型登录、删除、修改等自定义注解
操作模块用户管理、订单管理自定义注解
请求方法类名 + 方法名JoinPoint
请求参数方法入参JoinPoint.getArgs()
请求 IP客户端 IPRequestContextHolder
执行结果成功/失败返回值或异常
耗时方法执行时间System.currentTimeMillis()

二、整体设计

自定义注解 @SecurityLog → 标注在需要记录安全日志的方法上 → 切面拦截标注了 @SecurityLog 的方法 → 收集操作人、IP、参数、结果等信息 → 异步写入日志表或日志文件

三、自定义注解

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceSecurityLog{/** 操作模块 */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;}

五、获取当前用户和 IP

@ComponentpublicclassSecurityContextHolder{privatestaticfinalThreadLocal<String>CURRENT_USER=newThreadLocal<>();publicstaticvoidsetCurrentUser(Stringusername){CURRENT_USER.set(username);}publicstaticStringgetCurrentUser(){Stringuser=CURRENT_USER.get();returnuser!=null?user:"anonymous";}publicstaticvoidclear(){CURRENT_USER.remove();}}

获取 IP 的工具类:

publicclassIpUtils{publicstaticStringgetClientIp(){HttpServletRequestrequest=getRequest();if(request==null)return"unknown";Stringip=request.getHeader("X-Forwarded-For");if(isValid(ip))returnip.split(",")[0].trim();ip=request.getHeader("X-Real-IP");if(isValid(ip))returnip;ip=request.getHeader("Proxy-Client-IP");if(isValid(ip))returnip;returnrequest.getRemoteAddr();}privatestaticHttpServletRequestgetRequest(){RequestAttributesattrs=RequestContextHolder.getRequestAttributes();if(attrsinstanceofServletRequestAttributes){return((ServletRequestAttributes)attrs).getRequest();}returnnull;}privatestaticbooleanisValid(Stringip){returnip!=null&&!ip.isEmpty()&&!"unknown".equalsIgnoreCase(ip);}}

六、切面实现

@Aspect@Component@Order(1)@Slf4jpublicclassSecurityLogAspect{@AutowiredprivateSecurityLogServicesecurityLogService;@Pointcut("@annotation(com.example.annotation.SecurityLog)")publicvoidsecurityLogPointcut(){}@Around("securityLogPointcut() && @annotation(securityLog)")publicObjectaround(ProceedingJoinPointpjp,SecurityLogsecurityLog)throwsThrowable{// 1. 开始计时longstart=System.currentTimeMillis();// 2. 构建日志实体SecurityLogEntitylogEntity=newSecurityLogEntity();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{result=pjp.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(){RequestAttributesattrs=RequestContextHolder.getRequestAttributes();if(attrsinstanceofServletRequestAttributes){return((ServletRequestAttributes)attrs).getRequest().getRequestURI();}return"";}privateStringserializeArgs(Object[]args){if(args==null||args.length==0)return"";try{// 过滤掉 HttpServletRequest、HttpServletResponse 等不可序列化对象List<Object>filtered=Arrays.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(result==null)return"";try{returnnewObjectMapper().writeValueAsString(result);}catch(Exceptione){returnresult.toString();}}}

七、异步保存日志

安全日志不应该阻塞业务方法,用@Async异步保存。

@Service@Slf4jpublicclassSecurityLogService{@AutowiredprivateSecurityLogMappersecurityLogMapper;@Async("securityLogExecutor")publicvoidsaveAsync(SecurityLogEntitylogEntity){try{securityLogMapper.insert(logEntity);}catch(Exceptione){log.error("保存安全日志失败",e);}}}

线程池配置:

@Configuration@EnableAsyncpublicclassAsyncConfig{@Bean("securityLogExecutor")publicExecutorsecurityLogExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();executor.setCorePoolSize(2);executor.setMaxPoolSize(5);executor.setQueueCapacity(1000);executor.setThreadNamePrefix("security-log-");executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());executor.initialize();returnexecutor;}}

八、业务方法使用

@RestController@RequestMapping("/api/users")publicclassUserController{@AutowiredprivateUserServiceuserService;@SecurityLog(module="用户管理",type="DELETE",description="删除用户")@DeleteMapping("/{id}")publicResultdeleteUser(@PathVariableLongid){userService.deleteById(id);returnResult.success();}@SecurityLog(module="用户管理",type="UPDATE",description="修改用户角色")@PutMapping("/{id}/role")publicResultupdateRole(@PathVariableLongid,@RequestParamStringrole){userService.updateRole(id,role);returnResult.success();}@SecurityLog(module="认证",type="LOGIN",description="用户登录",saveParams=false)@PostMapping("/login")publicResultlogin(@RequestBodyLoginRequestrequest){returnuserService.login(request);}}

saveParams = false用于登录接口,避免密码被记录到日志中。

九、当前用户设置

在登录拦截器或过滤器中设置当前用户:

@ComponentpublicclassAuthInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){Stringtoken=request.getHeader("Authorization");if(token!=null){Stringusername=JwtUtils.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'安全日志表';

十一、扩展:敏感操作告警

安全日志不仅能记录,还能对敏感操作实时告警。

@Aspect@Component@Order(2)@Slf4jpublicclassSecurityAlertAspect{@AutowiredprivateAlertServicealertService;@AfterReturning("@annotation(securityLog)")publicvoidalert(JoinPointjp,SecurityLogsecurityLog){// 高风险操作触发告警if(isHighRisk(securityLog.type())){Stringmessage=String.format("【安全告警】用户 %s 在 %s 执行了 %s 操作",SecurityContextHolder.getCurrentUser(),IpUtils.getClientIp(),securityLog.description());alertService.send(message);}}privatebooleanisHighRisk(Stringtype){return"DELETE".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注解,剩下的事情由切面统一处理。结合异步保存、敏感信息脱敏、高风险告警,可以构建一套完整的安全审计体系。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询