LogInterceptor.java 12.8 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 123
        LogContext logContext = LogContextHolder.get();
        if (logContext != null) {
124
            LogContextHolder.remove();
125 126 127 128
            LogDO logDO = new LogDO();
            logDO.setCreateTime(logContext.getCreateTime());
            logDO.setElapsedTime(System.currentTimeMillis() - LocalDateTimeUtil.toEpochMilli(logDO.getCreateTime()));
            logDO.setStatus(LogStatusEnum.SUCCESS);
129

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

147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
    /**
     * 记录所属模块
     *
     * @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());
        }
    }

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

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

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

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

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

            for (String desensitizeProperty : operationLogProperties.getDesensitize()) {
258 259 260
                waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> ENCRYPT_SYMBOL);
                waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> ENCRYPT_SYMBOL);
                waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> ENCRYPT_SYMBOL);
261 262 263 264 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
            }
            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;
    }

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

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

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