mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-10-31 10:18:42 +08:00 
			
		
		
		
	项目结构调整 x 15 : 将 web、security、operatelog 等组件拆分出去
This commit is contained in:
		| @@ -0,0 +1,37 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.config; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; | ||||
| import cn.iocoder.yudao.framework.web.config.WebProperties; | ||||
| import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration; | ||||
| import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.boot.autoconfigure.AutoConfigureAfter; | ||||
| import org.springframework.boot.web.servlet.FilterRegistrationBean; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
|  | ||||
| import javax.servlet.Filter; | ||||
|  | ||||
| @Configuration | ||||
| @AutoConfigureAfter(YudaoWebAutoConfiguration.class) | ||||
| public class YudaoApiLogAutoConfiguration { | ||||
|  | ||||
|     /** | ||||
|      * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 | ||||
|      */ | ||||
|     @Bean | ||||
|     public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties, | ||||
|                                                                          @Value("${spring.application.name}") String applicationName, | ||||
|                                                                          ApiAccessLogFrameworkService apiAccessLogFrameworkService) { | ||||
|         ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService); | ||||
|         return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER); | ||||
|     } | ||||
|  | ||||
|     private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) { | ||||
|         FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter); | ||||
|         bean.setOrder(order); | ||||
|         return bean; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,112 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.filter; | ||||
|  | ||||
| import cn.hutool.core.exceptions.ExceptionUtil; | ||||
| import cn.hutool.core.map.MapUtil; | ||||
| import cn.hutool.extra.servlet.ServletUtil; | ||||
| import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | ||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO; | ||||
| import cn.iocoder.yudao.framework.util.monitor.TracerUtils; | ||||
| import cn.iocoder.yudao.framework.web.config.WebProperties; | ||||
| import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; | ||||
| import cn.iocoder.yudao.framework.util.date.DateUtils; | ||||
| import cn.iocoder.yudao.framework.util.json.JsonUtils; | ||||
| import cn.iocoder.yudao.framework.util.servlet.ServletUtils; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| 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 芋道源码 | ||||
|  */ | ||||
| @RequiredArgsConstructor | ||||
| @Slf4j | ||||
| public class ApiAccessLogFilter extends OncePerRequestFilter { | ||||
|  | ||||
|     private final WebProperties webProperties; | ||||
|     private final String applicationName; | ||||
|  | ||||
|     private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; | ||||
|  | ||||
|     @Override | ||||
|     protected boolean shouldNotFilter(HttpServletRequest request) { | ||||
|         // 只过滤 API 请求的地址 | ||||
|         return !request.getRequestURI().startsWith(webProperties.getApiPrefix()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||||
|             throws ServletException, IOException { | ||||
|         // 获得开始时间 | ||||
|         Date beginTim = new Date(); | ||||
|         // 提前获得参数,避免 XssFilter 过滤处理 | ||||
|         Map<String, String> queryString = ServletUtil.getParamMap(request); | ||||
|         String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtil.getBody(request) : null; | ||||
|  | ||||
|         try { | ||||
|             // 继续过滤器 | ||||
|             filterChain.doFilter(request, response); | ||||
|             // 正常执行,记录日志 | ||||
|             createApiAccessLog(request, beginTim, queryString, requestBody, null); | ||||
|         } catch (Exception ex) { | ||||
|             // 异常执行,记录日志 | ||||
|             createApiAccessLog(request, beginTim, queryString, requestBody, ex); | ||||
|             throw ex; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void createApiAccessLog(HttpServletRequest request, Date beginTime, | ||||
|                                     Map<String, String> queryString, String requestBody, Exception ex) { | ||||
|         ApiAccessLogCreateDTO accessLog = new ApiAccessLogCreateDTO(); | ||||
|         try { | ||||
|             this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex); | ||||
|             apiAccessLogFrameworkService.createApiAccessLogAsync(accessLog); | ||||
|         } catch (Throwable th) { | ||||
|             log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), JsonUtils.toJsonString(accessLog), th); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void buildApiAccessLogDTO(ApiAccessLogCreateDTO accessLog, HttpServletRequest request, Date beginTime, | ||||
|                                       Map<String, String> queryString, String requestBody, Exception ex) { | ||||
|         // 处理用户信息 | ||||
|         accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); | ||||
|         accessLog.setUserType(WebFrameworkUtils.getUserType(request)); | ||||
|         // 设置访问结果 | ||||
|         CommonResult<?> result = WebFrameworkUtils.getCommonResult(request); | ||||
|         if (result != null) { | ||||
|             accessLog.setResultCode(result.getCode()); | ||||
|             accessLog.setResultMsg(result.getMsg()); | ||||
|         } else if (ex != null) { | ||||
|             accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()); | ||||
|             accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); | ||||
|         } else { | ||||
|             accessLog.setResultCode(0); | ||||
|             accessLog.setResultMsg(""); | ||||
|         } | ||||
|         // 设置其它字段 | ||||
|         accessLog.setTraceId(TracerUtils.getTraceId()); | ||||
|         accessLog.setApplicationName(applicationName); | ||||
|         accessLog.setRequestUrl(request.getRequestURI()); | ||||
|         Map<String, Object> requestParams = MapUtil.<String, Object>builder().put("query", queryString).put("body", requestBody).build(); | ||||
|         accessLog.setRequestParams(JsonUtils.toJsonString(requestParams)); | ||||
|         accessLog.setRequestMethod(request.getMethod()); | ||||
|         accessLog.setUserAgent(ServletUtils.getUserAgent(request)); | ||||
|         accessLog.setUserIp(ServletUtil.getClientIP(request)); | ||||
|         // 持续时间 | ||||
|         accessLog.setBeginTime(beginTime); | ||||
|         accessLog.setEndTime(new Date()); | ||||
|         accessLog.setDuration((int) DateUtils.diff(accessLog.getEndTime(), accessLog.getBeginTime())); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.service; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO; | ||||
|  | ||||
| import javax.validation.Valid; | ||||
| import java.util.concurrent.Future; | ||||
|  | ||||
| /** | ||||
|  * API 访问日志 Framework Service 接口 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| public interface ApiAccessLogFrameworkService { | ||||
|  | ||||
|     /** | ||||
|      * 创建 API 访问日志 | ||||
|      * | ||||
|      * @param createDTO 创建信息 | ||||
|      * @return 是否创建成功 | ||||
|      */ | ||||
|     Future<Boolean> createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO); | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.service; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateDTO; | ||||
|  | ||||
| import javax.validation.Valid; | ||||
| import java.util.concurrent.Future; | ||||
|  | ||||
| /** | ||||
|  * API 错误日志 Framework Service 接口 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| public interface ApiErrorLogFrameworkService { | ||||
|  | ||||
|     /** | ||||
|      * 创建 API 错误日志 | ||||
|      * | ||||
|      * @param createDTO 创建信息 | ||||
|      * @return 是否创建成功 | ||||
|      */ | ||||
|     Future<Boolean> createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO); | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,85 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.service.dto; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import javax.validation.constraints.NotNull; | ||||
| import java.util.Date; | ||||
|  | ||||
| /** | ||||
|  * API 访问日志创建 DTO | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @Data | ||||
| public class ApiAccessLogCreateDTO { | ||||
|  | ||||
|     /** | ||||
|      * 链路追踪编号 | ||||
|      */ | ||||
|     private String traceId; | ||||
|     /** | ||||
|      * 用户编号 | ||||
|      */ | ||||
|     private Long 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 beginTime; | ||||
|     /** | ||||
|      * 结束请求时间 | ||||
|      */ | ||||
|     @NotNull(message = "结束请求时间不能为空") | ||||
|     private Date endTime; | ||||
|     /** | ||||
|      * 执行时长,单位:毫秒 | ||||
|      */ | ||||
|     @NotNull(message = "执行时长不能为空") | ||||
|     private Integer duration; | ||||
|     /** | ||||
|      * 结果码 | ||||
|      */ | ||||
|     @NotNull(message = "错误码不能为空") | ||||
|     private Integer resultCode; | ||||
|     /** | ||||
|      * 结果提示 | ||||
|      */ | ||||
|     private String resultMsg; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,109 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.service.dto; | ||||
|  | ||||
| import lombok.Data; | ||||
| import lombok.experimental.Accessors; | ||||
|  | ||||
| import javax.validation.constraints.NotNull; | ||||
| import java.io.Serializable; | ||||
| import java.util.Date; | ||||
|  | ||||
| /** | ||||
|  * API 错误日志创建 DTO | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @Data | ||||
| @Accessors(chain = true) | ||||
| public class ApiErrorLogCreateDTO implements Serializable { | ||||
|  | ||||
|     /** | ||||
|      * 链路编号 | ||||
|      */ | ||||
|     private String traceId; | ||||
|     /** | ||||
|      * 账号编号 | ||||
|      */ | ||||
|     private Long 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 exceptionTime; | ||||
|     /** | ||||
|      * 异常名 | ||||
|      */ | ||||
|     @NotNull(message = "异常名不能为空") | ||||
|     private String exceptionName; | ||||
|     /** | ||||
|      * 异常发生的类全名 | ||||
|      */ | ||||
|     @NotNull(message = "异常发生的类全名不能为空") | ||||
|     private String exceptionClassName; | ||||
|     /** | ||||
|      * 异常发生的类文件 | ||||
|      */ | ||||
|     @NotNull(message = "异常发生的类文件不能为空") | ||||
|     private String exceptionFileName; | ||||
|     /** | ||||
|      * 异常发生的方法名 | ||||
|      */ | ||||
|     @NotNull(message = "异常发生的方法名不能为空") | ||||
|     private String exceptionMethodName; | ||||
|     /** | ||||
|      * 异常发生的方法所在行 | ||||
|      */ | ||||
|     @NotNull(message = "异常发生的方法所在行不能为空") | ||||
|     private Integer exceptionLineNumber; | ||||
|     /** | ||||
|      * 异常的栈轨迹异常的栈轨迹 | ||||
|      */ | ||||
|     @NotNull(message = "异常的栈轨迹不能为空") | ||||
|     private String exceptionStackTrace; | ||||
|     /** | ||||
|      * 异常导致的根消息 | ||||
|      */ | ||||
|     @NotNull(message = "异常导致的根消息不能为空") | ||||
|     private String exceptionRootCauseMessage; | ||||
|     /** | ||||
|      * 异常导致的消息 | ||||
|      */ | ||||
|     @NotNull(message = "异常导致的消息不能为空") | ||||
|     private String exceptionMessage; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,8 @@ | ||||
| /** | ||||
|  * API 日志:包含两类 | ||||
|  * 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。 | ||||
|  * 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| package cn.iocoder.yudao.framework.apilog; | ||||
| @@ -0,0 +1,35 @@ | ||||
| package cn.iocoder.yudao.framework.jackson.config; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.jackson.core.databind.LocalDateTimeDeserializer; | ||||
| import cn.iocoder.yudao.framework.jackson.core.databind.LocalDateTimeSerializer; | ||||
| import cn.iocoder.yudao.framework.util.json.JsonUtils; | ||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||
| import com.fasterxml.jackson.databind.module.SimpleModule; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| @Configuration | ||||
| public class YudaoJacksonAutoConfiguration { | ||||
|  | ||||
|     @Bean | ||||
|     @SuppressWarnings("InstantiationOfUtilityClass") | ||||
|     public JsonUtils jsonUtils(ObjectMapper objectMapper) { | ||||
|         SimpleModule simpleModule = new SimpleModule(); | ||||
|         /* | ||||
|          * 1. 新增Long类型序列化规则,数值超过2^53-1,在JS会出现精度丢失问题,因此Long自动序列化为字符串类型 | ||||
|          * 2. 新增LocalDateTime序列化、反序列化规则 | ||||
|          */ | ||||
|         simpleModule | ||||
| //                .addSerializer(Long.class, ToStringSerializer.instance) | ||||
| //                    .addSerializer(Long.TYPE, ToStringSerializer.instance) | ||||
|                     .addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE) | ||||
|                     .addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE); | ||||
|  | ||||
|         objectMapper.registerModules(simpleModule); | ||||
|  | ||||
|         JsonUtils.init(objectMapper); | ||||
|         return new JsonUtils(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| package cn.iocoder.yudao.framework.jackson.core.databind; | ||||
|  | ||||
| import com.fasterxml.jackson.core.JsonParser; | ||||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||||
| import com.fasterxml.jackson.databind.DeserializationContext; | ||||
| import com.fasterxml.jackson.databind.JsonDeserializer; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.time.Instant; | ||||
| import java.time.LocalDateTime; | ||||
| import java.time.ZoneId; | ||||
|  | ||||
| /** | ||||
|  * LocalDateTime反序列化规则 | ||||
|  * <p> | ||||
|  * 会将毫秒级时间戳反序列化为LocalDateTime | ||||
|  */ | ||||
| public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> { | ||||
|  | ||||
|     public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer(); | ||||
|  | ||||
|     @Override | ||||
|     public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { | ||||
|         return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| package cn.iocoder.yudao.framework.jackson.core.databind; | ||||
|  | ||||
| import com.fasterxml.jackson.core.JsonGenerator; | ||||
| import com.fasterxml.jackson.databind.JsonSerializer; | ||||
| import com.fasterxml.jackson.databind.SerializerProvider; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.time.LocalDateTime; | ||||
| import java.time.ZoneId; | ||||
|  | ||||
| /** | ||||
|  * LocalDateTime序列化规则 | ||||
|  * <p> | ||||
|  * 会将LocalDateTime序列化为毫秒级时间戳 | ||||
|  */ | ||||
| public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> { | ||||
|  | ||||
|     public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer(); | ||||
|  | ||||
|     @Override | ||||
|     public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { | ||||
|         gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1 @@ | ||||
| package cn.iocoder.yudao.framework.jackson.core; | ||||
| @@ -0,0 +1 @@ | ||||
| package cn.iocoder.yudao.framework; | ||||
| @@ -0,0 +1,43 @@ | ||||
| package cn.iocoder.yudao.framework.swagger.config; | ||||
|  | ||||
| import lombok.Data; | ||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||||
|  | ||||
| import javax.validation.constraints.NotEmpty; | ||||
|  | ||||
| /** | ||||
|  * Swagger 配置属性 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @ConfigurationProperties("yudao.swagger") | ||||
| @Data | ||||
| public class SwaggerProperties { | ||||
|  | ||||
|     /** | ||||
|      * 标题 | ||||
|      */ | ||||
|     @NotEmpty(message = "标题不能为空") | ||||
|     private String title; | ||||
|     /** | ||||
|      * 描述 | ||||
|      */ | ||||
|     @NotEmpty(message = "描述不能为空") | ||||
|     private String description; | ||||
|     /** | ||||
|      * 作者 | ||||
|      */ | ||||
|     @NotEmpty(message = "作者不能为空") | ||||
|     private String author; | ||||
|     /** | ||||
|      * 版本 | ||||
|      */ | ||||
|     @NotEmpty(message = "版本不能为空") | ||||
|     private String version; | ||||
|     /** | ||||
|      * 扫描的包 | ||||
|      */ | ||||
|     @NotEmpty(message = "扫描的 package 不能为空") | ||||
|     private String basePackage; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,105 @@ | ||||
| package cn.iocoder.yudao.framework.swagger.config; | ||||
|  | ||||
| import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.http.HttpHeaders; | ||||
| import springfox.documentation.builders.ApiInfoBuilder; | ||||
| import springfox.documentation.builders.PathSelectors; | ||||
| import springfox.documentation.service.ApiInfo; | ||||
| import springfox.documentation.service.ApiKey; | ||||
| import springfox.documentation.service.AuthorizationScope; | ||||
| import springfox.documentation.service.Contact; | ||||
| import springfox.documentation.service.SecurityReference; | ||||
| import springfox.documentation.service.SecurityScheme; | ||||
| import springfox.documentation.spi.DocumentationType; | ||||
| import springfox.documentation.spi.service.contexts.SecurityContext; | ||||
| import springfox.documentation.spring.web.plugins.Docket; | ||||
| import springfox.documentation.swagger2.annotations.EnableSwagger2; | ||||
|  | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| import static springfox.documentation.builders.RequestHandlerSelectors.basePackage; | ||||
|  | ||||
| /** | ||||
|  * Swagger2 自动配置类 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @Configuration | ||||
| @EnableSwagger2 | ||||
| @EnableKnife4j | ||||
| @ConditionalOnClass({Docket.class, ApiInfoBuilder.class}) | ||||
| @ConditionalOnProperty(prefix = "yudao.swagger", value = "enable", matchIfMissing = true) | ||||
| // 允许使用 swagger.enable=false 禁用 Swagger | ||||
| @EnableConfigurationProperties(SwaggerProperties.class) | ||||
| public class YudaoSwaggerAutoConfiguration { | ||||
|  | ||||
|     @Bean | ||||
|     @ConditionalOnMissingBean | ||||
|     public SwaggerProperties swaggerProperties() { | ||||
|         return new SwaggerProperties(); | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     public Docket createRestApi() { | ||||
|         SwaggerProperties properties = swaggerProperties(); | ||||
|         // 创建 Docket 对象 | ||||
|         return new Docket(DocumentationType.SWAGGER_2) | ||||
|                 // 用来创建该 API 的基本信息,展示在文档的页面中(自定义展示的信息) | ||||
|                 .apiInfo(apiInfo(properties)) | ||||
|                 // 设置扫描指定 package 包下的 | ||||
|                 .select() | ||||
|                 .apis(basePackage(properties.getBasePackage())) | ||||
|                 .paths(PathSelectors.any()) | ||||
|                 .build() | ||||
|                 .securitySchemes(securitySchemes()) | ||||
|                 .securityContexts(securityContexts()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * API 摘要信息 | ||||
|      */ | ||||
|     private static ApiInfo apiInfo(SwaggerProperties properties) { | ||||
|         return new ApiInfoBuilder() | ||||
|                 .title(properties.getTitle()) | ||||
|                 .description(properties.getDescription()) | ||||
|                 .contact(new Contact(properties.getAuthor(), null, null)) | ||||
|                 .version(properties.getVersion()) | ||||
|                 .build(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 安全模式,这里配置通过请求头 Authorization 传递 token 参数 | ||||
|      */ | ||||
|     private static List<SecurityScheme> securitySchemes() { | ||||
|         return Collections.singletonList(new ApiKey(HttpHeaders.AUTHORIZATION, "Authorization", "header")); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 安全上下文 | ||||
|      * | ||||
|      * @see #securitySchemes() | ||||
|      * @see #authorizationScopes() | ||||
|      */ | ||||
|     private static List<SecurityContext> securityContexts() { | ||||
|         return Collections.singletonList(SecurityContext.builder() | ||||
|                 .securityReferences(securityReferences()) | ||||
|                 .forPaths(PathSelectors.regex("^(?!auth).*$")) | ||||
|                 .build()); | ||||
|     } | ||||
|  | ||||
|     private static List<SecurityReference> securityReferences() { | ||||
|         return Collections.singletonList(new SecurityReference(HttpHeaders.AUTHORIZATION, authorizationScopes())); | ||||
|     } | ||||
|  | ||||
|     private static AuthorizationScope[] authorizationScopes() { | ||||
|         return new AuthorizationScope[]{new AuthorizationScope("global", "accessEverything")}; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| /** | ||||
|  * 基于 Swagger + Knife4j 实现 API 接口文档 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| package cn.iocoder.yudao.framework.swagger; | ||||
| @@ -0,0 +1,37 @@ | ||||
| package cn.iocoder.yudao.framework.web.config; | ||||
|  | ||||
| import lombok.Data; | ||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||||
| import org.springframework.validation.annotation.Validated; | ||||
| import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; | ||||
|  | ||||
| import javax.validation.constraints.NotNull; | ||||
|  | ||||
| @ConfigurationProperties(prefix = "yudao.web") | ||||
| @Validated | ||||
| @Data | ||||
| public class WebProperties { | ||||
|  | ||||
|     /** | ||||
|      * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀 | ||||
|      * | ||||
|      * | ||||
|      * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题 | ||||
|      *      这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。 | ||||
|      * | ||||
|      * @see YudaoWebAutoConfiguration#configurePathMatch(PathMatchConfigurer) | ||||
|      */ | ||||
|     @NotNull(message = "API 前缀不能为空") | ||||
|     private String apiPrefix; | ||||
|  | ||||
|     /** | ||||
|      * Controller 所在包 | ||||
|      * | ||||
|      * 主要目的是,给该 Controller 设置指定的 {@link #apiPrefix} | ||||
|      * | ||||
|      * 因为我们有多个 modules 包里会包含 Controller,所以只需要写到 cn.iocoder.dashboard 这样的层级 | ||||
|      */ | ||||
|     @NotNull(message = "Controller 所在包不能为空") | ||||
|     private String controllerPackage; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,29 @@ | ||||
| package cn.iocoder.yudao.framework.web.config; | ||||
|  | ||||
| import lombok.Data; | ||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||||
| import org.springframework.validation.annotation.Validated; | ||||
|  | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * Xss 配置属性 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @ConfigurationProperties(prefix = "yudao.xss") | ||||
| @Validated | ||||
| @Data | ||||
| public class XssProperties { | ||||
|  | ||||
|     /** | ||||
|      * 是否开启,默认为 true | ||||
|      */ | ||||
|     private boolean enable = true; | ||||
|     /** | ||||
|      * 需要排除的 URL,默认为空 | ||||
|      */ | ||||
|     private List<String> excludeUrls = Collections.emptyList(); | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,88 @@ | ||||
| package cn.iocoder.yudao.framework.web.config; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; | ||||
| import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter; | ||||
| import cn.iocoder.yudao.framework.web.core.filter.DemoFilter; | ||||
| import cn.iocoder.yudao.framework.web.core.filter.XssFilter; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||
| 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.util.PathMatcher; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
| import org.springframework.web.cors.CorsConfiguration; | ||||
| import org.springframework.web.cors.UrlBasedCorsConfigurationSource; | ||||
| import org.springframework.web.filter.CorsFilter; | ||||
| import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; | ||||
| import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import javax.servlet.Filter; | ||||
|  | ||||
| @Configuration | ||||
| @EnableConfigurationProperties({WebProperties.class, XssProperties.class}) | ||||
| public class YudaoWebAutoConfiguration implements WebMvcConfigurer { | ||||
|  | ||||
|     @Resource | ||||
|     private WebProperties webProperties; | ||||
|  | ||||
|     @Override | ||||
|     public void configurePathMatch(PathMatchConfigurer configurer) { | ||||
|         // 设置 API 前缀,仅仅匹配 controller 包下的 | ||||
|         configurer.addPathPrefix(webProperties.getApiPrefix(), clazz -> | ||||
|                 clazz.isAnnotationPresent(RestController.class) | ||||
|                 && clazz.getPackage().getName().startsWith(webProperties.getControllerPackage())); // 仅仅匹配 controller 包 | ||||
|     } | ||||
|  | ||||
|     // ========== Filter 相关 ========== | ||||
|  | ||||
|     /** | ||||
|      * 创建 CorsFilter Bean,解决跨域问题 | ||||
|      */ | ||||
|     @Bean | ||||
|     public FilterRegistrationBean<CorsFilter> corsFilterBean() { | ||||
|         // 创建 CorsConfiguration 对象 | ||||
|         CorsConfiguration config = new CorsConfiguration(); | ||||
|         config.setAllowCredentials(true); | ||||
|         config.addAllowedOriginPattern("*"); // 设置访问源地址 | ||||
|         config.addAllowedHeader("*"); // 设置访问源请求头 | ||||
|         config.addAllowedMethod("*"); // 设置访问源请求方法 | ||||
|         // 创建 UrlBasedCorsConfigurationSource 对象 | ||||
|         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); | ||||
|         source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 | ||||
|         return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 | ||||
|      */ | ||||
|     @Bean | ||||
|     public FilterRegistrationBean<CacheRequestBodyFilter> requestBodyCacheFilter() { | ||||
|         return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建 XssFilter Bean,解决 Xss 安全问题 | ||||
|      */ | ||||
|     @Bean | ||||
|     public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) { | ||||
|         return createFilterBean(new XssFilter(properties, pathMatcher), WebFilterOrderEnum.XSS_FILTER); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建 DemoFilter Bean,演示模式 | ||||
|      */ | ||||
|     @Bean | ||||
|     @ConditionalOnProperty(value = "yudao.demo", havingValue = "true") | ||||
|     public FilterRegistrationBean<DemoFilter> demoFilter() { | ||||
|         return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER); | ||||
|     } | ||||
|  | ||||
|     private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) { | ||||
|         FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter); | ||||
|         bean.setOrder(order); | ||||
|         return bean; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| package cn.iocoder.yudao.framework.web.core.filter; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.util.servlet.ServletUtils; | ||||
| 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; | ||||
|  | ||||
| /** | ||||
|  * Request Body 缓存 Filter,实现它的可重复读取 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| public class CacheRequestBodyFilter extends OncePerRequestFilter { | ||||
|  | ||||
|     @Override | ||||
|     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||||
|             throws IOException, ServletException { | ||||
|         filterChain.doFilter(new CacheRequestBodyWrapper(request), response); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected boolean shouldNotFilter(HttpServletRequest request) { | ||||
|         // 只处理 json 请求内容 | ||||
|         return !ServletUtils.isJsonRequest(request); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,63 @@ | ||||
| package cn.iocoder.yudao.framework.web.core.filter; | ||||
|  | ||||
| import cn.hutool.extra.servlet.ServletUtil; | ||||
|  | ||||
| import javax.servlet.ReadListener; | ||||
| import javax.servlet.ServletInputStream; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletRequestWrapper; | ||||
| import java.io.BufferedReader; | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStreamReader; | ||||
|  | ||||
| /** | ||||
|  *  Request Body 缓存 Wrapper | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { | ||||
|  | ||||
|     /** | ||||
|      * 缓存的内容 | ||||
|      */ | ||||
|     private final byte[] body; | ||||
|  | ||||
|     public CacheRequestBodyWrapper(HttpServletRequest request) { | ||||
|         super(request); | ||||
|         body = ServletUtil.getBodyBytes(request); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public BufferedReader getReader() throws IOException { | ||||
|         return new BufferedReader(new InputStreamReader(this.getInputStream())); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public ServletInputStream getInputStream() throws IOException { | ||||
|         final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); | ||||
|         // 返回 ServletInputStream | ||||
|         return new ServletInputStream() { | ||||
|  | ||||
|             @Override | ||||
|             public int read() { | ||||
|                 return inputStream.read(); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean isFinished() { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean isReady() { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void setReadListener(ReadListener readListener) {} | ||||
|  | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,35 @@ | ||||
| package cn.iocoder.yudao.framework.web.core.filter; | ||||
|  | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||
| import cn.iocoder.yudao.framework.util.servlet.ServletUtils; | ||||
| import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; | ||||
| import org.springframework.web.filter.OncePerRequestFilter; | ||||
|  | ||||
| import javax.servlet.FilterChain; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY; | ||||
|  | ||||
| /** | ||||
|  * 演示 Filter,禁止用户发起写操作,避免影响测试数据 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| public class DemoFilter extends OncePerRequestFilter { | ||||
|  | ||||
|     @Override | ||||
|     protected boolean shouldNotFilter(HttpServletRequest request) { | ||||
|         String method = request.getMethod(); | ||||
|         return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE")  // 写操作时,不进行过滤率 | ||||
|                 || WebFrameworkUtils.getLoginUserId(request) == null; // 非登陆用户时,不进行过滤 | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { | ||||
|         // 直接返回 DEMO_DENY 的结果。即,请求不继续 | ||||
|         ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY)); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,51 @@ | ||||
| package cn.iocoder.yudao.framework.web.core.filter; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.web.config.XssProperties; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.util.PathMatcher; | ||||
| 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; | ||||
|  | ||||
| /** | ||||
|  * Xss 过滤器 | ||||
|  * | ||||
|  * 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/ | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @AllArgsConstructor | ||||
| public class XssFilter extends OncePerRequestFilter { | ||||
|  | ||||
|     /** | ||||
|      * 属性 | ||||
|      */ | ||||
|     private final XssProperties properties; | ||||
|     /** | ||||
|      * 路径匹配器 | ||||
|      */ | ||||
|     private final PathMatcher pathMatcher; | ||||
|  | ||||
|     @Override | ||||
|     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||||
|             throws IOException, ServletException { | ||||
|         filterChain.doFilter(new XssRequestWrapper(request), response); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected boolean shouldNotFilter(HttpServletRequest request) { | ||||
|         // 如果关闭,则不过滤 | ||||
|         if (!properties.isEnable()) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         // 如果匹配到无需过滤,则不过滤 | ||||
|         String uri = request.getRequestURI(); | ||||
|         return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri)); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,136 @@ | ||||
| package cn.iocoder.yudao.framework.web.core.filter; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.io.IoUtil; | ||||
| 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.yudao.framework.util.servlet.ServletUtils; | ||||
|  | ||||
| import javax.servlet.ReadListener; | ||||
| import javax.servlet.ServletInputStream; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletRequestWrapper; | ||||
| import java.io.BufferedReader; | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStreamReader; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * Xss 请求 Wrapper | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| public class XssRequestWrapper extends HttpServletRequestWrapper { | ||||
|  | ||||
|     /** | ||||
|      * 基于线程级别的 HTMLFilter 对象,因为它线程非安全 | ||||
|      */ | ||||
|     private static final ThreadLocal<HTMLFilter> HTML_FILTER = ThreadLocal.withInitial(() -> { | ||||
|         HTMLFilter htmlFilter = new HTMLFilter(); | ||||
|         // 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 " 字符 | ||||
|         ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false); | ||||
|         return htmlFilter; | ||||
|     }); | ||||
|  | ||||
|     public XssRequestWrapper(HttpServletRequest request) { | ||||
|         super(request); | ||||
|     } | ||||
|  | ||||
|     private static String filterXss(String content) { | ||||
|         if (StrUtil.isEmpty(content)) { | ||||
|             return content; | ||||
|         } | ||||
|         return HTML_FILTER.get().filter(content); | ||||
|     } | ||||
|  | ||||
|     // ========== IO 流相关 ========== | ||||
|  | ||||
|     @Override | ||||
|     public BufferedReader getReader() throws IOException { | ||||
|         return new BufferedReader(new InputStreamReader(this.getInputStream())); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public ServletInputStream getInputStream() throws IOException { | ||||
|         // 如果非 json 请求,不进行 Xss 处理 | ||||
|         if (!ServletUtils.isJsonRequest(this)) { | ||||
|             return super.getInputStream(); | ||||
|         } | ||||
|  | ||||
|         // 读取内容,并过滤 | ||||
|         String content = IoUtil.readUtf8(super.getInputStream()); | ||||
|         content = filterXss(content); | ||||
|         final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes()); | ||||
|         // 返回 ServletInputStream | ||||
|         return new ServletInputStream() { | ||||
|  | ||||
|             @Override | ||||
|             public int read() { | ||||
|                 return newInputStream.read(); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean isFinished() { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean isReady() { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void setReadListener(ReadListener readListener) {} | ||||
|  | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     // ========== Param 相关 ========== | ||||
|  | ||||
|     @Override | ||||
|     public String getParameter(String name) { | ||||
|         String value = super.getParameter(name); | ||||
|         return filterXss(value); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String[] getParameterValues(String name) { | ||||
|         String[] values = super.getParameterValues(name); | ||||
|         if (ArrayUtil.isEmpty(values)) { | ||||
|             return values; | ||||
|         } | ||||
|         // 过滤处理 | ||||
|         for (int i = 0; i < values.length; i++) { | ||||
|             values[i] = filterXss(values[i]); | ||||
|         } | ||||
|         return values; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Map<String, String[]> getParameterMap() { | ||||
|         Map<String, String[]> valueMap = super.getParameterMap(); | ||||
|         if (CollUtil.isEmpty(valueMap)) { | ||||
|             return valueMap; | ||||
|         } | ||||
|         // 过滤处理 | ||||
|         for (Map.Entry<String, String[]> entry : valueMap.entrySet()) { | ||||
|             String[] values = entry.getValue(); | ||||
|             for (int i = 0; i < values.length; i++) { | ||||
|                 values[i] = filterXss(values[i]); | ||||
|             } | ||||
|         } | ||||
|         return valueMap; | ||||
|     } | ||||
|  | ||||
|     // ========== Header 相关 ========== | ||||
|  | ||||
|     @Override | ||||
|     public String getHeader(String name) { | ||||
|         String value = super.getHeader(name); | ||||
|         return filterXss(value); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,275 @@ | ||||
| package cn.iocoder.yudao.framework.web.core.handler; | ||||
|  | ||||
| import cn.hutool.core.exceptions.ExceptionUtil; | ||||
| import cn.hutool.core.map.MapUtil; | ||||
| import cn.hutool.extra.servlet.ServletUtil; | ||||
| import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateDTO; | ||||
| import cn.iocoder.yudao.framework.util.monitor.TracerUtils; | ||||
| import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; | ||||
| import cn.iocoder.yudao.framework.util.json.JsonUtils; | ||||
| import cn.iocoder.yudao.framework.util.servlet.ServletUtils; | ||||
| import io.github.resilience4j.ratelimiter.RequestNotPermitted; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.apache.commons.lang3.exception.ExceptionUtils; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.security.access.AccessDeniedException; | ||||
| import org.springframework.util.Assert; | ||||
| import org.springframework.validation.BindException; | ||||
| import org.springframework.validation.FieldError; | ||||
| import org.springframework.web.HttpRequestMethodNotSupportedException; | ||||
| import org.springframework.web.bind.MethodArgumentNotValidException; | ||||
| import org.springframework.web.bind.MissingServletRequestParameterException; | ||||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||||
| import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; | ||||
| import org.springframework.web.servlet.NoHandlerFoundException; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.validation.ConstraintViolation; | ||||
| import javax.validation.ConstraintViolationException; | ||||
| import javax.validation.ValidationException; | ||||
| import java.util.Date; | ||||
| import java.util.Map; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; | ||||
|  | ||||
| /** | ||||
|  * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @RestControllerAdvice | ||||
| @Slf4j | ||||
| public class GlobalExceptionHandler { | ||||
|  | ||||
|     @Value("${spring.application.name}") | ||||
|     private String applicationName; | ||||
|  | ||||
|     @Resource | ||||
|     private ApiErrorLogFrameworkService apiErrorLogFrameworkService; | ||||
|  | ||||
|     /** | ||||
|      * 处理所有异常,主要是提供给 Filter 使用 | ||||
|      * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 | ||||
|      * | ||||
|      * @param request 请求 | ||||
|      * @param ex 异常 | ||||
|      * @return 通用返回 | ||||
|      */ | ||||
|     public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) { | ||||
|         if (ex instanceof MissingServletRequestParameterException) { | ||||
|             return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); | ||||
|         } | ||||
|         if (ex instanceof MethodArgumentTypeMismatchException) { | ||||
|             return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); | ||||
|         } | ||||
|         if (ex instanceof MethodArgumentNotValidException) { | ||||
|             return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); | ||||
|         } | ||||
|         if (ex instanceof BindException) { | ||||
|             return bindExceptionHandler((BindException) ex); | ||||
|         } | ||||
|         if (ex instanceof ConstraintViolationException) { | ||||
|             return constraintViolationExceptionHandler((ConstraintViolationException) ex); | ||||
|         } | ||||
|         if (ex instanceof ValidationException) { | ||||
|             return validationException((ValidationException) ex); | ||||
|         } | ||||
|         if (ex instanceof NoHandlerFoundException) { | ||||
|             return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); | ||||
|         } | ||||
|         if (ex instanceof HttpRequestMethodNotSupportedException) { | ||||
|             return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); | ||||
|         } | ||||
|         if (ex instanceof RequestNotPermitted) { | ||||
|             return requestNotPermittedExceptionHandler(request, (RequestNotPermitted) ex); | ||||
|         } | ||||
|         if (ex instanceof ServiceException) { | ||||
|             return serviceExceptionHandler((ServiceException) ex); | ||||
|         } | ||||
|         if (ex instanceof AccessDeniedException) { | ||||
|             return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); | ||||
|         } | ||||
|         return defaultExceptionHandler(request, ex); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 SpringMVC 请求参数缺失 | ||||
|      * | ||||
|      * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 | ||||
|      */ | ||||
|     @ExceptionHandler(value = MissingServletRequestParameterException.class) | ||||
|     public CommonResult<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { | ||||
|         log.warn("[missingServletRequestParameterExceptionHandler]", ex); | ||||
|         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 SpringMVC 请求参数类型错误 | ||||
|      * | ||||
|      * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String | ||||
|      */ | ||||
|     @ExceptionHandler(MethodArgumentTypeMismatchException.class) | ||||
|     public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { | ||||
|         log.warn("[missingServletRequestParameterExceptionHandler]", ex); | ||||
|         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 SpringMVC 参数校验不正确 | ||||
|      */ | ||||
|     @ExceptionHandler(MethodArgumentNotValidException.class) | ||||
|     public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { | ||||
|         log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); | ||||
|         FieldError fieldError = ex.getBindingResult().getFieldError(); | ||||
|         assert fieldError != null; // 断言,避免告警 | ||||
|         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 | ||||
|      */ | ||||
|     @ExceptionHandler(BindException.class) | ||||
|     public CommonResult<?> bindExceptionHandler(BindException ex) { | ||||
|         log.warn("[handleBindException]", ex); | ||||
|         FieldError fieldError = ex.getFieldError(); | ||||
|         assert fieldError != null; // 断言,避免告警 | ||||
|         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 Validator 校验不通过产生的异常 | ||||
|      */ | ||||
|     @ExceptionHandler(value = ConstraintViolationException.class) | ||||
|     public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) { | ||||
|         log.warn("[constraintViolationExceptionHandler]", ex); | ||||
|         ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next(); | ||||
|         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 | ||||
|      */ | ||||
|     @ExceptionHandler(value = ValidationException.class) | ||||
|     public CommonResult<?> validationException(ValidationException ex) { | ||||
|         log.warn("[constraintViolationExceptionHandler]", ex); | ||||
|         // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 | ||||
|         return CommonResult.error(BAD_REQUEST); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 SpringMVC 请求地址不存在 | ||||
|      * | ||||
|      * 注意,它需要设置如下两个配置项: | ||||
|      * 1. spring.mvc.throw-exception-if-no-handler-found 为 true | ||||
|      * 2. spring.mvc.static-path-pattern 为 /statics/** | ||||
|      */ | ||||
|     @ExceptionHandler(NoHandlerFoundException.class) | ||||
|     public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) { | ||||
|         log.warn("[noHandlerFoundExceptionHandler]", ex); | ||||
|         return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 SpringMVC 请求方法不正确 | ||||
|      * | ||||
|      * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 | ||||
|      */ | ||||
|     @ExceptionHandler(HttpRequestMethodNotSupportedException.class) | ||||
|     public CommonResult<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { | ||||
|         log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); | ||||
|         return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 Resilience4j 限流抛出的异常 | ||||
|      */ | ||||
|     @ExceptionHandler(value = RequestNotPermitted.class) | ||||
|     public CommonResult<?> requestNotPermittedExceptionHandler(HttpServletRequest req, RequestNotPermitted ex) { | ||||
|         log.warn("[requestNotPermittedExceptionHandler][url({}) 访问过于频繁]", req.getRequestURL(), ex); | ||||
|         return CommonResult.error(TOO_MANY_REQUESTS); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 Spring Security 权限不足的异常 | ||||
|      * | ||||
|      * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 | ||||
|      */ | ||||
|     @ExceptionHandler(value = AccessDeniedException.class) | ||||
|     public CommonResult<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { | ||||
|         log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req), | ||||
|                 req.getRequestURL(), ex); | ||||
|         return CommonResult.error(FORBIDDEN); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理业务异常 ServiceException | ||||
|      * | ||||
|      * 例如说,商品库存不足,用户手机号已存在。 | ||||
|      */ | ||||
|     @ExceptionHandler(value = ServiceException.class) | ||||
|     public CommonResult<?> serviceExceptionHandler(ServiceException ex) { | ||||
|         log.info("[serviceExceptionHandler]", ex); | ||||
|         return CommonResult.error(ex.getCode(), ex.getMessage()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理系统异常,兜底处理所有的一切 | ||||
|      */ | ||||
|     @ExceptionHandler(value = Exception.class) | ||||
|     public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) { | ||||
|         log.error("[defaultExceptionHandler]", ex); | ||||
|         // 插入异常日志 | ||||
|         this.createExceptionLog(req, ex); | ||||
|         // 返回 ERROR CommonResult | ||||
|         return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | ||||
|     } | ||||
|  | ||||
|     private void createExceptionLog(HttpServletRequest req, Throwable e) { | ||||
|         // 插入错误日志 | ||||
|         ApiErrorLogCreateDTO errorLog = new ApiErrorLogCreateDTO(); | ||||
|         try { | ||||
|             // 初始化 errorLog | ||||
|             initExceptionLog(errorLog, req, e); | ||||
|             // 执行插入 errorLog | ||||
|             apiErrorLogFrameworkService.createApiErrorLogAsync(errorLog); | ||||
|         } catch (Throwable th) { | ||||
|             log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(),  JsonUtils.toJsonString(errorLog), th); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void initExceptionLog(ApiErrorLogCreateDTO errorLog, HttpServletRequest request, Throwable e) { | ||||
|         // 处理用户信息 | ||||
|         errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); | ||||
|         errorLog.setUserType(WebFrameworkUtils.getUserType(request)); | ||||
|         // 设置异常字段 | ||||
|         errorLog.setExceptionName(e.getClass().getName()); | ||||
|         errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); | ||||
|         errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); | ||||
|         errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e)); | ||||
|         StackTraceElement[] stackTraceElements = e.getStackTrace(); | ||||
|         Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); | ||||
|         StackTraceElement stackTraceElement = stackTraceElements[0]; | ||||
|         errorLog.setExceptionClassName(stackTraceElement.getClassName()); | ||||
|         errorLog.setExceptionFileName(stackTraceElement.getFileName()); | ||||
|         errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); | ||||
|         errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); | ||||
|         // 设置其它字段 | ||||
|         errorLog.setTraceId(TracerUtils.getTraceId()); | ||||
|         errorLog.setApplicationName(applicationName); | ||||
|         errorLog.setRequestUrl(request.getRequestURI()); | ||||
|         Map<String, Object> requestParams = MapUtil.<String, Object>builder() | ||||
|                 .put("query", ServletUtil.getParamMap(request)) | ||||
|                 .put("body", ServletUtil.getBody(request)).build(); | ||||
|         errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); | ||||
|         errorLog.setRequestMethod(request.getMethod()); | ||||
|         errorLog.setUserAgent(ServletUtils.getUserAgent(request)); | ||||
|         errorLog.setUserIp(ServletUtil.getClientIP(request)); | ||||
|         errorLog.setExceptionTime(new Date()); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,45 @@ | ||||
| package cn.iocoder.yudao.framework.web.core.handler; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||
| import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; | ||||
| import org.springframework.core.MethodParameter; | ||||
| import org.springframework.http.MediaType; | ||||
| import org.springframework.http.server.ServerHttpRequest; | ||||
| import org.springframework.http.server.ServerHttpResponse; | ||||
| import org.springframework.http.server.ServletServerHttpRequest; | ||||
| import org.springframework.web.bind.annotation.ControllerAdvice; | ||||
| import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; | ||||
|  | ||||
| /** | ||||
|  * 全局响应结果(ResponseBody)处理器 | ||||
|  * | ||||
|  * 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult}, | ||||
|  * 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。 | ||||
|  * 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构 | ||||
|  * | ||||
|  * 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果, | ||||
|  * 方便 {@link cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter} 记录访问日志 | ||||
|  */ | ||||
| @ControllerAdvice | ||||
| public class GlobalResponseBodyHandler implements ResponseBodyAdvice { | ||||
|  | ||||
|     @Override | ||||
|     @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 | ||||
|     public boolean supports(MethodParameter returnType, Class converterType) { | ||||
|         if (returnType.getMethod() == null) { | ||||
|             return false; | ||||
|         } | ||||
|         // 只拦截返回结果为 CommonResult 类型 | ||||
|         return returnType.getMethod().getReturnType() == CommonResult.class; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 | ||||
|     public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, | ||||
|                                   ServerHttpRequest request, ServerHttpResponse response) { | ||||
|         // 记录 Controller 结果 | ||||
|         WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult<?>) body); | ||||
|         return body; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| package cn.iocoder.yudao.framework.web.core.util; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||
| 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; | ||||
|  | ||||
| /** | ||||
|  * 专属于 web 包的工具类 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| public class WebFrameworkUtils { | ||||
|  | ||||
|     private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; | ||||
|  | ||||
|     private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; | ||||
|  | ||||
|     public static void setLoginUserId(ServletRequest request, Long userId) { | ||||
|         request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获得当前用户的编号,从请求中 | ||||
|      * | ||||
|      * @param request 请求 | ||||
|      * @return 用户编号 | ||||
|      */ | ||||
|     public static Long getLoginUserId(HttpServletRequest request) { | ||||
|         if (request == null) { | ||||
|             return null; | ||||
|         } | ||||
|         return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); | ||||
|     } | ||||
|  | ||||
|     public static Long getLoginUserId() { | ||||
|         HttpServletRequest request = getRequest(); | ||||
|         return getLoginUserId(request); | ||||
|     } | ||||
|  | ||||
|     public static Integer getUserType(HttpServletRequest request) { | ||||
|         return UserTypeEnum.ADMIN.getValue(); // TODO 芋艿:等后续优化 | ||||
|     } | ||||
|  | ||||
|     public static void setCommonResult(ServletRequest request, CommonResult<?> result) { | ||||
|         request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result); | ||||
|     } | ||||
|  | ||||
|     public static CommonResult<?> getCommonResult(ServletRequest request) { | ||||
|         return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); | ||||
|     } | ||||
|  | ||||
|     private static HttpServletRequest getRequest() { | ||||
|         RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); | ||||
|         if (!(requestAttributes instanceof ServletRequestAttributes)) { | ||||
|             return null; | ||||
|         } | ||||
|         ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; | ||||
|         return servletRequestAttributes.getRequest(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,4 @@ | ||||
| /** | ||||
|  * 针对 SpringMVC 的基础封装 | ||||
|  */ | ||||
| package cn.iocoder.yudao.framework.web; | ||||
		Reference in New Issue
	
	Block a user
	 YunaiV
					YunaiV