跨域訪問CORS
- 原理
- 基本概念
- 簡單請求
- 非簡單請求(預檢請求)
- 代碼實現
- 服務器端Cors的關鍵配置
- 服務端解析預檢請求
- 服務端填充響應
- 抓包分析
原理
基本概念
在瀏覽器安全模型中,同源策略是最重要的安全基石。
一個“域”是由3個要素組成的:
- 協議(如:http 或 https)
- 主機(Host,如 www.example.com 或 127.0.0.1)
- 端口(Port,如 80 或 8080)
只要這三個完全一致,就是同源的。
例如:
- http://example.com:80 和 http://example.com:8080 是不同源
- http://example.com 和 https://example.com 是不同源
下面是官網解釋跨域的圖解:
兩種請求
:瀏覽器將 CORS 請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
簡單請求
按照 W3C 的 CORS 規范,只有完全滿足「安全要求」的跨域請求,瀏覽器才會把它直接當成簡單請求,直接發送給服務端,不需要先發 OPTIONS。
必須同時滿足以下條件
- 請求方法必須是(
GET/HEAD/POST
)三者之一 - 請求頭不能超出以下幾個字段(
Accept、Accept-Language、Content-Language、Content-Type
)等 Content-Type
(如果存在的話),其值只能是application/x-www-form-urlencoded、multipart/form-data、text/plain
非簡單請求(預檢請求)
非簡單請求是指那些對服務器有特殊要求的請求,例如:
- 使用PUT或DELETE方法
- 設置Content-Type為application/json
即不滿足簡單請求的條件的都是預檢請求(非簡單請求)。
對于這類請求,瀏覽器會在正式通信前額外發送一次HTTP查詢請求(即預檢請求),這個過程叫做預檢(Preflight)
。該請求會確認:
- 當前網頁域名是否在服務器的許可名單中
- 允許使用的HTTP方法和頭信息字段
只有在獲得服務器肯定答復后,瀏覽器才會發出正式的XMLHttpRequest請求,否則將報錯。
“預檢請求”用的請求方法是 OPTIONS
,表示這個請求是用來詢問的。頭信息里面,關鍵字是 Origin
,表示請求來自哪個源。
除了 Origin 字段,“預檢請求”的頭信息包括兩個特殊字段。
? Access-Control-Request-Method
:必須字段,列出瀏覽器的 CORS 請求會用到哪些 HTTP 方法;
? Access-Control-Request-Headers
:這個字段是一個逗號 , 分隔的字符串,指定瀏覽器 CORS 請求會額外發送的頭信息字段,上面示例是 X-Custom-Header。
代碼實現
CORS(Cross-Origin Resource Sharing,跨域資源共享)通過在響應頭里加上一組特殊字段來告訴瀏覽器,這個資源允許被某些源訪問。
服務器端Cors的關鍵配置
struct CorsConfig
{std::vector<std::string> allowedOrigins;//允許哪些域名可以訪問std::vector<std::string> allowedMethods; //允許哪些方法可以跨域調用std::vector<std::string> allowedHeaders; //允許前端請求里帶哪些請求頭bool allowCredentials = false; //不允許攜帶Cookie/Authorization header/TLS client cert 這類憑證信息int maxAge = 3600; //瀏覽器緩存預檢請求的最大時長, 1 小時內同樣的跨域請求只會發送一次 OPTIONS,之后直接用緩存的結果static CorsConfig defaultConfig() {CorsConfig config;config.allowedOrigins = {"*"}; //這里允許的是所有域名config.allowedMethods = {"GET", "POST", "PUT", "DELETE", "OPTIONS"}; //在預檢請求(OPTIONS)的響應里告訴瀏覽器:后端接受哪些方法config.allowedHeaders = {"Content-Type", "Authorization"}; //允許前端帶Content-Type(比如 application/json)和Authorization(攜帶JWT Token等)return config;}
};
服務端解析預檢請求
處理客戶端發來的請求的流程如下:
- 判斷是否是預檢請求,如果是,進入下一步;否則不做處理(正常的請求,繼續后續的處理流程,響應)
- 檢查當前請求的源是否被允許,如果允許當前請求源則在響應頭中添加該源字段,狀態碼為204 No content,響應體為空,進入下一步
- 直接拋出特殊的響應對象(中斷后續的處理流程)
總結
:如果是預檢請求,設置Cors的相關字段,直接返回;否則就進入正常的處理流程。
/*** @brief 請求前鉤子,所有請求進來時都會先執行* 如果是跨域的 OPTIONS 預檢請求,直接構造響應并拋出,跳過后續中間件/路由邏輯。*/
void CorsMiddleware::before(HttpRequest& request)
{LOG_DEBUG << "CorsMiddleware::before - Processing request";// 如果是瀏覽器發起的預檢請求(CORS Preflight)if (request.method() == HttpRequest::Method::kOptions) {LOG_INFO << "Processing CORS preflight request";HttpResponse response; // 創建預檢響應handlePreflightRequest(request, response);// 直接中斷后續處理流程,拋出特殊的響應對象throw response;}
}/*** @brief 處理 CORS 預檢請求(OPTIONS)* 會校驗 Origin,并返回允許的跨域頭*/
void CorsMiddleware::handlePreflightRequest(const HttpRequest& request, HttpResponse& response)
{ // 從請求頭獲取 Originconst std::string& origin = request.getHeader("Origin");// 校驗是否允許跨域if (!isOriginAllowed(origin)) {LOG_WARN << "Origin not allowed: " << origin;response.setStatusCode(HttpResponse::k403Forbidden);return;}// 添加允許的跨域頭,對預檢請求返回 204 No Content;//即響應體為空,對應返回的是options字段的預檢請求的響應addCorsHeaders(response, origin);response.setStatusCode(HttpResponse::k204NoContent);LOG_INFO << "Preflight request processed successfully";
}/*** @brief 檢查給定 Origin 是否在允許列表里* @param origin 來自瀏覽器請求頭的 Origin* @return true 如果允許跨域,否則 false*/
bool CorsMiddleware::isOriginAllowed(const std::string& origin) const
{return config_.allowedOrigins.empty() || std::find(config_.allowedOrigins.begin(), config_.allowedOrigins.end(), "*") != config_.allowedOrigins.end() ||std::find(config_.allowedOrigins.begin(), config_.allowedOrigins.end(), origin) != config_.allowedOrigins.end();}/*** @brief 給響應添加標準的 CORS 頭信息* @param response 當前響應對象* @param origin 本次請求允許的 Origin*/
void CorsMiddleware::addCorsHeaders(HttpResponse& response, const std::string& origin)
{try { // 設置允許的 Originresponse.addHeader("Access-Control-Allow-Origin", origin);// 是否允許攜帶 cookieif (config_.allowCredentials) {response.addHeader("Access-Control-Allow-Credentials", "true");}// 設置允許的方法列表,以,分割if (!config_.allowedMethods.empty()) {response.addHeader("Access-Control-Allow-Methods", join(config_.allowedMethods, ", "));}// 設置允許的自定義請求頭列表if (!config_.allowedHeaders.empty()) {response.addHeader("Access-Control-Allow-Headers", join(config_.allowedHeaders, ", "));}// 設置預檢結果的緩存時間(單位:秒)response.addHeader("Access-Control-Max-Age", std::to_string(config_.maxAge));LOG_DEBUG << "CORS headers added successfully";} catch (const std::exception& e) {LOG_ERROR << "Error adding CORS headers: " << e.what();}
}
/*** @brief 工具函數:把字符串數組用指定分隔符拼接起來* @param strings 字符串列表* @param delimiter 分隔符,如 ", "* @return 拼接后的字符串*/
std::string CorsMiddleware::join(const std::vector<std::string>& strings, const std::string& delimiter)
{std::ostringstream result;for (size_t i = 0; i < strings.size(); ++i) {if (i > 0) result << delimiter;result << strings[i];}return result.str();
}
服務端填充響應
這里的響應指的是正常處理客戶端發來的請求后,在最終的響應頭中添加CORS的相關信息
/*** @brief 請求后鉤子,正常請求處理完畢后執行。* 不管是否跨域,都會在最終響應頭里加上 CORS 相關頭信息。*/
void CorsMiddleware::after(HttpResponse& response)
{LOG_DEBUG << "CorsMiddleware::after - Processing response";// 直接添加CORS頭,簡化處理邏輯if (!config_.allowedOrigins.empty()) {// 如果允許所有源if (std::find(config_.allowedOrigins.begin(), config_.allowedOrigins.end(), "*") != config_.allowedOrigins.end()) {addCorsHeaders(response, "*");} else {// 簡單處理:只取第一個允許的來源(可以按需改成匹配實際請求來源)addCorsHeaders(response, config_.allowedOrigins[0]);}}
}
抓包分析
上圖展示了請求與響應的抓包分析數據:當客戶端向服務器請求加載登錄頁面時,服務器不僅會在響應體中返回HTML文件,還會在響應頭中附帶CORS配置信息供瀏覽器解析。
CORS 詳解,終于不用擔心跨域問題了