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