如何解决前后端跨域问题(详细)

19,727 阅读3分钟

一、经典疑问:为什么会跨域?

我们把问题分解

  • 谁出现的跨域? ==》 浏览器!

  • 为何出现? ==》 同源策略

    • 同源策略?

      • 同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSSCSFR等攻击。
      • 所谓同源 ==》指的是 “协议+域名+端口” 三者的相同 只要有一个不同就会导致跨域问题
    • 目的:简单来说就是禁止的是来自不同源的"document"或脚本,对当前"document"读取或设置某些属性。

  • 解决方案?

    • 前端做代理服务器

    • 跨域技术-CORS (CrossOrigin Resources Sharing,跨源资源共享)  (推荐)

      • CORS,是 HTML5 的一项特性,它定义了一种浏览器和服务器交互的方式来确定是否允许跨域请求。
      • 使用原理:浏览器一旦发现 axios 请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。 服务器根据这些附加的值,决定是否同意此次请求。
    • JSONP

      • 改变请求方式 dataType: 'jsonp', // 请求方式为jsonp
      • 原理: JSONP 是通过动态添加
      • // 前端实现
        $.ajax({
        url: 'http://www.domain2.com:8080/login',
        type: 'get',
        dataType: 'jsonp', // 请求方式为jsonp
        jsonpCallback: "onBack", // 自定义回调函数名
        data: {}
        });
        ​
        // 后台实现
        @ControllerAdvice(basePackages = "com.zkn.learnspringboot.web.controller")
        public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice{
        ​
        public JsonpAdvice() {
        ​
        super("callback","jsonp");
        }
        }
        ​
        
  • 解决最优解 ==》 CORS

    • 相比于 jsonp 只能用于get 请求来说 cors对于所有的请求都通用
    • jsonp 的优势在于可以在于支持老式浏览器,以及可以向不支持 cors 的网站请求数据。

二、如何解决?

  • 前端代理服务器(vue)

    • vue.config.js ==> 配置文件

      • // 配置代理
            devServer: {
                port: 9086,
                open: false,
                overlay: {
                    warnings: false,
                    errors: true
                },
                proxy: {
                    "/dev_api": {
                        target: "http://11.111.111.11:8000/", ==》 此处写明自己应该访问的代码
                        changeOrigin: true,
                        pathRewrite: {
                            "^/dev_api": ""
                        }
                    }
                }
            },
        
    • request.js 中配置baseUrl

      • let instance = axios.create({
            baseURL: "/dev_api",
            withCredentials: true, // send cookies when cross-domain requests
            timeout:5000,
        })
        // 注意: dev_api 之前的 “/” 一定不可省略 别问我为啥知道的
        // 此处的dev_api 要与上面的相对应 当然 也可以起自己的名字
        
    • 本地运行比较顺畅 , 但是项目打包到服务器 就需要对地址进行一个调试了 ,跟后台协调好!

  • 跨域技术-CORS  (推荐)

    • 简介

      • CORS 通信过程都是浏览器自动完成的 不需要用户参与
      • 对于开发者一样,CORS 通信与普通的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨域,就会自动添加一些附加的头信息
      • 因此 实现CORS的关键是服务器,只要服务器实现了CORS接口 就可以跨域通信
    • 简单请求(get post)

      • 当浏览器发现跨域之后,就会向请求头信息里面自动添加一个Origin字段

        • GET /cors HTTP/1.1
          Origin: http://api.bob.com
          Host: api.alice.com
          Accept-Language: en-US
          Connection: keep-alive
          User-Agent: Mozilla/5.0...
          
        • Origin字段说明了本次请求来自哪个域(协议+域名+端口)
      • 如果发现 Origin为指定的源 (白名单中),服务器会响应成功,并在响应头中多几个信息字段

        • // 该字段是必须的 该字段要么 * 要么 是请求时 Origin 中的值
          Access-Control-Allow-Origin: http://api.bob.com  // 该字段可选 是否允许发送cookie
          Access-Control-Allow-Credentials: true/* CORS 请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个服务器返回的基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。*/
          ​
          Access-Control-Expose-Headers: FooBar
          
      • 如果发现 Origin不在许可的范围内,服务器会返回一个正常的Http回应,并且抛出错误 XMLHttpRequest...

    • 如何配置?

      • 前面已经说到实现CORS的关键是服务端 因此前端只需要配置**withCredentials** 属性

      • CROS 请求默认不包含 cookie 信息 如果我们想要包含就必须在请求中将 withCredentials 属性 配置为 true

        • // 创建axios 实例  ===> 代理服务器一定要写/!!!!
          let instance = axios.create({
              baseURL: "/dev_api",
              withCredentials: true, // send cookies when cross-domain requests
              timeout:5000,
          })
          ​
          instance.defaults.withCredentials = true;  //允许携带cookie   ==> 这个太关键
          
      • 同样 需要注意的是 如果要发送cookie Access-Control-Allow-Origin就不可以设置为 ***** !! 也就意味这必须指定明确的、与请求网页一致的域名

