diff --git a/http-client.env.json b/http-client.env.json index b733618d33..8cad138965 100644 --- a/http-client.env.json +++ b/http-client.env.json @@ -1,5 +1,6 @@ { "local": { - "baseUrl": "http://127.0.0.1:8080" + "baseUrl": "http://127.0.0.1:8080/api", + "token": "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MDk2ODE2MzEsInN1YiI6ImE3ZGE1MWE2YWUyYTQxOWRhNmExYTlkYmJiMTVmZjc4In0.RXG7alSz64lE9oPSgbnYT_KsX7kvoHVhF5oHxXHztr1KjsttOqOppSmHGBYFI7Y75bsjEBSxSqbGsS1O1S2b1w" } } diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development index 0cb1c61317..069b536cdf 100644 --- a/ruoyi-ui/.env.development +++ b/ruoyi-ui/.env.development @@ -3,6 +3,7 @@ ENV = 'development' # 若依管理系统/开发环境 VUE_APP_BASE_API = '/dev-api' +# VUE_APP_BASE_API = '/api' # 路由懒加载 VUE_CLI_BABEL_TRANSPILE_MODULES = true diff --git a/ruoyi-ui/src/api/login.js b/ruoyi-ui/src/api/login.js index 9971357edd..f65942a991 100644 --- a/ruoyi-ui/src/api/login.js +++ b/ruoyi-ui/src/api/login.js @@ -34,7 +34,7 @@ export function logout() { // 获取验证码 export function getCodeImg() { return request({ - url: '/captchaImage', + url: '/captcha/get-image', method: 'get' }) -} \ No newline at end of file +} diff --git a/ruoyi-ui/src/utils/request.js b/ruoyi-ui/src/utils/request.js index ae89f24dee..2e246035d9 100644 --- a/ruoyi-ui/src/utils/request.js +++ b/ruoyi-ui/src/utils/request.js @@ -8,7 +8,7 @@ axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' // 创建axios实例 const service = axios.create({ // axios中请求配置有baseURL选项,表示请求URL公共部分 - baseURL: process.env.VUE_APP_BASE_API, + baseURL: process.env.VUE_APP_BASE_API + '/api/', // 此处的 /api/ 地址,原因是后端的基础路径为 /api/ // 超时 timeout: 10000 }) @@ -76,13 +76,13 @@ service.interceptors.response.use(res => { }) return Promise.reject('error') } else { - return res.data + return res.data.data // 第二层 data 才是后端返回的 CommonResult.data } }, error => { console.log('err' + error) let { message } = error; - if (message == "Network Error") { + if (message === "Network Error") { message = "后端接口连接异常"; } else if (message.includes("timeout")) { diff --git a/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java index 5b423c9f27..b550511aff 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java +++ b/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java @@ -57,9 +57,7 @@ public class SwaggerAutoConfiguration { .paths(PathSelectors.any()) .build() .securitySchemes(securitySchemes()) - .securityContexts(securityContexts()) -// .pathMapping() TODO 芋艿:稍后解决,统一 api 前缀 - ; + .securityContexts(securityContexts()); } /** 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 new file mode 100644 index 0000000000..187b336a9a --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java @@ -0,0 +1,80 @@ +package cn.iocoder.dashboard.framework.web.config; + +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson.support.config.FastJsonConfig; +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +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 java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +/** + * Web 配置类 + */ +@Configuration +@EnableConfigurationProperties(WebProperties.class) +public class WebConfiguration implements WebMvcConfigurer { + + @Resource + private WebProperties webProperties; + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.addPathPrefix(webProperties.getApiPrefix(), clazz -> + clazz.isAnnotationPresent(RestController.class) + && clazz.getPackage().getName().contains("cn.iocoder.dashboard")); + } + + // ========== MessageConverter 相关 ========== + + @Override + public void configureMessageConverters(List> converters) { + // 创建 FastJsonHttpMessageConverter 对象 + FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); + // 自定义 FastJson 配置 + FastJsonConfig fastJsonConfig = new FastJsonConfig(); + fastJsonConfig.setCharset(Charset.defaultCharset()); // 设置字符集 + fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect, // 剔除循环引用 + SerializerFeature.WriteNonStringKeyAsString); // 解决 Integer 作为 Key 时,转换为 String 类型,避免浏览器报错 + fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig); + // 设置支持的 MediaType + fastJsonHttpMessageConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON)); + // 添加到 converters 中 + converters.add(0, fastJsonHttpMessageConverter); // 注意,添加到最开头,放在 MappingJackson2XmlHttpMessageConverter 前面 + } + + // ========== Filter 相关 ========== + + /** + * 创建 CorsFilter Bean,解决跨域问题 + */ + @Bean + @Order(Integer.MIN_VALUE) + public CorsFilter corsFilter() { + // 创建 CorsConfiguration 对象 + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); // 设置访问源地址 + config.addAllowedHeader("*"); // 设置访问源请求头 + config.addAllowedMethod("*"); // 设置访问源请求方法 + // 创建 UrlBasedCorsConfigurationSource 对象 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 + return new CorsFilter(source); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java new file mode 100644 index 0000000000..bffac3fd38 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java @@ -0,0 +1,27 @@ +package cn.iocoder.dashboard.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 WebConfiguration#configurePathMatch(PathMatchConfigurer) + */ + @NotNull(message = "API 前缀不能为空") + private String apiPrefix; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000000..27d8ba69cb --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java @@ -0,0 +1,257 @@ +package cn.iocoder.dashboard.framework.web.core.handler; + +import cn.iocoder.dashboard.common.exception.GlobalException; +import cn.iocoder.dashboard.common.exception.ServiceException; +import cn.iocoder.dashboard.common.pojo.CommonResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +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.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.ValidationException; + +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.*; + +/** + * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * 处理所有异常,主要是提供给 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 ServiceException) { + return serviceExceptionHandler((ServiceException) ex); + } + if (ex instanceof GlobalException) { + return globalExceptionHandler(request, (GlobalException) 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.getCode(), "请求参数不正确"); + } + + /** + * 处理 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())); + } + + /** + * 处理业务异常 ServiceException + * + * 例如说,商品库存不足,用户手机号已存在。 + */ + @ExceptionHandler(value = ServiceException.class) + public CommonResult serviceExceptionHandler(ServiceException ex) { + log.info("[serviceExceptionHandler]", ex); + return CommonResult.error(ex.getCode(), ex.getMessage()); + } + + /** + * 处理全局异常 ServiceException + * + * 例如说,Dubbo 请求超时,调用的 Dubbo 服务系统异常 + */ + @ExceptionHandler(value = GlobalException.class) + public CommonResult globalExceptionHandler(HttpServletRequest req, GlobalException ex) { + // 系统异常时,才打印异常日志 + if (INTERNAL_SERVER_ERROR.getCode().equals(ex.getCode())) { + // 插入异常日志 + this.createExceptionLog(req, ex); + // 普通全局异常,打印 info 日志即可 + } else { + log.info("[globalExceptionHandler]", ex); + } + // 返回 ERROR CommonResult + return CommonResult.error(ex); + } + + /** + * 处理系统异常,兜底处理所有的一切 + */ + @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.getMessage()); + } + + // TODO 芋艿:增加异常日志 + public void createExceptionLog(HttpServletRequest req, Throwable e) { +// // 插入异常日志 +// SystemExceptionLogCreateDTO exceptionLog = new SystemExceptionLogCreateDTO(); +// try { +// // 增加异常计数 metrics TODO 暂时去掉 +//// EXCEPTION_COUNTER.increment(); +// // 初始化 exceptionLog +// initExceptionLog(exceptionLog, req, e); +// // 执行插入 exceptionLog +// createExceptionLog(exceptionLog); +// } catch (Throwable th) { +// log.error("[createExceptionLog][插入访问日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th)); +// } + } + +// // TODO 优化点:后续可以增加事件 +// @Async +// public void createExceptionLog(SystemExceptionLogCreateDTO exceptionLog) { +// try { +// systemExceptionLogRpc.createSystemExceptionLog(exceptionLog); +// } catch (Throwable th) { +// log.error("[addAccessLog][插入异常日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th)); +// } +// } +// +// private void initExceptionLog(SystemExceptionLogCreateDTO exceptionLog, HttpServletRequest request, Throwable e) { +// // 设置账号编号 +// exceptionLog.setUserId(CommonWebUtil.getUserId(request)); +// exceptionLog.setUserType(CommonWebUtil.getUserType(request)); +// // 设置异常字段 +// exceptionLog.setExceptionName(e.getClass().getName()); +// exceptionLog.setExceptionMessage(ExceptionUtil.getMessage(e)); +// exceptionLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); +// exceptionLog.setExceptionStackTrace(ExceptionUtil.getStackTrace(e)); +// StackTraceElement[] stackTraceElements = e.getStackTrace(); +// Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); +// StackTraceElement stackTraceElement = stackTraceElements[0]; +// exceptionLog.setExceptionClassName(stackTraceElement.getClassName()); +// exceptionLog.setExceptionFileName(stackTraceElement.getFileName()); +// exceptionLog.setExceptionMethodName(stackTraceElement.getMethodName()); +// exceptionLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); +// // 设置其它字段 +// exceptionLog.setTraceId(MallUtils.getTraceId()) +// .setApplicationName(applicationName) +// .setUri(request.getRequestURI()) // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。 +// .setQueryString(HttpUtil.buildQueryString(request)) +// .setMethod(request.getMethod()) +// .setUserAgent(HttpUtil.getUserAgent(request)) +// .setIp(HttpUtil.getIp(request)) +// .setExceptionTime(new Date()); +// } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/core/package-info.java b/src/main/java/cn/iocoder/dashboard/framework/web/core/package-info.java new file mode 100644 index 0000000000..1d87e7d62e --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/web/core/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.dashboard.framework.web.core; diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java b/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java new file mode 100644 index 0000000000..f4b8100a3d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * 针对 SpringMVC 的基础封装 + */ +package cn.iocoder.dashboard.framework.web; diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md b/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md new file mode 100644 index 0000000000..82c1fe55f5 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md @@ -0,0 +1 @@ + diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 34d01a504c..95f968d372 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -20,6 +20,8 @@ spring: # 芋道配置项,设置当前项目所有自定义的配置 yudao: + web: + api-prefix: /api security: token-header: Authorization token-secret: abcdefghijklmnopqrstuvwxyz