一 問題概述
版本:ruoyi前后端分離版,ruoyi版本3.9.0 前端Vue3 后端Spring Boot 2.5.15
本地測試環境
ruoyi界面中系統工具下的系統接口集成了Swagger,當對其頁面上的接口進行請求測試時卻發生了404報錯。具體表現如下圖
二 問題排查
1、與Vue2進行對比
1.1 接口地址對比
通過瀏覽器開發者工具查看接口地址信息
直接啟動Vue2的前端,后端使用同樣的代碼再進行測試
當使用了Vue2的前端后,接口測試是成功的。這里可以看到Vue3接口的地址為http://localhost:8080/dev-api/test/user/1,但Vue2接口的地址是http://localhost/dev-api/test/user/1,(1為請求參數)一個是8080端口一個是80端口。
1.2 接口真實地址
通過上述對比,Vue2是可以成功請求的,那么是不是Vue3的接口地址生成不對呢
先來看下后端真實的接口地址是什么
yaml配置
# Swagger配置
swagger:# 是否開啟swaggerenabled: true# 請求前綴pathMapping: /dev-api
swaggerConfig
@Configuration
public class SwaggerConfig
{/** 系統基礎配置 */@Autowiredprivate RuoYiConfig ruoyiConfig;/** 是否開啟swagger */@Value("${swagger.enabled}")private boolean enabled;/** 設置請求的統一前綴 */@Value("${swagger.pathMapping}")private String pathMapping;/*** 創建API*/@Beanpublic Docket createRestApi(){return new Docket(DocumentationType.OAS_30)// 是否啟用Swagger.enable(enabled)// 用來創建該API的基本信息,展示在文檔的頁面中(自定義展示的信息).apiInfo(apiInfo())// 設置哪些接口暴露給Swagger展示.select()// 掃描所有有注解的api,用這種方式更靈活.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))// 掃描指定包中的swagger注解// .apis(RequestHandlerSelectors.basePackage("com.ruoyi.project.tool.swagger"))// 掃描所有 .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build()/* 設置安全模式,swagger可以設置訪問token */.securitySchemes(securitySchemes()).securityContexts(securityContexts()).pathMapping(pathMapping);}.......}
TestController
/*** swagger 用戶測試方法* * @author ruoyi*/
@Api("用戶信息管理")
@RestController
@RequestMapping("/test/user")
public class TestController extends BaseController
{private final static Map<Integer, UserEntity> users = new LinkedHashMap<Integer, UserEntity>();{users.put(1, new UserEntity(1, "admin", "admin123", "15888888888"));users.put(2, new UserEntity(2, "ry", "admin123", "15666666666"));}@ApiOperation("獲取用戶詳細")@ApiImplicitParam(name = "userId", value = "用戶ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)@GetMapping("/{userId}")public R<UserEntity> getUser(@PathVariable Integer userId){if (!users.isEmpty() && users.containsKey(userId)){return R.ok(users.get(userId));}else{return R.fail("用戶不存在");}}......}
從代碼中可以看出(后端應用端口為8080)獲取用戶詳細信息接口的真實地址為:http://localhost:8080/test/user ,pathMapping里配置了 “/dev-api” swagger在生成所有接口路徑時,前面都要加上 “/dev-api”
1.3 代理對比
為了解決跨域問題,Vue里會有代理配置,先來查看下Vue2中的配置
.env.development
VUE_APP_TITLE = 若依管理系統# 開發環境配置
ENV = 'development'# 若依管理系統/開發環境
VUE_APP_BASE_API = '/dev-api'# 路由懶加載
VUE_CLI_BABEL_TRANSPILE_MODULES = true
vue.config.js
......const baseUrl = 'http://localhost:8080' // 后端接口const port = process.env.port || process.env.npm_config_port || 80 // 端口......// webpack-dev-server 相關配置devServer: {host: '0.0.0.0',port: port,open: true,proxy: {// detail: https://cli.vuejs.org/config/#devserver-proxy[process.env.VUE_APP_BASE_API]: {target: baseUrl,changeOrigin: true,pathRewrite: {['^' + process.env.VUE_APP_BASE_API]: ''}},// springdoc proxy'^/v3/api-docs/(.*)': {target: baseUrl,changeOrigin: true}},disableHostCheck: true}......
這里看到其會攔截端口為80帶有“/dev-api”的請求轉向http://localhost:8080并去掉“/dev-api”
再來看Vue3中的代理配置
vite.config.js
......const baseUrl = 'http://localhost:8080'......// vite 相關配置
server: {port: 80,host: true,open: true,proxy: {// https://cn.vitejs.dev/config/#server-proxy'/dev-api': {target: baseUrl,changeOrigin: true,rewrite: (p) => p.replace(/^\/dev-api/, '')},// springdoc proxy'^/v3/api-docs/(.*)': {target: baseUrl,changeOrigin: true,}}
}......
同樣是會攔截端口為80帶有“/dev-api”的請求轉向http://localhost:8080并去掉“/dev-api”
Vue2中通過webpack-dev-server進行代理,Vue3通過vite代理
報錯原因
Vue2的Swagger中接口地址為http://localhost/dev-api/test/user/,當請求該地址時就會被webpack-dev-server攔截然后轉向http://localhost:8080/test/user 這是沒問題的,但是Vue3的Swagger中接口地址為http://localhost:8080/dev-api/test/user/ 其并不會被vite攔截,因為端口不是80,而后端接口真實地址并不包含"/dev-api",所以就會產生404報錯。
1.4 問題定位
1.那么為什么Vue3中swagger的接口的端口地址為8080而Vue2中卻是80呢?
ruoyi前端打開swagger的地址是http://localhost/dev-api/swagger-ui/index.html,無論在Vue2還是Vue3中都會被攔截轉發到http://localhost:8080/swagger-ui/index.html
在Vue2中訪問http://localhost/dev-api/swagger-ui/index.html查看Swagger界面的Servers為http://localhost:80
在Vue2中訪問訪問http://localhost:8080/swagger-ui/index.html查看Swagger界面的Servers為http://localhost:8080
在Vue3中訪問http://localhost/dev-api/swagger-ui/index.html查看Swagger界面的Servers為http://localhost:8080
在Vue3中訪問http://localhost:8080/swagger-ui/index.html查看Swagger界面的Servers為http://localhost:8080
Vue3這里端口都是8080
這里的服務地址是怎么生成的呢?
Spring Boot接收到請求后會優先使用X-Forwarded-*頭來推斷服務器URL。如果沒有X-Forwarded頭,它會使用請求中的Host頭。Swagger在生成頁面時會讀取這些信息。
2.那么這就來看一下請求中的請求頭headers里的X-Forwarded-*與Host都有什么。
在后端TestController中新增一個方法來打印請求中的headers
@ApiOperation("請求頭打印")
@GetMapping("/debug/headers")
public Map<String, String> listAllHeaders(HttpServletRequest request) {Enumeration<String> headerNames = request.getHeaderNames();Map<String, String> headers = new HashMap<>();while (headerNames.hasMoreElements()) {String key = headerNames.nextElement();headers.put(key, request.getHeader(key));}return headers;
}
重啟后端服務并訪問http://localhost/dev-api/test/user/debug/headers
注意這里需要有身份認證,要在請求的headers里加上Authorization
這里通過接口工具來請求,請求地址與headers如圖
啟動Vue2的前端代碼并發送請求,查看結果
再換Vue3的代碼啟動并請求
可以看到Vue2的代理轉發后會附帶有X-Forwarded-*信息,其中"x-forwarded-host": "localhost"是轉發前的地址,Spring Boot優先讀取的就是它。而Vue3的代理轉發后沒有,說明vite的代理轉發后沒有添加X-Forwarded-*頭信息。但是因為都設置了changeOrigin: true,Vue2和Vue3都更改了Host為localhost:8080,Spring Boot在Vue3這里讀取不到x-forwarded-host就會讀取Host,所以端口就會變成8080。
三 解決方案
1、后端解決方式
后端解決方式比較簡單,可以把ruoyi-admin模塊中yml的swagger配置的pathMapping值置空,這樣swagger中接口地址就會變成http://localhost:8080/test/user也就是后端接口的真正地址,再請求就不會發生404的報錯了。
2、前端解決方式
可以將Vue3中vite配置的changeOrigin改為false,這樣就不會更改Host值,在springdoc讀取的時候也會讀取到原地址的Host。但這樣有可能會對別的接口造成影響。
也可以給vite配置中加上添加X-Forwarded-*頭信息,加上后的vite配置為
// vite 相關配置
server: {port: 80,host: true,open: true,proxy: {// https://cn.vitejs.dev/config/#server-proxy'/dev-api': {target: baseUrl,changeOrigin: true,rewrite: (p) => p.replace(/^\/dev-api/, ''),configure: (proxy, options) => {proxy.on('proxyReq', (proxyReq, req, res) => {// 添加X-Forwarded-*頭proxyReq.setHeader('X-Forwarded-Host', req.headers.host);proxyReq.setHeader('X-Forwarded-Proto', 'http');proxyReq.setHeader('X-Forwarded-For', req.socket.remoteAddress);});}},// springdoc proxy'^/v3/api-docs/(.*)': {target: baseUrl,changeOrigin: true,}}
}