第二百七十四节,同源策略和跨域Ajax

同源策略和跨域Ajax

 

什么是同源策略

 尽管浏览器的安全措施多种多样,但是要想黑掉一个Web应用,只要在浏览器的多种安全措施中找到某种措施的一个漏洞或者绕过一种安全措施的方法即可。浏览器的各种保安措施之间都试图保持相互独立,但是攻击者只要能在出错的地方注入少许JavaScript,所有安全控制几乎全部瓦解——最后还起作用的就是最弱的安全防线:同源策略。同源策略管辖着所有保安措施,然而,由于浏览器及其插件,诸如Acrobat Reader、Flash 和Outlook Express漏洞频出,致使同源策略也频频告破。
  既然web应用的最弱安全防线是同源策略,那什么是同源策略呢?如何去攻破同源策略呢?如何黑web应用呢?
  同源策略,它是由Netscape提出的一个著名的安全策略。所有支持javascript的网站都会使用同源策略来保护自己的web应用。
  同源策略又名同域策略,通俗易懂的来说,同源就是(主机名+协议+端口号【若存在】)三者相同。也就是说javascript只可以操作自己域下的东西,不能操作其他域下的东西。比如百度下javascript是不可操作谷歌下的页面。

  那为什么要同源策略呢?
  如上述所述,同源策略,javascript操作自己web应用,不得操作别人web应用。既然如此,也就是别人无法操作自己web应用,是保证web安全的一种方式。
  假设别人可以操作自己web应用,试想是否很可怕?假如你打开网银页面,蹦出一个恶意广告,当你关闭广告,若没有同源策略,是不是代表恶意广告会操作你网银页面,通过javascript窃取你网银的信息。
  那了解了同源策略,下面举几个url判断是否在同一个域中。
  假设url地址:http://store.company.com/dir/page.html检测下面是否他的同源地址。
  其url的协议:http;主机名:store.company.com 端口号没有或者其相应服务器的默认值


URL 结果 原因
http://store.company.com/dir2/other.html
http://store.company.com/dir/inner/another.html
https://store.company.com/secure.html 不是 协议不相同
http://store.company.com:81/dir/etc.html 不是 端口号不相同
http://news.company.com/dir/other.html 不是 主机名不相同

 
 
违背了同源策略,有些跨域请求会被浏览器拦截的
 
在浏览器中,<script>、<img>、<iframe>、<link>等标签都可以加载跨域资源,而不受同源限制,但浏览器限制了JavaScript的权限使其不能读、写加载的内容。也就是说不同源的JavaScript是无法获取HTML元素,和JavaScript通讯请求的
  
另外同源策略只对网页的HTML文档做了限制,对加载的其他静态资源如javascript、css、图片等仍然认为属于同源。
 
 
下面讲解ajax跨域请求被阻止
 
举例:
首先在本地hosts文件配置两个域名访问IP作为测试用

127.0.0.1 www.fwd.com
127.0.0.1 www.khd.com

 
http://www.khd.com:8002/khd 文件里用ajax请求http://www.fwd.com:8001/fwd
 
http://www.khd.com:8002/khd
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src='{{static_url("jquery.min.js")}}' charset="UTF-8"></script>
</head>
<body>
<input type="button" value="提交" onclick="doajax();"/>
<script>
    function doajax() {
        $.ajax({
            url: 'http://www.fwd.com:8001/fwd',
            type: 'POST',
            data:{'k1':'v1'},
            success: function (responseText, statusText) {
            },
            error: function (event, errorText, errorType) {

            }
        });
    }
</script>
</body>
</html>

http://www.fwd.com:8001/fwd

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src='{{static_url("jquery.min.js")}}' charset="UTF-8"></script>
    <script type="text/javascript" src='{{static_url("jquery.form.js")}}' charset="UTF-8"></script>
</head>
<body>
服务端
<script>

</script>
</body>
</html>

此时点击按钮发起ajax请求后,可以看到请求已被浏览器的同源策略阻止

 
跨域阻止原理图
 
 
 
 突破浏览器同源策略ajax跨域请求,有两个办法
 
办法一、JSONP方式
 
自定义JSONP跨域请求数据
 
