什麼是 CORS 我遇到 CORS 問題該如何解決

你是否常在開發時遇到「CORS 被封鎖」的錯誤?本文深入解析跨來源資源共享(CORS)是什麼、為什麼會出錯,並教你如何透過後端設定與前端 Proxy 有效解決,完整附上 Hono、Vite、Next.js 實作範例。

CChia1104
文章5 分鐘閱讀

大家在做前端開發時應該都遇過類似問題,那就是 CORS ,之前在面試時也很常被問到這問題,『遇到 CORS 該怎麼辦,要如何解決』。

cors error

'https://example.com' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

這錯誤大家一定都見過,很常照字面意思有些人會直接在前端設定 mode="no-cors"但是這是錯的 !!

若把 fetchmode 設定成 no-cors 其實只是讓瀏覽器取消 preflight request,並且回應是不透明的,這意味著它的 headers 和 body 不能被 JavaScript 所存取。

const opaqueResponse = await fetch(url, {
    mode: "no-cors", 
  })

什麼是 CORS

CORS(跨來源資源共享)是一種瀏覽器安全機制,用於限制網頁從不同來源請求資源,這意味這 CORS 只會發生在瀏覽器的環境。

一般情境下瀏覽器都會在每個 http request 前先發一個 preflight request,來取得該 api 所允許的資源存取。

preflight request

並且該方法為 OPTIONS 的請求,可以注意他這裡會回應 Access-Control-Allow-Origin: http://localhost:3001 這就代表該資源(api)只允許這個 origin 做請求,當瀏覽器知道當下該 client 的 origin 若不是 http://localhost:3001 就會回 CORS 的錯誤。

option request details

而這個設定(Access-Control-Allow-Origin)就是後端 api 需要設定的。

Allow

  • Access-Control-Allow-Credentials: 是否允許 client 設定 credentials MDN
  • Access-Control-Allow-Headers: 允許 client 攜帶哪些 headers,像是 X-Custom-Header MDN
  • Access-Control-Allow-Methods: 允許 client 發哪些 http 請求的方法,包含一般的 GET, POST, PATCH, PUT, DELETE MDN
  • Access-Control-Allow-Origin: 允許哪些 client 的 origin 做請求 MDN

Expose

  • Access-Control-Expose-Headers: server 允許 client 端存取的 headers,例如 Retry-After MDN

如何解決

再回來看『遇到 CORS 該怎麼辦,要如何解決』,其實答案很明顯了,那就是請後端做 CORS 的設定。

後端設定 CORS

這裡以 Hono 做範例。

import { Hono } from "hono";

const app = new Hono();

/**
 * 針對 `/` 路徑設定 CORS
 */
app.options("/", (c) => {
  c.header("Access-Control-Allow-Origin", "*");
  c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
  c.header("Access-Control-Allow-Headers", "Content-Type");
  c.status(204);

  return c.body(null);
});

app.get("/", (c) => c.text("Hono!"));

export default app;

這裡要注意,每個路徑都需要有對應的 OPTIONS routes,當然現在這類框架都有對應的套件幫助我們在 middleware 那層統一設定 CORS 了,這樣也不需要個別在對應的 routes 做修改。

import { cors } from "hono/cors";
import { env } from "@/env";

const getCORSAllowedOrigin = (): string[] | string => {
  if (!env.CORS_ALLOWED_ORIGIN) return "*";
  return (
    env.CORS_ALLOWED_ORIGIN?.split(",").map((item) => {
      return item.trim();
    }) ?? "*"
  );
};

app.use(
    cors({
      origin: getCORSAllowedOrigin(),
      credentials: true,
      // default settings
    })
  );

前端 Proxy

最後就是如何單從前端來解,其實這問題應該最常發生在本地的開發階段,有時候若是因為安全疑慮,後端無法設定 localhost 供使用的話,也可透過自行架 proxy server 來解,因為 CORS 不會發生在 server 跟 server 之間的互動。

Vite

Vite 也是現在的主流 full-stack 開發工具,而他的 vite.config.mts 就有提供 proxy 的功能。

vite.config.mts
/// <reference types="vitest" />
import { defineConfig, loadEnv, type ConfigEnv } from 'vite';
import https from 'https';

export default ({ mode }: ConfigEnv) => {
	process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };

	return defineConfig({
		server: { 
			proxy: { 
				'/proxy-api': { 
					target: process.env.VITE_APP_APIBASE, 
					changeOrigin: true, 
					rewrite: path => path.replace(/^\/proxy-api/, ''), 
					agent: new https.Agent(), 
				}, 
			}, 
		}, 
	});
};

前端在 fetch 的時候再用 /proxy-api 的路徑 prefix 即可。

Next.js

Next.js 雖然是個 react 全端框架,可以自己實作 api,但這樣就會需要自己處理 proxy 前後的 request 跟 response,這裡我們可以直接在 next.config.ts 裡做設定,直接把 /proxy-api/* 的路徑改寫成後端的 api 路徑。

next.config.ts
const nextConfig: NextConfig = {
	rewrites: async () => [ 
		{ 
			source: '/proxy-api/:path*', 
			destination: `${env.NEXT_PUBLIC_APP_AIP_HOST}/:path*`, 
		}, 
	], 
};

export default nextConfig;

Written by: Chia1104 CC BY-NC-SA 4.0

Chia1104
©
Chia1104