API安全(八)-审计

1、审计所在安全链路的位置,为什么

  如图所示,审计应该做在认证之后,授权之前。因为只有在认证之后,我们在记录日志的时候,在知道请求是那个用户发过来的;做在授权之前,哪些请求被拒绝了,在响应的时候,也可以把它记录下来。如果放到授权之后 ,那么被拒绝的请求就不能记录了。

  审计日志一定要持久化,方便我们对问题的追溯,可以把它放到数据库中,也可以写到磁盘中。实际工作中,一般会发送到公司统一的日志服务上,由日志服务来存储。

2、审计采用的组件,及安全链路顺序的保障

  首先,我们来明确一下各组件在请求中的执行顺序,如下图,依次是 Filter -> Interceptor -> ControllerAdvice -> AOP -> Controller

  对于Filter之间,我们可以使用@Order注解来确定执行顺序;对于Interceptor之间根据注册的先后顺序执行。这里我们的审计功能选择Filter和Interceptor都可以,根据自己的喜好即可。

3、实现审计功能

  3.1、审计日志类及持久层接口

/**
 * 审计日志
 *
 * @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

 

posted @ 2020-01-29 01:48  caofanqi  阅读(1634)  评论(0编辑  收藏  举报