nginx設定解決跨域存取

2023-10-21 18:02:04

場景:前後的分離專案,前端vue框架,打包後放在Tomcat裡存取,埠是8080,後端伺服器埠8058。存取前端專案時,呼叫後端介面報跨域。

後端環境

正常存取埠8058

經過nginx設定(文末具體展示)後,去除埠,如下:

前端開發環境

1. 設定開發和生產的環境變數

.env.development檔案

# API服務路徑
VITE_APP_BASE_URL = ""

.env.production檔案

# API服務路徑,注意沒有埠號,是經過nginx處理後的後端服務地址
VITE_APP_BASE_URL = "http://192.168.1.4/"

2. vite.config.ts 檔案設定開發代理

import { warpperEnv } from "./build";
import { UserConfigExport, ConfigEnv, loadEnv } from "vite";

/** 當前執行node命令時資料夾的地址(工作目錄) */
const root: string = process.cwd();

export default ({ command, mode }: ConfigEnv): UserConfigExport => {
  const { VITE_CDN, VITE_PORT, VITE_COMPRESSION, VITE_PUBLIC_PATH } =
    warpperEnv(loadEnv(mode, root));
  return {
    base: VITE_PUBLIC_PATH,
    root,
    resolve: {
      alias
    },
    // 伺服器端渲染
    server: {
      // 是否開啟 https
      https: false,
      // 埠號
      port: VITE_PORT,
      host: "0.0.0.0",
      // 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy
      proxy: {
        "^/api": {
          target: "http://192.168.1.4:8058",
          changeOrigin: true,
          rewrite: path => path.replace(/^\/api/, "")
        }
      }
    },
    // 後面省略...
  }
};

3. http 檔案封裝 axios,使用環境變數設定的基礎API路徑

// 相關設定請參考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = {
  // 請求地址
  baseURL: import.meta.env.VITE_APP_BASE_URL,
  // 請求超時時間
  timeout: 5000,
  headers: {
    Accept: "application/json, text/plain, */*",
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest"
  }
};

4. api 檔案裡使用範例

import { http } from "@/utils/http";

export type UserResult = {
  success: boolean;
  data: {
    username: string;
    roles: Array<string>;
    accessToken: string;
    refreshToken: string;
    expires: Date;
  };
};

/** 登入 */
export const getLogin = (data?: object) => {
  return http.request<UserResult>("post", "/api/login", { data });
};

5. 開發環境專案預覽

由於使用了代理,所以不會有跨域的問題,如圖:

前端生產環境

生產環境採用Tomcat,前端專案打包好後,放在Tomcat的 webapps/ROOT 目錄下即可,如圖:

啟動Tomcat,雙擊 apache-tomcat-8.5.93/bin/ 目錄下的 startup.bat,linux機器到 bin 目錄下,執行 ./startup.sh

存取,Tomcat預設埠為8080,存取後臺介面報跨域錯誤

nginx設定後,去除埠,存取正常

nginx設定

以上可以看出,nginx設定的目的,就是去除前後端的埠差異,從而解決跨域的問題。組態檔 nginx/conf/nginx.conf 修改如下:

server {
  listen       80;
  # IP/域名都可以
  server_name  http://192.168.1.4;
  #charset koi8-r;
  #access_log  logs/host.access.log  main;
  
  location / {
    root   html;
    index  index.html index.htm;
    proxy_pass http://192.168.1.4:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_connect_timeout 5;
  }
  
  location ^~ /api/ {
    add_header 'Access-Control-Allow-Origin' $http_origin;
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,Authorization,Accept,Origin,Keep-Alive,User-Agent,X-Mx-ReqToken,X-Data-Type,X-Auth-Token,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    if ($request_method = 'OPTIONS') {
      add_header 'Access-Control-Max-Age' 1728000;
      add_header 'Content-Type' 'text/plain; charset=utf-8';
      add_header 'Content-Length' 0;
      return 204;
    }
    proxy_pass http://192.168.1.4:8058/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_connect_timeout 5;
  }
}

疑問解答

1. 此範例專案,本地模擬的線上環境,作業系統 windows,後端服務 nodejs。換成 linux 系統或其他後端服務(如:SpringBoot 等)還適用嗎,為什麼?

答:同樣適用。
因為跨域問題是瀏覽器為了安全保證,當你前端服務存取跨域資源時,被瀏覽器預設禁止了,所以和後端服務是什麼,沒有必然關係;所以解決問題的思路也是讓我們的存取請求,達到瀏覽器的要求,即不跨域(同一域名/IP、埠、協定),那當然使用 nginx 就可以。

2. 前端請求裡的代理裡的 ^api,和nginx設定裡的 ^~ /api/ 有什麼關係?

答:有關係,但是不是你想象的關係。
首先我們要確認,你的實際的請求地址裡,其實不包含 api 這段路徑。如圖:

開發環境加這個只是為了代理區分,因為你可能要請求多個不同域名的後端服務:我可以在 vite.config.ts 檔案的 proxy 裡再加一個以 ^/auth 開頭的,導向其他域名的後端服務。
其次程式碼裡也可以看到 rewrite: path => path.replace(/^\/api/, ""),所以實際的請求地址,最終是去掉了 api
那為什麼nginx裡要設定成 api 呢?
因為線上環境是沒有執行代理的,也就是說 rewrite: path => path.replace(/^\/api/, "") 這一段是沒有生效的,那你前端實際的請求地址就是 http://192.168.1.4/api/login,而真正的地址裡沒有 api 這一段,所以多出來的這一段,正好讓nginx去代理,^~ /api/ 變成 http://192.168.1.4:8058/,從而使請求地址變成 http://192.168.1.4:8058/login

3. 接上,這兩 api 雖然不是我想象的關係,但是事實上必須保持一致,有沒有什麼可以解耦的辦法?

答:有。
nginx一般不會去隨便改動,所以我們的解決辦法優先放在前端,讓前端去適配nginx。
參考 vite.config.ts 檔案的代理地址替換,那我們在實際請求地址時,也可以做一個替換處理:在 http 檔案裡的請求攔截器中,判斷是否生產環境,如果是,則替換 ^api 為nginx設定的開頭路徑即可。

4. 接上,vite.config.ts 可以設定多個代理,nginx也可以設定多個後端服務,但是 http 檔案裡的 axios 封裝只有一個 baseURL

答:不是有攔截器嗎。
env 環境設定裡增加多個域名欄位,假如以 auth 開頭是另一個域名,那就在攔截器裡使用條件判斷,如果是 auth 開頭,修改 config.baseURL 地址。
或者封裝多個 axois?不建議,程式碼重複率又高了。

5. 既然nginx已經把 api 開頭的都代理到了後端服務,那我前端就不能有 api 開頭的路由?

答:可以。
看看存取路徑,前端是雜湊路由,有 # 號分隔的。如果是 history 路由呢,有試過的同學可以在評論區吱個聲