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