三、Token 、Cookie 与 CORS 的爱恨情仇

  • 用户验证何来?

    • 众所周知,当客户端多次向服务端请求数据时,服务端就需要多次从数据库中查询用户名和密码并进行对比,判断用户名和密码是否正确,并作出相应提示。
    • 但这样无疑会增加服务器端的运行压力,是否可以有一种方式只需要验证用户就是之前的用户而不需要每次在客户端请求数据时都需要查询数据库判断用户名和密码是否正确。
    • 就这样 为了避免大量的查询数据库 减小服务器端的运行压力 令牌机制应运而生
  • 令牌如何运作?

    • token

      • 前端登录成功后 后台返回token

      • 前端要对token 进行处理 存储并放到请求头中 每次请求都会将token 放入 请求头中

        • instance.interceptors.request.use(
            (config) => {
              // 判断某些接口不需要token
              if (!config.noAuthorization) {
                // 加入token
                let token = RootStore.userStore.allData.accessToken;
                if (token) {
                  config.headers.Authorization = `Bearer ${token}`;
                }
              }
              return config;
            },
            (err) => {
              console.log(err);
            },
          );
          
    • cookie 中存储

      • 后端将令牌(sessionId) 存到浏览器的Cookie 中

      • 前端请求自动从cookie 将sessionId 传到后端

        • image-20210910223347774
  • 当 token cookie 遇到跨域 🤦‍

    • token

      • 跨域了? 我们可以直接在服务端 Access-Control-Allow-Origin就设置为 ***** 前端 withCredentials属性为 false
      • 也就是说 允许所有的 origin 访问
    • cookie

      • 理论上 我们可以遵循token的方法进项解决,但

      • 上文所说 ==》需要注意的是 如果要发送cookie Access-Control-Allow-Origin就不可以设置为 ***** !! 也就意味这必须指定明确的、与请求网页一致的域名

      • 也就意味这 ,我们需要指定明确的 域名 进行访问 并且要告诉后端 我们的的域名是什么,并让他加入白名单中

      • 最重要的 我们要在前端配置**withCredentials** 属性 配置为 true

        • // 创建axios 实例  ===> 代理服务器一定要写/!!!!
          let instance = axios.create({
              baseURL: "/dev_api",
              withCredentials: true, // send cookies when cross-domain requests
              timeout:5000,
          })
          ​
          instance.defaults.withCredentials = true;  //允许携带cookie   ==> 这个太关键
          
      • 同时在后端配置 access-Control-Allow-Credentials 为 true

        • 2021910-224358
      • 这样在 所给的的origin url之下 我们就可以实现登录 并解决跨域

      • 小问题

        • 只有用浏览器访问服务器地址下的路径,浏览器才会自动带上cookies,那么我们本地调试带不上cookie怎么办?

          • 方案一: 利用服务器代理 ==》 利用上述服务器代理解决跨域的问题

            • 缺点:但是上服务器的时候需要进行修改 baseURL
          • 方案二:利用服务器上已经传入的sessionId 对本地进行写入 ==> 控制台

            • 没有的话进行写入
            • image-20210910225423053
            • 缺点:sessionId 更新时间较短不适用 需要频繁的修改写入 sesionId
    • 总结

      • 由篇幅量可得,token 比 cookie 便捷 事实也是 cookies有很多限制和麻烦 就导致比较的麻烦
      • 对于安全性来讲:并不是麻烦了安全 两者都是用令牌进行传送 ,只不过方式不一样 ,安全性还是差不多的

参考文献: