LogInterceptor.java 13.1 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
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;
29
import io.swagger.v3.oas.annotations.tags.Tag;
30

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
import cn.hutool.core.exceptions.ExceptionUtil;
42
import cn.hutool.core.util.ObjectUtil;
43 44 45
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.extra.spring.SpringUtil;
46
import cn.hutool.http.HttpStatus;
47 48
import cn.hutool.json.JSONUtil;

49
import top.charles7c.cnadmin.common.model.dto.LogContext;
50 51 52 53 54 55
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;
56
import top.charles7c.cnadmin.monitor.enums.LogStatusEnum;
57
import top.charles7c.cnadmin.monitor.model.entity.LogDO;
58 59

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

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

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

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

92 93 94
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        // 记录所属模块
        this.logModule(logDO, handlerMethod);
95
        // 记录日志描述
96
        this.logDescription(logDO, handlerMethod);
97
        // 记录请求信息
98
        this.logRequest(logDO, request);
99
        // 记录响应信息
100
        this.logResponse(logDO, response);
101

102
        // 保存系统日志
103
        SpringUtil.getApplicationContext().publishEvent(logDO);
104 105 106
    }

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

    /**
117
     * 记录请求耗时及异常详情
118
     *
119
     * @return 系统日志信息
120
     */
121
    private LogDO logElapsedTimeAndException() {
122
        LogContext logContext = LogContextHolder.get();
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
        try {
            if (logContext != null) {
                LogDO logDO = new LogDO();
                logDO.setCreateTime(logContext.getCreateTime());
                logDO
                    .setElapsedTime(System.currentTimeMillis() - LocalDateTimeUtil.toEpochMilli(logDO.getCreateTime()));
                logDO.setStatus(LogStatusEnum.SUCCESS);

                // 记录错误信息(非未知异常不记录异常详情,只记录错误信息)
                String errorMsg = logContext.getErrorMsg();
                if (StrUtil.isNotBlank(errorMsg)) {
                    logDO.setStatus(LogStatusEnum.FAILURE);
                    logDO.setErrorMsg(errorMsg);
                }
                // 记录异常详情
                Exception exception = logContext.getException();
                if (exception != null) {
                    logDO.setStatus(LogStatusEnum.FAILURE);
                    logDO.setExceptionDetail(ExceptionUtil.stacktraceToString(exception, -1));
                }
                return logDO;
144
            }
145 146
        } finally {
            LogContextHolder.remove();
147 148 149 150
        }
        return null;
    }

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
    /**
     * 记录所属模块
     *
     * @param logDO
     *            系统日志信息
     * @param handlerMethod
     *            处理器方法
     */
    private void logModule(LogDO logDO, HandlerMethod handlerMethod) {
        Tag classTag = handlerMethod.getBeanType().getDeclaredAnnotation(Tag.class);
        Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
        Log methodLog = handlerMethod.getMethodAnnotation(Log.class);

        // 例如:@Tag(name = "部门管理") -> 部门管理
        // (本框架代码规范)例如:@Tag(name = "部门管理 API") -> 部门管理
        if (classTag != null) {
            String name = classTag.name();
            logDO.setModule(StrUtil.isNotBlank(name) ? name.replace("API", "").trim() : "请在该接口类上指定所属模块");
        }
        // 例如:@Log(module = "部门管理") -> 部门管理
        if (classLog != null && StrUtil.isNotBlank(classLog.module())) {
            logDO.setModule(classLog.module());
        }
        if (methodLog != null && StrUtil.isNotBlank(methodLog.module())) {
            logDO.setModule(methodLog.module());
        }
    }

179 180 181
    /**
     * 记录日志描述
     *
182
     * @param logDO
183
     *            系统日志信息
184 185
     * @param handlerMethod
     *            处理器方法
186
     */
187 188 189
    private void logDescription(LogDO logDO, HandlerMethod handlerMethod) {
        Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
        Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
190

191
        // 例如:@Operation(summary="新增部门") -> 新增部门
192
        if (methodOperation != null) {
193
            logDO.setDescription(StrUtil.blankToDefault(methodOperation.summary(), "请在该接口方法上指定日志描述"));
194
        }
195
        // 例如:@Log("新增部门") -> 新增部门
196
        if (methodLog != null && StrUtil.isNotBlank(methodLog.value())) {
197
            logDO.setDescription(methodLog.value());
198 199 200 201 202 203
        }
    }

    /**
     * 记录请求信息
     *
204
     * @param logDO
205
     *            系统日志信息
206 207 208
     * @param request
     *            请求对象
     */
