手把手用代码教你怎么解决跨域问题

跨域是个老生常谈的问题,都谈臭了,我在实际工做中,其实遇到很少。如今基本都是先后端分离开发,开发阶段(通常是本地起个服务),用 webpack 代理就能够解决跨域的问题,实在不行,用非安全模式的 chrome 也能够(我原先就这么干过);部署阶段,咱们前端须要作的也很少,咱们只需打个包出来就能够了(顶多就是打包前的配置,路由模式设置等)。可是,面试必问啊,并且一直对 cors 这种方案似懂非懂,因此就用代码撸一遍咯~javascript

跨域彻底就是浏览器搞得鬼,因为浏览器同源策略的限制,协议、域名、端口号只要有一个不一样,就是不一样源。html

本文记录的都是本身敲出来并验证过的,先后端都是本地起的服务,前端 vue-cli3 搭的 vue 工程,封装 axios 请求,后端用的 express + mysql。废话很少说,直接上代码:前端

1. webpack 代理,主要用于开发阶段

// vue.config.js
...
devServer: {
  host: '0.0.0.0',
  port: 8080,
  open: true,
  overlay: {
    warning: false,
    errors: true
  },
  proxy: {
    '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        secure: false,
        pathRewrite: {
          '^/api': ''
        }
    }
  }
}
...

代码中 target 就是实际提供接口的地址,本文中的接口完整都是这样 http://localhost:3001/user/login, http://localhost:3001/user/get_user_info/api 是为了识别哪些请求须要代理,不然 js 等静态资源请求也会被代理的。前端发起一个请求时,如登陆 http://localhost:8080/api/user/login (下文有说明),就会被转发至 http://localhost:3001/api/user/login 这个接口中,可是咱们的接口是这样子的 http://localhost:3001/user/login,没有 api, pathRewrite 的做用就是把 api 去掉的。vue

// request.js
...
axios.defaults.baseURL = '/api' // 默认为'/',即 http://localhost:8080/
// 设置 post 请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
axios.defaults.timeout = 5000
...

上述的代码中,baseURL 加个 api,是为了让 webpack 可以识别哪些请求须要代理。java

具体的每一个接口node

// api/user.js
import { get, post } from '../utils/request'

export const login = params => post('/user/login', params)
export const getUserInfo = params => post('/user/get_user_info', params)
export const getList = params => get('/user/get_list', params)
...

这样,开发阶段就能愉快地开发了mysql

image.png

可是面试老师问时,感受仍是没答到重点,嗯,那就看看 corswebpack

2. cors

cors 全称是跨域资源共享,具体能够看 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORScors 分为简单请求和非简单请求,具体的区别网上一大堆,本文以简单请求为例。仍是以以前登录的接口为例,这回就关掉 webpack 来代理了,直接 axios 将服务地址写死:ios

// request.js
...
axios.defaults.baseURL = 'http://localhost:3001'
...

重启 webpack,走一波,毫无心外,浏览器有错误nginx

image.png

可是,请求结果结果仍是 200,响应数据也能够看到

image.png

image.png

以前说过,这个和后端没毛线关系,就是浏览器搞得鬼,浏览器发现响应头里面没有 Access-Control-Allow-Origin 这个字段,就知道这个请求有问题,就抛出个错误,这个错误被 XMLHttpRequestonerror 回调函数捕获。那怎么解决呢,这其实就要用到 cors 了,前端几乎不须要作什么,只需后端改改就能够了。

安装:npm install --save cors

// app.js
...
const cors = require('cors')
app.use(cors())
...

这样就全部的源就能够请求了

image.png

果真,响应头里面就有 Access-Control-Allow-Origin 这个字段了,固然也能够指定某些域才能请求

// app.js
...
const cors = require('cors')
app.use(cors({
  origin: 'http://localhost:8080'
}))
...

浏览器就是根据这个字段来判断是否是存在跨域,这样跨域就解决了。可是,还有一个须要提一下,嗯,cookie。前一篇文章中 jwt存在哪 说到,登陆成功后,jwt 保存在 cookie 中了,之后每一个请求都会带上 cookie 的,可是,用了 cors 以后,login 接口正常了,get_user_info 这个接口报错了:

