1. 程式人生 > 實用技巧 >API安全(八)-審計

API安全(八)-審計

1、審計所在安全鏈路的位置,為什麼

  如圖所示,審計應該做在認證之後,授權之前。因為只有在認證之後,我們在記錄日誌的時候,在知道請求是那個使用者發過來的;做在授權之前,哪些請求被拒絕了,在響應的時候,也可以把它記錄下來。如果放到授權之後 ,那麼被拒絕的請求就不能記錄了。

  審計日誌一定要持久化,方便我們對問題的追溯,可以把它放到資料庫中,也可以寫到磁碟中。實際工作中,一般會發送到公司統一的日誌服務上,由日誌服務來儲存。

2、審計採用的元件,及安全鏈路順序的保障

  首先,我們來明確一下各元件在請求中的執行順序,如下圖,依次是 Filter -> Interceptor -> ControllerAdvice -> AOP -> Controller

  對於Filter之間,我們可以使用@Order註解來確定執行順序;對於Interceptor之間根據註冊的先後順序執行。這裡我們的審計功能選擇Filter和Interceptor都可以,根據自己的喜好即可。

3、實現審計功能

  3.1、審計日誌類及持久層介面

/**
 * 審計日誌
 */
@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
 */
public interface AuditLogRepository extends JpaRepositoryImplementation<AuditLogDO,Long> {
}

  3.2、開啟JPA審計功能配置

/**
 * JPA相關配置
 */
@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)

/**
 * 審計過濾器
 */
@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同時只能使用一個
 */
@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配置類
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {


    @Resource
    private AuditLogInterceptor auditLogInterceptor;

    /**
     * 註冊攔截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(auditLogInterceptor);
    }

}

  3.6.3、進行3.5的測試效果相同