前端跨域问题之自我总结笔记

279 阅读9分钟

前端跨域问题

前言

本人为前端小白一枚,结合自身理解和阅读相关博客的心得体会,如有总结不到位的地方请大佬留言纠正补充,目前仅接触到2种跨域解决方案

参考博客

跨域以及CSRF

目录

一、背景

  1. 什么是同源?
  2. 同源政策的目的是什么?

二、跨域问题的表现

  1. 什么是跨域?
  2. 什么是CSRF?

三、解决跨域问题的常用方案

  1. JSONP
  2. CORS跨域资源共享

一、背景

1.什么是同源?

如果两个页面拥有相同的协议、域名和端口,那么这两个页面就属于同一个源,其中只要有一个不相同,就是不同源。 假设用户页面url:www.example.com/dir2/other.…

URL是否同源是否允许通讯说明
example.com/dir/other.h… example.com/dir/other2.…允许协议、域名和端口相同(端口默认为80端口),只有路径不同
example.com/dir/other.h…不允许协议不同
example123.com/dir/other.h…不允许域名不同
example.com:81/dir/other.h…不允许端口不同

2.同源政策的目的是什么?

MDN官方解释: 同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

浏览器的同源策略的目的就是为了防止 XSS,CSRF 等恶意攻击,为了保证用户信息的安全,防止恶意的网站窃取数据。最初的同源政策是指 A 网站在客户端设置的 Cookie,B网站是不能访问的。

随着互联网的发展,同源政策也越来越严格,在不同源的情况下,其中有一项规定就是无法向非同源地址发送Ajax 请求,如果请求,浏览器就会报错。 image.png 同源策略的交互方式有三种:

  • 通常允许跨域写操作,例如链接,重定向以及表单提交等。
  • 通常允许跨域嵌套资源,例如 img,script 标签等。
  • 通常不允许跨域读操作。

二、跨域问题的表现

1.什么是跨域?

跨域是指去向一个为非本origin(协议、域名、端口任意一个不同)的目标地址发送请求的过程,这样之所以会产生问题是因为浏览器的同源策略限制。看起来同源策略影响了我们开发的顺畅性.实则不然,同源策略存在的必要性之一是为了隔离攻击。

2.CSRF

CSRF,又称跨站请求伪造,指非法网站挟持用户cookie在已登陆网站上实施非法操作的攻击,这是基于使用cookie在网站免登和用户信息留存的实用性,接下来来讲讲正常网站免登的请求流程。 请求流程如下:

  1. 我们进入一个网站,发送登陆请求给后端
  2. 后端接受登陆请求,判断登陆信息是否准确
  3. 判断信息准确后后端后会发送response给浏览器并在response header中加入set-cookie字段
  4. 浏览器接受response返给用户,并将header中的cookie进行保存
  5. 用户关闭当前网站窗口后再次打开后,浏览器会自动将cookie加入request header实现免登

我们设想这样一个场景

  • 小a登陆了网银网站,小a所在浏览器记录了网银回馈的cookie
  • 这时他qq上收到个链接,什么澳门赌场,美女荷官,在线送钱的网站b
  • 他点开那个链接之后,网站b就可以携带浏览器设置的cookie向网银系统上发送请求

  结果不言而喻,轻则信息泄漏,重则钱财损失,而且cookie正常的存储时间是直到关闭浏览器为止,而不是关闭网站,所以很多用户会以为关闭网站了再去打开澳门的网站就安全了emmmm。   在一些安全性要求高的网站,同源策略还是有存在的必要的,需要跨域实现的请求也最好设置限制,比如设置指定的白名单origin。

三、解决跨域问题的常用方案

1.JSONP

