详细剖析让前端头疼的跨域问题是怎么产生的,又该如何解决
CSDN 2024-06-29 13:03:05 阅读 93
✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN新星创作者,51CTO博客专家等。
🏆《博客》:Python全栈,前后端开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,linux,shell脚本等实操经验,网站搭建,面试宝典等分享。
所属的专栏:前端零基础,实战进阶教学
景天的主页:景天科技苑
文章目录
同源策略实现跨域请求的几种方法(1)利用jsonp进行跨域(2)利用cors进行跨域请求(3)proxy代理模式进行跨域
同源策略
同源策略,是浏览器为了保护用户信息安全的一种安全机制。
所谓的同源就是指代通信的两个地址(例如服务端接口地址与浏览器客户端页面地址)之间比较,是否协议、域名和端口相同。
不同源的客户端脚本[javascript]在没有明确授权的情况下,没有权限读写对方信息。
同源策略机制(Same Origin Policy,SOP)是一种约定,它是浏览器最核心也是最基本的安全功能,如果缺少了同源策略。
则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础上的,浏览器只是针对同源策略的一种实现。
当一个浏览器的两个tab页中分别打开百度和谷歌的页面时,当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。
如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。
不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。所以google.com下的js脚本采用ajax读取baidu.com里面的文件数据是会报错的。
跨域请求,首先浏览器为了安全,做了一个同源策略机制,也就是所谓的同源安全策略,本质上,其实是不存在所谓的跨不跨域的,
把浏览器想象成一个发送网络请求的软件,按照道理来说,请求都是可以发送出去的,但是在 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中的success将无法获取到数据
return cb + "("+json.dumps(data)+")"
在发送ajax的时候. 需要指定dataType为jsonp, 以及自由配置回调函数的参数名
$(function(){
$.ajax({
url: "/process_jsonp",
method:"get",
// 典型, 京东.
dataType: "jsonp", // 它的执行逻辑是. 请求服务上的一个js. 然后会自动执行该js.将js函数内的东西. 丢给success
jsonp:"cb", // 传递给服务器的时候. 自动带上cb=xxxxxx 服务器端接收cb即可
success: 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代码,运行后调用success中的函数
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不会自动的去执行success
// 而是去执行callback对应的那个函数jQuery19103699368267737122_1694573801081(data)
// success:访问成功之后自动回调该函数
success: function(data){ // 这里的参数,是返回的数据
console.log(data); // 使用jsonp。默认的这玩意.拿不到数据
}
})
})
})
//正常jsonp这样才能调用success里面的方法,并把参数data传进去,但是callback对应的方法名貌似个带有时间戳的方法名。
// 很多,很随机。故无法在前端实现
// function jQuery19103699368267737122_1694573801081(data){
// success(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 字段.
我们拿不到数据是因为浏览器在中间做了一层劫持。服务器的响应头里面没有设置 Access-Control-Allow-Origin 。所以数据被浏览器劫持了,无法获取
跨域获取不到数据的原因很简单:
这是一次跨域请求
请求确实发送到服务器了
服务器也确实把数据返回到了浏览器
但是服务器返回的响应头里面,没有告诉浏览器哪个域名可以访问这些数据,(也就是没有设置Access-Control-Allow-Origin )
于是浏览器就把这个数据丢弃了,我们也就无法获取到这个数据。
这个时候,只需要后台在相应头里加上一个 Access-Control-Allow-Origin:* 即可完成跨域数据获取.
CORS 跨域方案简单总结:
当浏览器发送一个请求是跨域的时候,请求头会自动加上Origin
需要后台配合设置响应头Access-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的头信息不超出以下几种字段:(如果比这些请求头多,那么一定是非简单请求)
Accept
Accept-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等请求,则服务端需要设置允许某请求,否则“预检”不通过
Access-Control-Request-Method
=> 如果复杂请求设置了请求头,则服务端需要设置允许某请求头,否则“预检”不通过
Access-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["Access-Control-Allow-Origin"] = "*"
obj["Access-Control-Allow-Origin"] = "http://127.0.0.1:8000"
print(request.method)
#处理预检的options请求,这个预检的响应,我们需要在响应头里面加上下面的内容
if request.method == 'OPTIONS':
# obj['Access-Control-Allow-Headers'] = "Content-Type" #"Content-Type",首字母小写也行
# obj['Access-Control-Allow-Headers'] = "content-type" #"Content-Type",首字母小写也行。这个content-type的意思是,什么样的请求体类型数据都可以,我们前面说了content-type等于application/json时,是复杂请求,复杂请求先进行预检,预检的响应中我们加上这个,就是告诉浏览器,不要拦截
obj['Access-Control-Allow-Headers'] = "content-type,b" #发送来的请求里面的请求头里面的内容可以定义多个,后端需要将头配置上才能访问
return obj
支持跨域,简单请求
服务器设置响应头:Access-Control-Allow-Origin = ‘域名’ 或 ‘*’
支持跨域,复杂请求 分两种形式,第一种是方法不属于简单请求,另一种是请求头超过了简单请求的请求头字段
由于复杂请求时,首先会发送“预检”请求,如果“预检”成功,则发送真实数据。
“预检”请求时,允许请求方式则需服务器设置响应头:Access-Control-Request-Method
“预检”请求时,允许请求头则需服务器设置响应头:Access-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['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8000' #允许该地址的请求正常获取响应数据
# ret['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8000,http://127.0.0.1:8002'
ret['Access-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(跨域资源共享)方案,在服务端设置响应头 Access-Control-Allow-Origin
def data(request):
info = { 'name':'景浩','age':18}
ret = JsonResponse(info,json_dumps_params={ 'ensure_ascii': False})
ret['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8000' #注意,填写指定网站时,网站后面不能 / 等路径
# ret['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8000,http://127.0.0.1:8002' #允许多个网站访问,逗号隔开
ret['Access-Control-Allow-Origin'] = '*' #允许所有地址请求都能拿到数据
return ret
此时浏览器访问
使用this的时候,要是不知道是谁,就打印一下
由打印可知,shuju这个变量以获取到值,但是由于axios2服务使用了Django的模板渲染,导致变量取值只能从后端render那里发过来,axios没发挥作用
由于后端没传值,所以一直是空的
axios2视图函数处,这里传值了,前端html才能取到值
有上面打印可知,看html,vue中的shuju属性值已被修改
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。