23.7.8. 关于跨域问题的解决办法

1.什么是跨域

1.1 浏览器的同源策略

  • 同源策略问题的出现原理

../../../_images/image-20220222115314779.png
  • 传统网络请求模型

../../../_images/image-20220222115220040.png
  • 前后端分离项目之后的网络传输流程。

../../../_images/image-20220222115353557.png

(1)在浏览器端通过URL访问前端项目。

(2)前端项目返回数据。

(3)浏览器开始对前端返回的数据进行解析,对数据中包含的样式进行渲染,对数据中包含的逻辑代码进行执行。

(4)这时,在这些逻辑代码中,有一条命令,让浏览器端向后端项目的URL地址发送数据请求。

(5)浏览器执行这条命令,向后端发起了数据请求。

(6)后端项目向浏览器端返回请求的数据。

(7)浏览器端会对此数据进行解析,发现这些数据的来源与第(1)步中的URL不一样,于是拒绝对这些数据做进一步的执行与渲染,然后报出跨域问题的错误提示。

如果没有同源策略,会存在安全问题。假如用户的电脑遭遇病毒攻击,病毒的功能是用户只要通过浏览器访问网站A,就会在浏览器解析从网站A返回的数据之前,将让浏览器访问其他的非法网站B的命令塞进这些数据里,这样非法网站B就会获取用户在网站A的Cookie。

这个时候,非法网站B只要带着用户的Cookie访问网站A,就可以用户的身份,在网站A中进行操作了。如果网站A是购物网站,相对比较安全,毕竟在完成支付的时候,还需要进行手机验证码操作;假如网站A是一家网络借贷公司的网站后果不堪设想。所以,浏览器的同源策略非常重要。

1.2 什么情况下会发生跨域问题

../../../_images/image-20220222115454457.png

2.跨域问题的几种解决思路

2.1 通过jsonp跨域

原理:标签不受浏览器同源策略限制。比如在线引用jQuery,其实也是一种跨域的实现:

<script src="http://code.jquery.com/jquery-latest.js"></script>

将src对应的网址替换为后端项目的网址,即可实现前后端分离项目开发者想要达到的效果。

注意: jsonp有很大的一个局限性,就是只能实现get请求。

2.2 document.domain+iframe跨域

原理:不同域指向的两个页面,都通过JS强制设置document.domain为基础主域,就可以实现同域。

2.3 CORS(跨域资源共享)

通过在服务端设置Access-Control-Allow-Origin即可解决跨域问题,前端无需设置。此方案目前是主流的跨域问题解决方案。

CORS(Cross-origin resource sharing跨域资源共享),是一个W3C标准。

CORS的原理是只需要向响应头header中注入Access-Control-Allow-Origin,浏览器检测到header中的Access-Control-Allow-Origin,就可以跨域操作了。

CORS允许浏览器向跨源服务器发出XMLHttpRequest请求,从而克服AJAX只能同源使用的限制。

2.4 Nginx代理跨域

(1)Nginx配置解决iconfont跨域。

浏览器跨域访问JS、CSS和Img等常规静态资源被同源策略许可,但iconfont字体文件(eot/otf/ttf/woff/svg)例外,此时可在Nginx的静态资源服务器中加入以下配置:

location / {
  add_header Access-Control-Allow-Origin *;
  }

(2)Nginx反向代理。

原理:通过Nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口。

服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,因为不需要同源策略,也就不存在跨越问题。

通过这些解决方案可以总结出一个规律,那就是所有的解决方案,基本上都在绕开一件事儿,那就是尽量绕开JS脚本中与网络请求有关的命令。

浏览器对于JS脚本做网络请求的限制非常大,在浏览器中运行的JS脚本也因此不可以作为网络爬虫来运行,因为浏览器中运行的JS脚本在发送网络请求时,是不允许自定义协议头的。

3.创建后端项目

(1)我们新建Django项目命名为demo9,同时新建App,命名为app01。

(2)在app01/models.py中新建一个书籍类:

from django.db import models
from datetime import datetime
# Create your models here.


class Book(models.Model):
    """
    书籍
    """
    title = models.CharField(max_length=32, verbose_name='书名')
    author = models.CharField(max_length=10, verbose_name='作者名')
    add_time = models.DateTimeField(default=datetime.now, verbose_name='添加时间')

    class Meta:
        verbose_name = '书籍表'
        db_table = "book"
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.title

(3)执行数据更新命令:

python manage.py makemigrations
python manage.py migrate

(4)在书籍表格内输入两条书籍记录。

(5)安装Django REST framework及其依赖包markdown和django-filter。

pip install djangorestframework markdown Django-filter -i "https://pypi.doubanio.com/simple/"

(6)在settings.py中添加注册app01和rest_framework的代码:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01.apps.App01Config',
    'rest_framework'
]

(7)在demo9/app01/views.py中编写视图函数:

from django.shortcuts import render
from .models import Book
from .serializers import BookSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer, BrowsableAPIRenderer


# Create your views here.

