由于需要统一记录素有 api 请求操作,所以开发日志相关模块。
大概思路
1.通过 filter 将 部分 LogEvent 信息写入到 HttpServletRequest
2.HandlerInterceptorAdapter 获取到步骤一中的 LogEvent 信息,以及请求资源中的 Annotation 信息整理成最终的
3.LogEventHandler 中,通过 EventListener 来持久化数据到 db
代码:
LogEntity.java
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.util.Date;
@ApiModel("系统日志(一般用在列表中)")
public class LogEntity {
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.id
*
* @mbg.generated
*/
@ApiModelProperty("主键 ID")
private String id;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.event_id
*
* @mbg.generated
*/
@ApiModelProperty("事件唯一ID")
private String eventId;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.thread_id
*
* @mbg.generated
*/
@ApiModelProperty("操作会话ID")
private String threadId;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.operation_time
*
* @mbg.generated
*/
@ApiModelProperty("操作时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date operationTime;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.operator_username
*
* @mbg.generated
*/
@ApiModelProperty("操作账号")
private String operatorUsername;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.operator_real_name
*
* @mbg.generated
*/
@ApiModelProperty("操作人")
private String operatorRealName;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.category
*
* @mbg.generated
*/
@ApiModelProperty("操作类型")
private String category;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.target
*
* @mbg.generated
*/
@ApiModelProperty("操作目标")
private String target;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.in_params
*
* @mbg.generated
*/
@ApiModelProperty("操作入参")
private String inParams;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.out_params
*
* @mbg.generated
*/
@ApiModelProperty("操作出参")
private String outParams;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.outcome
*
* @mbg.generated
*/
private Integer outcome;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.operator_perm
*
* @mbg.generated
*/
private String operatorPerm;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.description
*
* @mbg.generated
*/
@ApiModelProperty("操作描述")
private String description;
/**
*
* This field was generated by MyBatis Generator.
* This field corresponds to the database column audit_log.source_ip
*
* @mbg.generated
*/
private String sourceIp;
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.id
*
* @return the value of audit_log.id
*
* @mbg.generated
*/
public String getId() {
return id;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.id
*
* @param id the value for audit_log.id
*
* @mbg.generated
*/
public void setId(String id) {
this.id = id;
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.event_id
*
* @return the value of audit_log.event_id
*
* @mbg.generated
*/
public String getEventId() {
return eventId;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.event_id
*
* @param eventId the value for audit_log.event_id
*
* @mbg.generated
*/
public void setEventId(String eventId) {
this.eventId = eventId == null ? null : eventId.trim();
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.thread_id
*
* @return the value of audit_log.thread_id
*
* @mbg.generated
*/
public String getThreadId() {
return threadId;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.thread_id
*
* @param threadId the value for audit_log.thread_id
*
* @mbg.generated
*/
public void setThreadId(String threadId) {
this.threadId = threadId == null ? null : threadId.trim();
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.operation_time
*
* @return the value of audit_log.operation_time
*
* @mbg.generated
*/
public Date getOperationTime() {
return operationTime;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.operation_time
*
* @param operationTime the value for audit_log.operation_time
*
* @mbg.generated
*/
public void setOperationTime(Date operationTime) {
this.operationTime = operationTime;
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.operator_username
*
* @return the value of audit_log.operator_username
*
* @mbg.generated
*/
public String getOperatorUsername() {
return operatorUsername;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.operator_username
*
* @param operatorUsername the value for audit_log.operator_username
*
* @mbg.generated
*/
public void setOperatorUsername(String operatorUsername) {
this.operatorUsername = operatorUsername == null ? null : operatorUsername.trim();
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.operator_real_name
*
* @return the value of audit_log.operator_real_name
*
* @mbg.generated
*/
public String getOperatorRealName() {
return operatorRealName;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.operator_real_name
*
* @param operatorRealName the value for audit_log.operator_real_name
*
* @mbg.generated
*/
public void setOperatorRealName(String operatorRealName) {
this.operatorRealName = operatorRealName == null ? null : operatorRealName.trim();
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.category
*
* @return the value of audit_log.category
*
* @mbg.generated
*/
public String getCategory() {
return category;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.category
*
* @param category the value for audit_log.category
*
* @mbg.generated
*/
public void setCategory(String category) {
this.category = category == null ? null : category.trim();
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.target
*
* @return the value of audit_log.target
*
* @mbg.generated
*/
public String getTarget() {
return target;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.target
*
* @param target the value for audit_log.target
*
* @mbg.generated
*/
public void setTarget(String target) {
this.target = target == null ? null : target.trim();
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.in_params
*
* @return the value of audit_log.in_params
*
* @mbg.generated
*/
public String getInParams() {
return inParams;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.in_params
*
* @param inParams the value for audit_log.in_params
*
* @mbg.generated
*/
public void setInParams(String inParams) {
this.inParams = inParams == null ? null : inParams.trim();
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.out_params
*
* @return the value of audit_log.out_params
*
* @mbg.generated
*/
public String getOutParams() {
return outParams;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.out_params
*
* @param outParams the value for audit_log.out_params
*
* @mbg.generated
*/
public void setOutParams(String outParams) {
this.outParams = outParams == null ? null : outParams.trim();
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.outcome
*
* @return the value of audit_log.outcome
*
* @mbg.generated
*/
public Integer getOutcome() {
return outcome;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.outcome
*
* @param outcome the value for audit_log.outcome
*
* @mbg.generated
*/
public void setOutcome(Integer outcome) {
this.outcome = outcome;
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.operator_perm
*
* @return the value of audit_log.operator_perm
*
* @mbg.generated
*/
public String getOperatorPerm() {
return operatorPerm;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.operator_perm
*
* @param operatorPerm the value for audit_log.operator_perm
*
* @mbg.generated
*/
public void setOperatorPerm(String operatorPerm) {
this.operatorPerm = operatorPerm == null ? null : operatorPerm.trim();
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.description
*
* @return the value of audit_log.description
*
* @mbg.generated
*/
public String getDescription() {
return description;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.description
*
* @param description the value for audit_log.description
*
* @mbg.generated
*/
public void setDescription(String description) {
this.description = description == null ? null : description.trim();
}
/**
* This method was generated by MyBatis Generator.
* This method returns the value of the database column audit_log.source_ip
*
* @return the value of audit_log.source_ip
*
* @mbg.generated
*/
public String getSourceIp() {
return sourceIp;
}
/**
* This method was generated by MyBatis Generator.
* This method sets the value of the database column audit_log.source_ip
*
* @param sourceIp the value for audit_log.source_ip
*
* @mbg.generated
*/
public void setSourceIp(String sourceIp) {
this.sourceIp = sourceIp == null ? null : sourceIp.trim();
}
}
LogRepository.java
import com.dimpt.common.log.command.SearchLogCommand;
import com.dimpt.common.log.entity.LogEntity;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface LogRepository {
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table audit_log
*
* @mbg.generated
*/
int deleteByPrimaryKey(Long id);
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table audit_log
*
* @mbg.generated
*/
int insert(LogEntity record);
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table audit_log
*
* @mbg.generated
*/
LogEntity selectByPrimaryKey(Long id);
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table audit_log
*
* @mbg.generated
*/
List<LogEntity> selectAll();
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table audit_log
*
* @mbg.generated
*/
int updateByPrimaryKey(LogEntity record);
List<LogEntity> selectIf(@Param("query") SearchLogCommand query);
LogEntity selectByEventId(String eventId);
}
LogService.java
import com.dimpt.common.log.application.OperationOutcome;
import com.dimpt.common.log.command.SearchLogCommand;
import com.dimpt.common.log.entity.LogEntity;
import com.dimpt.common.log.repository.LogRepository;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.apache.commons.lang3.EnumUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class LogService {
@Autowired
private LogRepository auditLogRepository;
public boolean insertAuditLog(LogEntity auditLog)
{
return auditLogRepository.insert(auditLog)>0;
}
public PageInfo<LogEntity> listAuditLogs(SearchLogCommand command, int page, int limit)
{
PageHelper.startPage(page,limit);
List<LogEntity> list= auditLogRepository.selectIf(command);
return new PageInfo<>(list);
}
/**
* 通过事件id查询
* @param eventId 事件id
* @return
*/
public LogEntity selectByEventId(String eventId)
{
return auditLogRepository.selectByEventId(eventId);
}
/**
* 查询所有操作结果List
* @return
*/
public List<Map<String, Object>> getOutcomeList()
{
List<OperationOutcome> enumList = EnumUtils.getEnumList(OperationOutcome.class);
List<Map<String,Object>> data = new ArrayList<>();
enumList.forEach(item->{
Map<String,Object> map = new HashMap<>();
map.put("value",item.value());
map.put("description",item.description());
data.add(map);
});
return data;
}
}
LogEvent.java
import com.dimpt.common.util.IdUtils;
import java.io.Serializable;
import java.time.ZonedDateTime;
/**
* 审计事件类
*
* <p>创建人:</p>
* <p>创建时间:2018年11月8日 14:31</p>
*/
public class LogEvent implements Serializable {
private static final long serialVersionUID = -2147518420420074858L;
/**
* 事件ID。默认会使用UUID自动生成
*/
private String id = IdUtils.nextId(IdUtils.PREFIX_EVENT_ID);
/**
* 操作会话ID。必填。所有相关的操作应共享同一个操作会话ID。若不提供将会自动生成AuditEventHandler
*/
private String threadId = IdUtils.nextId(IdUtils.PREFIX_EVENT_THREAD_ID);
/**
* 事件发生时间。默认会使用当前事件
*/
private ZonedDateTime time = ZonedDateTime.now();
/**
* 事件关联操作者。选填。对于系统在处理流程上拆分的操作,请填写“SYSTEM”
*/
private String username;
/**
* 事件关联操作者真实姓名。选填。对于系统在处理流程上拆分的操作,请填写“SYSTEM”
*/
private String realName;
/**
* 操作分类。必填。请根据操作所在的模块填写
*/
private String category;
/**
* 操作对象。必填,对于一般请求,可选取值为LOGIN/LOGOUT/接口调用的目标URL。对于系统在处理流程上拆分的操作,请根据业务逻辑取值
*/
private String target;
/**
* 操作对象描述。必填,对于一般请求,可选的取值为登录/登出/接口调用目标的描述。对于系统在处理流程上拆分的操作,请根据业务逻辑填写具体的描述
*/
private String description;
/**
* 操作结果,必填
*/
private OperationOutcome outcome;
/**
* 操作者权限列表(逗号分隔)。选填,仅对一般请求日志有效
*/
private String ownedPermissions;
/**
* 操作入参。选填
*/
private String inParams;
/**
* 操作出参。选填
*/
private String outParams;
/**
* 来源IP。选填。在登录/登出/一般请求时应填写发起请求的IP地址。其余情况应留空
*/
private String sourceIp;
public String getId() {
return id;
}
protected void setId(String id) {
this.id = id;
}
public ZonedDateTime getTime() {
return time;
}
public void setTime(ZonedDateTime time) {
this.time = time;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public OperationOutcome getOutcome() {
return outcome;
}
public void setOutcome(OperationOutcome outcome) {
this.outcome = outcome;
}
public String getOwnedPermissions() {
return ownedPermissions;
}
public void setOwnedPermissions(String ownedPermissions) {
this.ownedPermissions = ownedPermissions;
}
public void setSourceIp(String sourceIp) {
this.sourceIp = sourceIp;
}
public String getSourceIp() {
return sourceIp;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getInParams() {
return inParams;
}
public void setInParams(String inParams) {
this.inParams = inParams;
}
public String getOutParams() {
return outParams;
}
public void setOutParams(String outParams) {
this.outParams = outParams;
}
public String getThreadId() {
return threadId;
}
public void setThreadId(String threadId) {
this.threadId = threadId;
}
@Override
public String toString() {
return "AuditEvent{" +
"id=‘" + id + ‘\‘‘ +
", threadId=‘" + threadId + ‘\‘‘ +
", time=" + time +
‘}‘;
}
}
LogFilter.java
import com.dimpt.common.util.IpUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.NestedServletException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.time.ZonedDateTime;
import java.util.*;
/**
* 与Spring Security整合用的关于审计日志记录的Filter
* <br>
* 此Filter的注册位置位于登录Filter({@link org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter})之前,
* 用于对所有进入系统的请求进行预处理和后处理
* <br>
* 预处理部分包括根据请求URL构建对应的 {@link LogEvent} 实例
* 后处理部分包括根据请求的处理结果发布实际的审计事件。审计事件会进一步被其余处理器进一步处理
* <br>
* 预处理和后处理之间会有额外的处理工作。该工作将交由其它协同类进行处理
*
* <p>创建人:</p>
* <p>创建时间:2018年11月8日 14:31</p>
*/
public class LogFilter extends OncePerRequestFilter implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
/**
* 获取全部的接口信息
*/
private List<RequestMappingHandlerMapping> handlerMappings;
/**
* Spring Security用于处理登录的请求地址
*/
private RequestMatcher loginRequestMatcher = new AntPathRequestMatcher("/login");
/**
* Spring Security用于处理登出的请求地址
*/
private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/logout");
/**
* 普通请求的URL匹配器。默认为所有请求拦截
*/
private RequestMatcher genericRequestMatcher = AnyRequestMatcher.INSTANCE;
/**
* 设置用于处理登录的URL地址(仅用作检测)。默认为/login
*
* @param loginProcessingUrl 登录处理URL
*
* <p>创建人:陈柱辉</p>
* <p>创建时间:2018年11月8日 14:31</p>
*/
public void setLoginProcessingUrl(String loginProcessingUrl) {
this.loginRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl);
}
/**
* 设置用于处理登出的URL地址(仅用作检测)。默认为/logout
*
* @param logoutProcessingUrl 登出处理URL
*
* <p>创建人:陈柱辉</p>
* <p>创建时间:2018年11月8日 14:31</p>
*/
public void setLogoutProcessingUrl(String logoutProcessingUrl) {
this.logoutRequestMatcher = new AntPathRequestMatcher(logoutProcessingUrl);
}
/**
* 设置常规接口请求的请求匹配器。默认为匹配所有请求
*
* @param genericRequestMatcher 常规接口的请求匹配器
* <p>创建人:陈柱辉</p>
* <p>创建时间:2018年11月8日 14:31</p>
*/
public void setGenericRequestMatcher(RequestMatcher genericRequestMatcher) {
this.genericRequestMatcher = genericRequestMatcher;
}
@Autowired(required = false)
public void setHandlerMappings(List<RequestMappingHandlerMapping> handlerMappings) {
this.handlerMappings = handlerMappings;
}
@SuppressWarnings("ConstantConditions")
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
LogEvent event;
if (loginRequestMatcher.matches(httpServletRequest)) {
event = new LogEvent();
event.setCategory("登录/登出");
event.setTarget("LOGIN");
event.setDescription("登录");
event.setTime(ZonedDateTime.now());
} else if (logoutRequestMatcher.matches(httpServletRequest)) {
event = new LogEvent();
event.setCategory("登录/登出");
event.setTarget("LOGOUT");
event.setDescription("登出");
event.setTime(ZonedDateTime.now());
} else if (genericRequestMatcher.matches(httpServletRequest)) {
event = new LogEvent();
event.setTarget(httpServletRequest.getMethod() + " " + httpServletRequest.getRequestURI());
event.setTime(ZonedDateTime.now());
} else {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
event.setSourceIp(IpUtils.getIpAddress(httpServletRequest));
// 由于审计日志需要记录入参和出参的情况,所以这里需要对请求和响应对象做响应的封装
// 对请求的封装是因为提取入参时会消耗输入流,为了让输入流能被重复读取,需要进行封装
// 对响应的封装也是类似的
CachedHttpServletRequestWrapper req = new CachedHttpServletRequestWrapper(httpServletRequest);
CachedHttpServletResponseWrapper resp = new CachedHttpServletResponseWrapper(httpServletResponse);
// 将当前登录的用户信息写入到AuditEvent中
Object principal =
Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Authentication::getPrincipal)
.orElse(null);
if (principal != null) {
if (principal instanceof User) {
User user = (User) principal;
event.setUsername(user.getUsername());
// if (principal instanceof AdminUserSecurityPrincipal) {
// event.setRealName(
// Optional.of((AdminUserSecurityPrincipal) principal)
// .map(AdminUserSecurityPrincipal::getDetails).map(AdminUserDto::getRealName).orElse(null));
// }
} else {
event.setUsername(principal.toString());
}
event.setOwnedPermissions(StringUtils.join(AuthorityUtils.authorityListToSet(SecurityContextHolder.getContext().getAuthentication().getAuthorities()), ‘,‘));
}
httpServletRequest.setAttribute(Constant.AUDIT_EVENT_OBJECT_NAME, event);
// 捕获后续处理过程中出现的异常。对于后续处理中outcome未进行设置的情况下,根据捕获到的异常信息对outcome进行默认的设置
Throwable thrownTh = null;
try {
filterChain.doFilter(req, resp);
} catch (Throwable ex) {
thrownTh = ex;
}
event = (LogEvent) req.getAttribute(Constant.AUDIT_EVENT_OBJECT_NAME);
auditingPostHandling(event, req, resp, thrownTh);
applicationEventPublisher.publishEvent(event);
// 重新抛出异常
if (thrownTh != null) {
if (thrownTh instanceof ServletException) {
throw (ServletException) thrownTh;
} else if (thrownTh instanceof IOException) {
throw (IOException) thrownTh;
} else {
// 由于doFilter只能抛出已声明的checked exception或者unchecked exception,
// 因此此处只可能是RuntimeException或其子类
throw (RuntimeException) thrownTh;
}
}
}
private void auditingPostHandling(LogEvent event, CachedHttpServletRequestWrapper req, CachedHttpServletResponseWrapper resp, Throwable thrownTh) {
// 对于接口的分类及目标描述的部分而言,由于它们高度依赖于SpringMVC的拦截器来从swagger的注解中提取信息,
// 所以对于因为未登录或者权限不足等情况导致请求在filter层面就已经被拒绝处理的情况,会没有办法拿到接口的分类
// 及目标描述
// ApiAuditingInfoHolder类在这个事情上获取了全部 request handler的信息。从这个信息中可以再次尝试找到接口的
// 相关描述
if (StringUtils.isEmpty(event.getDescription())) {
Object handler = lookupHandler(req);
if (handler instanceof HandlerMethod) {
// 能找到接口定义。从接口定义中再次尝试抽取描述
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
Class<?> declaringClass = method.getDeclaringClass();
Api apiAnnotation = declaringClass.getAnnotation(Api.class);
ApiOperation operationAnnotation = method.getAnnotation(ApiOperation.class);
if (apiAnnotation != null && apiAnnotation.tags().length > 0) {
event.setCategory(apiAnnotation.tags()[0]);
}
if (operationAnnotation != null) {
event.setDescription(operationAnnotation.value());
}
} else {
// 其余情况,作为未知接口显示
event.setCategory("未知");
event.setDescription("接口未定义");
}
}
// 关于结果部分的处理。当事件的结果未设置时,会根据是否捕获到异常、异常的类型来确定事件的结果
if (event.getOutcome() == null) {
if (thrownTh == null) {
event.setOutcome(OperationOutcome.NORMAL);
} else {
event.setOutcome(OperationOutcome.INVOCATION_ERROR);
}
}
// 入参出参部分
// 入参有点小特殊,除了post body以外,query param也是很重要的一部分
StringBuilder sb = new StringBuilder("Query:\n");
reconstructQueryParam(req, sb);
sb.append("\n\nBody:\n");
byte[] body = req.getReadData();
if (body.length == 0) {
sb.append("<无>");
} else {
sb.append(new String(body));
}
event.setInParams(sb.toString());
// 当出参数据太庞大的时候,为了避免写入过长数据(从而影响数据库写入),出参部分以“出参数据量过大未作记录”标识
// 当前的阈值暂时写死65K(2^16次方)
// TODO: 后期如果要解决,解决方案应考虑以非关系数据库的形式保存数据,例如以文件的形式保存出入参,并提供出下载链接。
// 但需要注意如果网关以集群的形式进行部署,那么文件的保存也得支持这种分布式才行
/* if (resp.getWrittenData().length >= (1<<16)) {
logger.warn(String.format("注意:由于出参数据量过于庞大(合计 %.2fKB),本次日志未记录出参", resp.getWrittenData().length / 1024.0f));
event.setOutParams("出参数据量过大未作记录");
}*/
//输出参数大于20k做特殊处理
if (resp.getWrittenData().length >= 10<<11) {
logger.warn(String.format("注意:由于出参数据量过于庞大(合计 %.2fKB),本次日志未记录出参", resp.getWrittenData().length / 1024.0f));
event.setOutParams("出参数据量过大未作记录");
}
else {
event.setOutParams(new String(resp.getWrittenData()));
}
}
private void reconstructQueryParam(HttpServletRequest httpServletRequest, StringBuilder sb) {
Map<String, String[]> params = httpServletRequest.getParameterMap();
if (params.size() == 0) {
sb.append("<无>");
}
Iterator<Map.Entry<String, String[]>> iter = params.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, String[]> entry = iter.next();
String key = entry.getKey();
String[] values = entry.getValue();
for (int i = 0; i < values.length; ++i) {
if (i != 0) {
sb.append(‘&‘);
}
sb.append(key).append(‘=‘).append(values[i]);
}
if (iter.hasNext()) {
sb.append(‘&‘);
}
}
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.applicationEventPublisher = publisher;
}
/**
* 对传入的 {@link Throwable} 进行分析,找出导致异常的真正原因
* <br>
* 当前的分析原则:
* 1. 对于 {@link NestedServletException},取它的cause进行进一步分析
* 2. 对于 {@link RuntimeException},如果它的cause非空,去它的cause进行进一步分析
* 3. 重复上述步骤。如果最后导致无法找到实际的目标异常(例如一直上溯到了异常链的顶部,最终的
* cause为空),那么返回传入的异常
*/
private Throwable getRealRootCause(Throwable th) {
Throwable realTh = th;
while (realTh != null) {
if (realTh instanceof NestedServletException) {
realTh = realTh.getCause();
} else if (realTh instanceof RuntimeException) {
if (realTh.getCause() != null) {
realTh = realTh.getCause();
} else {
break;
}
} else {
break;
}
}
if (realTh == null) {
realTh = th;
}
return realTh;
}
private Object lookupHandler(HttpServletRequest request) {
if (this.handlerMappings == null) {
return null;
}
return handlerMappings.stream()
.map(hm -> {
try {
HandlerExecutionChain chain = hm.getHandler(request);
return chain == null ? null : chain.getHandler();
} catch (Exception e) {
// 不用管,返回null即可
return null;
}
})
.filter(Objects::nonNull)
.findFirst().orElse(null);
}
}
LogEventHandler.java
import com.dimpt.common.log.entity.LogEntity;
import com.dimpt.common.log.service.LogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.Date;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
/**
* 审计事件处理器
*
* <p>创建人:陈柱辉</p>
* <p>创建时间:2018年11月8日 14:31</p>
*/
@Component
public class LogEventHandler {
private static final Logger auditLog = LoggerFactory.getLogger("audit");
@Resource
private LogService auditLogService;
/**
* 将审计事件写入到日志文件
*
* @param event 审计事件
* <p>创建人:</p>
* <p>创建时间:2018年11月8日 14:31</p>
*/
@EventListener(LogEvent.class)
public void writeAuditEventToLog(LogEvent event) {
// 写审计日志文件。这部分应该和整个请求的处理流程串行处理,保证审计日志在整体日志中的连续性
// 由于是串行,同时需要保证万一日志出现问题,不应该影响整个系统的运行,但出现此种错误时应
// 尽快排查原因
try {
if (event.getOutcome() == null) {
if (auditLog.isWarnEnabled()) {
auditLog.warn(constructAuditLogFromEvent(event));
}
} else {
if (auditLog.isInfoEnabled()) {
auditLog.info(constructAuditLogFromEvent(event));
}
}
} catch (Exception e) {
auditLog.warn("写入审计日志时发生错误。请联系开发人员尽快排查出错原因并修复,以免影响审计记录", e);
}
}
/**
* 审计事件写入数据库的处理器。写入的处理是异步进行的
*
* @param event 审计事件
* <p>创建人:陈柱辉</p>
* <p>创建时间:2018年11月8日 14:31</p>
*/
@Async
@EventListener(LogEvent.class)
public void writeAuditEventToDb(LogEvent event) {
// 将审计日志保存至数据库中。这部分脱离请求处理流程,异步处理,以提高系统对请求的处理能力
LogEntity log = new LogEntity();
log.setEventId(event.getId());
log.setThreadId(event.getThreadId());
log.setDescription(event.getDescription());
log.setOperationTime(Date.from(event.getTime().toInstant()));
log.setOperatorPerm(event.getOwnedPermissions());
log.setOperatorUsername(event.getUsername());
log.setOperatorRealName(event.getRealName());
log.setCategory(event.getCategory());
log.setTarget(event.getTarget());
log.setSourceIp(event.getSourceIp());
log.setOutcome(event.getOutcome().ordinal());
log.setInParams(event.getInParams());
log.setOutParams(event.getOutParams());
auditLogService.insertAuditLog(log);
}
private String constructAuditLogFromEvent(LogEvent event) {
return String.format("" +
"审计事件:%s\n" +
"会话ID:%s\n" +
"时间:%s\n" +
"来源IP:%s\n"+
"操作人:%s(%s)\n" +
"操作人权限:%s\n" +
"操作:%s\n" +
"操作描述:%s\n" +
"操作结果:%s",
event.getId(),
event.getThreadId(),
zonedDateTimeString(event.getTime()),
getStringOrDefault(event.getSourceIp(), "-"),
nameString(event.getUsername()), nameString(event.getRealName()),
getStringOrDefault(event.getOwnedPermissions()),
getStringOrDefault(event.getTarget()),
getStringOrDefault(event.getDescription(), "-"),
outcomeString(event.getOutcome()));
}
private static String zonedDateTimeString(ZonedDateTime value) {
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(value);
}
private static String nameString(String value) {
return getStringOrDefault(value, "-");
}
private static String getStringOrDefault(String value) {
return getStringOrDefault(value, "无");
}
private static String getStringOrDefault(String value, String nullValue) {
return value != null ? value : nullValue;
}
private static String outcomeString(OperationOutcome value) {
if (value == null) {
return "未知";
}
switch (value) {
case NORMAL:
return "正常";
case LOGIN_REQUIRED:
return "需要登录";
case ACCESS_DENIED:
return "拒绝访问";
case INVOCATION_ERROR:
return "处理异常";
}
return "未知";
}
}
LogExceptionCallback.java
import com.dimpt.common.handler.ExceptionTranslationControllerAdvice;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import javax.servlet.http.HttpServletRequest;
/**
* 用于处理审计的异常回调。详见 {@link ExceptionTranslationControllerAdvice.ExceptionCallback}
* <br>
* <p>创建人:</p>
* <p>创建时间:2018年11月09日 16:56</p>
*/
@Component
public class LogExceptionCallback implements ExceptionTranslationControllerAdvice.ExceptionCallback {
@Override
public void onException(Exception ex, WebRequest request) {
if (!(request instanceof ServletWebRequest)) {
return;
}
HttpServletRequest req = ((ServletWebRequest) request).getRequest();
LogEvent event = (LogEvent) req.getAttribute(Constant.AUDIT_EVENT_OBJECT_NAME);
if (event != null) {
event.setOutcome(OperationOutcome.INVOCATION_ERROR);
}
}
}
Constant.java
/**
* 常量定义
*
* <p>创建人:</p>
* <p>创建时间:2018年11月8日 14:31</p>
*/
public interface Constant {
/**
* 审计模块用于保存请求中审计事件对象的名称
*/
String AUDIT_EVENT_OBJECT_NAME = "_ORCHESTRATOR_AUDIT_EVENT_OBJECT";
}
CachedHttpServletResponseWrapper.java
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* 对 {@link HttpServletResponse} 进行封装,对输出流的写入进行记录然后委派,并且允许获取写入到输入流的内容
* <p>创建人:</p>
* <p>创建时间:2018年11月19日 11:07</p>
*/
public class CachedHttpServletResponseWrapper extends HttpServletResponseWrapper {
private final CachedOutputStream caughtOuput;
public CachedHttpServletResponseWrapper(HttpServletResponse response) throws IOException {
super(response);
caughtOuput = new CachedOutputStream(response.getOutputStream());
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return caughtOuput;
}
public byte[] getWrittenData() {
return caughtOuput.getWritten().toByteArray();
}
private class CachedOutputStream extends ServletOutputStream {
private final ServletOutputStream delegatingOutputStream;
private final ByteArrayOutputStream written;
public CachedOutputStream(ServletOutputStream outputStream) {
this.delegatingOutputStream = outputStream;
this.written = new ByteArrayOutputStream();
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener listener) {
this.delegatingOutputStream.setWriteListener(listener);
}
@Override
public void write(int b) throws IOException {
this.written.write(b);
delegatingOutputStream.write(b);
}
public ByteArrayOutputStream getWritten() {
return written;
}
}
}
CachedHttpServletRequestWrapper.java
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* 对 {@link HttpServletRequest} 进行封装,记录输入流中的数据,并允许在后续过程中重新读出
* <p>创建人:</p>
* <p>创建时间:2018年11月19日 10:42</p>
*/
public class CachedHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final CachedInputStream cachedInputStream;
public CachedHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.cachedInputStream = new CachedInputStream(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() throws IOException {
return cachedInputStream;
}
public byte[] getReadData() {
return this.cachedInputStream.getCachedInput().toByteArray();
}
/**
* 将输入流缓存的辅助类
* <p>创建人:陈柱辉</p>
* <p>创建时间:2018年11月19日 10:57</p>
*/
public static class CachedInputStream extends ServletInputStream {
private final ServletInputStream delegatingInputStream;
private final ByteArrayOutputStream cachedInput;
public CachedInputStream(ServletInputStream delegatingInputStream) {
this.delegatingInputStream = delegatingInputStream;
this.cachedInput = new ByteArrayOutputStream();
}
@Override
public boolean isFinished() {
return this.delegatingInputStream.isFinished();
}
@Override
public boolean isReady() {
return this.delegatingInputStream.isReady();
}
@Override
public void setReadListener(ReadListener listener) {
this.delegatingInputStream.setReadListener(listener);
}
@Override
public int read() throws IOException {
int read = this.delegatingInputStream.read();
if (read != -1) {
this.cachedInput.write(read);
}
return read;
}
public ByteArrayOutputStream getCachedInput() {
return cachedInput;
}
}
}
WebSecurityConfig.java
import com.dimpt.common.log.application.LogFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/**
* @Description
* @Author 胡俊敏
* @Date 2019/10/25 14:33
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//注解开启在方法上的保护功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public LogFilter auditLoggingFilter() {
LogFilter filter = new LogFilter();
filter.setLoginProcessingUrl("/api/login");
filter.setLogoutProcessingUrl("/api/logout");
filter.setGenericRequestMatcher(new AntPathRequestMatcher("/api/**"));
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置哪些请求需要验证
http.csrf().disable()
.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(auditLoggingFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
FilterConfig.java
import com.dimpt.common.filter.OperaterFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Autowired
private OperaterFilter operatorFilter;
@Bean
public FilterRegistrationBean registerOperatorFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(operatorFilter);
registration.addUrlPatterns("/*");
registration.setName("operatorFilter");
registration.setOrder(1); //值越小,Filter越靠前。
return registration;
}
}
SpringMvcConfigurer.java
import com.dimpt.common.log.application.HandlerMethodAuditingInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SpringMvcConfigurer implements WebMvcConfigurer {
@Bean
public HandlerMethodAuditingInterceptor requestHandlerAuditingInterceptor() {
return new HandlerMethodAuditingInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestHandlerAuditingInterceptor());
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("*")
.allowedMethods("*")
.allowedOrigins("*");
}
}
java 与Spring Security整合用的关于审计日志记录的Filter
原文:https://www.cnblogs.com/hujunmin/p/12778737.html