HTTP同源策略与跨域资源共享(CORS)机制详解
一、同源策略(Same-Origin Policy)
1.1 同源策略定义
同源策略是浏览器实施的一种重要安全机制,它限制了一个源(origin)的文档或脚本如何与另一个源的资源进行交互。
1.2 同源判定标准
两个URI被认为是同源当且仅当它们的以下三个部分完全相同:
- 协议(protocol)
- 主机(host)
- 端口(port)
例如:
http://example.com/page1和http://example.com/page2是同源http://example.com和https://example.com不同源(协议不同)http://example.com和http://sub.example.com不同源(主机不同)http://example.com和http://example.com:8080不同源(端口不同)
1.3 受同源策略限制的操作
浏览器在以下操作时会检查同源策略:
- 以跨站点方式调用XMLHttpRequest或Fetch API
- Web字体(用于CSS中@font-face的跨域字体使用)
- WebGL textures
- 使用drawImage绘制到canvas的图像/视频帧
- 样式表(用于CSSOM访问)
1.4 同源策略的两种表现
- 限制发起AJAX请求:阻止跨域的XMLHttpRequest或Fetch请求
- 拦截跨站请求的返回结果:允许请求发送但阻止JavaScript获取响应
二、跨域资源共享(CORS)
2.1 CORS概述
跨域资源共享(Cross-Origin Resource Sharing)是一种解决跨域请求的方案,通过使用额外的HTTP头来告诉浏览器允许跨域请求。
2.2 CORS工作机制
- 浏览器自动在跨域请求中添加
Origin头 - 服务器通过
Access-Control-Allow-Origin等响应头决定是否允许该跨域请求 - 对于非简单请求,浏览器会先发送预检请求(OPTIONS)
三、简单请求与非简单请求
3.1 简单请求条件
请求必须同时满足以下所有条件才被视为简单请求:
HTTP方法限制:
- GET
- HEAD
- POST
Content-Type限制:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
HTTP头限制:
只能包含以下安全的首部字段:
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
其他限制:
- 请求中的XMLHttpRequestUpload对象没有注册任何事件监听器
- 请求中没有使用ReadableStream对象
3.2 简单请求的处理流程
- 浏览器直接发送请求,不发送预检请求
- 服务器响应中包含
Access-Control-Allow-Origin头 - 浏览器检查响应头,决定是否让JavaScript获取响应
3.3 非简单请求条件
满足以下任一条件即为非简单请求:
HTTP方法:
- PUT
- DELETE
- CONNECT
- OPTIONS
- TRACE
- PATCH
HTTP头:
- 设置了CORS安全首部字段集合之外的其他首部字段
Content-Type:
- 不是application/x-www-form-urlencoded、multipart/form-data或text/plain
其他:
- XMLHttpRequestUpload对象注册了事件监听器
- 使用了ReadableStream对象
3.4 非简单请求的处理流程
- 浏览器先发送OPTIONS预检请求
- 服务器响应预检请求,包含CORS相关头
- 浏览器检查预检响应,决定是否发送实际请求
- 如果允许,发送实际请求
- 服务器响应实际请求
- 浏览器检查响应头,决定是否让JavaScript获取响应
四、CORS相关HTTP头
4.1 请求头
-
Origin: 表示请求的来源
- 示例:
Origin: http://example.com
- 示例:
-
Access-Control-Request-Method: 用于预检请求,表示实际请求将使用的方法
- 示例:
Access-Control-Request-Method: POST
- 示例:
-
Access-Control-Request-Headers: 用于预检请求,表示实际请求将携带的自定义头
- 示例:
Access-Control-Request-Headers: X-Custom-Header
- 示例:
4.2 响应头
-
Access-Control-Allow-Origin: 指定允许访问资源的源
- 示例:
Access-Control-Allow-Origin: http://example.com - 特殊值:
*(表示允许任何源,但不能与credentials一起使用)
- 示例:
-
Access-Control-Allow-Credentials: 是否允许浏览器发送凭据(cookie等)
- 示例:
Access-Control-Allow-Credentials: true
- 示例:
-
Access-Control-Allow-Methods: 用于预检响应,表示允许的HTTP方法
- 示例:
Access-Control-Allow-Methods: POST, GET, OPTIONS
- 示例:
-
Access-Control-Allow-Headers: 用于预检响应,表示允许的自定义头
- 示例:
Access-Control-Allow-Headers: X-Custom-Header
- 示例:
-
Access-Control-Expose-Headers: 允许JavaScript访问的额外响应头
- 示例:
Access-Control-Expose-Headers: X-Custom-Header
- 示例:
-
Access-Control-Max-Age: 预检请求的缓存时间(秒)
- 示例:
Access-Control-Max-Age: 86400
- 示例:
五、CORS实现示例
5.1 简单请求示例
前端代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>AJAX</title>
</head>
<script>
function submitRequest() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://127.0.0.1:8888/get", true);
xhr.withCredentials = true; // 携带凭据
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200){
alert(xhr.responseText)
}
}
}
</script>
<button onclick="submitRequest()">AJAX</button>
</html>
后端代码(Flask):
@app.route('/get', methods=['GET'])
def get():
if session.get('user','')=='admin':
ret = "Admin do something!"
else:
ret = "No Privilege..."
resp = make_response(ret)
resp.headers['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000"
resp.headers['Access-Control-Allow-Credentials'] = 'true'
resp.headers['Access-Control-Allow-Methods'] = "POST, GET, OPTIONS, PUT, DELETE, PATCH"
return resp
5.2 非简单请求示例
前端代码:
<html>
<title>{{ Evil }}</title>
<center><h1> Reset Password </h1>
<head>
<script type="text/javascript">
function submitRequest() {
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://127.0.0.1:8888/json", true);
xhr.setRequestHeader("Accept", "application/json");
xhr.setRequestHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
xhr.withCredentials = true;
xhr.send(JSON.stringify({"action":"change passwd"}));
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200){
alert(xhr.responseText)
}
}
}
</script>
</head>
<body>
<button onclick="submitRequest()">Conform</button>
</body>
</html>
后端代码(Flask):
@app.route('/json', methods=['GET','POST','OPTIONS'])
def json():
if request.method == 'GET':
return render_template('json.html', Evil="Benign")
else:
if session.get('user','')=='admin':
data=request.json
ret='Admin do '+data["action"]
else:
ret="No Privilege2..."
resp=make_response(jsonify({'result': ret}))
resp.headers['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000"
resp.headers['Access-Control-Allow-Credentials'] = 'true'
resp.headers['Access-Control-Allow-Methods'] = "POST, GET, OPTIONS, PUT, DELETE, PATCH"
resp.headers['Access-Control-Allow-Headers'] = "origin, content-type, accept, x-requested-with"
return resp
六、安全注意事项
-
Credentials与通配符:当使用
Access-Control-Allow-Credentials: true时,Access-Control-Allow-Origin不能为*,必须指定明确的源。 -
CSRF防护:虽然CORS提供了一定保护,但仍需防范CSRF攻击,特别是对于简单请求(GET、POST等)。
-
敏感信息暴露:谨慎设置
Access-Control-Expose-Headers,避免暴露敏感头信息。 -
预检请求缓存:合理设置
Access-Control-Max-Age可以优化性能,但过长可能带来安全风险。
七、常见问题
-
为什么我的请求被拦截了?
- 检查是否为跨域请求
- 检查服务器是否正确设置了CORS头
- 检查请求是否为非简单请求但没有正确处理预检请求
-
为什么设置了
Access-Control-Allow-Origin: *还是无法获取响应?- 如果请求需要携带凭据(cookie等),则不能使用
*,必须指定明确的源
- 如果请求需要携带凭据(cookie等),则不能使用
-
如何调试CORS问题?
- 使用浏览器开发者工具查看网络请求
- 检查请求是否包含
Origin头 - 检查响应是否包含正确的CORS头
-
CORS与JSONP的区别?
- CORS是现代浏览器支持的官方标准
- JSONP是旧式跨域解决方案,只支持GET请求
- JSONP存在安全隐患(XSS风险)