详细剖析让前端头疼的跨域问题是怎么产生的,又该如何解决

✨✨ 欢迎大家来到景天科技苑✨✨

🎈🎈 养成好习惯,先赞后看哦~🎈🎈

🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN新星创作者,51CTO博客专家等。
🏆《博客》:Python全栈,前后端开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,linux,shell脚本等实操经验,网站搭建,面试宝典等分享。

所属的专栏:前端零基础,实战进阶教学
景天的主页:景天科技苑

同源策略

同源策略,是浏览器为了保护用户信息安全的一种安全机制。
所谓的同源就是指代通信的两个地址(例如服务端接口地址与浏览器客户端页面地址)之间比较,是否协议、域名和端口相同。
不同源的客户端脚本[javascript]在没有明确授权的情况下,没有权限读写对方信息。

同源策略机制(Same Origin Policy,SOP)是一种约定,它是浏览器最核心也是最基本的安全功能,如果缺少了同源策略。
则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础上的,浏览器只是针对同源策略的一种实现。

当一个浏览器的两个tab页中分别打开百度和谷歌的页面时,当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。
如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。
不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。所以google.***下的js脚本采用ajax读取baidu.***里面的文件数据是会报错的。

跨域请求,首先浏览器为了安全,做了一个同源策略机制,也就是所谓的同源安全策略,本质上,其实是不存在所谓的跨不跨域的,
把浏览器想象成一个发送网络请求的软件,按照道理来说,请求都是可以发送出去的,但是在 web 端,浏览器里,这么做的就不合适,
如果网络上的接口可以不受限制的被任意人调用,那将是一个非常混乱的场景,所以,为了防止这种情况,浏览器做了这个同源策略来防止这种情况发生。

总结:协议相同+域名相同+端口号相同,浏览器才认为是同一个网站,才不会受到同源策略的影响,才可以正常的发送Ajax请求。对于 ,

所谓的同源策略是浏览器实现的,而和后台服务器无关,A 发送请求到 B. 请求实际上是发送到了 B 后台, B后台接受到数据后。
其实也有返回,只不过,这个数据返回到浏览器之后,浏览器把这个数据给劫持了,不让A网站使用

既然跨域这么麻烦,为什么要进行跨域?
因为当一个项目变大时,把所有的内容都丢在一个网站或者是后台服务器中是不现实的.

比如:

一个网站体量很大.有很多可以独立且复杂的业务

比如有一个订单管理的后台数据API网站服务.
比如有一个用户管理的后台数据API网站服务.
比如有一个新闻管理的后台数据API网站服务.
最后剩下的就是web的UI层面的东西,把这个UI层面的东西和哪个数据服务API的网站集成在一起比较合适呢?

需注意即使两个不同的域名指向同一个IP地址,也是跨域

实现跨域请求的几种方法

(1)利用jsonp进行跨域

jsonp 跨域是里用在浏览器中,下载一个script标签是不受同源策略限制的特性.

具体实现原理大概分下面几步

客户端动态的创建一个 script 标签
设置 script 标签的src 为跨域的服务器后台.
src 中需要带一个 callback 查询字符串,告诉后台,前端提供的方法名是什么.
然后后端,直接返回一一串 callback(data) 的字符串给浏览器即可.
jsonp使用总结:

利用 script 标签异步加载,而非传统的ajax
异步 script 加载不受浏览器同源策略限制.
给script.src=callback=fn来告知请求的后台前端提供的fn是什么
基本就是前端提供方法,后端提供数据并调用前端的这个方法.
jsonp 只支持 get 请求.(本质上,不就是下载文件吗?下载文件一般都是get请求)

jsonp的逻辑是. 在发送请求的时候. 带上一个callback字符串. 该字符串自动发送给服务器. 服务器返回数据的时候. 会带上该callback字符串. 我们在抓包中看到的就是这样的效果:

在Python中. 接下来, 我们还原一下该效果.

首先, 在flask中. 必须接收到前端返回的callback, 然后在返回数据的时候. 需要用前端返回的callback字符串. 将数据包裹

