LogInterceptor.java 11.6 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 128 129 130 131 132
            // 记录错误信息(非未知异常不记录异常详情,只记录错误信息)
            String errorMsg = logContext.getErrorMsg();
            if (StrUtil.isNotBlank(errorMsg)) {
                sysLog.setStatus(LogStatusEnum.FAILURE);
                sysLog.setErrorMsg(errorMsg);
            }
            // 记录异常详情
133
            Exception exception = logContext.getException();
134
            if (exception != null) {
135
                sysLog.setStatus(LogStatusEnum.FAILURE);
136
                sysLog.setExceptionDetail(ExceptionUtil.stacktraceToString(exception, -1));
137 138 139 140 141 142 143 144 145 146
            }
            return sysLog;
        }
        return null;
    }

    /**
     * 记录日志描述
     *
     * @param sysLog
147
     *            系统日志信息
148 149 150
     * @param handler
     *            处理器
     */
151
    private void logDescription(SysLog sysLog, Object handler) {
152 153 154 155 156 157
        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(
158
                StrUtil.isNotBlank(methodOperation.summary()) ? methodOperation.summary() : "请在该接口方法上指定日志描述");
159 160 161 162 163 164 165 166 167 168 169
        }
        // 例如:@Log("获取验证码") -> 获取验证码
        if (methodLog != null && StrUtil.isNotBlank(methodLog.value())) {
            sysLog.setDescription(methodLog.value());
        }
    }

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

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

    /**
     * 数据脱敏
     *
     * @param waitDesensitizeData
     *            待脱敏数据
     * @return 脱敏后的 JSON 字符串数据
     */
218
    @SuppressWarnings("unchecked")
219 220 221 222 223 224 225 226
    private String desensitize(Map waitDesensitizeData) {
        String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData);
        try {
            if (CollUtil.isEmpty(waitDesensitizeData)) {
                return desensitizeDataStr;
            }

            for (String desensitizeProperty : operationLogProperties.getDesensitize()) {
227 228 229
                waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> ENCRYPT_SYMBOL);
                waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> ENCRYPT_SYMBOL);
                waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> ENCRYPT_SYMBOL);
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 265 266 267 268 269 270
            }
            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;
    }

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

285 286 287 288 289 290 291
        // 2、检查是否需要记录内网 IP 操作
        boolean isInnerIp = IpUtils.isInnerIP(ServletUtil.getClientIP(request));
        if (isInnerIp && Boolean.FALSE.equals(operationLogProperties.getIncludeInnerIp())) {
            return false;
        }

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