LogInterceptor.java 11.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/*
 * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package top.charles7c.cnadmin.monitor.interceptor;

19
import java.time.LocalDateTime;
20 21 22 23 24 25 26 27 28 29 30
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import io.swagger.v3.oas.annotations.Operation;

import org.springframework.core.annotation.AnnotationUtils;
31
import org.springframework.lang.NonNull;
32 33 34 35 36 37 38 39
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import cn.hutool.core.collection.CollUtil;
40
import cn.hutool.core.date.LocalDateTimeUtil;
41 42 43 44
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.extra.spring.SpringUtil;
45
import cn.hutool.http.HttpStatus;
46 47
import cn.hutool.json.JSONUtil;

48
import top.charles7c.cnadmin.common.model.dto.LogContext;
49 50 51 52 53 54
import top.charles7c.cnadmin.common.util.IpUtils;
import top.charles7c.cnadmin.common.util.ServletUtils;
import top.charles7c.cnadmin.common.util.helper.LoginHelper;
import top.charles7c.cnadmin.common.util.holder.LogContextHolder;
import top.charles7c.cnadmin.monitor.annotation.Log;
import top.charles7c.cnadmin.monitor.config.properties.LogProperties;
55
import top.charles7c.cnadmin.monitor.enums.LogStatusEnum;
56 57 58
import top.charles7c.cnadmin.monitor.model.entity.SysLog;

/**
59
 * 系统日志拦截器
60 61 62 63 64 65 66 67 68 69
 *
 * @author Charles7c
 * @since 2022/12/24 21:14
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class LogInterceptor implements HandlerInterceptor {

    private final LogProperties operationLogProperties;
70
    private static final String ENCRYPT_SYMBOL = "****************";
71 72

    @Override
73 74 75
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
        @NonNull Object handler) {
        if (checkIsNeedRecord(handler, request)) {
76
            // 记录时间
77
            this.logCreateTime();
78 79 80 81 82
        }
        return true;
    }

    @Override
83 84
    public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
        @NonNull Object handler, Exception e) {
85 86 87 88 89 90
        // 记录请求耗时及异常信息
        SysLog sysLog = this.logElapsedTimeAndException();
        if (sysLog == null) {
            return;
        }

91
        // 记录日志描述
92 93 94 95 96 97
        this.logDescription(sysLog, handler);
        // 记录请求信息
        this.logRequest(sysLog, request);
        // 记录响应信息
        this.logResponse(sysLog, response);

98
        // 保存系统日志
99 100 101 102
        SpringUtil.getApplicationContext().publishEvent(sysLog);
    }

    /**
103
     * 记录时间
104 105
     */
    private void logCreateTime() {
106 107 108 109
        LogContext logContext = new LogContext();
        logContext.setCreateUser(LoginHelper.getUserId());
        logContext.setCreateTime(LocalDateTime.now());
        LogContextHolder.set(logContext);
110 111 112 113 114
    }

    /**
     * 记录请求耗时及异常信息
     *
115
     * @return 系统日志信息
116 117
     */
    private SysLog logElapsedTimeAndException() {
118 119
        LogContext logContext = LogContextHolder.get();
        if (logContext != null) {
120 121
            LogContextHolder.remove();
            SysLog sysLog = new SysLog();
122
            sysLog.setCreateTime(logContext.getCreateTime());
123
            sysLog.setElapsedTime(System.currentTimeMillis() - LocalDateTimeUtil.toEpochMilli(sysLog.getCreateTime()));
124
            sysLog.setStatus(LogStatusEnum.SUCCESS);
125 126

            // 记录异常信息
127
            Exception exception = logContext.getException();
128
            if (exception != null) {
129
                sysLog.setStatus(LogStatusEnum.FAILURE);
130
                sysLog.setException(ExceptionUtil.stacktraceToString(exception, -1));
131 132 133 134 135 136 137 138 139 140
            }
            return sysLog;
        }
        return null;
    }

    /**
     * 记录日志描述
     *
     * @param sysLog
141
     *            系统日志信息
142 143 144
     * @param handler
     *            处理器
     */
145
    private void logDescription(SysLog sysLog, Object handler) {
146 147 148 149 150 151
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        Operation methodOperation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Operation.class);
        Log methodLog = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Log.class);

        if (methodOperation != null) {
            sysLog.setDescription(
152
                StrUtil.isNotBlank(methodOperation.summary()) ? methodOperation.summary() : "请在该接口方法上指定日志描述");
153 154 155 156 157 158 159 160 161 162 163
        }
        // 例如:@Log("获取验证码") -> 获取验证码
        if (methodLog != null && StrUtil.isNotBlank(methodLog.value())) {
            sysLog.setDescription(methodLog.value());
        }
    }

    /**
     * 记录请求信息
     *
     * @param sysLog
164
     *            系统日志信息
165 166 167
     * @param request
     *            请求对象
     */
