API 安全机制 | 审计日志
2020-07-12 本文已影响0人
乌鲁木齐001号程序员
审计日志
- 审计在认证之后,授权之前;
- 在认证之后,才知道是谁;
- 在授权之前,才能记录下被授权机制拒绝的请求;
- 审计日志一定要持久化,数据库或文件中,一般会直接发到统一的的日志服务上,由日志服务去记日志;
- 审计日志要做两次,一次是在进入的时候,一次是在出去的时候;
Spring 的拦截机制
Spring 的拦截机制.png- Filter 不是 Spring 的机制,而是 Servlet 规范中定义的;
- ControllerAdvice 一般做全局的异常处理;
Filter 和 Intercepor 的区别
- Filter 对请求进入业务逻辑之前和之后的拦截是在一个方法 doFilter 中完成的,如果下游的业务方法在执行的时候出现异常,在 Filter 中是拦截不到的;
- Interceptor 对请求进入业务逻辑之前和之后的拦截是在多个方法 preHandle、postHandle、afterCompletion 中完成的,如果在下游的业务方法在执行的时候出现异常,可以在 aflterCompletion 中拦截到;
- 所有的 Filter 都在 Interceptor 之前执行;
审计日志 | 实现思路
- 先确定审计对象,也就是表结构,因为这里的方案是将审计日志记录在数据库中;
- 再写一个拦截器,在请求的前后,向审计日志保存在数据库中;
- 完了把拦截器添加到 Spring 的上下文中;
- 在配置类上启用 @EnableJpaAuditing 注解,在审计类上启用 @CreatedBy 注解,并创建一个为 @CreatedBy 服务的 Bean,用来获取当前修改数据的人;
- 通过 @RestControllerAdvice 标注的类,解决在业务方法执行异常的时候,会重定向到 /error,从而导致 2 条审计记录的产生;
审计类
package com.lixinlei.security.api.entity;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.Data;
@Entity
@Data
@EntityListeners(AuditingEntityListener.class)
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Temporal(TemporalType.TIMESTAMP)
@CreatedDate
private Date createdTime;
@Temporal(TemporalType.TIMESTAMP)
@LastModifiedDate
private Date modifyTime;
@CreatedBy
private String username;
private String method;
private String path;
private Integer status;
}
审计日志拦截器
package com.lixinlei.security.api.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.lixinlei.security.api.entity.AuditLog;
import com.lixinlei.security.api.dao.AuditLogRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
@Component
public class AuditLogInterceptor extends HandlerInterceptorAdapter {
@Autowired
private AuditLogRepository auditLogRepository;
/**
* 在业务方法执行之前执行
* postHandle 是在业务方法执行成功之后执行
* @param request
* @param response
* @param handler
* @return 如果返回 false,就可以拒绝请求的执行,直接返回
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println(3);
AuditLog log = new AuditLog();
log.setMethod(request.getMethod());
log.setPath(request.getRequestURI());
// 保存日志
auditLogRepository.save(log);
request.setAttribute("auditLogId", log.getId());
return true;
}
/**
* 业务方法处理成功与否都会执行
* @param request
* @param response
* @param handler
* @param ex 如果业务方法处理成功,就不会有这个异常
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex)
throws Exception {
Long auditLogId = (Long) request.getAttribute("auditLogId");
AuditLog log = auditLogRepository.findById(auditLogId).get();
// 就算业务方法执行失败,这里的 status 还是 200,然后会跳到一个 /error 的路径,这个的 status 是 500
log.setStatus(response.getStatus());
// 更新日志
auditLogRepository.save(log);
}
}
添加拦截器到 Spring 中
package com.lixinlei.security.api.config;
import java.util.Optional;
import com.lixinlei.security.api.interceptor.AuditLogInterceptor;
import com.lixinlei.security.api.vo.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
// 这个注解和 @EntityListeners(AuditingEntityListener.class) 没有的话,@CreatedBy 就不知道把谁注入到其标注的属性中
@EnableJpaAuditing
public class SecurityConfig implements WebMvcConfigurer {
@Autowired
private AuditLogInterceptor auditLogInterceptor;
/**
* 先添加的拦截器先生效
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(auditLogInterceptor);
}
/**
* 这个 Bean 是用来让 @CreatedBy 知道是谁修改了数据
* @CreatedBy 标注的属性的类型是 String,这里 AuditorAware 的泛型就用 String
* @return
*/
@Bean
public AuditorAware<String> auditorAware() {
return new AuditorAware<String>() {
@Override
public Optional<String> getCurrentAuditor() {
ServletRequestAttributes servletRequestAttributes
= (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
UserInfo info = (UserInfo)servletRequestAttributes.getRequest().getSession().getAttribute("user");
String username = null;
if(info != null) {
username = info.getUsername();
}
return Optional.ofNullable(username);
}
};
}
}
@RestControllerAdvice
package com.lixinlei.security.api.controller.advice;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import lombok.extern.slf4j.Slf4j;
@RestControllerAdvice
@Slf4j
public class ErrorHandler {
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public Map<String, Object> handle(Exception ex){
log.error("system error", ex);
Map<String, Object> info = new HashMap<>();
info.put("message", ex.getMessage());
info.put("time", new Date().getTime());
return info;
}
}