1

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

跨域完全就是浏览器搞得鬼,由于浏览器同源策略的限制,协议、域名、端口号只要有一个不同,就是不同源。

本文记录的都是自己敲出来并验证过的,前后端都是本地起的服务,前端 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 去掉的。

// 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 能够识别哪些请求需要代理。

具体的每个接口

// 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)
...

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

image.png

但是面试老师问时,感觉还是没答到重点,嗯,那就看看 cors

2. cors

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

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

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

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

完事啦


见贤思齐
66 声望8 粉丝

写代码的


« 上一篇
jwt 存在哪