209 210
    private void logRequest(LogDO logDO, HttpServletRequest request) {
        logDO.setRequestUrl(StrUtil.isBlank(request.getQueryString()) ? request.getRequestURL().toString()
211
            : request.getRequestURL().append("?").append(request.getQueryString()).toString());
212 213
        logDO.setRequestMethod(request.getMethod());
        logDO.setRequestHeaders(this.desensitize(ServletUtil.getHeaderMap(request)));
214 215
        String requestBody = this.getRequestBody(request);
        if (StrUtil.isNotBlank(requestBody)) {
216
            logDO.setRequestBody(this.desensitize(
217 218
                JSONUtil.isTypeJSON(requestBody) ? JSONUtil.parseObj(requestBody) : ServletUtil.getParamMap(request)));
        }
219 220 221
        logDO.setClientIp(ServletUtil.getClientIP(request));
        logDO.setLocation(IpUtils.getCityInfo(logDO.getClientIp()));
        logDO.setBrowser(ServletUtils.getBrowser(request));
222
        logDO.setCreateUser(ObjectUtil.defaultIfNull(logDO.getCreateUser(), LoginHelper.getUserId()));
223 224 225 226 227
    }

    /**
     * 记录响应信息
     *
228
     * @param logDO
229
     *            系统日志信息
230 231 232
     * @param response
     *            响应对象
     */
233
    private void logResponse(LogDO logDO, HttpServletResponse response) {
234
        int status = response.getStatus();
235 236
        logDO.setStatusCode(status);
        logDO.setResponseHeaders(this.desensitize(ServletUtil.getHeadersMap(response)));
237 238 239
        // 响应体(不记录非 JSON 响应数据)
        String responseBody = this.getResponseBody(response);
        if (StrUtil.isNotBlank(responseBody) && JSONUtil.isTypeJSON(responseBody)) {
240
            logDO.setResponseBody(responseBody);
241
        }
242
        // 操作失败:>= 400
243
        logDO.setStatus(status >= HttpStatus.HTTP_BAD_REQUEST ? LogStatusEnum.FAILURE : logDO.getStatus());
244 245 246 247 248 249 250 251 252
    }

    /**
     * 数据脱敏
     *
     * @param waitDesensitizeData
     *            待脱敏数据
     * @return 脱敏后的 JSON 字符串数据
     */
253
    @SuppressWarnings("unchecked")
254 255 256 257 258 259 260
    private String desensitize(Map waitDesensitizeData) {
        String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData);
        try {
            if (CollUtil.isEmpty(waitDesensitizeData)) {
                return desensitizeDataStr;
            }

261
            for (String desensitizeProperty : operationLogProperties.getDesensitizeFields()) {
262 263 264
                waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> ENCRYPT_SYMBOL);
                waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> ENCRYPT_SYMBOL);
                waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> ENCRYPT_SYMBOL);
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
            }
            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;
    }

    /**
306
     * 检查是否要记录系统日志
307 308
     *
     * @param handler
309
     *            处理器
310
     * @param request
311
     *            请求对象
312 313 314
     * @return true 需要记录,false 不需要记录
     */
    private boolean checkIsNeedRecord(Object handler, HttpServletRequest request) {
315
        // 1、未启用时,不需要记录系统日志
316 317 318 319
        if (!(handler instanceof HandlerMethod) || Boolean.FALSE.equals(operationLogProperties.getEnabled())) {
            return false;
        }

320
        // 2、检查是否需要记录内网 IP 操作
321
        boolean isInnerIp = IpUtils.isInnerIp(ServletUtil.getClientIP(request));
322 323 324 325 326
        if (isInnerIp && Boolean.FALSE.equals(operationLogProperties.getIncludeInnerIp())) {
            return false;
        }

        // 3、排除不需要记录系统日志的接口
327
        HandlerMethod handlerMethod = (HandlerMethod)handler;
328
        Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
329
        // 3.1 如果接口方法上既没有 @Log 注解,也没有 @Operation 注解,则不记录系统日志
330
        Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
331 332 333
        if (methodLog == null && methodOperation == null) {
            return false;
        }
334 335 336 337
        // 3.2 请求方式不要求记录且接口方法上没有 @Log 注解,则不记录系统日志
        if (methodLog == null && operationLogProperties.getExcludeMethods().contains(request.getMethod())) {
            return false;
        }
338
        // 3.3 如果接口被隐藏,不记录系统日志
339 340 341
        if (methodOperation != null && methodOperation.hidden()) {
            return false;
        }
342 343 344 345 346 347
        // 3.4 如果接口方法或类上有 @Log 注解,但是要求忽略该接口,则不记录系统日志
        if (methodLog != null && methodLog.ignore()) {
            return false;
        }
        Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
        return classLog == null || !classLog.ignore();
348 349
    }
}