@app.route("/process_jsonp", methods=["GET"])
def process_jsonp():
    # 获取回调字符串
    cb = request.args.get("cb")
    print(cb)
    data = {
        "name": "alex",
        "age": 18
    }
    import json
    #      用回调字符串将真实要返回的数据包裹起来
    #      如果不包裹起來。前端ajax中的su***ess将无法获取到数据
    return cb + "("+json.dumps(data)+")"

在发送ajax的时候. 需要指定dataType为jsonp, 以及自由配置回调函数的参数名

$(function(){
    $.ajax({
        url: "/process_jsonp",
        method:"get",
        // 典型, 京东.
        dataType: "jsonp", // 它的执行逻辑是. 请求服务上的一个js. 然后会自动执行该js.将js函数内的东西. 丢给su***ess
        jsonp:"cb", // 传递给服务器的时候. 自动带上cb=xxxxxx  服务器端接收cb即可
        su***ess: function(data){ // 此时data可以直接收取到数据
            console.log(data);
        }
    })
})

如果我们向下拉滚动条,确定数据有刷新,但是我们看XHR数据包为空,此时我们查看JS数据包

在js数据包里面我们查看preview,可以看到jsonp响应数据

后端代码

from flask import Flask, render_template, request, make_response

import json

# 创建一个flask对象. 参数实际上是一个文件夹
server = Flask('jinghao')

# 你要处理的是哪个请求,写路由
# http://127.0.0.1:5000/
@server.route('/')
def gen(): # 通过网路请求 -> flask -> /  -> gen()
    print("我是一个普通函数")
    dic = {
        "name": "wusir",
        "hobby": "洗脚"
    }
    # # 后端(服务器端渲染) , 数据怼到html, 发生在???????
    # f = open("templates/index.html", mode="r", encoding="utf-8")
    # c = f.read().replace("{{username}}", dic['name'])
    # c = c.replace("{{hobby}}", dic['hobby'])

    # 下面的逻辑和上面一致
    # render_template(文件, {{变量}}=值)
    #页面渲染,自动到项目目录下templates下的html文件
    return render_template("index.html", username=dic['name'], hobby=dic['hobby'])  # 返回的时候, 数据已经在html中了

@server.route('/hehehehehe',methods = ['post','get'])
def hehehe():
    sth = request.form.get('xxm')
    print('form data,',sth)
    #返回json字符串
    # return json.dumps({'name':'景浩','age':18},ensure_ascii=False)
    #后端返回给前端一段js代码,运行后调用su***ess中的函数
    cb = request.args.get('callback')
    print('回调函数名:',cb)
    return cb +'('+json.dumps({'name':'景浩','age':18},ensure_ascii=False) + ')'




if __name__ == '__main__':
    # 让服务器跑起来
    # debug=True 修改了代码. 它自动重启服务
    # 有时候不管用. 依然需要手动重启
    server.run(debug=True)

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="/static/jquery.min.js"></script>
    <script>

        $(function(){
            $("#btn").click(function(){
                console.log(123);
                // 访问服务器.加载数据
                // js里发送请求 -> ajax
                // $.get()  $.post()
                // 下面这用的多
                $.ajax({
                    url: "/hehehehehe",
                    method: "post", // post
                    data: { // json.dumps()
                        "xxm": "meishaqubie"
                    },
                    dataType: 'jsonp', // 加上这个东西. 此时该请求就是一个jsonp
                    // 在抓包中能看到url的后面加上了
                    //callback=jQuery19103699368267737122_1694573801081
                    // 当响应成功回来之后. 注意, jsonp不会自动的去执行su***ess
                    // 而是去执行callback对应的那个函数jQuery19103699368267737122_1694573801081(data)
                    // su***ess:访问成功之后自动回调该函数
                    su***ess: function(data){// 这里的参数,是返回的数据
                        console.log(data); // 使用jsonp。默认的这玩意.拿不到数据

                    }
                })
            })
        })

        //正常jsonp这样才能调用su***ess里面的方法,并把参数data传进去,但是callback对应的方法名貌似个带有时间戳的方法名。
        // 很多,很随机。故无法在前端实现
        // function jQuery19103699368267737122_1694573801081(data){
        //     su***ess(data)
        // }
    </script>
</head>
<body>
  我是一个完美的html页面
    <span>{{username}}</span>
    <span>{{hobby}}</span>
  <input type="button" value="你敢点我????" id="btn">
