axios 发送两次请求原因及解决方法

出现问题

最近 Vue 项目中使用 axios 组件,在页面交互中发现 axios 会发送两次请求,一种请求方式为 OPTIONS,另外一种为自己设置的(如 GET、POST 等等)。

如下图所示,所有请求都发送了两次。

image-20210112152514664

仔细查看发现,其中第一次是 OPTIONS 请求,且没有携带 Authorization 信息:

image-20210112152255588

第二次才是正常的 GET 请求,正常携带了 Authorization 信息:

image-20210112152315002

CORS 通信

CORS 是一个 W3C 标准,全称是 "跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。

实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨域通信。

CORS 两种请求

浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

简单请求

只要同时满足以下两大条件,就属于简单请求。

  1. 请求方法是以下三种方法之一:
    • HEAD
    • GET
    • POST
  2. HTTP 的头信息不超出以下几种字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain

非简单请求

凡是不同时满足上面两个条件,就属于非简单请求。

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUTDELETE,或者 Content-Type 字段的类型是 application/json

非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 "预检" 请求(preflight)。

浏览器处理

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

浏览器对这两种请求的处理,是不一样的。

两次请求原因

前后端未满足 “同源策略 / SOP”,俗称请求跨域。

浏览器一旦发现请求跨域,就会使用 CORS 通信,自动添加一些附加的头信息,简单请求只会有一次请求,只有非简单请求会附加一次请求。

解决方法

服务期端直接通过 “预检” 请求,服务器新建拦截器,拦截所有请求,筛选所有 Requset Method:OPTIONS 的请求,不做任何处理直接返回即可。

Java JWT

package com.yhzy.zytx.jwt.Interceptor;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * @ClassName JwtInterceptor
 * @Description JWT拦截器
 */
public class JwtInterceptor implements HandlerInterceptor {
    

    private final static Logger logger = LoggerFactory.getLogger(JwtInterceptor.class);

    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        // 从 http 请求头中取出 token
        String token = httpServletRequest.getHeader("Authorization");

        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        Method method=handlerMethod.getMethod();

        // OPTIONS请求类型直接返回不处理
        if (RequestMethod.OPTIONS.name().equals(httpServletRequest.getMethod())){
            return false;
        }
        
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest,
                           HttpServletResponse httpServletResponse,
                           Object o, ModelAndView modelAndView) throws Exception {

    }
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse,
                                Object o, Exception e) throws Exception {
    }
}