java 与Spring Security整合用的关于审计日志记录的Filter
由于需要统一记录素有 api 请求操作,所以开发日志相关模块。
大概思路
1.通过 filter 将 部分 LogEvent 信息写入到 HttpServletRequest
2.HandlerInterceptorAdapter 获取到步骤一中的 LogEvent 信息,以及请求资源中的 Annotation 信息整理成最终的
3.applicationEventPublisher.publishEvent(event);
4.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("*"); } }