168
    private void logRequest(SysLog sysLog, HttpServletRequest request) {
169 170 171 172 173 174 175 176 177
        sysLog.setRequestUrl(StrUtil.isBlank(request.getQueryString()) ? request.getRequestURL().toString()
            : request.getRequestURL().append("?").append(request.getQueryString()).toString());
        sysLog.setRequestMethod(request.getMethod());
        sysLog.setRequestHeader(this.desensitize(ServletUtil.getHeaderMap(request)));
        String requestBody = this.getRequestBody(request);
        if (StrUtil.isNotBlank(requestBody)) {
            sysLog.setRequestBody(this.desensitize(
                JSONUtil.isTypeJSON(requestBody) ? JSONUtil.parseObj(requestBody) : ServletUtil.getParamMap(request)));
        }
178 179
        sysLog.setClientIp(ServletUtil.getClientIP(request));
        sysLog.setLocation(IpUtils.getCityInfo(sysLog.getClientIp()));
180 181 182 183 184 185 186 187
        sysLog.setBrowser(ServletUtils.getBrowser(request));
        sysLog.setCreateUser(sysLog.getCreateUser() == null ? LoginHelper.getUserId() : sysLog.getCreateUser());
    }

    /**
     * 记录响应信息
     *
     * @param sysLog
188
     *            系统日志信息
189 190 191 192
     * @param response
     *            响应对象
     */
    private void logResponse(SysLog sysLog, HttpServletResponse response) {
193 194
        int status = response.getStatus();
        sysLog.setStatusCode(status);
195
        sysLog.setResponseHeader(this.desensitize(ServletUtil.getHeadersMap(response)));
196 197 198 199 200
        // 响应体(不记录非 JSON 响应数据)
        String responseBody = this.getResponseBody(response);
        if (StrUtil.isNotBlank(responseBody) && JSONUtil.isTypeJSON(responseBody)) {
            sysLog.setResponseBody(responseBody);
        }
201
        // 操作失败:>= 400
202
        sysLog.setStatus(status >= HttpStatus.HTTP_BAD_REQUEST ? LogStatusEnum.FAILURE : sysLog.getStatus());
203 204 205 206 207 208 209 210 211
    }

    /**
     * 数据脱敏
     *
     * @param waitDesensitizeData
     *            待脱敏数据
     * @return 脱敏后的 JSON 字符串数据
     */
212
    @SuppressWarnings("unchecked")
213 214 215 216 217 218 219 220
    private String desensitize(Map waitDesensitizeData) {
        String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData);
        try {
            if (CollUtil.isEmpty(waitDesensitizeData)) {
                return desensitizeDataStr;
            }

            for (String desensitizeProperty : operationLogProperties.getDesensitize()) {
221 222 223
                waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> ENCRYPT_SYMBOL);
                waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> ENCRYPT_SYMBOL);
                waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> ENCRYPT_SYMBOL);
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
            }
            return JSONUtil.toJsonStr(waitDesensitizeData);
        } catch (Exception ignored) {
        }
        return desensitizeDataStr;
    }

    /**
     * 获取请求体
     *
     * @param request
     *            请求对象
     * @return 请求体
     */
    private String getRequestBody(HttpServletRequest request) {
        String requestBody = "";
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            requestBody = StrUtil.utf8Str(wrapper.getContentAsByteArray());
        }
        return requestBody;
    }

    /**
     * 获取响应体
     *
     * @param response
     *            响应对象
     * @return 响应体
     */
    private String getResponseBody(HttpServletResponse response) {
        String responseBody = "";
        ContentCachingResponseWrapper wrapper =
            WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            responseBody = StrUtil.utf8Str(wrapper.getContentAsByteArray());
        }
        return responseBody;
    }

    /**
265
     * 检查是否要记录系统日志
266 267
     *
     * @param handler
268
     *            处理器
269
     * @param request
270
     *            请求对象
271 272 273
     * @return true 需要记录,false 不需要记录
     */
    private boolean checkIsNeedRecord(Object handler, HttpServletRequest request) {
274
        // 1、未启用时,不需要记录系统日志
275 276 277 278
        if (!(handler instanceof HandlerMethod) || Boolean.FALSE.equals(operationLogProperties.getEnabled())) {
            return false;
        }

279 280 281 282 283 284 285
        // 2、检查是否需要记录内网 IP 操作
        boolean isInnerIp = IpUtils.isInnerIP(ServletUtil.getClientIP(request));
        if (isInnerIp && Boolean.FALSE.equals(operationLogProperties.getIncludeInnerIp())) {
            return false;
        }

        // 3、排除不需要记录系统日志的接口
286 287
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        Log methodLog = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Log.class);
288
        // 3.1 请求方式不要求记录且请求上没有 @Log 注解,则不记录系统日志
289 290 291
        if (operationLogProperties.getExcludeMethods().contains(request.getMethod()) && methodLog == null) {
            return false;
        }
292
        // 3.2 如果接口上既没有 @Log 注解,也没有 @Operation 注解,则不记录系统日志
293 294 295 296
        Operation methodOperation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Operation.class);
        if (methodLog == null && methodOperation == null) {
            return false;
        }
297
        // 3.3 如果接口被隐藏,不记录系统日志
298 299 300
        if (methodOperation != null && methodOperation.hidden()) {
            return false;
        }
301
        // 3.4 如果接口上有 @Log 注解,但是要求忽略该接口,则不记录系统日志
302 303 304
        return methodLog == null || !methodLog.ignore();
    }
}