一、同源策略
浏览器的同源策略(Same-Origin Policy)要求:只有协议、域名和端口都相同的请求才被视为同源,才允许正常访问。
两个URL在以下三个方面完全相同时称为"同源":
- 协议相同(如都是http或https)
- 域名相同(如都是example.com)
- 端口相同(如都是80端口)
二、同源策略的限制
- 读取非同源的DOM(iframe、窗口等)
- 发送AJAX请求到非同源地址
- 读取非同源的Cookie、LocalStorage等存储数据
例如:
https://example.com/page1
和https://example.com/page2
是同源http://example.com
和https://example.com
不同源(协议不同)https://example.com
和https://sub.example.com
不同源(域名不同)https://example.com
和https://example.com:8080
不同源(端口不同)
三、同源策略的作用
**限制跨源DOM访问:**同源策略规定,不同源的页面无法直接访问彼此的DOM。例如,恶意网站 http://malicious.com
无法通过 JavaScript 读取或修改 http://target.com
的表单数据、页面结构或用户输入内容。这从根本上阻止了XSS攻击者窃取敏感信息(如登录凭据)或篡改页面内容。
**隔离Cookie访问:**浏览器仅允许同源页面访问当前域的Cookie。如果没有这一限制,攻击者可以通过恶意脚本窃取目标网站的会话Cookie,从而冒充用户身份。同源策略确保 http://malicious.com
无法读取 http://target.com
的Cookie,防止会话劫持等攻击。
**独立的脚本执行环境:**不同源的JavaScript运行在隔离的上下文中,无法直接访问其他源的全局变量、函数或对象。例如,即使攻击者在目标网站的评论区注入恶意脚本,该脚本也无法绕过同源策略去窃取或篡改主站的关键数据,从而限制了XSS攻击的危害范围。
**严格限制跨域请求:**默认情况下,浏览器禁止脚本发起跨域HTTP请求(如 fetch
或 XMLHttpRequest
),除非目标服务器明确允许(如通过CORS)。这一机制防止攻击者将窃取的数据自动发送到恶意服务器,阻断了XSS攻击的数据外泄途径。
可见同源策略主要是在防止跨站脚本攻击(XSS)
四、同源策略的例外
同源策略虽然是Web安全的重要基石,但为了满足实际开发需求,浏览器也提供了一些合理的例外情况:
静态资源加载:<script>
、<img>
、<link>
、<video>
、<audio>
等标签允许加载跨域资源,静态资源(如图片、视频)通常不包含敏感数据,同时,虽然可以加载,但JavaScript无法读取这些资源的内容(除非CORS允许)。
CORS(跨源资源共享):通过预检请求和特殊响应头实现受控的跨域访问。现代Web应用需要合法的跨域通信(如前后端分离架构),通过服务器显式声明允许的跨域请求,兼顾安全与功能。
使用JSONP技术进行跨域请求:在CORS出现前的过渡方案,利用脚本标签不受同源限制的特性。
document.domain:允许子域和父域通过设置相同domain进行通信,大型网站常有多个子域(如a.example.com
和b.example.com
),允许同一组织控制的不同子域间安全通信,只能设置为当前域或其父域
五、CORS
解决跨域问题
5.1、对于简单请求
什么是简单请求,可以遵循以下定义:
- 方法为
GET
、POST
或HEAD
- 头部仅包含允许的字段(如
Accept
、Content-Type
为text/plain
/multipart/form-data
/application/x-www-form-urlencoded
等) - 无自定义头部
服务器在响应中添加 Access-Control-Allow-Origin
头,指定允许的源(或 *
表示允许任意源)
Access-Control-Allow-Origin: https://example.com
浏览器检查该头与当前源匹配后,才会允许响应数据通过。
5.2、对于非简单请求
对于非简单请求(如 PUT
、DELETE
、自定义头部、Content-Type: application/json
等),浏览器会先发送一个 OPTIONS
方法的预检请求,询问服务器是否允许实际请求。
客户端发送的预检请求如下:
OPTIONS /api/data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, X-Custom-Header
服务器需响应预检请求,明确声明允许的方法、头部等:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com // 允许的请求源
Access-Control-Allow-Methods: POST, GET, OPTIONS // 允许的请求方法
Access-Control-Allow-Headers: Content-Type, X-Custom-Header // 允许的请求头
Access-Control-Max-Age: 86400 // 缓存预检结果时间(秒)
预检通过的话,客户端就可以发送真实请求:
POST /api/data HTTP/1.1
Origin: https://example.com
Content-Type: application/json
X-Custom-Header: foo
如果预检失败,浏览器会直接拦截真实请求,并在控制台报错。
关键点:
- 开发者无需手动处理
OPTIONS
请求,浏览器会自动完成。 - 若预检响应头缺失或错误,真实请求会被拦截。
Access-Control-Max-Age
可减少重复预检请求,提升性能。
5.3、对于携带凭据的请求(Credentials)
客户端:请求时必须做额外设置
若请求需要携带 Cookie 或认证信息(如 withCredentials: true
),服务器需额外声明:
Access-Control-Allow-Origin: https://example.com // 不能为 *
Access-Control-Allow-Credentials: true
同时,客户端需显式设置 withCredentials
属性(如 Fetch API 或 Axios)。
Fetch API
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include', // 必须设置为 include 才能发送凭据
headers: {
'Content-Type': 'application/json',
},
});
XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.withCredentials = true; // 必须设置为 true
xhr.send();
Axios
axios.get('https://api.example.com/data', {
withCredentials: true, // 必须设置为 true
});
关键点:
credentials: 'include'
(Fetch)或withCredentials: true
(XHR/Axios)必须显式设置,否则浏览器不会发送 Cookies 等凭据。- 即使设置了
withCredentials
,服务端也必须正确响应 CORS 头,否则请求会被拦截。
服务端:响应 CORS 头(必须严格匹配)
服务端必须在响应中包含以下头部:
Access-Control-Allow-Origin: https://example.com // 不能是 *,必须明确指定请求来源
Access-Control-Allow-Credentials: true // 必须为 true
Access-Control-Allow-Methods: GET, POST, OPTIONS // 允许的方法
Access-Control-Allow-Headers: Content-Type // 允许的请求头(如自定义头)
关键限制:
Access-Control-Allow-Origin
不能为*
Access-Control-Allow-Credentials: true
必须存在- 如果对于非简单请求,还需要进行预检请求,和 5.1 节 5.2 节一样了
六、Gin框架中配置CORS跨域
Gin 官方推荐使用 github.com/gin-contrib/cors
中间件来配置跨域。
package main
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://foo.com"}, // 明确允许访问的域名
AllowMethods: []string{"PUT", "PATCH"}, // 允许的http方法
AllowHeaders: []string{"Origin"}, // 允许客户端在请求中携带的头部字段
ExposeHeaders: []string{"Content-Length"}, // 允许客户端访问的额外响应头(默认只能访问简单头,如 Cache-Control、Content-Language 等)
AllowCredentials: true, // 允许跨域请求携带凭据
AllowOriginFunc: func(origin string) bool { // 动态验证请求的 Origin 是否合法,优先级高于 AllowOrigins 字段
return origin == "https://github.com"
},
MaxAge: 12 * time.Hour, // 预检请求(OPTIONS)的缓存时间
}))
router.Run()
}