1、审计所在安全链路的位置,为什么
如图所示,审计应该做在认证之后,授权之前。因为只有在认证之后,我们在记录日志的时候,在知道请求是那个用户发过来的;做在授权之前,哪些请求被拒绝了,在响应的时候,也可以把它记录下来。如果放到授权之后 ,那么被拒绝的请求就不能记录了。
审计日志一定要持久化,方便我们对问题的追溯,可以把它放到数据库中,也可以写到磁盘中。实际工作中,一般会发送到公司统一的日志服务上,由日志服务来存储。
2、审计采用的组件,及安全链路顺序的保障
首先,我们来明确一下各组件在请求中的执行顺序,如下图,依次是 Filter -> Interceptor -> ControllerAdvice -> AOP -> Controller
对于Filter之间,我们可以使用@Order注解来确定执行顺序;对于Interceptor之间根据注册的先后顺序执行。这里我们的审计功能选择Filter和Interceptor都可以,根据自己的喜好即可。
3、实现审计功能
3.1、审计日志类及持久层接口
/*** 审计日志 * * @authorcaofanqi * @date 2020/1/28 22:55 */@Data @Entity @Table(name = "audit_log") @EntityListeners(value = AuditingEntityListener.class) public classAuditLogDO { @Id @GeneratedValue(strategy =GenerationType.IDENTITY) privateLong id; privateString httpMethod; privateString path; privateInteger httpStatus; @CreatedBy privateString username; @CreatedDate privateLocalDateTime requestTime; @LastModifiedDate privateLocalDateTime responseTime; privateString errorMessage; }
/*** 审计日志Repository * @authorcaofanqi * @date 2020/1/28 23:13 */ public interface AuditLogRepository extends JpaRepositoryImplementation<AuditLogDO,Long>{ }
3.2、开启JPA审计功能配置
/*** JPA相关配置 * * @authorcaofanqi * @date 2020/1/29 1:13 */@Configuration @EnableJpaAuditing public classJpaConfig { /*** 获取当前登陆用户 */@Bean public AuditorAware<String>auditorAware() { return () ->{ HttpServletRequest request =((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); UserDO user = (UserDO) request.getAttribute("user"); if (user != null) { returnOptional.of(user.getUsername()); } else{ return Optional.of("anonymous"); } }; } }
此处不懂的,可以去看我写的JPA文章:https://www.cnblogs.com/caofanqi/p/11996718.html
3.3、基于Filter实现审计功能AuditLogFilter,流控过滤器设置@Order(1)、认证过滤器设置@Order(2)
/*** 审计过滤器 * * @authorcaofanqi * @date 2020/1/29 0:08 */@Slf4j @Order(3) @Component public class AuditLogFilter extendsOncePerRequestFilter { @Resource privateAuditLogRepository auditLogRepository; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throwsServletException, IOException { log.info("++++++3、审计++++++"); AuditLogDO auditLogDO = newAuditLogDO(); auditLogDO.setHttpMethod(request.getMethod()); auditLogDO.setPath(request.getRequestURI()); //放入持久化上下文中,供异常处理使用 auditLogRepository.save(auditLogDO); request.setAttribute("auditLogId",auditLogDO.getId()); //执行请求 filterChain.doFilter(request,response); //执行完成,从持久化上下文中获取,并记录响应信息 auditLogDO =auditLogRepository.findById(auditLogDO.getId()).get(); auditLogDO.setHttpStatus(response.getStatus()); auditLogRepository.save(auditLogDO); } }
3.4、异常处理ControllerAdvice
/*** * @parame 系统异常 * @return系统异常及时间 */@ExceptionHandler @ResponseStatus(code =HttpStatus.INTERNAL_SERVER_ERROR) public Map<String,Object>exceptionHandler(Exception e){ /** 如果有异常的化,将审计日志取出,记录异常信息 */HttpServletRequest request =((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); Long auditLogId = (Long) request.getAttribute("auditLogId"); AuditLogDO auditLogDO = auditLogRepository.findById(auditLogId).orElse(newAuditLogDO()); auditLogDO.setErrorMessage(e.getMessage()); auditLogRepository.save(auditLogDO); Map<String, Object> info =Maps.newHashMap(); info.put("message", e.getMessage()); info.put("time", LocalDateTime.now()); returninfo; }
3.5、启动项目,进行测试,访问http://127.0.0.1:9090/users/40,并填写正确的用户名密码
执行顺序如下
数据库审计日志表
准备一个有错误的方法
@DeleteMapping("/{id}") public voiddelete(@PathVariable Long id){ int i = 1 / 0; }
测试如下:
数据库审计日志表
3.6、如果想基于Interceptors来实现,做如下修改
3.6.1、AuditLogInterceptor拦截器
/*** 基于Interceptor的审计拦截器 ,与AuditLogFilter同时只能使用一个 * * @authorcaofanqi * @date 2020/1/28 23:12 */@Slf4j @Component public class AuditLogInterceptor extendsHandlerInterceptorAdapter { @Resource privateAuditLogRepository auditLogRepository; @Override public booleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { log.info("++++++3、审计++++++"); AuditLogDO auditLogDO = newAuditLogDO(); auditLogDO.setHttpMethod(request.getMethod()); auditLogDO.setPath(request.getRequestURI()); auditLogRepository.save(auditLogDO); request.setAttribute("auditLogId",auditLogDO.getId()); return true; } @Override public voidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,Exception ex){ Long auditLogId = (Long) request.getAttribute("auditLogId"); AuditLogDO auditLogDO = auditLogRepository.findById(auditLogId).orElse(newAuditLogDO()); auditLogDO.setHttpStatus(response.getStatus()); auditLogRepository.save(auditLogDO); } }
3.6.2、注册拦截器
/*** web配置类 * * @authorcaofanqi * @date 2020/1/28 22:32 */@Configuration public class WebConfig implementsWebMvcConfigurer { @Resource privateAuditLogInterceptor auditLogInterceptor; /*** 注册拦截器 */@Override public voidaddInterceptors(InterceptorRegistry registry) { registry.addInterceptor(auditLogInterceptor); } }
3.6.3、进行3.5的测试效果相同
项目源码:https://github.com/caofanqi/study-security/tree/dev-auditing