diff --git a/ruoyi-common/src/main/resources/logback.xml b/ruoyi-common/src/main/resources/logback.xml deleted file mode 100644 index a360583fa..000000000 --- a/ruoyi-common/src/main/resources/logback.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - ${log.pattern} - - - - - - ${log.path}/sys-info.log - - - - ${log.path}/sys-info.%d{yyyy-MM-dd}.log - - 60 - - - ${log.pattern} - - - - INFO - - ACCEPT - - DENY - - - - - ${log.path}/sys-error.log - - - - ${log.path}/sys-error.%d{yyyy-MM-dd}.log - - 60 - - - ${log.pattern} - - - - ERROR - - ACCEPT - - DENY - - - - - - ${log.path}/sys-user.log - - - ${log.path}/sys-user.%d{yyyy-MM-dd}.log - - 60 - - - ${log.pattern} - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java b/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java new file mode 100644 index 000000000..c93985e18 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/enums/UserTypeEnum.java @@ -0,0 +1,25 @@ +package cn.iocoder.dashboard.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 全局用户类型枚举 + */ +@AllArgsConstructor +@Getter +public enum UserTypeEnum { + + MEMBER(1, "会员"), // 面向 c 端,普通用户 + ADMIN(2, "管理员"); // 面向 b 端,管理后台 + + /** + * 类型 + */ + private final Integer value; + /** + * 类型名 + */ + private final String name; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java new file mode 100644 index 000000000..b36a1df5d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/config/ApiLogConfiguration.java @@ -0,0 +1,30 @@ +package cn.iocoder.dashboard.framework.logger.apilog.config; + +import cn.iocoder.dashboard.framework.logger.apilog.core.filter.ApiAccessLogFilter; +import cn.iocoder.dashboard.framework.web.config.WebProperties; +import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.Filter; + +@Configuration +public class ApiLogConfiguration { + + /** + * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 + */ + @Bean + public FilterRegistrationBean apiAccessLogFilter(WebProperties webProperties) { + ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties); + return createFilterBean(filter, FilterOrderEnum.API_ACCESS_LOG_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java new file mode 100644 index 000000000..bd839610f --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/filter/ApiAccessLogFilter.java @@ -0,0 +1,71 @@ +package cn.iocoder.dashboard.framework.logger.apilog.core.filter; + +import cn.hutool.extra.servlet.ServletUtil; +import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO; +import cn.iocoder.dashboard.framework.web.config.WebProperties; +import cn.iocoder.dashboard.util.servlet.ServletUtils; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.Map; + +/** + * API 访问日志 Filter + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Slf4j +public class ApiAccessLogFilter extends OncePerRequestFilter { + + private final WebProperties webProperties; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return !request.getRequestURI().startsWith(webProperties.getApiPrefix()); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // 获得开始时间 + Date startTime = new Date(); + // 提前获得参数,避免 XssFilter 过滤处理 + Map queryString = ServletUtil.getParamMap(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtil.getBody(request) : null; + + try { + // 继续过滤器 + filterChain.doFilter(request, response); + // 正常执行,记录日志 + createApiAccessLog(request, startTime, queryString, requestBody, null); + } catch (Exception ex) { + // 异常执行,记录日志 + createApiAccessLog(request, startTime, queryString, requestBody, ex); + throw ex; + } + } + + private void createApiAccessLog(HttpServletRequest request, Date startTime, + Map queryString, String requestBody, Exception ex) { + try { + ApiAccessLogCreateDTO createDTO = this.buildApiAccessLogDTO(request, startTime, queryString, requestBody, ex); + + } catch (Exception e) { + log.error("[createApiAccessLog][url({}) 发生异常]", request.getRequestURI(), ex); + } + } + + private ApiAccessLogCreateDTO buildApiAccessLogDTO(HttpServletRequest request, Date startTime, + Map queryString, String requestBody, Exception ex) { + return null; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java new file mode 100644 index 000000000..be4176de8 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/dto/ApiAccessLogCreateDTO.java @@ -0,0 +1,82 @@ +package cn.iocoder.dashboard.framework.logger.apilog.core.service.dto; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +/** + * API 访问日志创建 DTO + * + * @author 芋道源码 + */ +public class ApiAccessLogCreateDTO { + + /** + * 链路追踪编号 + */ + private String traceId; + /** + * 用户编号 + */ + private Integer userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 开始请求时间 + */ + @NotNull(message = "开始请求时间不能为空") + private Date startTime; + /** + * 结束请求时间 + */ + @NotNull(message = "结束请求时间不能为空") + private Date endTime; + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + /** + * 结果码 + */ + @NotNull(message = "错误码不能为空") + private Integer resultCode; + /** + * 结果提示 + */ + private String resultMsg; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/package-info.java b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/package-info.java new file mode 100644 index 000000000..4d76b11b2 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.dashboard.framework.logger.apilog.core.service; diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java index 0212294ea..759970631 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java +++ b/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java @@ -37,6 +37,5 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { } // 返回成功 ServletUtils.writeJSON(response, null); -// ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.OK.value(), "退出成功"))); } } diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java index 405cdfaa8..38a841229 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java +++ b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java @@ -1,11 +1,12 @@ package cn.iocoder.dashboard.framework.web.config; +import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum; import cn.iocoder.dashboard.framework.web.core.filter.RequestBodyCacheFilter; import cn.iocoder.dashboard.framework.web.core.filter.XssFilter; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; import org.springframework.util.PathMatcher; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.cors.CorsConfiguration; @@ -15,10 +16,8 @@ import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; +import javax.servlet.Filter; -/** - * Web 配置类 - */ @Configuration @EnableConfigurationProperties({WebProperties.class, XssProperties.class}) public class WebConfiguration implements WebMvcConfigurer { @@ -39,8 +38,7 @@ public class WebConfiguration implements WebMvcConfigurer { * 创建 CorsFilter Bean,解决跨域问题 */ @Bean - @Order(Integer.MIN_VALUE) - public CorsFilter corsFilter() { + public FilterRegistrationBean corsFilterBean() { // 创建 CorsConfiguration 对象 CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); @@ -50,25 +48,29 @@ public class WebConfiguration implements WebMvcConfigurer { // 创建 UrlBasedCorsConfigurationSource 对象 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 - return new CorsFilter(source); + return createFilterBean(new CorsFilter(source), FilterOrderEnum.CORS_FILTER); } /** * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 */ @Bean - @Order(Integer.MIN_VALUE) - public RequestBodyCacheFilter requestBodyCacheFilter() { - return new RequestBodyCacheFilter(); + public FilterRegistrationBean requestBodyCacheFilter() { + return createFilterBean(new RequestBodyCacheFilter(), FilterOrderEnum.REQUEST_BODY_CACHE_FILTER); } /** * 创建 XssFilter Bean,解决 Xss 安全问题 */ @Bean - @Order(Integer.MIN_VALUE + 1000) // 需要保证在 RequestBodyCacheFilter 后面 - public XssFilter xssFilter(XssProperties properties, PathMatcher pathMatcher) { - return new XssFilter(properties, pathMatcher); + public FilterRegistrationBean xssFilter(XssProperties properties, PathMatcher pathMatcher) { + return createFilterBean(new XssFilter(properties, pathMatcher), FilterOrderEnum.XSS_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; } } diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java new file mode 100644 index 000000000..31b25087e --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/enums/FilterOrderEnum.java @@ -0,0 +1,22 @@ +package cn.iocoder.dashboard.framework.web.core.enums; + +/** + * 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 + * + * @author 芋道源码 + */ +public interface FilterOrderEnum { + + int CORS_FILTER = Integer.MIN_VALUE; + + int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; + + // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 + + int API_ACCESS_LOG_FILTER = -104; // 需要保证在 RequestBodyCacheFilter 后面 + + int XSS_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面 + + // Spring Security Filter 默认为 -100,可见 SecurityProperties 配置属性类 + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/RequestBodyCacheFilter.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/RequestBodyCacheFilter.java index deff00cb4..6bd560299 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/RequestBodyCacheFilter.java +++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/RequestBodyCacheFilter.java @@ -1,7 +1,6 @@ package cn.iocoder.dashboard.framework.web.core.filter; -import cn.hutool.core.util.StrUtil; -import org.springframework.http.MediaType; +import cn.iocoder.dashboard.util.servlet.ServletUtils; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.ContentCachingRequestWrapper; @@ -29,7 +28,7 @@ public class RequestBodyCacheFilter extends OncePerRequestFilter { @Override protected boolean shouldNotFilter(HttpServletRequest request) { // 只处理 json 请求内容 - return !StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); + return !ServletUtils.isJsonRequest(request); } } diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java index d7780ed9b..c5256e7af 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java +++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/filter/XssRequestWrapper.java @@ -6,6 +6,7 @@ import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HTMLFilter; +import cn.iocoder.dashboard.util.servlet.ServletUtils; import org.springframework.http.MediaType; import javax.servlet.ReadListener; @@ -56,7 +57,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper { @Override public ServletInputStream getInputStream() throws IOException { // 如果非 json 请求,不进行 Xss 处理 - if (!StrUtil.startWithIgnoreCase(super.getContentType(), MediaType.APPLICATION_JSON_VALUE)) { + if (!ServletUtils.isJsonRequest(this)) { return super.getInputStream(); } diff --git a/src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/job/InfJobLogDO.java b/src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/job/InfJobLogDO.java index 31be9a7be..08fe7f58c 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/job/InfJobLogDO.java +++ b/src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/job/InfJobLogDO.java @@ -9,7 +9,7 @@ import lombok.*; import java.util.Date; /** - * 定时任务的执行日志 sys_job_log + * 定时任务的执行日志 * * @author 芋道源码 */ diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiAccessLogDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiAccessLogDO.java new file mode 100644 index 000000000..4ded8181c --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiAccessLogDO.java @@ -0,0 +1,103 @@ +package cn.iocoder.dashboard.modules.system.dal.dataobject.logger; + +import cn.iocoder.dashboard.common.enums.UserTypeEnum; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Date; + +/** + * API 访问日志 + * + * @author 芋道源码 + */ +@TableName("sys_api_access_log") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SysApiAccessLogDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Integer id; + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户编号 + */ + private Integer userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 应用名 + * + * 目前读取 `spring.application.name` 配置项 + */ + private String applicationName; + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + /** + * 开始请求时间 + */ + private Date startTime; + /** + * 结束请求时间 + */ + private Date endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 结果码 + * + * 目前使用的 {@link CommonResult#getCode()} 属性 + */ + private Integer resultCode; + /** + * 结果提示 + * + * 目前使用的 {@link CommonResult#getMsg()} 属性 + */ + private String resultMsg; + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiErrorLogDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiErrorLogDO.java new file mode 100644 index 000000000..af1391677 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/logger/SysApiErrorLogDO.java @@ -0,0 +1,4 @@ +package cn.iocoder.dashboard.modules.system.dal.dataobject.logger; + +public class SysApiErrorLogDO { +} diff --git a/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java b/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java index 35f3d5765..3828775c4 100644 --- a/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java +++ b/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java @@ -1,6 +1,7 @@ package cn.iocoder.dashboard.util.servlet; import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.extra.servlet.ServletUtil; import cn.iocoder.dashboard.util.json.JsonUtils; import org.springframework.http.MediaType; @@ -8,6 +9,7 @@ import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @@ -86,4 +88,8 @@ public class ServletUtils { return ServletUtil.getClientIP(request); } + public static boolean isJsonRequest(ServletRequest request) { + return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); + } + }