class BookView(APIView):
    """
    书籍列表
    """

    renderer_classes = [JSONRenderer]  # 渲染器

    def get(self, request):
        book_list = Book.objects.all()
        re = BookSerializer(book_list, many=True)
        return Response(re.data)

在demo9/app01/serializers.py中编写序列化器:

from rest_framework import serializers
from .models import Book


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = "__all__"

(8)在demo9/urls.py中配置路由代码:

from django.contrib import admin
from django.urls import path
from app01.views import BookView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('book/', BookView.as_view(), name='book'),
]

(9)运行项目demo9,然后通过浏览器访问 http://127.0.0.1:9000/book/.获得了书籍列表的数据。

../../../_images/image-20220222145218082.png

4.前端项目解决跨域问题

4.1 webpack与webpack-simple的区别

如果涉及的前后端分离项目业务需求比较简单,可以选择使用webpack-simple建立前端项目。

一般情况下,在实际生产项目中,大多数的情况,如果前端项目是基于Vue的,那么会选择使用webpack建立前端项目。

(1)新建时,Webpack和Webpack-Simple安装依赖库的方式不同。

#设置淘宝镜像源
npm config set registry https://registry.npm.taobao.org

# 查看npm源
npm config get registry

#需要换回时改为官方的镜像源
npm config set registry https://registry.npmjs.org


# 安装淘宝npm
npm install -g cnpm --registry=https://registry.npm.taobao.org
# vue-cli 安装依赖包
cnpm install --g vue-cli

使用webpack-simple新建Vue项目demo9_1:

cnpm install –global vue-cli
vue init webpack-simple demo9_1
cd demo9_1
cnpm install

注意: 我们默认电脑里已经安装了Node.js和淘宝镜像。

使用webpack新建Vue项目demo9_2:

cnpm install –global vue-cli
vue init webpack demo9_2
? Project name demo9_2
? Project description A Vue.js project
? Author 建力 <1879324764@qq.com>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? No
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recommended) yarn

   vue-cli · Generated "demo9_2".

cd demo9_2
cnpm install

(2)目录结构不同。

Webpack-Simple目录结构

 D:\Django_drf\demo9_1 的目录

2022/02/22  14:08    <DIR>          .
2022/02/22  14:08    <DIR>          ..
2022/02/22  13:56                72 .babelrc
2022/02/22  13:56               147 .editorconfig
2022/02/22  13:56               127 .gitignore
2022/02/22  13:56               201 index.html
2022/02/22  14:00    <DIR>          node_modules
2022/02/22  13:56               837 package.json
2022/02/22  13:56               322 README.md
2022/02/22  13:56    <DIR>          src
2022/02/22  13:56             1,600 webpack.config.js

Webpack目录结构

 D:\Django_drf\demo9_2 的目录

2022/02/22  14:16    <DIR>          .
2022/02/22  14:16    <DIR>          ..
2022/02/22  14:13               402 .babelrc
2022/02/22  14:13               147 .editorconfig
2022/02/22  14:13               213 .gitignore
2022/02/22  14:13               246 .postcssrc.js
2022/02/22  14:13    <DIR>          build
2022/02/22  14:13    <DIR>          config
2022/02/22  14:13               269 index.html
2022/02/22  14:16    <DIR>          node_modules
2022/02/22  14:13             2,292 package.json
2022/02/22  14:13               550 README.md
2022/02/22  14:13    <DIR>          src
2022/02/22  14:13    <DIR>          static
2022/02/22  14:13    <DIR>          test
2022/02/22  14:16           437,517 yarn.lock

如果在实际操作中就能感受到,这个执行过程是非常慢的。这是因为webpack与webpack-simple安装依赖包的顺序是不一样的,

webpack-simple是先新建项目,然后由用户手动进入项目目录,可以选择是使用npm还是使用cnpm安装依赖包,我们选择了cnpm。

而webpack则是在新建项目的时候,就开始安装依赖包,并且默认是以npm安装依赖包,速度极慢。

(3)解决跨域的难易程度不同。

解决跨域的难易程度不同,是webpack和webpack-simple的主要区别。

在webpack的项目中,有config目录,该目录下有index.js文件,在这个文件内,进行简单的配置就可以解决跨域问题;而使用webpack-simple新建的项目,则没有这个目录,也没有这个文件。

注意: 虽然使用webpack新建的项目可以通过修改index.js文件中的一些配置代码轻松地解决跨域问题,而使用webpack-simple新建的项目没有这个文件,

这并不是说,使用webpack-simple新建的项目就不可以在前端项目内解决跨域问题,只是解决方法的操作更麻烦一些。

4.2 在前端项目中解决跨域问题

从前端项目中解决跨域问题,首先在index.js中做相关配置,然后向后端发送数据请求,这样获取到的数据就可以避免跨域问题了。具体的操作步骤如下:

vue2中配置:

(1)打开demo9_2/config/index.js,然后在proxyTable中添加代码:

proxyTable: {
  '/api': { //替换代理地址名称
    target: 'http://127.0.0.1:9000/', //代理地址
    changeOrigin: true, //可否跨域
    pathRewrite: {
      '^/api': '' //重写接口,去掉/api
    }
  }
},

vue3中配置:

vue.config.js

module.exports = {
    devServer: {
        open: true,
        host: 'localhost',
        port: 8080,
        https: false,
        //以上的ip和端口是我们本机的;下面为需要跨域的
        proxy: {  //配置跨域
            '/api': {
                target: 'http://127.0.0.1:8088/api',  //这里后台的地址模拟的;应该填写你们真实的后台接口
                ws: true,
                changOrigin: true,  //允许跨域
                pathRewrite: {
                    '^/api': ''  //请求的时候使用这个api就可以
                }
            }
        }
    }
}

(2)在demo9_2中安装axios模块:

cnpm install axios --save

(3)在demo9_2/src/App.vue中编写向后端请求数据的代码:

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <router-view/>
  </div>
</template>
<script>
//引入axios模块
import axios from 'axios'
export default {
  name: 'App',
  data(){
    return{
      msg:''
    }
  },
  methods:{
//向后端项目请求数据方法
    getData(){
      axios({
        url:'api/book/'
      }).then(res=>{
        console.log(res)
      })
    }
  },
  mounted(){
    this.getData()
  }
}
</script>
<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

启动demo9_2项目:

npm run dev

(4)至此,我们在前端就已经将跨域问题解决了。接下来验证是否已解决跨域问题。

然后使用浏览器访问:http://127.0.0.1:8080/

(5)运行我们在本章新建的后端项目demo9。

(6)如下所示,在浏览器中按F12键,打开开发者模式。

在浏览器的Console面板中,收到了来自后端项目的数据:

{data: Array(2), status: 200, statusText: 'OK', headers: {…}, config: {…}, …}
config: {transitional: {…}, transformRequest: Array(1), transformResponse: Array(1), timeout: 0, adapter: ƒ, …}
data: Array(2)
0: {id: 1, title: 'python入门到精通', author: '胡建力', add_time: null}
1: {id: 2, title: 'go入门到精通', author: '胡建力', add_time: null}
length: 2
[[Prototype]]: Array(0)
headers: {allow: 'GET, HEAD, OPTIONS', connection: 'keep-alive', content-length: '153', content-type: 'application/json', cross-origin-opener-policy: 'same-origin', …}
request: XMLHttpRequest {onreadystatechange: null, readyState: 4, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, …}
status: 200
statusText: "OK"
[[Prototype]]: Object

4.3 在后端项目中解决跨域问题

环境准备:

新建的另一个前端项目demo9_2,以及后端项目demo9

在Django项目中,使用CORS(跨域资源共享)解决跨域问题非常方便。

接下来就通过一个实例来演示如何使用CORS解决跨域问题。具体步骤如下:

(1)安装跨域模块:

pip install django-cors-headers -i "https://pypi.doubanio.com/simple/"

(2)在settings.py中注册模块:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01.apps.App01Config',
    'rest_framework',
    'corsheaders'
]

(3)在settings.py中增加中间件的配置代码:

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',                    # 放到中间件顶部
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

(4)在settings.py中新增配置项,即可解决本项目中的跨域问题。

CORS_ORIGIN_ALLOW_ALL = True

(5)重新运行demo9项目。

(6)在demo9_1中安装axios模块:

cnpm install axios --save

(7)在demo9_1/src/App.vue中编写向后端请求数据的代码:

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

<script>
//引入axios模块
import axios from 'axios'

export default {
  name: 'App',
  data() {
    return {
      msg: ''
    }
  },
  methods: {
//向后端发送数据请求
    getData() {
      axios({
        url: "http://127.0.0.1:9000/book/"
      }).then(res => {
        console.log(res)
      })
    }
  },
  mounted() {
    this.getData()
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

注意: <script>标签中的URL却是不同的,大家要注意不要与demo9_2混淆。

(8)运行demo9_1项目:

npm run dev

(9)使用浏览器访问:http://127.0.0.1:8080

(10)如图9-24所示,按F12键,打开浏览器的开发者模式。可以看到Console控制台没有报错,而且获得了后端传来的数据:

{data: Array(2), status: 200, statusText: 'OK', headers: {…}, config: {…}, …}
config: {transitional: {…}, transformRequest: Array(1), transformResponse: Array(1), timeout: 0, adapter: ƒ, …}
data: Array(2)
0: {id: 1, title: 'python入门到精通', author: '胡建力', add_time: null}
1: {id: 2, title: 'go入门到精通', author: '胡建力', add_time: null}
length: 2
[[Prototype]]: Array(0)
headers: {content-length: '153', content-type: 'application/json'}
request: XMLHttpRequest {onreadystatechange: null, readyState: 4, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, …}
status: 200
statusText: "OK"
[[Prototype]]: Object

至此,我们在后端项目中实现了解决跨域问题的功能。