只能是get方法的请求,也就是利用script标签和script标签的src发送get请求
1在要请求数据的页面用js创建一个script标签src等于要请求数据的地址
2响应页面返回的必须是一个带有参数的函数名称
3自定义对应响应页面返回的函数,这样自定义的函数也就会执行了,就能得到数据
请求页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src='{{static_url("jquery.min.js")}}' charset="UTF-8"></script>
</head>
<body>
<input type="button" value="跨域提交" onclick="doajax();"/>
<script>
    //利用原生js自己创建一个跨域请求
    function doajax() {                                 //当点击提交按钮是执行函数
        var tag = document.createElement('script');     //创建一个script标签
        tag.src = 'http://www.fwd.com:8001/fwd';        //设置script标签的src地址,为要请求数据的地址,因为src是不受同源策略
        document.head.appendChild(tag);                 //将script标签添加到head标签里面
        document.head.removeChild(tag);                 //添加后会立即发送请求,所以这里可以将添加的script标签删除了
    }
    //响应网站返回,shuju({'k1':'v1'});  也就相当于返回了一个带有参数的函数名称
    function shuju(shj) {                               //自定义执行函数,和响应网站返回的名称相同,也就会执行自定义函数,而参数就是响应网站返回的数据
        for(var i in shj){                              //循环响应网站返回的数据
            alert(i + ':' + shj[i]);                    //打印出数据的键和值
        }
    }
</script>
</body>
</html>

响应端

import tornado.ioloop
import tornado.web                                              #导入tornado模块下的web文件


class fwdHandler(tornado.web.RequestHandler):
    def get(self):                                      #接收get请求
        # self.render("fwd.html")
        self.write("shuju({'k1':'v1'});")               #返回数据  
    def post(self):
        self.write("post请求成功")

settings = {                                            #html文件归类配置,设置一个字典
    "template_path":"views",                            #键为template_path固定的,值为要存放HTML的文件夹名称
    "static_path":"statics",                            #键为static_path固定的,值为要存放js和css的文件夹名称
}

#路由映射
application = tornado.web.Application([                 #创建一个变量等于tornado.web下的Application方法
    (r"/fwd", fwdHandler),
],**settings)                                           #将html文件归类配置字典,写在路由映射的第二个参数里

if __name__ == "__main__":
    #内部socket运行起来
    application.listen(8001)                            #设置端口
    tornado.ioloop.IOLoop.instance().start()

 
 
ajax的JSONP方式
ajax跨域请求本质上就是上面我们自定义跨域的方式,只不过ajax帮我们封装好了,不用我们去创建script标签
请求页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src='{{static_url("jquery.min.js")}}' charset="UTF-8"></script>
</head>
<body>
<input type="button" value="跨域提交" onclick="doajax();"/>
<script>
    function doajax() {                                 //当点击提交按钮是执行函数
        $.ajax({
            type: 'GET',
            url: 'http://www.fwd.com:8001/fwd',
            dataType: 'jsonp',   //JSONP,数据类型
            jsonpCallBack:'shj'  //接收响应页面带有参数的函数名称
        });
    }
    //响应网站返回,shuju({'k1':'v1'});  也就相当于返回了一个带有参数的函数名称
    function shuju(shj) {                               //自定义执行函数,和响应网站返回的名称相同,也就会执行自定义函数,而参数就是响应网站返回的数据
        for (var i in shj) {                            //循环响应网站返回的数据
            alert(i + ':' + shj[i]);                    //打印出数据的键和值
        }
    }
</script>
</body>

响应端

#!/usr/bin/env python
#coding:utf-8

import tornado.ioloop
import tornado.web                                              #导入tornado模块下的web文件


class fwdHandler(tornado.web.RequestHandler):
    def get(self):                                      #接收get请求
        # self.render("fwd.html")
        self.write("shuju({'k1':'v1'});")               #返回数据
    def post(self):
        self.write("post请求成功")

settings = {                                            #html文件归类配置,设置一个字典
    "template_path":"views",                            #键为template_path固定的,值为要存放HTML的文件夹名称
    "static_path":"statics",                            #键为static_path固定的,值为要存放js和css的文件夹名称
}

#路由映射
application = tornado.web.Application([                 #创建一个变量等于tornado.web下的Application方法
    (r"/fwd", fwdHandler),
],**settings)                                           #将html文件归类配置字典,写在路由映射的第二个参数里

if __name__ == "__main__":
    #内部socket运行起来
    application.listen(8001)                            #设置端口
    tornado.ioloop.IOLoop.instance().start()

 
 
 
方法二、基于CORS实现跨域Ajax
请求页面不变就像同域一样,只是响应端返回数据的时候带有响应头标识,也就是告诉浏览器允许跨域请求,这是最新的方式,新浏览器支持,老浏览器不支持,将来会都支持
随着技术的发展,现在的浏览器可以支持主动设置从而允许跨域请求,即:跨域资源共享(CORS,Cross-Origin Resource Sharing),其本质是设置响应头,使得浏览器允许跨域请求。
 
CORS实现跨域Ajax有两种方式,简单方式和复杂方式两种

简单请求和非简单请求的区别?
简单请求:一次请求
非简单请求:两次请求,在发送数据之前会先发一次请求用于做“预检”,只有“预检”通过后才再发送一次请求用于数据传输。

简单请求
简单请求:一次请求

条件:
  1、请求方式:HEAD、GET、POST
  2、请求头信息:
    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
    Content-Type 对应的值是以下三个中的任意一个
      application/x-www-form-urlencoded
      multipart/form-data
      text/plain
注意:同时满足以上两个条件时,则是简单请求,否则为复杂请求

 

self.set_header()方法,在响应端的逻辑处理的get或者post里使用,功能:给返回数据加上响应头标识告诉浏览器允许跨域请求
使用方法:两个参数,参数1,响应头标识,参数2,允许跨域请求的域名(多个域名,号隔开),(*代表所有域名支持跨域请求)
self.set_header('Access-Control-Allow-Origin','http://www.jxiou.com/')
self.set_header('Access-Control-Allow-Origin','*')

响应端

#!/usr/bin/env python
#coding:utf-8

import tornado.ioloop
import tornado.web                                              #导入tornado模块下的web文件


class fwdHandler(tornado.web.RequestHandler):
    def get(self):                                      #接收get请求
        pass
    def post(self):
        self.set_header('Access-Control-Allow-Origin','*')
        self.write("{'k1':'v1'}")  # 返回数据

settings = {                                            #html文件归类配置,设置一个字典
    "template_path":"views",                            #键为template_path固定的,值为要存放HTML的文件夹名称
    "static_path":"statics",                            #键为static_path固定的,值为要存放js和css的文件夹名称
}

#路由映射
application = tornado.web.Application([                 #创建一个变量等于tornado.web下的Application方法
    (r"/fwd", fwdHandler),
],**settings)                                           #将html文件归类配置字典,写在路由映射的第二个参数里

if __name__ == "__main__":
    #内部socket运行起来
    application.listen(8001)                            #设置端口
    tornado.ioloop.IOLoop.instance().start()

请求页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src='{{static_url("jquery.min.js")}}' charset="UTF-8"></script>
</head>
<body>
<input type="button" value="跨域提交" onclick="doajax();"/>
<script>
    function doajax() {                                 //当点击提交按钮是执行函数
        $.ajax({
            type: 'POST',
            url: 'http://www.fwd.com:8001/fwd',
            data: {"a":'b'},
            success: function (response, status, xhr) {
                alert(response);                            //打印返回数据
            }
        });
    }
</script>
</body>
</html>

 

 

 

 

复杂请求

复杂请求:两次请求,在发送数据之前会先发一次OPTIONS请求用于做“预检”,只有“预检”通过后才再发送一次请求用于数据传输。

self.set_header('Access-Control-Allow-Origin', "http://www.xxx.com") 预检请求或者数据请求时允许跨域,允许跨域的域名(多个逗号隔开)(*标识所有域名允许)
self.set_header('Access-Control-Allow-Headers', "k1,k2") 预检请求时允许请求页面ajax设置请求头headers属性跨域,允许跨域的headers属性请求头(多个逗号隔开)
self.set_header('Access-Control-Allow-Methods', "PUT,DELETE") 预检请求时允许请求页面的请求方式跨域,允许跨域的请求方式(多个逗号隔开)
self.set_header('Access-Control-Max-Age', 10) 预检请求时设置预检有效时间,如设置10,表示这次预检后10秒后的请求再次预检

响应端

#!/usr/bin/env python
#coding:utf-8

import tornado.ioloop
import tornado.web                                              #导入tornado模块下的web文件


class fwdHandler(tornado.web.RequestHandler):
    def get(self):                                      #接收get请求
        pass
    def options(self):                                                  #接收预检的options请求
        self.set_header('Access-Control-Allow-Origin', '*')             #预检请求或者数据请求时允许跨域,允许跨域的域名(多个逗号隔开)(*标识所有域名允许)
        self.set_header('Access-Control-Allow-Methods', 'PUT')          #预检请求时允许请求页面的请求方式跨域,允许跨域的请求方式(多个逗号隔开)
        self.set_header('Access-Control-Allow-Headers', "k1,k2")        #预检请求时允许请求页面ajax设置请求头headers属性跨域,允许跨域的headers属性请求头(多个逗号隔开)
    def put(self):
        self.set_header('Access-Control-Allow-Origin','*')
        self.write("{'k1':'v1'}")                       # 返回数据

settings = {                                            #html文件归类配置,设置一个字典
    "template_path":"views",                            #键为template_path固定的,值为要存放HTML的文件夹名称
    "static_path":"statics",                            #键为static_path固定的,值为要存放js和css的文件夹名称
}

#路由映射
application = tornado.web.Application([                 #创建一个变量等于tornado.web下的Application方法
    (r"/fwd", fwdHandler),
],**settings)                                           #将html文件归类配置字典,写在路由映射的第二个参数里

if __name__ == "__main__":
    #内部socket运行起来
    application.listen(8001)                            #设置端口
    tornado.ioloop.IOLoop.instance().start()

请求页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src='{{static_url("jquery.min.js")}}' charset="UTF-8"></script>
</head>
<body>
<input type="button" value="跨域提交" onclick="doajax();"/>
<script>
    function doajax() {                                 //当点击提交按钮是执行函数
        $.ajax({
            type: 'PUT',                                //复杂请求
            url: 'http://www.fwd.com:8001/fwd',
            headers: {'k1': 'qqtou'},                   //设置请求头
            data: {"a":'b'},
            success: function (response, status, xhr) {
                alert(response);                            //打印返回数据
            }
        });
    }
</script>
</body>
</html>

 

 

 

跨域传输cookie

在跨域请求中,默认情况下,HTTP Authentication信息,Cookie头以及用户的SSL证书无论在预检请求中或是在实际请求都是不会被发送。

如果想要发送:
  浏览器端ajax属性:xhrFields的withCredentials为true
  服务器端的预检请求和数据请求:Access-Control-Allow-Credentials为true
  注意:服务器端响应的 Access-Control-Allow-Origin 不能是通配符 *

响应端

/usr/bin/env python
#coding:utf-8

import tornado.ioloop
import tornado.web                                              #导入tornado模块下的web文件


class fwdHandler(tornado.web.RequestHandler):
    def get(self):                                      #接收get请求
        pass
    def options(self):                                                                        #接收预检的options请求
        self.set_header('Access-Control-Allow-Credentials', "true")                           #传递Cookie头以及用户的SSL证书
        self.set_header('Access-Control-Allow-Origin', 'http://www.khd.com:8002')             #预检请求或者数据请求时允许跨域,允许跨域的域名
        self.set_header('Access-Control-Allow-Methods', 'PUT')                                #预检请求时允许请求页面的请求方式跨域,允许跨域的请求方式(多个逗号隔开)
        self.set_header('Access-Control-Allow-Headers', "k1,k2")                              #预检请求时允许请求页面ajax设置请求头headers属性跨域,允许跨域的headers属性请求头(多个逗号隔开)
    def put(self):
        self.set_header('Access-Control-Allow-Credentials', "true")                           # 传递Cookie头以及用户的SSL证书
        self.set_header('Access-Control-Allow-Origin','http://www.khd.com:8002')
        self.write("{'k1':'v1'}")                       # 返回数据
        self.set_cookie('kkkkk', 'vvvvv')               #写入cookie
        print(self.get_cookie('kkkkk'))                 #获取cookie

settings = {                                            #html文件归类配置,设置一个字典
    "template_path":"views",                            #键为template_path固定的,值为要存放HTML的文件夹名称
    "static_path":"statics",                            #键为static_path固定的,值为要存放js和css的文件夹名称
}

#路由映射
application = tornado.web.Application([                 #创建一个变量等于tornado.web下的Application方法
    (r"/fwd", fwdHandler),
],**settings)                                           #将html文件归类配置字典,写在路由映射的第二个参数里

if __name__ == "__main__":
    #内部socket运行起来
    application.listen(8001)                            #设置端口
    tornado.ioloop.IOLoop.instance().start()

请求页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src='{{static_url("jquery.min.js")}}' charset="UTF-8"></script>
</head>
<body>
<input type="button" value="跨域提交" onclick="doajax();"/>
<script>
    function doajax() {                                 //当点击提交按钮是执行函数
        $.ajax({
            type: 'PUT',                                //复杂请求
            url: 'http://www.fwd.com:8001/fwd',
            headers: {'k1': 'qqtou'},                   //设置请求头
            xhrFields:{withCredentials: true},          //传递Cookie头以及用户的SSL证书
            data: {"a":'b'},
            success: function (response, status, xhr) {
                alert(response);                            //打印返回数据
            }
        });
    }
</script>
</body>
</html>

 

 
 
 
 
 
补充,响应端设置响应头,请求端是无法获取到响应端设置的响应头的,如果要获取到,响应端必须做设置允许请求端获取

self.set_header('xxoo', "bili")  设置响应头第一个是响应头名称,第二个是响应头值
self.set_header('Access-Control-Expose-Headers', "xxoo,bili")    允许设置的响应头,请求端获取

 响应端
#!/usr/bin/env python
#coding:utf-8

import tornado.ioloop
import tornado.web                                              #导入tornado模块下的web文件


class fwdHandler(tornado.web.RequestHandler):
    def get(self):                                      #接收get请求
        pass
    def options(self):                                                                        #接收预检的options请求
        self.set_header('Access-Control-Allow-Credentials', "true")                           #传递Cookie头以及用户的SSL证书
        self.set_header('Access-Control-Allow-Origin', 'http://www.khd.com:8002')             #预检请求或者数据请求时允许跨域,允许跨域的域名
        self.set_header('Access-Control-Allow-Methods', 'PUT')                                #预检请求时允许请求页面的请求方式跨域,允许跨域的请求方式(多个逗号隔开)
        self.set_header('Access-Control-Allow-Headers', "k1,k2")                              #预检请求时允许请求页面ajax设置请求头headers属性跨域,允许跨域的headers属性请求头(多个逗号隔开)
    def put(self):
        self.set_header('Access-Control-Allow-Credentials', "true")                           # 传递Cookie头以及用户的SSL证书
        self.set_header('Access-Control-Allow-Origin','http://www.khd.com:8002')
        self.set_header('xxoo', "bili")                                                      #设置响应头第一个是响应头名称,第二个是响应头值
        self.set_header('Access-Control-Expose-Headers', "xxoo,bili")                         #允许设置的响应头,请求端获取


        self.write("{'k1':'v1'}")                       # 返回数据
        self.set_cookie('kkkkk', 'vvvvv')               #写入cookie
        print(self.get_cookie('kkkkk'))                 #获取cookie

settings = {                                            #html文件归类配置,设置一个字典
    "template_path":"views",                            #键为template_path固定的,值为要存放HTML的文件夹名称
    "static_path":"statics",                            #键为static_path固定的,值为要存放js和css的文件夹名称
}

#路由映射
application = tornado.web.Application([                 #创建一个变量等于tornado.web下的Application方法
    (r"/fwd", fwdHandler),
],**settings)                                           #将html文件归类配置字典,写在路由映射的第二个参数里

if __name__ == "__main__":
    #内部socket运行起来
    application.listen(8001)                            #设置端口
    tornado.ioloop.IOLoop.instance().start()

 
 
posted @ 2017-05-28 08:35  林贵秀  阅读(373)  评论(0编辑  收藏  举报