同源策略及其解决方案
一、同源策略
1.1 定义:同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略,它是由Netscape提出的一个著名的安全策略。当一个浏览器的两个tab页中分别打开来 百度和谷歌的页面。当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。 [1] 如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。
同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。
同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。所以baidu.com下的js脚本采用ajax读取google.com里面的文件数据是会被拒绝的。
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
浏览器执行javascript脚本时,会检查这个脚本属于哪个页面,如果不是同源页面,就不会被执行
二、一个源的定义
如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。
举个例子:
下表给出了相对http://a.xyz.com/dir/page.html同源检测的示例:
URL | 结果 | 原因 |
http://a.xyz.com/dir2/other.html |
成功 | |
http://a.xyz.com/dir/inner/another.html |
成功 | |
https://a.xyz.com/secure.html |
失败 | 不同协议 ( https和http ) |
http://a.xyz.com:81/dir/etc.html |
失败 | 不同端口 ( 81和80) |
http://a.opq.com/dir/other.html |
失败 | 不同域名 ( xyz和opq) |
三、不受同源策略限制的
- 页面中的链接,重定向以及表单提交是不会受到同源策略限制的。
- 跨域资源的引入是可以的。但是js不能读写加载的内容。如嵌入到页面中的
,,,等。
以上参考这篇博文https://www.cnblogs.com/rain-chenwei/p/9520240.html
四、如果没有同源策略的危险场
4.1 没有同源策略的接口请求:
当你进行了接口请求的时候,服务器验证之后会在响应头set-cookie字段,下次再发起请求的时候,浏览器自动将cookie附加在Http请求头字段cookie中,就可以验证登录过。当我们登录一个网站的时候,收到一个其他网站的链接,点击进入这个其他网站时候,如果没有同源策略的话,这个其他网站的就可以在无需登录的情况下进入你之前的网站,可以加载之前的资源。
4.2 没有同源策略的Dom查询:
可以通过获取不同源网站dom,比如你在www.abc.com中输入密码的input标签的内容,www.def.com这样通过set-cookie字段就可以轻松拿到用户名和密码,这样就有点太危险了吧!
五、解决方案
解决方案有很多种,比如 jsonp,CORS,websocket,nginx 等等,每种方案基本都有各自的适用场景,优缺点,目前普遍场景用的是 nginx + CORS
5.1 JSONP跨域,script标签请求资源不受同源策略限制
jsonp 原理是利用 script 标签没有同源限制,依赖服务端的一种方案,局限性是只能发送 get 请求
浏览器在遇到 script 标签时,会去访问 src 属性的地址,将 js 文件下载下来,并执行
那么,只要把 src 属性赋值需要跨域请求的 url,然后本地先声明一个 callback 函数,这样,服务端在接收到请求时,返回的 js 代码就是调用 callback,需要返回的数据以参数传递给 callback,浏览器在下载完这 js 并执行时,就等效于调用了 callback
所以这方案需要依赖于后端,前端需要告知后端这请求的回调函数名,由后端来写调用的代码
$.ajax({ url: "http://localhost:9090/student", type: "GET", dataType: "jsonp", //指定服务器返回的数据类型 success: function (data) { var result = JSON.stringify(data); //json对象转成字符串 $("#text").val(result); } });
jsonp 指定服务器返回的数据类型为jsonp类型,自动带一个callback=xxx,xxx是jquery随机生成的一个回调函数。支持get方法
json返回的是一串数据;而jsonp返回的是脚本代码(包含一个函数调用);
5.2 CORS,后端通过拦截器Filter来实现
CORS:跨域资源共享,通过响应头中的 Access-Control-Allow-Origin 字段来声明,本资源是否允许外域访问
这种方案需要依赖于后端,请求的资源可以是某个资源文件,也可以是 json 接口,但后端需要对这些请求的响应头中添加字段,来声明,本资源或接口允许外域访问
这样,浏览器在看到响应头中有这个字段,就不会对跨域请求的响应结果做拦截
这是一种根本解决方案,缺点是需要依赖后端
5.2.1 定义:
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器发出XMLHttpRequest请求,从而解决AJAX只能同源使用的限制。
5.2.2 实现
CORS需要浏览器和服务器同时支持。目前基本上主流的浏览器都支持CORS。所以只要后端服务支持CORS,就能够实现跨域。
用@Configuration注册这个类为bean,相当于xml文件里的然后用@Bean将这个方法也注册成为一个bean,相当于一个xml文件里面的
然后application.yml里面也要配置路由信息
public class CrossDomainFilter implements Filter { public CrossDomainFilter() { } public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse)res; response.setHeader("Access-Control-Allow-Origin", "*");//主要这个 response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, souche-security-token-inc, Souche-Security-Token"); chain.doFilter(req, res); } public void init(FilterConfig filterConfig) { } public void destroy() { } }
同时记得在web.xml中配置这个
<filter> <filter-name>crossDomain</filter-name> <filter-class>com.souche.optimus.core.interceptor.CrossDomainFilter</filter-class> </filter> <filter-mapping> <filter-name>crossDomain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
一个请求需要同时满足以下两大条件才属于简单请求。
(1) 请求方法是以下三种方法之一: HEAD GET POST (2)HTTP的头信息不超出以下几种字段: Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
简单请求的处理方式
在跨域场景下,当浏览器发送简单请求时,浏览器会自动在请求头中添加表明请求来源的 Origin 字段。
我们的后端程序只需要在返回的响应头中加上 Access-Control-Allow-Origin 字段,并且把该字段的值设置为 跨域请求的来源地址或简单的设置为 * 就可以了。
例如:我们可以在Django中间件中的process_response方法来给相应对象添加该字段。
from django.utils.deprecation import MiddlewareMixin class CorsMiddleware(MiddlewareMixin): def process_response(self, request, response): # 给响应头加上 Access-Control-Allow-Origin 字段 并简单的设置为 * response['Access-Control-Allow-Origin'] = '*' return response
非简单请求的处理方式
我们开发中常用到的那些请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json的都是非简单请求。
对于非简单请求,浏览器通常都会在请求之前发送一次 OPTIONS 预检 请求。该请求会像后端服务询问是否允许从当前源发送请求并且询问允许的 请求方法 和 请求头字段。
举个例子:
我们前端使用axios向后端发送PUT请求,结果:
看看发送的具体请求:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y8tBEStT-1600695343191)(%E5%90%8C%E6%BA%90%E7%AD%96%E7%95%A5%E5%8F%8A%E5%85%B6%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88.assets/867021-20190109182132577-1441943751.png)]
解决办法也很简单,我们可以在后端简单的给响应对象添加上 常用请求方法(PUT、DELETE)的支持就可以了。
在上面Django的中间件中添加如下代码:
from django.utils.deprecation import MiddlewareMixin class CorsMiddleware(MiddlewareMixin): def process_response(self, request, response): # 给响应头加上 Access-Control-Allow-Origin 字段 并简单的设置为 * response['Access-Control-Allow-Origin'] = '*' if request.method == 'OPTIONS': # 允许发送 PUT 请求 response['Access-Control-Allow-Methods'] = 'PUT, DELETE' # 允许在请求头中携带 Content-type字段,从而支持发送json数据 response['Access-Control-Allow-Headers'] = 'Content-type' return response
使用django-cors-headers
我们这个中间件确实能解决目前的CORS跨域问题,但是我们的土方法肯定是不够严谨的,已经有人造好轮子-- django-cors-headers 了。
我们只需要安装这个包,然后按需要配置一下就可以了。
安装
pip install django-cors-headers
注册APP
INSTALLED_APPS = [ ... 'app01.apps.App01Config', 'corsheaders', # 将 corsheaders 这个APP注册 ]
添加中间件
必须放在最前面,因为要先解决跨域的问题。只有允许跨域请求,后续的中间件才会正常执行。
MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # 添加中间件 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
配置
你可以选择不限制跨域访问
CORS_ORIGIN_ALLOW_ALL = True
或者你可以选择设置允许访问的白名单
CORS_ORIGIN_ALLOW_ALL = False CORS_ORIGIN_WHITELIST = ( # '<YOUR_DOMAIN>[:PORT]', '127.0.0.1:8080' )
5.3: nginx
借助 nginx 可以有两种方案,一种是直接用 nginx 做代理转发,另一种则是结合 CORS
- 代理转发
浏览器有同源限制,那只要保证请求的是同源的也就没问题了
假如 http://domain1.com 的网页里有对 http://domain2.com 的请求,因为域名不一样,请求的结果被浏览器拦截了
借助 nginx 的转发,那么,前端可以这么处理跨域的请求:将所有对 http://domain2.com 也就是非本源的请求,都修改成类似 http://domain1.com/domain2 的请求,也就是使用同协议、同域名、同端口,但根据不同路径来区分,这样就不存在跨域问题
前端这样修改后,因为实际请求是需要发送到 http://domain2.com 的,所以,就需要 nginx 在接收到请求时,根据不同路径,将请求转发到真正的目的地,拿到结果后再返回
这种方案,需要前端配合,让前端访问一个同源的虚假 url,再交由 nginx 做转发,去访问真正的 url
缺点就是,划分的路径需要区分同源的原本请求以及虚假 url 请求
这种方案也挺常用的
- 结合 CORS
只利用 nginx 代理转发的话,就需要保证所有请求都是同协议、同域名、同端口的,只能根据路径不同做区分,这样灵活性不高
所以,还可以结合 CORS,因为只要响应头中返回跨域资源共享的相关字段,浏览器就不会进行同源限制
所以,只要保证前端的请求都经过 nginx,那么协议、域名、端口一不一样无所谓了,在 nginx 上配置这些跨域请求返回的响应头中都加上跨域资源共享允许的字段,就可以了
这种方案其实就是利用 nginx 的转发 + CORS,是目前比较常用的一种
六、实现跨域的9种方法(点击可跳转详情页面)
- jsonp
- cors
- postMessage
- document.domain
- window.name
- location.hash
- http-proxy 后续会有详细文章阐述
- nginx 后续会有详细版块阐述
- websocket