跨域问题

142 阅读5分钟

什么是跨域?

跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。

例如:a页面想获取b页面资源,如果a、b页面的协议、域名、端口、子域名不同,所进行的访问行动都是跨域的,而浏览器为了安全问题一般都限制了跨域访问,也就是不允许跨域请求资源。注意:跨域限制访问,其实是浏览器的限制。理解这一点很重要!!!

同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;

解决方案

网上一大堆解决方案:比如jsonp,cors,iframe等等,这里不展开讨论了,我这里主要从实际项目出发,把其中遇到的跨域问题以及解决方案分享出来,希望能给遇到同样问题的同学一点启发;另外多说一句,跨域绝对不是那么简单的设置响应头就解决所有问题,实际项目遇到的问题可能会很复杂。

问题

本项目是基于vue-cli3.0创建的,使用了axios作为发送http请求的工具,对于axios,也参照网上的做法,封装了一个统一的http工具,里面封装了get和post方法,http.js

    // 引入axios
import axios from 'axios'; 
import store from '@/store/index';
import qs from 'qs';
// 环境的切换
if (process.env.NODE_ENV == 'development') {    
    axios.defaults.baseURL = 'http://127.0.0.1:3000/';} 
else if (process.env.NODE_ENV == 'debug') {    
    axios.defaults.baseURL = 'https://www.ceshi.com';
} 
else if (process.env.NODE_ENV == 'production') {    
    axios.defaults.baseURL = 'https://www.production.com';
}
axios.defaults.timeout = 10000;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
axios.interceptors.request.use(    
    config => {        
        // 每次发送请求之前判断vuex中是否存在token        
        // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况
        // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断 
        /*const token = store.state.token;        
        token && (config.headers.Authorization = token);  */   
        return config;    
    },    
    error => {        
        return Promise.error(error);    
});

function checkStatus (response) {
  // loading
  // 如果http状态码正常,则直接返回数据
  if (response && (response.status === 200 || response.status === 304 || response.status === 400)) {
    return response.data
    // 如果不需要除了data之外的数据,可以直接 return response.data
  }
  // 异常状态下,把错误信息返回去
  return {
    status: -404,
    msg: '网络异常'
  }
}

function checkCode (res) {
  // 如果code异常(这里已经包括网络错误,服务器错误,后端抛出的错误),可以弹出一个错误提示,告诉用户
  if (res.status === -404) {
    alert(res.msg)
  }
  if (res.data && (!res.data.success)) {
    alert(res.data.error_msg)
  }
  return res
}

export default {
  post (url, data) {
    return axios({
      method: 'post',
      url,
      data: qs.stringify(data),
      timeout: 10000,
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'Content-Type': 'application/x-www-form-urlencoded;'
      }
    }).then(
      (response) => {
        return checkStatus(response)
      }
    ).then(
      (res) => {
        return checkCode(res)
      }
    )
  },
  get (url, params) {
    return axios({
      method: 'get',
      url,
      params, // get 请求时带的参数
      timeout: 10000,
      headers: {
        'X-Requested-With': 'XMLHttpRequest'
      }
    }).then(
      (response) => {
        return checkStatus(response)
      }
    ).then(
      (res) => {
        return checkCode(res)
      }
    )
  }
}

下面是api.js

import http from './http';
//登录接口
export const login = (param) =>http.post('/test',param);

然后就可以调用了

    login(){
        var that = this;
        login({username:'test'}).then(function(data){
            that.saveUserName(data.name);
            that.saveToken(data.token);
            that.$router.push('home');
        })
    },

结果可想而知,

浏览器报错,翻译过来就是:跨域了,没有Access-Control-Allow-Origin这个头。由于我的后台是koa,我直接在服务端加上这个头

ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With');
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');

然后重启服务,再调一次,发现还是报同样的错,这就有点儿颠覆常识了,跟网上说的不一样啊!再仔细看浏览器报错信息,怎么有个options请求报404呢?

经过多番查找,翻阅网上资料,主要是这篇官方文档 developer.mozilla.org/zh-CN/docs/… 总结起来就是:对于简单请求不会触发cors的预检请求,非简单请求会触发预检请求

预检请求:“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

简单请求:若请求满足所有下述条件,则该请求可视为“简单请求”

    1. 使用下列方法之一: GET HEAD POST
    1. Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为: Accept Accept-Language Content-Language Content-Type (需要注意额外的限制) DPR Downlink Save-Data Viewport-Width Width
  • 3.Content-Type 的值仅限于下列三者之一: text/plain multipart/form-data application/x-www-form-urlencoded
  • 4.请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
  • 5.请求中没有使用 ReadableStream 对象。

也就是要同时满足上面五点条件,这个请求就是简单请求,否则是非简单请求;由于我们在封装http.js的时候在请求头里加了这样一个头:'X-Requested-With': 'XMLHttpRequest',这个头不属于 Accept Accept-Language Content-Language Content-Type DPR Downlink Save-Data Viewport-Width Width这其中的,因此,这个请求属于非简单请求,浏览器会发送预检请求,即options请求,而我们的后台没有这个接口,因此报了一个404

好了,问题找到了,解决起来也就有了思路了,我们有两种方案来解决

  • 1.前端解决:改变我们的请求头,使其成为一个简单请求,在这个案例中,去掉'X-Requested-With': 'XMLHttpRequest',再调一次,发现没问题了,问题解决,而且也没有那个options请求了
  • 2如果非要在请求头里保留那个字段呢,这个时候就要让后台来解决了,刚刚报错的信息里说的是:没有method为post,/test这个接口,那我们就加上一个接口就行了,但是我们不可能一个一个的加吧。怎么办呢?我们可不可以让所有的options请求都直接响应200呢?这样貌似可以,于是我们来加上这样一段代码
app.use(async (ctx, next)=> {
    ctx.set('Access-Control-Allow-Origin', '*');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
    ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    if (ctx.method == 'OPTIONS') {
      ctx.body = 200; 
    } else {
      await next();
    }
});

重启服务,再调一次,发现成功了,而且post请求之前有个options请求

到此,本次跨域问题终于解决完了,并没有以前想象中那么简单啊!