image.png

403 Forbidden 其实就是这个请求没有携带 cookie

exports.get_user_info = function (req, res, next) {
  const token = req.cookies.token
  if (token) {
    jwt.verify(token, SECRET, async (error, decoded) => {
      if (error) {
        // token 过时
        return res.status(401).send({
          success: false,
          message: 'token 已过时,请从新登陆'
        })
      } else {
        const userInfo = await userModel.getUserById(decoded.id)
        return res.send({
          code: 100,
          message: '返回成功',
          data: {
            userInfo: userInfo[0]
          }
        })
      }
    })
  } else {
    // 没有拿到token 返回错误
    return res.status(403).send({
      success: false,
      message: '没有找到 token'
    })
  }
}

顺便说句题外话,以前一直没有搞明白,浏览器接收的状态码(200,304, 401,403, 500等)究竟是谁给出来的,我猜应该是 web 容器(nginx, apache)或者 nodejs 给的。

因此,cors 解决跨域还得配置请求时能够发送 cookie

// app.js
...
const cors = require('cors')
app.use(cors({
  origin: 'http://localhost:8080',
  credentials: true
}))
...

此外,前端也要稍微改一下

// request.js
...
// axios.defaults.withCredentials = true // 默认为 false,表示跨域请求时是否须要使用凭证,如 cookie
...

这样就能够愉快地请求啦

image.png

3. nginx 反向代理,主要部署时

nginx 反向代理主要用于部署时,尤为是先后端代码分别部署的时候,本文后端代码就不涉及到部署,仍是用本地 3001 端口的服务,express 中也把 cors 给去掉了。前端代码也部署在本地,本地起个 nginx 来跑。

首先打包:执行 npm run build,静态资源路径等所有用的是 vue-cli 3 默认的配置,生成好了 dist 文件。

本地安装 nginx,我用的是 mac,我通常喜欢编译安装,网上教程一大推,你们自行安装。安装好后,进入 nginx 文件夹,大概是长这样子的

image.png

直接执行 sudo ./sbin/nginx 就能够启动 nginx 啦,若是没有反应,说明启动成功,在浏览器中输入 http://localhost,不出意外就会出现如下界面

image.png

若是启动时出现这个报错:

image.png

说明这个地址被占用了,能够查看端口占用状况 lsof -i:80,以前我就遇到这种状况,在 mac 启动过 Apache(mac 自带有),停掉它就能够了 sudo apachectl stop

接下来仍是终端进入 conf 文件中,里面有个 nginx.conf 文件,这是默认的配置文件,在该目录下新建一个 vhosts 文件夹(名字随便取),而后再里面新建 dev.conf 文件

sudo vim dev.conf

i 输入一下配置内容

server {
        listen 8001; # dist 包在 8001 端口启动
        server_name localhost;
        index index.html;
        root /Users/liuzhiqin/Documents/you/path/to/dist;
        
        # 如路由模式是 history,还得配置这个
        location / {
            try_files $uri /index.html;
        }

        location /api { # 匹配 url 中带有 api 的,并转发到http://localhost:3001/api
                rewrite ^/api/(.*)$ /$1 break; # 去掉 api 前缀,和前面 webpack 相似
                proxy_pass http://localhost:3001;
        }

}

wq 保存退出,最后别忘了在 nginx.conf 文件中引入该配置,在最后引入

