当前位置: 首页 > 科技观察

熟练使用自定义注解,一行代码搞定审计日志,你学会了吗?

时间:2023-03-21 12:19:00 科技观察

1。引言任何软件系统都不可避免地会遇到【信息安全】这个词,尤其是对于刚入行的新人,比如我。刚入行的时候,领导让我做一个数据报表导出功能,我就照他说的去做。至于谁有权限操作导出,导出的包含敏感信息的数据如何处理,后台接口是否有权限控制防止恶意抢夺,我基本不关心这些问题。只想一心一意尽快实现需求,然后顺利完成任务交付。事实上,随着工作经验的增加,你会越来越觉得,实现业务方的需求只是在软件系统开发中完成“可用”的需求;服务是否“可靠”可能需要从架构层来判断。至于是不是【安全】,我们需要更多的从【信息安全】的角度思考,尤其是当我们的软件系统面临外界的恶意干扰和攻击的时候,它还能保护用户吗?正常使用,对于大公司来说,这可能是重中之重,因为一个小漏洞,一个不小心,可能会给公司造成上千万的损失!最常见的是电商系统和支付系统,尤其是在需求旺季,经常会有黑客专门攻击这些电商系统,导致大量服务宕机,影响用户正常下单。这样的攻击每天都在发生,有的公司甚至直接向黑客发火,出钱消灾!但这种做法绝对不是长久之计,最重要的是要积极提高系统的【安全】防御系数。由于信息安全涉及的需求比较多,今天只给大家介绍一下【审计日志】的需求和具体应用,其他的需求以后再给大家介绍。【审计日志】,简单的说,系统需要记录谁,什么时间,对什么数据做了什么改动!这份日志数据极其珍贵,如果后期业务运营出现问题,可以很方便的进行运营回顾。同时,如果有什么IT系统需要审计,这个任务基本上是必填项目!好了,需求说清楚了,具体应用如下图!2.在实践中实现【审计日志】的需求,我们有一个很好的技术方案,就是利用Spring的切面编程,创建一个代理类,使用afterReturning和afterThrowing方法来记录日志。具体实现步骤如下首先创建审计日志表CREATETABLE`tb_audit_log`(`id`bigint(20)NOTNULLCOMMENT'审计日志,主键ID',`table_name`varchar(500)DEFAULT''COMMENT'操作的表名,多个以逗号分隔',`operate_desc`varchar(200)DEFAULT''COMMENT'操作描述',`request_param`varchar(200)DEFAULT''COMMENT'请求参数',`result`int(10)COMMENT'执行结果,0:成功,1:失败',`ex_msg`varchar(200)DEFAULT''COMMENT'异常信息',`user_agent`textCOLLATEutf8mb4_unicode_ciCOMMENT'用户代理信息',`ip_address`varchar(32)NOTNULLDEFAULT''COMMENT'运行期间的设备IP',`ip_address_name`varchar(32)DEFAULT''COMMENT'运行期间的设备IP地址',`operate_time`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'运行时间',`operate_user_id`varchar(32)DEFAULT''COMMENT'operatorID',`operate_user_name`varchar(32)DEFAULT''COMMENT'operator',PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ciCOMMENT='审计日志表';然后写一个注解类@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE,ElementType.METHOD})@Documentedpublic@interfaceSystemAuditLog{/***操作的表名*@return*/StringtableName()default"";/***日志描述*@return*/Stringdescription()default"";}然后写一个Proxy类@Component@AspectpublicclassSystemAuditLogAspect{@AutowiredprivateSystemAuditLogServicesystemAuditLogService;/***定义入口点,切入所有标有该注解的类和方法*/@Pointcut("@within(com.example.demo.core.annotation.SystemAuditLog)||@annotation(com.example.demo.core.annotation.SystemAuditLog)")publicvoidmethodAspect(){}/***在方法调用前拦截*/@Before("methodAspect()")publicvoidbefore(){System.out.println("SystemAuditLogProxy->在调用方法执行之前...");}/***方法调用后拦截*/@After("methodAspect()")publicvoidafter(){System.out.println("SystemAuditLogProxy->Aftercallingmethodexecution...");}/***调用方法结束拦截*/@AfterReturning(value="methodAspect()")publicvoidafterReturning(JoinPointjoinPoint)throwsException{System.out.println("SystemAuditLogProxy->Callmethodendintercept...");//封装数据AuditLogentity=warpAuditLog(joinPoint);实体.setResult(0);//插入数据库systemAuditLogService.add(entity);}/***抛出异常拦截*/@AfterThrowing(value="methodAspect()",throwing="ex")publicvoidafterThrowing(JoinPointjoinPoint,Exceptionex)throwsException{System.out.println("SystemAuditLogProxy->抛出异常拦截...");//封装数据AuditLogentity=warpAuditLog(joinPoint);实体.setResult(1);//封装错误信息entity.setExMsg(ex.getMessage());//插入数据库systemAuditLogService.add(entity);}/***封装插入的实体*@paramjoinPoint*@return*@throwsException*/privateAuditLogwarpAuditLog(JoinPointjoinPoint)throwsException{//获取请求上下文HttpServletRequestrequest=getHttpServletRequest();//获取注解上的参数值SystemAuditLogsystemAuditLog=getServiceMethodDescription(joinPoint);//获取请求参数ObjectrequestObj=getServiceMethodParams(joinPoint);//封装数据AuditLogauditLog=newAuditLog();auditLog.setId(SnowflakeIdWorker.getInstance().nextId());//从请求上下文对象中获取对应的数据if(Objects.nonNull(request)){auditLog.setUserAgent(request.getHeader("User-Agent"));//获取登录时的ip地址auditLog.setIpAddress(IpAddressUtil.getIpAddress(request));//调用外部接口获取IP位置auditLog.setIpAddressName(IpAddressUtil.getLoginAddress(auditLog.getIpAddress()));}//封装操作的表和描述if(Objects.nonNull(systemAuditLog)){auditLog.setTableName(systemAuditLog.tableName());auditLog.setOperateDesc(systemAuditLog.description());}//封装请求参数auditLog.setRequestParam(JSON.toJSONString(requestObj));//封装请求者if(Objects.nonNull(requestObj))&&;requestObjinstanceofBaseRequest){auditLog.setOperateUserId(((BaseRequest)requestObj).getLoginUserId());auditLog.setOperateUserName(((BaseRequest)requestObj).getLoginUserName());审计日志;/***获取当前请求*如果这里报空指针异常,是因为单独使用spring获取请求*需要在配置文件中添加监听**如果是spring项目,通过以下方式注入***org.springframework.web.context.request.RequestContextListener****如果是springboot项目,在配置类,通过以下方式注入*@Bean*publicRequestContextListenerrequestContextListener(){*returnnewRequestContextListener();*}*@return*/privateHttpServletRequestgetHttpServletRequest(){RequestAttributesra=RequestContextHolder.getRequestAttributes();ServletRequestAttributessra=(ServletRequestA属性)ra;HttpServletRequest请求=sra.getRequest();退货申请;}/***获取请求对象*@paramjoinPoint*@return*@throwsException*/privateObjectgetServiceMethodParams(JoinPointjoinPoint){Object[]arguments=joinPoint.getArgs();if(Objects.nonNull(arguments)&&arguments.length>0){返回参数[0];}返回空值;}/***获取自定义注解中的参数*@paramjoinPoint*@return返回注解中的日志描述*@throwsException*/privateSystemAuditLoggetServiceMethodDescription(JoinPointjoinPoint)throwsException{//类名StringtargetName=joinPoint.getTarget().getClass().getName();//方法名StringmethodName=joinPoint.getSignature().getName();//参数对象[]arguments=joinPoint.getArgs();//通过反射获取示例对象ClasstargetClass=Class.forName(targetName);//通过实例对象方法数组遇到hod[]方法=targetClass.getMethods();for(Methodmethod:methods){//判断方法名是否相同if(method.getName().equals(methodName)){//比较参数数组的长度Class[]clazzs=method.getParameterTypes();if(clazzs.length==arguments.length){//获取注解中的日志信息returnmethod.getAnnotation(SystemAuditLog.class);}}}返回空值;}}最后,只需要在相应的接口或方法上添加审计日志注解@RestController@RequestMapping("api")publicclassLoginController{/***用户登录,添加审计日志注解*@paramrequest*/@SystemAuditLog(tableName="tb_user",description="用户登录")@PostMapping("login")publicvoidlogin(UserLoginDTOrequest){//登录逻辑处理}}相关实体类@DatapublicclassAuditLog{/***审计日志,主键ID*/privateLongid;/***操作表名,逗号分隔*/privateStringtableName;/***操作说明*/privateString操作描述;/***请求参数*/privateStringrequestParam;/***执行结果,0:成功,1:失败*/privateIntegerresult;/***异常信息*/privateStringexMsg;/***请求代理信息*/privateStringuserAgent;/***运行时的设备IP*/privateStringipAddress;/***运行时的设备IP地址*/privateStringipAddressName;/***操作时间*/privateDateoperateTime;/***运营商ID*/privateStringoperateUserId;/***运算符*/privateStringoperateUserName;}publicclassBaseRequestimplementsSerializable{/***请求令牌*/privateStringtoken;/***登录用户ID*/privateStringloginUserId;/***登录名*/privateStringloginUserName;publicStringgetToken(){返回令牌;}publicvoidsetToken(Stringtoken){this.token=token;}publicStringgetLoginUserId(){返回loginUserId;}公共电话idsetLoginUserId(StringloginUserId){this.loginUserId=loginUserId;}publicStringgetLoginUserName(){返回登录用户名;}publicvoidsetLoginUserName(StringloginUserName){this.loginUserName=loginUserName;}}@DatapublicclassUserLoginDTOextendsBaseRequest{/***用户名*/privateStringuserName;/***Password*/privateStringpassword;}3.总结一下整个程序的实现过程,主要是利用SpringAOP的特性来拦截前后具体的方法,从而实现业务端,我们在下一篇中后面会详细介绍SpringAOP的使用!