浏览器在解析script标签时是允许通过script标签中的src请求路径进行跨域发送请求的,不会收同源政策的影响,那么解决跨域问题的第一种方案就可以冲原生的script标签入手。

  • 我们在前端代码中写入一个script标签,将我们所需要进行跨域的请求地址url放在script标准的src属性中,当浏览器渲染到这个script标签就会发送请求给目标地址
    <!-- 声明fn函数-->
    <script>
        function fn(data) {
            console.log(data);
        }
    </script>
    <!-- 将非同源的请求路径放在script标签中的src属性中 -->
    <script src="http://localhost:3001/test1"></script>
  • 在目标地址的服务端就会接受到我们的请求进行处理,返回一个字符串,在字符串中我们只需要将经过处理的数据放在一个函数的参数位置上将函数用引号进行包裹,返回给客户端
/*使用express框架搭建Web服务器*/
    npm install express
    //引入express框架
    const express = require('express');
    //创建Web服务器
    const app = express();
    // 处理/test请求路由
    app.get('/test', (req, res) => {
    //fn为在
    res.send("fn({username:'andy'})");
});
  • 客户端接受到服务端响应的字符串进行解析时就会调用该函数,该该函数需要在script标签生成进行声明,并且是全局下的函数(可以挂载到全局对象windom下)

image.png

image.png

将这种思想抽象出来封装成原生的jsonp函数(没有考虑到请求失败的情况,还有待改善)

    // 封装的jsonp函数
    /*
    参数options:
            type:Object{
                    url:{
                    type:String,
                    requied:true,
                    description:进行跨域的请求地址
                    },
                    success:{
                        type:Function,
                        requied:true,
                        description:回调函数,处理跨域的请求成功后服务器端响应回来的数据
                    },
                    data:{
                        type:Object,
                        required:false,
                        description:发送的请求参数
                    }
            }
*/
    function jsonp(options) {
    // 请求路径
    var script = document.createElement('script');
    // 拼接字符串的变量
    var params = '';
    // 循环data对象拼接参数
    for (let attr in options.data) {
        params += '&' + attr + '=' + options.data[attr];
    }
    // 随机生成函数名称避免多个jsonp函数被触发时同名函数会被覆盖  例如:myfnName0.121212 =>myfnName0121212
    var fnName = 'myfnName' + Math.random().toString().replace('.', '');
    // 为了解决函数必选在全局下声明的问题 将函数挂载到全局对象windom中
    window[fnName] = options.success;
    // 将跨域请求地址放到script标签中src中
    script.src = options.url + '?callback=' + fnName + params;
    // 将script标签插入文档中
    document.body.appendChild(script);
    // 监听script标签创建完成
    script.onload = function () {
        //创建完成时将生成的script标签移除 优化文档结构
        document.body.removeChild(this);
    }
}

服务器端的代码如下:

    app.get('/better', (req, res) => {
	// 接收客户端传递过来的函数的名称
	// const fnName = req.query.callback;
	// 将函数名称对应的函数调用代码返回给客户端
	// const data = JSON.stringify({name: "张三"});
        // 对响应内容进行拼接
	// const result = fnName + '('+ data +')';
	// res.send(result);
        // 在express框架中已经帮我们省去了以上的步骤 使用response对象下的jsonp方法即可
	res.jsonp({name: 'lisi', age: 20});
});

2.CORS跨域资源共享

CORS:全称为 Cross-origin resource sharing,即跨域资源共享,它允许浏览器向跨域服务器发送 Ajax 请求,克服了 Ajax 只能同源使用的限制。 通过在服务器端配置响应头中的Access-Control-Allow-Origin和Access-Control-Allow-Origin,即配置白名单的方式,让浏览器端能接受跨域请求的响应数据,对于客户端可正常书写ajax请求代码。

image.png

    //设置中间件拦截所有请求 设置其响应头
    app.use((req, res, next) => {
     //告诉服务器哪些客户端可以访问我 *表示允许所有的客户端访问我
     res.header('Access-Control-Allow-Origin', '*');
     //告诉服务器客户端可以用什么方式访问我 多个请求方式用逗号隔开
     res.header('Access-Control-Allow-Methods', 'GET, POST');
     //这个步骤很关键 我们很容易将这步遗漏了 导致服务器处理不了客户端请求
     //将请求放行 让后续中间件对请求进行处理
     next();
 })