# 导入配置文件
  include /usr/local/nginx/conf/vhosts/*.conf;

修改了配置文件,须要从新启动 nginx

sudo ./sbin/nginx -s reload

在浏览器中跑一下看看 http://localhost:8001

image.png

呃,403 forbidden 啦,mac 上很容易 403 forbidden,这种状况通常是先查看日志的,打开 logs 文件中的 error.log 日志文件

image.png

其实就是权限问题,查看一下 nginx 进程,ps aux | grep nginx

image.png

发现 nginx 工做用户是 nobody,在 nginx.conf 中修改一下便可

image.png

去掉 user 前的注释,将 nobody 改为当前用户就能够,mac 好像要加上 owner,保存重启 nginx,这时在查看一下 nginx 进程

image.png

浏览器刷新一下,发现能够正常运行了

image.png

发现这种方案最省事了,只需部署时弄弄配置文件就能够,不须要先后端干吗,一劳永逸。

4. 万恶的 JSONP

jsonp 实在是不想写,现今基本已经淘汰,但无赖面试官仍是会问。jsonp 只能用于 get 请求。
先简单说一下 jsonp 的原理,咱们知道咱们能够在 img 标签中引入其余站点中的图片(在开发过程当中,须要线上图片时,我常常打开某宝,从上面找一张图片),将图片地址放到 imgsrc 中就能够了,这是由于具备 src 属性的标签不受跨域的影响,如 script, img, iframe 等,咱们就能够用这个特性变通实现,先看个小例子

...
<head>
    <title>demo</title>
    <script type="text/javascript">
    const getData = function(data){
        console.log(data);
    };
    </script>
    <script type="text/javascript" src="http://another.com/dataList.js"></script>
</head>
...

其余域中的 js 文件

// dataList.js
// 假设 list 是咱们接口返回的数据
const list = {
    "code":100,
    "message":"返回成功",
    "data":{
        "list":[
            {"id":1,"title":"我是标题","name":"admin","pageviews":1234,"status":1,"display_time":"2019-10-23 05:57:43"},
            {"id":2,"title":"要啥标题","name":"zhiqin","pageviews":562367,"status":2,"display_time":"2019-10-16 21:08:53"}
        ]
     }
}

// 而后执行 getData 这个方法,并将数据传进去
getData(list)

上述代码中,咱们本地有个 html 文件,并加载了一个其余域中的 js 文件,这个 js 能够执行咱们本地 js 脚本中的方法,这样就能够实现将其余域中的数据传递过来,拿到其余域中的数据啦。这个地址 http://another.com/dataList.js 就至关于咱们的接口地址(固然接口不会是个 js 文件的)。

须要注意的是,接口是提供数据的,供其余系统来调用的,接口那边并不知道每一个系统调用时传过来的方法名是什么(上述例子咱们在接口中写死了为 getData 了),并且你们本地的方法名确定是不同的,那就动态生成好啦。

本地 express 接口地址 http://localhost:3001/user/get_list,方便起见,我直接在模板文件 index.html 中进行操做,首先看看跨域的状况

// index.html
...
<div id="app"></div>
<!-- built files will be auto injected -->
<script>
fetch('http://localhost:3001/user/get_list')
    .then(response => response.json())
    .then(res => {
        console.log(res)
    })
    .catch(e => {
        console.log('err: ', e)
    })
</script>
...

显然报错

image.png

接下来直接上代码了

前端

<div id="app"></div>
<!-- built files will be auto injected -->
<script>
    function getData(data) {
        console.log(data)
    }
      
    let url = 'http://localhost:3001/user/get_list' // 接口地址
    url += '?callback=getData' // 将方法传过去, 接口是 express 写的,参数名必须是 callback,其余语言不知道
      
    const script = document.createElement('script')
    script.setAttribute('src', url)

    // 把script标签加入head,此时调用开始
    document.getElementsByTagName('head')[0].appendChild(script)
</script>

后端

exports.get_list = async function (req, res, next) {
  const { callback } = req.query
  console.log(callback)
  const ret = await userModel.getList()
  if (ret.length > 0) {
    return res.jsonp({ // express 直接封装好了 jsonp
      code: 100,
      message: '返回成功',
      data: {
        list: ret
      }
    })
  }
}

跑一下,控制台看到打印出的数据啦

image.png

而且 Elements 中也能够看到动态生成的 script

image.png

Network js 请求中也能够看到这条请求

image.png

你们可能会奇怪,接口中也没有执行 getData 这个方法啊,这实际上是 res.jsonp 帮咱们作好啦(其余语言你们自行 google),咱们能够看一下响应结果

image.png

完事啦

相关文章
相关标签/搜索