同源政策
Ajax请求限制:
Ajax只能向自己的服务器发送请求。比如现在有一个A网站、 有一个B网站, A网站中的HTML文件只能向A网站服务器中发送Ajax请求,B网站中的HTML文件只能向B网站中发送Ajax请求,但是A网站是不能向B网站发送Ajax请求的,同理,B网站也不能向A网站发送Ajax请求。
什么是同源:
如果两个页面拥有相同的协议、域名和端口,那么这两个页面就属于同一个源,其中只要有一个不相同,就是不同源。
http://www.example.com/dir/page.html
http://www.example.com/dir2/other.html:同源
http://example.com/dir/other.html:不同源(域名不同)
http://v2.www.example.com/dir/other.html:不同源(域名不同)
http://www.example.com:81/dir/other.html:不同源(端口不同)
https://www.example.com/dir/page.html:不同源(协议不同)
同源政策的目的:
同源政策是为了保证用户信息的安全,防止恶意的网站窃取数据。最初的同源政策是指A网站在客户端设置的Cookie,B网站是不能访问的。
随着互联网的发展,同源政策也越来越严格,在不同源的情况下,其中有一项规定就是无法向非同源地址发送Ajax请求,如果请求,浏览器就会报错。
以下有几种跨域请求的方法:
1. 使用JSONP解决同源限制问题
jsonp是json with padding的缩写,它不属于Ajax请求,但它可以模拟Ajax请求。
注意:JSONP不是Ajax,只是模拟Ajax发送数据
① 将不同源的服务器端请求地址写在script标签的src属性中
在<script>的src属性中是不受同源政策的限制的,也就是说它可以写非同源的网站
<script src="www.example.com"></script> <script src=“https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
② 服务器端响应数据必须是一个函数的调用, 真正要发送给客户端的数据需要作为函数调用的参数
const data = 'fn({name: "张三", age: "20"})';
res.send(data);
③ 在客户端全局作用域下定义函数fn
function fn (data) { }
④ 在fn函数内部对服务器端返回的数据进行处理
function fn (data) { console.log(data); }
注意:jsonp解决方案中的请求属于get请求,因为它是通过script标签中的src属性发送的请求,所以它传递的参数也是get请求参数,具体的参数拼接在请求地址的后面。
JSONP代码优化:
(1)客户端需要将函数名称传递到服务器端。
客户端写的函数如何在服务器端调用呢?
注意,客户端的这个函数是全局函数,而且必须要写在最前面。
<script> function fn (data) { console.log('客户端的fn函数被调用了') console.log(data); } </script> <!-- 将非同源服务器端的请求地址写在script标签的src属性中 --> <script src="http://localhost:3001/test"></script>
// 服务器端调用客户端的fn函数 app.get('/test', (req, res) => { const result = 'fn()'; res.send(result); });
(2)将script请求的发送变成动态请求。
但是上面的代码有三个问题:
① 客户端函数是立即调用的,但我们想要的效果是动态请求发送,当点击按钮之后,创建一个script标签,然后再将函数名传递到服务器端。
② 添加这个点击按钮之后出现了另外一个问题:
点击一次按钮,新增一个script标签,多次点击就会创建很多个script标签,但是我们只需要一个就够了。
解决方案:当script标签将请求地址中的内容加载完成以后,需要将它从body内部删除掉
③ 服务器端返回的函数调用名称必须与客户端定义的函数名称保持一致。如果客户端的函数名称需要修改,则服务器端的函数名称也必须要跟着修改,开发人员的沟通成本就比较高。
解决方案:只需要将客户端函数的名字作为请求参数发送到服务器端,服务器端只需要接收到函数的名字,然后返回函数调用即可。
修改后的代码如下:
<button id="btn">点我发送请求</button> <script> function fn2 (data) { console.log('客户端的fn函数被调用了') console.log(data); } </script> <script type="text/javascript"> // 获取按钮 var btn = document.getElementById('btn'); // 为按钮添加点击事件 btn.onclick = function () { // 创建script标签 var script = document.createElement('script'); // 设置src属性 script.src = 'http://localhost:3001/better?callback=fn2'; // 将script标签追加到页面中 document.body.appendChild(script); // 为script标签添加onload事件 script.onload = function () { // 将body中的script标签删除掉 document.body.removeChild(script); } } </script>
// 服务器端调用客户端的fn函数 app.get('/better', (req, res) => { // 接收客户端传递过来的函数的名称 const fnName = req.query.callback; // 将函数名称对应的函数调用代码返回给客户端 const result = fnName + '({name: "张三"})'; res.send(result); });
(3)封装jsonp函数,方便请求发送。
function jsonp (options) { // 动态创建script标签 var script = document.createElement('script'); // 为script标签添加src属性 script.src = options.url; // 将script标签追加到页面中 document.body.appendChild(script); // 为script标签添加onload事件, 等待script标签加载完之后再删除 script.onload = function() { // 将body中的script标签删除掉 document.body.removeChild(script); } }
封装jsonp方法有两个问题:
① 虽然上面已经封装了jsonp函数用于发送请求,但是在客户端,jsonp函数的其他地方,还需要另外定义一个全局函数,用于接收服务器端返回的数据,现在是发送一个请求要用到两个函数,而且两个函数是独立的,这样的话就破坏了jsonp函数的封装性,我们不能一眼就看出来哪个请求跟哪个函数是关联的。如果可以像Ajax封装函数一样,将用于接收服务器端返回来的数据的函数当作参数传递过去,即将处理请求函数变成success函数,这样的话函数的封装性就比较好。
但是这样就出现了另外两个问题:
- 这个函数就不是全局函数了,服务器端在返回调用函数的时候就找不到这个函数了
解决方案:要想办法把它变成一个全局函数,只需要将该函数挂载在window全局对象下面就可以了。
- 这个函数就变成了匿名函数了,这样我们在向服务器端传递名字的时候该传递什么呢?
解决方法:函数名字的问题同下面的问题②的解决方案,注意:函数名字不能是纯数字
② 在真实的情况中可能要发送多次请求,每一次请求都要对应自己的函数处理返回的结果,函数取名字也变成一个问题。如何解决函数名字的问题呢?只需要让函数的名字随机产生就可以了。
代码修改如下:
function jsonp (options) { // 动态创建script标签 var script = document.createElement('script'); // 拼接字符串的变量 var params = ''; for (var attr in options.data) { params += '&' + attr + '=' + options.data[attr]; } // myJsonp0124741 var fnName = 'myJsonp' + Math.random().toString().replace('.', ''); // 它已经不是一个全局函数了 // 我们要想办法将它变成全局函数 window[fnName] = options.success; // 为script标签添加src属性 script.src = options.url + '?callback=' + fnName + params; // 将script标签追加到页面中 document.body.appendChild(script); // 为script标签添加onload事件 script.onload = function () { document.body.removeChild(script); } }
// 获取按钮 var btn = document.getElementById('btn'); // 为按钮添加点击事件 btn.onclick = function () { jsonp({ // 请求地址 url: 'http://localhost:3001/better', data: { name: 'lisi', age: 30 }, success: function (data) { console.log(data) } }) }
(4)服务器端代码优化之res.jsonp方法。
express框架中提供了一个jsonp方法,jsonp方法内部干的其实就是注释的那些事情:
接收客户端传递过来的参数,将真实的数据转换为字符串再把它拼接起来,最终返回给客户端。
app.get('/better', (req, res) => { // 接收客户端传递过来的函数名称 // const fnName = req.query.callback; // 将函数名称对应的函数调用代码返回给客户端 // const data = JSON.stringify({name: "张三"}); // const result = fnName + '(' + data + ')'; // setTimeout(() => { // res.send(result); // }, 1000); res.jsonp({name: 'lisi', age: 20}); });
2. CORS跨域资源共享
除了jsonp方法可以实现跨域请求,另一种方式就是CORS跨域请求。
它跟jsonp的解决方案是不一样的,jsonp是绕过了同源限制,发送的也不是Ajax请求。
而CORS直接允许浏览器向跨域的服务器发送Ajax请求,从而克服了Ajax只能同源使用的限制。
简单来说,CORS这种解决方案就是,服务器端允许你跨域访问它,你就可以跨域访问它,服务器端不允许你跨域访问它,你就不能访问它。
这种解决方案主要是再服务器端做一些配置,客户端保持原有的Ajax代码不变即可。
CORS:全称为Cross-origin resource sharing,即跨域资源共享,它允许浏览器向跨域服务器发送Ajax请求,克服了Ajax只能同源使用的限制。
origin: http://localhost:3000
Access-Control-Allow-Origin: 'http://localhost:3000'
Access-Control-Allow-Origin: '*'
origin存储的就是A网站的域名信息,包含协议、域名和端口号。服务器端会根据该域名信息来决定是否同意这次的请求。不管是否同意请求,服务器端都会返回给客户端一个正常的HTTP响应。
浏览器端如何判断服务器端是否同意这次的请求呢?如果服务器端同意这次请求,会在响应头中加入Access-Control-Allow-Origin,如果不同意,则不会加。
这个字段的值通常是当前访问服务器端的客户端的原信息,或者是返回*号,表示允许所有的客户端都可以访问该服务器端。
具体的代码要如何实现呢?
客户端依然使用Ajax代码,不需要做出任何改变,客户端需要做的事情浏览器会自动帮我们做好。
对于服务器端而言,我们需要设置两项内容,一项是允许哪些客户端访问服务器端,另一项是客户端可以设置哪些请求方法来访问服务器端。是使用get方法还是使用post方法,或者是两者都可以,这要根据具体的需求来定。
这两项信息都需要设置在响应头中。
express中使用res.header方法设置响应头。
Node服务器端设置响应头示例代码:
// 在服务器端设置一个中间件,拦截所有的请求,然后再对所有的请求设置这两个响应头。只需要在所有路由的最上方写上app.use() // 注意:必须要调用next()方法,不然所有的代码都卡在这里了,就不会再继续往下执行了。 app.use((req, res, next) => { // 允许哪些客户端访问,*代表所有的客户端都可以访问 res.header('Access-Control-Allow-Origin', '*'); // 允许客户端使用哪些请求方式访问 res.header('Access-Control-Allow-Methods', 'GET, POST'); next(); })
3. 服务器端解决访问非同源数据
同源政策是浏览器给予Ajax技术的限制,服务器端是不存在同源政策限制。
第三种跨域请求方法,这种方法也是绕过客户端的同源政策的限制。
A网站的客户端向A网站的服务器端发送请求,A网站的服务器端向B网站的服务器端发送请求获取数据。
那如何使用A网站的服务器端向B网站的服务器端请求数据呢?这时我们需要用到node里面的一个第三方模块request
① 引入该模块
② 调用该模块的函数:第一个参数是其他服务器端的请求地址,第二个参数是一个回调函数,当这个请求返回数据的时候,这个回调函数就会被调用。
回调函数的第一个参数是error,如果发生了错误,则error就是一个对象类型,否则就是null。response是服务器端的响应信息,body是响应的主体内容。
A网站的服务器端把B网站服务器响应的数据返回给A网站的客户端
<script> // 获取按钮 var btn = document.getElementById('btn'); // 为按钮添加点击事件 btn.onclick = function() { ajax({ type: 'get', url: 'http://localhost:3000/server', success: function (data) { console.log(data); } }); } </script>
app.get('/server', (req, res) => { // A网站的服务器端把B网站服务器响应的数据返回给A网站的客户端 request('http://localhost:3001/cross', (err, response, body) => { res.send(body); }); });
跨域请求中携带cookie的问题:
什么是无状态请求:服务器端不关系客户端是谁,只关心请求,只要请求来了,服务器端就会对此做出响应,响应完了这次沟通也就结束了。当同一个客户端向服务器端再次发送请求时,服务器端并不知道客户端已经来过一次了,这就是无状态请求。客户端与服务器端沟通无记忆功能。
这种特性在早期的网站应用中是没有问题的,因为早期的网站应用中只是展示一些文字图片之类的信息,用户并不会与网站进行交互。比如现在很多电商网站,用户必须要进行登录才能购买商品,因为如果用户不登录,网站不知道是谁在购物,商品也不知道该邮寄到哪儿去。
cookie就是服务器端与客户端身份识别的一种技术。
如何进行身份识别呢?
当客户端第一次访问服务器端的时候,服务器端检测到当前这个客户端我并不认识,这时服务器端在对客户端做出响应的同时,还可以给客户端发一个小卡片,这个小卡片可以理解为是服务器端发给客户端的一个身份证,这个身份证就是cookie。当客户端再次发送请求的时候,这个身份证会随着请求被自动发送到服务器端。服务器端拿到身份证之后就知道客户端是谁。这样就建立了服务器端与客户端之间的持久联系。
如果想实现跨域登录功能,这时就需要用到cookie技术,但是由于是跨域请求,cookie不会自动发送到服务器端,这样就无法实现登录功能了。
如何解决呢?
使用withCredentials属性:在使用Ajax技术发送跨域请求时,默认情况下不会在请求中携带cookie信息。
withCredentials:指定在涉及到跨域请求时,是否携带cookie信息,默认值为false
Acss-Contnolollo-Credentias:tue表示允许客户端发送请求时携带cookie
如果客户端未携带cookie,服务器端不认识,那么即使登录成功后,用户状态还是处在未登录状态。
所以一定要设置withCredentials属性
<div class="container"> <form id="loginForm"> <div class="form-group"> <label>用户名</label> <input type="text" name="username" class="form-control" placeholder="请输入用户名"> </div> <div class="form-group"> <label>密码</label> <input type="password" name="password" class="form-control" placeholder="请输入用密码"> </div> <input type="button" class="btn btn-default" value="登录" id="loginBtn"> <input type="button" class="btn btn-default" value="检测用户登录状态" id="checkLogin"> </form> </div>
<script type="text/javascript"> // 获取登录按钮 var loginBtn = document.getElementById('loginBtn'); // 获取检测登录状态按钮 var checkLogin = document.getElementById('checkLogin'); // 获取登录表单 var loginForm = document.getElementById('loginForm'); // 为登录按钮添加点击事件 loginBtn.onclick = function () { // 将html表单转换为formData表单对象 var formData = new FormData(loginForm); // 创建ajax对象 var xhr = new XMLHttpRequest(); // 对ajax对象进行配置 xhr.open('post', 'http://localhost:3001/login'); // 当发送跨域请求时,携带cookie信息 xhr.withCredentials = true; // 发送请求并传递请求参数 xhr.send(formData); // 监听服务器端给予的响应内容 xhr.onload = function () { console.log(xhr.responseText); } } // 当检测用户状态按钮被点击时 checkLogin.onclick = function () { // 创建ajax对象 var xhr = new XMLHttpRequest(); // 对ajax对象进行配置 xhr.open('get', 'http://localhost:3001/checkLogin'); // 当发送跨域请求时,携带cookie信息 xhr.withCredentials = true; // 发送请求并传递请求参数 xhr.send(); // 监听服务器端给予的响应内容 xhr.onload = function () { console.log(xhr.responseText); } } </script>