</body>
</html>

(2)利用CORS进行跨域请求

cors 是 Cross-Origin-Resource-Sharing 的缩写,也是现代浏览器普遍支持的一种非常方便的跨域方案. 跨域资源共享
CORS,全称Cross-Origin Resource Sharing ,是一种允许当前域(domain)的资源(比如html/js/web service)被其他域(domain)的脚本请求访问的机制
它的具体工作流程是:

浏览器在检测到你发送的 ajax 请求不是同源请求时,会自动在 http 的头部添加一个 origin 字段.

我们拿不到数据是因为浏览器在中间做了一层劫持。服务器的响应头里面没有设置 A***ess-Control-Allow-Origin 。所以数据被浏览器劫持了,无法获取

跨域获取不到数据的原因很简单:

这是一次跨域请求
请求确实发送到服务器了
服务器也确实把数据返回到了浏览器
但是服务器返回的响应头里面,没有告诉浏览器哪个域名可以访问这些数据,(也就是没有设置A***ess-Control-Allow-Origin )
于是浏览器就把这个数据丢弃了,我们也就无法获取到这个数据。
这个时候,只需要后台在相应头里加上一个 A***ess-Control-Allow-Origin:* 即可完成跨域数据获取.

CORS 跨域方案简单总结:
当浏览器发送一个请求是跨域的时候,请求头会自动加上Origin
需要后台配合设置响应头A***ess-Control-Allow-Origin:*|Origin即可完成跨域.
CORS支持GET,POST常规请求
同时也支持PUT,DELETE等非post,get的请求,但会先发出一次预检请求。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。
浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:(也就是说如果你的请求方法是什么put、delete等肯定是非简单请求)
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:(如果比这些请求头多,那么一定是非简单请求)
A***ept
A***ept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain,也就是说,如果你发送的application/json格式的数据,那么肯定是非简单请求,vue的axios默认的请求体信息格式是json的,ajax默认是urlencoded的。

凡是不同时满足上面两个条件,就属于非简单请求。

我们改一下上一节的s1项目的index.html文件中的ajax里面的内容:

浏览器对这两种请求的处理,是不一样的。

  • 简单请求和非简单请求的区别?

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

  • 关于“预检”

  • 请求方式:OPTIONS
  • “预检”其实做检查,检查如果通过则允许传输数据,检查不通过则不再发送真正想要发送的消息
  • 如何“预检”
    => 如果复杂请求是PUT等请求,则服务端需要设置允许某请求,否则“预检”不通过
    A***ess-Control-Request-Method
    => 如果复杂请求设置了请求头,则服务端需要设置允许某请求头,否则“预检”不通过
    A***ess-Control-Request-Headers
from django.shortcuts import render
from django.http import JsonResponse
# Create your views here.
def books(request):

    # return JsonResponse(['西游记2','三国演义2','水浒传2'],safe=False)

    obj = JsonResponse(['西游记2','三国演义2','水浒传2'],safe=False)
    # obj["A***ess-Control-Allow-Origin"] = "*"
    obj["A***ess-Control-Allow-Origin"] = "http://127.0.0.1:8000"
    print(request.method)
    #处理预检的options请求,这个预检的响应,我们需要在响应头里面加上下面的内容
    if request.method == 'OPTIONS':
        # obj['A***ess-Control-Allow-Headers'] = "Content-Type" #"Content-Type",首字母小写也行
        # obj['A***ess-Control-Allow-Headers'] = "content-type" #"Content-Type",首字母小写也行。这个content-type的意思是,什么样的请求体类型数据都可以,我们前面说了content-type等于application/json时,是复杂请求,复杂请求先进行预检,预检的响应中我们加上这个,就是告诉浏览器,不要拦截
        obj['A***ess-Control-Allow-Headers'] = "content-type,b"  #发送来的请求里面的请求头里面的内容可以定义多个,后端需要将头配置上才能访问
    return obj

支持跨域,简单请求

服务器设置响应头:A***ess-Control-Allow-Origin = ‘域名’ 或 ‘*’

支持跨域,复杂请求 分两种形式,第一种是方法不属于简单请求,另一种是请求头超过了简单请求的请求头字段

由于复杂请求时,首先会发送“预检”请求,如果“预检”成功,则发送真实数据。

“预检”请求时,允许请求方式则需服务器设置响应头:A***ess-Control-Request-Method

“预检”请求时,允许请求头则需服务器设置响应头:A***ess-Control-Request-Headers

(3)proxy代理模式进行跨域

核心思想:让前端请求我们自己的后台,让后台去请求另一个后台,(因为后台和后台之间不用跨域,也没有同源策略机制,)获取真实的数据,然后把数据返回给前台,
实现方式可以利用nginx做反向代理,以及我们写一个后台中转的服务器。
这个proxy代理跨域是后端的实现,目前我只掌握了它的原理思路。具体的操作还没学会。

如上面我们做的案例,没请求拿到数据,就是同源机制,被浏览器拦截了
如果想让请求者拿到数据,响应者要告诉浏览器,让请求者拿到数据。让浏览器正常放行,这就是跨域访问

跨域(跨源)方案之CORS

CORS是一个W3C标准,全称是"跨域资源共享",它允许浏览器向跨源的后端服务器发出ajax请求,从而克服了AJAX只能同源使用的限制。

​实现CORS主要依靠后端服务器中响应数据中设置响应头信息返回的。
django的视图

def data(request):
    info = {'name': '世元', 'age': 18}
    ret = JsonResponse(info)
    # ret['A***ess-Control-Allow-Origin'] = 'http://127.0.0.1:8000'  #允许该地址的请求正常获取响应数据
    # ret['A***ess-Control-Allow-Origin'] = 'http://127.0.0.1:8000,http://127.0.0.1:8002'
    ret['A***ess-Control-Allow-Origin'] = '*' #允许所有地址请求都能拿到数据
    
    return ret

自己创建两个Django项目,分析同源问题

axios1作为服务端,axios2作为客户端

服务端axios:只响应数据

响应json数据,这样响应默认中文在浏览器展示会是乱码。这是由于json序列化时,默认只能显示ASSIC ii字符,如果不设置ensure_ascii=False 默认中文以unicode的编码显示

解决办法:
方法1:直接加这一句即可
, json_dumps_params={‘ensure_ascii’: False}

return JsonResponse({‘user’: ‘王’, ‘password’: 123456}, json_dumps_params={‘ensure_ascii’: False})

方法2:修改源码,原码默认序列化时只显示ASSIC ii码。我们可以修改jsonresponse原码。一般我们使用方法1
进入JsonResponse源码
将json_dumps_params=None,
修改为 json_dumps_params={‘ensure_ascii’:False}, 即可

我们采用方法1

可以正常显示中文了

运行axios服务端的时候,改下端口号,前后端两个网站不同源
Edit Configurations这里编辑运行参数

端口号改成8008

客户端axios2: 请求数据

axios获取不同网站信息,使用绝对路径

同源策略限制,没获取到数据

我们看得到请求头包含Origin,说明请求不是同源请求,请求跨域

如果想让客户端拿到数据,我们采取CORS(跨域资源共享)方案,在服务端设置响应头 A***ess-Control-Allow-Origin

def data(request):
    info = {'name':'景浩','age':18}
    ret = JsonResponse(info,json_dumps_params={'ensure_ascii': False})
    ret['A***ess-Control-Allow-Origin'] = 'http://127.0.0.1:8000'    #注意,填写指定网站时,网站后面不能  / 等路径
    # ret['A***ess-Control-Allow-Origin'] = 'http://127.0.0.1:8000,http://127.0.0.1:8002'    #允许多个网站访问,逗号隔开
    ret['A***ess-Control-Allow-Origin'] = '*' #允许所有地址请求都能拿到数据

    return ret

此时浏览器访问

使用this的时候,要是不知道是谁,就打印一下

由打印可知,shuju这个变量以获取到值,但是由于axios2服务使用了Django的模板渲染,导致变量取值只能从后端render那里发过来,axios没发挥作用
由于后端没传值,所以一直是空的

axios2视图函数处,这里传值了,前端html才能取到值

有上面打印可知,看html,vue中的shuju属性值已被修改

转载请说明出处内容投诉
CSS教程_站长资源网 » 详细剖析让前端头疼的跨域问题是怎么产生的,又该如何解决

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买