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("*");
    }
}

  

posted @ 2020-04-26 12:31  hujunmin  阅读(792)  评论(0编辑  收藏  举报