跨域問題多,要如何跨網域使用 cookie 呢?
前言
在前後端分離的開發模式下,滿常遇到跨域所衍生的問題,例如客戶規劃前端 website 與後端 API service 就不在相同 domain 下,此時原本在後端 API 提供的 swagger 頁面測試無誤的 cookie 機制轉移到前端開發時就會產生一些問題;例如登入後取得的 refresh token 是以 cookie 方式保存於前端,但在跨域的情況下預設是無法被保存在前端,因而造成整個機制失效,所以這篇文章就稍微整理一下要如何處理這類問題。
情境
假設目前有一個前端 website 及後端 API service 分別佈署在以下的 domain 中,而這兩者很明顯地就是跨域存取,因此我們就以此環境進行相關測試吧!主要的操作情境就是前端 website 呼叫後端 Login API 後,會收到一個 refresh token cookie 並保存在前端瀏覽器中,當前端 website 向後端叫用其他 API 如 GetLoginUser 時會夾帶著該 cookie 回到後端;我們就來看看在這跨域環境下會遇到什麼問題及要怎麼達成這個需求。
- 前端 website:
https://d8bc-61-216-31-115.ngrok.io
- 後端 API service:
http://localhost:10434/api/
發生 CORS 錯誤
在預設情況下,當前端 website 呼叫後端 API 時就會噴出 CORS 錯誤。
Access to XMLHttpRequest at 'http://localhost:10434/api/User/Login' from origin 'https://d8bc-61-216-31-115.ngrok.io' 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.
排除 CORS 錯誤
在「開發時期」我們可以直接在後端設定回應 Access-Control-Allow-Origin: * 標頭,表示允許任何來源叫用此服務以避免 CORS 錯誤。我們通常會在後端建立一個 IServiceCollection 的擴充方法 AddCorsServices 來設定各環境的 CORS Policy,接著在 CorsPolicyBuilder 的 CORS policy 設定 AllowAnyOrigin 來允許任何 Origin 來源的呼叫。
public static class CorsExtensions
{
public static IServiceCollection AddCorsServices(this IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("Dev", policy =>
{
policy
.AllowAnyOrigin() // Access-Control-Allow-Origin: *
.AllowAnyHeader()
.AllowAnyMethod();
});
// ... 略 ...
});
return services;
}
}
正式環境請依需求決定是否要明確指定 Origin 資訊來限制跨域的呼叫。
最後在 startup 中加入 CORS 設定,並且指定要使用哪一個 CORS 的設定就好了;完成後可以發現情求回應時已加上 Access-Control-Allow-Origin: * 標頭,並且不會再出現 CORS 的錯誤訊息。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// ... 略 ...
services.AddCorsServices(); // 加入 CORS 設定
// ... 略 ...
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... 略 ...
app.UseCors(env.IsDevelopment() ? "Dev" : "Production"); // 開發時使用名為 DEV 的 CORS 設定
// ... 略 ...
}
}
保存/夾帶跨域 cookie
解決了 CORS 錯誤後,由於目前狀況仍舊是跨域請求,所以預設 cookie 是不會被保存及夾帶,因此如果需要使用跨域 API 的 cookie 資訊,就要設定 withCredentials 來在前端發「跨域」請求時,夾帶 cookie 資訊至 API 服務端,以及在接收 set cookie 標頭時保存 cookie 於前端。
withCredentials
當前端「跨域」向 API 服務端發出請求時,預設是不會夾帶 cookie 的,因此前端需要加上 withCredentials 設定;這個設定簡單來說就是前端發出「跨域」請求時,是否要夾帶 cookie 資訊至 API 服務端,如果以 axios 為例就是:
axios.defaults.withCredentials = true;
此設定是針對「跨域」請求,因此在相同來源的請求設定 withCredentials 是沒有任何效果。
當前端設定 withCredentials 後,後端需將 header 設定成對應值:
- Access-Control-Allow-Credentials: true
- Access-Control-Allow-Origin: 請求的 Origin 位置
當 header 設定為 'Access-Control-Allow-Credentials: true' 時,不可將 Access-Control-Allow-Origin 設為 wildcard (*) 喔!
在剛剛建立的 CORS policy 中使用 AllowCredentials 來設置 Access-Control-Allow-Credentials: true,透過 SetIsOriginAllowed 允許所有 Origin 請求加註在 Access-Control-Allow-Origin 標頭上。
public static class CorsExtensions
{
public static IServiceCollection AddCorsServices(this IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("Dev", policy =>
{
policy
.AllowCredentials() // Access-Control-Allow-Credentials: true
.SetIsOriginAllowed(_ => true) // Access-Control-Allow-Origin: 請求的 Origin 位置
.AllowAnyHeader()
.AllowAnyMethod();
});
// ... 略 ...
});
return services;
}
}
注意此處是針對 Dev 開發時的設定(允許所有請求跨域叫用服務),實際上線版本請依照實際情況設定 Origin 白名單;可在 SetIsOriginAllowed 寫入判斷邏輯或透過 WithOrigins(new string[] { "https://website-domain" }) 明確指定允許的來源清單。
設定完,從 https://d8bc-61-216-31-115.ngrok.io 站台呼叫 API 時,回應的 access-control-allow-origin 就是該站台位置,同時也可以順利完成 API 叫用。但跨域的 cookie 是否真的就順利地被保存起來了呢? 我們繼續看下去。
Same-Site
在跨域情況下 cookie 還會受到 Same-Site 的規範,因為站台與 API 來源 domain 不同,因此在瀏覽器預設的 Same-Site 規範 Lax 下會被擋掉。
提醒一下 same site 只看 domain 不看 port ,所以 localhost:8080 & localhost:3000 視為 same site 喔!
因此我們要在後端新增 cookie 時須設定:
- SameSite = SameSiteMode.None
- Secure = true (SameSite = None 模式下,必須要 Secure 為 true 才可生效)
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Expires = DateTime.UtcNow.AddMinutes(_appSettings.AppSetting.Auth.RefreshTokenExpireMins),
SameSite = SameSiteMode.None,
Secure = true,
};
接著測試一下,執行登入後就會順利收到 response cookie - refreshToken。
可以從 response header 中看到我們先前於後端的設定,使用目前叫用此 API 的來源位址於 Access-Control-Allow-Origin 中,並取得 Set-Cookie 資訊,讓瀏覽器會將此 Cookie 保存起來。
確認一下,確實 refreshToken cookie 已經瀏覽器保存起來了。
當我們再向該 API 服務呼叫其他 API 時,該 refreshToken 就會被自動夾帶出去囉!
讀取跨域 Response Header
最後還會遇到 Response Header 讀不到的問題,因為在跨域的情況下預設只能讀取到六種基本 Response Header 資訊,至於其他的如自定義 Header 就無法讀取,因此我們可以在後端設定 Access-Control-Expose-Headers 讓用戶端取到特定允許露出的 response header 資訊。
public static class CorsExtensions
{
public static IServiceCollection AddCorsServices(this IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("Dev", policy =>
{
policy
.AllowCredentials()
.SetIsOriginAllowed(_ => true)
.AllowAnyHeader()
.AllowAnyMethod()
.WithExposedHeaders("X-ERROR-CODE", "X-OTHER-HEADER"); // 設定要露出的 Header 名稱
});
// ... 略 ...
});
return services;
}
}
參考資訊
Make Axios send cookies in its requests automatically
SignalR Core 2.2 CORS AllowAnyOrigin() breaking change
axios.defaults.withCredentials = true 前端跨域設定
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !