原理
基本概念
在浏览器安全模型中,同源策略是最重要的安全基石。
一个“域”是由3个要素组成的:
- 协议(如:http 或 https)
- 主机(Host,如 www.example.com 或 127.0.0.1)
- 端口(Port,如 80 或 8080)
只要这三个完全一致,就是同源的。
例如:
下面是官网解释跨域的图解:
![image]()
两种请求
:浏览器将 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的关键配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| struct CorsConfig { std::vector<std::string> allowedOrigins; std::vector<std::string> allowedMethods; std::vector<std::string> allowedHeaders; bool allowCredentials = false; int maxAge = 3600; static CorsConfig defaultConfig() { CorsConfig config; config.allowedOrigins = {"*"}; config.allowedMethods = {"GET", "POST", "PUT", "DELETE", "OPTIONS"}; config.allowedHeaders = {"Content-Type", "Authorization"}; return config; } };
|
服务端解析预检请求
处理客户端发来的请求的流程如下:
- 判断是否是预检请求,如果是,进入下一步;否则不做处理(正常的请求,继续后续的处理流程,响应)
- 检查当前请求的源是否被允许,如果允许当前请求源则在响应头中添加该源字段,状态码为204 No content,响应体为空,进入下一步
- 直接抛出特殊的响应对象(中断后续的处理流程)
总结
:如果是预检请求,设置Cors的相关字段,直接返回;否则就进入正常的处理流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
|
void CorsMiddleware::before(HttpRequest& request) { LOG_DEBUG << "CorsMiddleware::before - Processing request"; if (request.method() == HttpRequest::Method::kOptions) { LOG_INFO << "Processing CORS preflight request"; HttpResponse response; handlePreflightRequest(request, response); throw response; } }
void CorsMiddleware::handlePreflightRequest(const HttpRequest& request, HttpResponse& response) { const std::string& origin = request.getHeader("Origin"); if (!isOriginAllowed(origin)) { LOG_WARN << "Origin not allowed: " << origin; response.setStatusCode(HttpResponse::k403Forbidden); return; }
addCorsHeaders(response, origin); response.setStatusCode(HttpResponse::k204NoContent); LOG_INFO << "Preflight request processed successfully"; }
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(); }
void CorsMiddleware::addCorsHeaders(HttpResponse& response, const std::string& origin) { try { response.addHeader("Access-Control-Allow-Origin", origin); if (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(); } }
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的相关信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
void CorsMiddleware::after(HttpResponse& response) { LOG_DEBUG << "CorsMiddleware::after - Processing response"; if (!config_.allowedOrigins.empty()) { if (std::find(config_.allowedOrigins.begin(), config_.allowedOrigins.end(), "*") != config_.allowedOrigins.end()) { addCorsHeaders(response, "*"); } else { addCorsHeaders(response, config_.allowedOrigins[0]); } } }
|
抓包分析
![image]()
![image]()
上图展示了请求与响应的抓包分析数据:当客户端向服务器请求加载登录页面时,服务器不仅会在响应体中返回HTML文件,还会在响应头中附带CORS配置信息供浏览器解析。
CORS 详解,终于不用担心跨域问题了