API安全(八)-審計
1、審計所在安全鏈路的位置,為什麼
如圖所示,審計應該做在認證之後,授權之前。因為只有在認證之後,我們在記錄日誌的時候,在知道請求是那個使用者發過來的;做在授權之前,哪些請求被拒絕了,在響應的時候,也可以把它記錄下來。如果放到授權之後 ,那麼被拒絕的請求就不能記錄了。
審計日誌一定要持久化,方便我們對問題的追溯,可以把它放到資料庫中,也可以寫到磁碟中。實際工作中,一般會發送到公司統一的日誌服務上,由日誌服務來儲存。
2、審計採用的元件,及安全鏈路順序的保障
首先,我們來明確一下各元件在請求中的執行順序,如下圖,依次是 Filter -> Interceptor -> ControllerAdvice -> AOP -> Controller
對於Filter之間,我們可以使用@Order註解來確定執行順序;對於Interceptor之間根據註冊的先後順序執行。這裡我們的審計功能選擇Filter和Interceptor都可以,根據自己的喜好即可。
3、實現審計功能
/** * 審計日誌 */ @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的測試效果相同