上面提到了CSRF问题,现在我们需要进行跨域登录验证,验证用户是否为登录状态,此时我们需要了解到ajax对象的属性withCredentials以及在服务器端中配置响应头中Access-Control-Allow-Credentials的属性值 我们可以这样做:

html代码:

    <form id="loginForm">
            <div class="form-group">
                <input type="text" name='username' class="form-control">
            </div>
            <div class="form-group">
                <input type="password" name="password" class="form-control">
            </div>
            <input type='button' class="btn btn-default" id="loginBtn" value="登录"></input>
            <input type='button' class="btn btn-default" id='checkLogin' value="检测登录状态"></input>
        </form>

JS代码:

    // 获取元素
        var loginBtn = document.querySelector('#loginBtn');
        var checkLogin = document.querySelector('#checkLogin');
        var loginForm = document.querySelector('#loginForm');
        loginBtn.addEventListener('click', function () {
            // 创建FormData对象
            var form = new FormData(loginForm);
            // 创建Ajax对象
            var xhr = new XMLHttpRequest();
            // 配置Ajax对象
            xhr.open('post', 'http://localhost:3001/login');
            // 发送ajax请求
            xhr.send(form);
            // 监听onload事件
            xhr.onload = function () {
                console.log(xhr.responseText);
            }
        })
        checkLogin.addEventListener('click', function () {
            // 创建FormData对象
            var form = new FormData(loginForm);
            // 创建Ajax对象
            var xhr = new XMLHttpRequest();
            // 配置Ajax对象
            xhr.open('get', 'http://localhost:3001/checkLogin');
            // 发送ajax请求
            xhr.send();
            // 监听onload事件
            xhr.onload = function () {
                console.log(xhr.responseText);// {"message":"处于未登录状态"} 此时跨域存在cookie没有被携带
            }
        })

node服务器端代码:设置一个已注册 用户名为lisi 密码为123456进行验证

    // 解决跨域登录携带cookie问题
app.post('/login', (req, res) => {
    // 创建表单解析对象
    var form = formidable.IncomingForm();
    // 解析表单
    form.parse(req, (err, fields, file) => {
        // 接收客户端传递过来的用户名和密码
        const { username, password } = fields;
        // 用户名密码比对
        if (username == 'lisi' && password == '123456') {
            // 设置session
            req.session.isLogin = true;
            res.send({ message: '登录成功' });
        } else {
            res.send({ message: '登录失败, 用户名或密码错误' });
        }
    })
});

app.get('/checkLogin', (req, res) => {
    // 判断用户是否处于登录状态
    if (req.session.isLogin) {
        res.send({ message: '处于登录状态' })
    } else {
        res.send({ message: '处于未登录状态' })
    }
});

结果图如下:

image.png image.png 由图可以知道 我们第一次发送请求到服务器时,服务器返回一个唯一的cookie值并且传递登录成功的响应数据给我们但是,我们在进行检测是否为登录状态时,在发送一次请求给服务器,发现服务器并没有获取到对应客户端的cookie对象,响应为未登录状态。

解决方法 在js书写中设置ajax对象中的withCredentails属性值为true:

    // 客户端允许携带cookie信息
     xhr.withCredentials = true;

在Node服务器端中 设置响应头中的属性Access-Control-Allow-Credentials的值为true

 //设置中间件拦截所有请求 设置其响应头
    app.use((req, res, next) => {
     //告诉服务器哪些客户端可以访问我 *表示允许所有的客户端访问我
     res.header('Access-Control-Allow-Origin', '*');
     //告诉服务器客户端可以用什么方式访问我 多个请求方式用逗号隔开
     res.header('Access-Control-Allow-Methods', 'GET, POST');
     // 允许客户端发送跨域请求时携带cookie信息
     res.header('Access-Control-Allow-Credentials', true);
     //这个步骤很关键 我们很容易将这步遗漏了 导致服务器处理不了客户端请求
     //将请求放行 让后续中间件对请求进行处理
     next();
 })
    

再做一次登录测试:

image.png image.png