點燈坊

失くすものさえない今が強くなるチャンスよ

如何解決 Vue 開發環境違反 Same-Origin Policy ?

Sam Xiao's Avatar 2020-05-22

若前後端分離,且同時在本機開發 Vue 前端與 Node 後端時,很容易遇到違反 Same-Origin Policy 問題,本文介紹多種方式解決。

Version

macOS 10.15.4
WebStorm 2020.1.1
Vue 2.6.11
Vue CLI 4.3.1
Node 12.4.0
Chrome 83.0.4103.61

Same-Origin Policy

Browser 的 same-origin policy 不允許存取不同 domain 的 API,舉例來說,若在本機同時開發 Vue 與 Node,Vue CLI 啟動在 http://localhost:8080,而 Node 啟動在 https://localhost:3000,對於 browser 而言,儘管 host 相同,port 不同就視為不同 domain,這種 cross domain 的 API 存取違反 same-orgin policy,會被 browser 擋下而無法執行。

Node

index.js

let express = require('express')

let app = express()
let port = 3000

app.get('/hello-world', (req, res) => res.send('Hello World!'));

app.listen(port, _ => console.log(`Node listening on port ${port}!`));

使用 Node + Express 建立 /hello-world GET,並啟動在 3000 port。

cors000

使用 Postman 可正確打到 http://localhost:3000/hello-world 並回傳 Hello World!

Vue

App.vue

<template>
  <div>
    {{ message }}
  </div>
</template>

<script>
import axios from 'axios'

let mounted = function() {
  axios.get('http://localhost:3000/hello-world')
    .then(x => x.data)
    .then(x => this.message = x)
}

export default {
  name: 'app',
  data: () => ({
    message: ''
  }),
  mounted
}
</script>

11 行

axios.get('http://localhost:3000/hello-world')
  .then(x => x.data)
  .then(x => this.message = x)

在 Vue 使用 Axios 直接打 http://localhost:3000/hello-world

cors001

Browser 會顯示因為 header 沒有 Access-Control-Allow-Origin,所以打 http://localhost:3000/hello-world 違反 same-origin policy 而失敗。

CORS

$ yarn add cors --dev

若 API 也自己開發,可安裝 cors middleware。

cors002

app.js

let express = require('express')
let cors = require('cors')

let app = express().use(cors())
let port = 3000

app.get('/hello-world', (req, res) => res.send('Hello World!'))

app.listen(port, _ => console.log(`Node listening on port ${port}!`))

第 2 行

let cors = require('cors')

cors module require 進來。

第 4 行

let app = express().use(cors());

使用 cors middleware。

其他部分不用改變,Vue 也不用改變,重新 yarn api 啟動 Node。

cors003

Vue 可正確收到 API 回傳值。

Reverse Proxy

vue.config.js

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        pathRewrite: {'^/api' : ''}
      }
    }
  },
}

若 API 不是由自己開發,可暫時由前端 Webpack DevServer 的 reverse proxy 解決。

在專案目錄下新增 vue.config.js,當 yarn serve 發現 vue.config.js,就會採用這個檔案。

第 4 行

'/api': {
  target: 'http://localhost:3000',
  pathRewrite: { '^/api': '' }
}

當 Vue 打 /api/hello-world 時,DevServer 的 reverse proxy 會將 /apihttp://localhost:3000 rewrite 成 http://localhost:3000/hello-world

也由於 Vue 是打 /api/hello-world,browser 視為相同 domain,因此沒有違反 same-origin policy。

location /api/ {
  proxy_pass 'http://localhost:3000';
}

在此特別要強調的是同樣是 reverse proxy,但 rule 寫法 DevServer 與 Nginx 不一樣!

Nginx 在 location 最後要加上 /,也就是 /api/ 而不是 /api,server 最後也要加上 /,也就是 http://localhost:3000 而不是 http://localhost:3000/,且最後也不需 rewrite

App.vue

<template>
  <div>
    {{ message }}
  </div>
</template>

<script>
import axios from 'axios'

let mounted = function() {
  axios.get('api/hello-world')
    .then(x => x.data)
    .then(x => this.message = x)
}

export default {
  name: 'app',
  data: () => ({
    message: ''
  }),
  mounted
}
</script>

11 行

axios.get('api/hello-world')
  .then(x => x.data)
  .then(x => this.message = x)

Vue 從原本的 http://localhost:3000/hello-world 改打 /api/hello-world

其他部分不用改變,Node 也不用改變,重新 yarn start 啟動 DevServer 即可。

cors003

Vue 也可正確收到 API 回傳值。

Chrome

$ open -n -a /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security

若不想改 Node 也不想改 Vue,也可暫時啟動沒有執行 same-origin policy 的 Chrome。

cors004

警告可暫時忽略。

Moesif Origin & CORS Changer

cors005

也可安裝 Chrome extension 解決。

cors006

在 extension 為 on 狀態下,Vue 與 Node 都不必修改程式就可避開 same-origin policy 檢查。

Conclusion

  • Same-origin policy 理論上該由後端以 CORS 處理,若暫時無法由後端解決,前端可先 workaround 用 reverse proxy,或乾脆暫時讓 Chrome 不要執行 same-origin policy

Sample Code

完整的範例可以在我的 GitHub 上找到

Reference

Vue CLI, devServer.proxy
Chimurai, http-proxy-middleware