文章目录
  1. 1. 主要代码
    1. 1.1. 1. 前端ajax请求
    2. 1.2. 2. 后端代码:Test.java 模拟业务执行
    3. 1.3. 3. 后端代码 跨域支持
    4. 1.4. 4. 拦截器代码,根据日志确认是否同一个跨域请求发送了两次
  2. 2. 执行代码
  3. 3. 解释
  4. 4. 小结

跨域请求过程中,出现一个请求发送两次的现象,第一次是以OPTION方法发送的,第二次才是真正的请求。

这种现象着实令人好奇,因此将代码执行过程及分析记录下来。

主要代码

1. 前端ajax请求

前端是一个简单的ajax请求,直接使用了JQuery的ajax方法。代码如下:

<script type="text/javascript">
$.ajax({
        type: "POST",
        url: "http://localhost:8081/test",
        contentType: "application/json; charset=utf-8",
        data: JSON.stringify(GetJsonData()),
        dataType: "json",
        success: function (message) {
            if (message > 0) {
                alert("请求已提交!我们会尽快与您取得联系:" + message);
            }
        },
        error: function (message) {
            alert("提交数据失败!");
        }
    });

function GetJsonData() {
    var json = {
        "username":"snowalker",
        "passwd":"123123"
    };
    return json;
}
</script>

就是向后端发送一个post请求,请求类型为application/json.

2. 后端代码:Test.java 模拟业务执行

@RestController
@RequestMapping("/")
public class Test {

    @RequestMapping(value = "test", method = {RequestMethod.POST, RequestMethod.GET})
    public @ResponseBody Result test(HttpServletRequest request, HttpServletResponse response,
                                     @RequestBody Demo demo) {
        Result<Demo> result = new Result<>();
        if (demo == null) {
            result.setData(null);
            result.setResult(false);
        } else {
            result.setResult(true);
            result.setData(demo);
        }
        return result;
    }

}
class Result<T> {
    private boolean result;
    private T data;

    public boolean isResult() {
        return result;
    }

    public void setResult(boolean result) {
        this.result = result;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
class Demo {
    private String username;
    private String passwd;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
}

模拟业务执行过程,解析前端传输的参数并进行回显。接收的参数类型为application/json,回显参数类型也是application/json.

3. 后端代码 跨域支持

由于是ajax请求,真实环境前后端分离时为跨域执行的,因此在后端添加跨域支持

public class Bootstrap extends WebMvcConfigurerAdapter {

    ......部分代码省略.....

    /**
     * 注册请求id过滤器
     * @return
     */
    @Bean
    public FilterRegistrationBean requestFilter() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new RequestFilter());
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RequestInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                System.out.println("跨域支持");
                registry.addMapping("/**")
                        .allowedMethods("*")
                        .allowedOrigins("*")
                        .allowedHeaders("*");
            }
        };
    }
}

由于是demo,直接放行所有HTTP请求方法,支持所有路径跨域。

4. 拦截器代码,根据日志确认是否同一个跨域请求发送了两次

public class RequestFilter implements Filter {

    private static final Logger LOGGER = LoggerFactory.getLogger(RequestFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String requestId = UUID.randomUUID().toString().replace("-", "");
        LOGGER.info("[RequestFilter]请求拦截成功,为线程添加请求id开始,requestId={},requestUrl={},requestMethod={}",
                requestId, request.getServletPath(), request.getMethod());
        RequestHolder.add(requestId);
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

这里主要对请求进行拦截并输出HTTP方法名称,用于确定具体请求的HTTP方法,打印日志并放行请求。

执行代码

对上述代码进行执行,截图如下

option请求

post请求

可以看到先发送了option请求之后再发起的post请求,执行了真正的业务逻辑。

解释

那么为什么会出现这种现象呢,传统的请求不都是一次性执行成功的吗?为何这种跨域请求会执行两次呢?

其实这里是因为浏览器对简单跨域请求和复杂跨域请求的处理区别。

XMLHttpRequest会遵守同源策略(same-origin policy). 也即脚本只能访问相同协议/相同主机名/相同端口的资源, 如果要突破这个限制, 那就是所谓的跨域, 此时需要遵守CORS(Cross-Origin Resource Sharing)机制。

那么, 允许跨域, 不就是服务端设置Access-Control-Allow-Origin: *就可以了吗? 普通的请求才是这样子的, 除此之外, 还一种叫请求叫preflighted request。

preflighted request 在发送真正的请求前, 会先发送一个方法为OPTIONS的预请求(preflight request), 用于试探服务端是否能接受真正的请求,如果options获得的回应是拒绝性质的,比如404\403\500等http状态,就会停止post、put等请求的发出。

这里对Option请求简单介绍下:

OPTIONS请求方法的主要用途有两个:

1、获取服务器支持的HTTP请求方法;也是黑客经常使用的方法。

2、用来检查服务器的性能。例如:AJAX进行跨域请求时的预检,
需要向另外一个域名的资源发送一个HTTP OPTIONS请求头,
用以判断实际发送的请求是否安全。

总之,OPTIONS请求相当于一个检测目标是否安全的操作,类似于心跳机制。
所以我们在后台拦截器里面应该把这个请求过滤掉。

看看官方是怎样解释的吧。

It uses methods other than GET, HEAD or POST. 
Also, if POST is used to send request data with a Content-Type other than 
application/x-www-form-urlencoded, multipart/form-data, ortext/plain, e.g. 

if the POST request sends an XML payload to the server using application/xmlor text/xml, 
then the request is preflighted.

It sets custom headers in the request (e.g. the request uses a header such as X-PINGOTHER)

中文的解释小结如下,一个请求会变成preflighted request的原因为如下三个原因

1、请求方法不是GET/HEAD/POST,也就是请求不是上述三种简单请求之一的复杂请求;

2、POST请求的Content-Type并非application/x-www-form-urlencoded, 
multipart/form-data, 或text/plain

3、请求设置了自定义的header字段,即除了浏览器在请求头上增加的信息(如Connection,User-Agent等),
开发者仅可以增加Accept,Accept-Language,Content-Type等;

回到我们的例子,由于我的请求类型为application/json,是复杂请求。因此在发出复杂请求的之前,就会出现一次OPTIONS请求。

小结

对于非简单请求的跨源请求,浏览器会在真实请求发出前,增加一次 OPTION 请求,称为预检请求(preflight request)。

预检请求将真实请求的信息,包括请求方法、自定义头字段、源信息添加到 HTTP 头信息字段中,询问服务器是否允许这样的操作。

对于预检请求我们可以采取策略进行放行或者拦截,这取决于具体的业务逻辑。

通过本文的说明,之后遇到复杂请求发送了包含OPTION的两次请求的情况就不会再疑惑了。感兴趣的小伙伴也可以根据本文给出的例子
自己搭建一个demo进行试验,相信实践之后更能加深你的理解。

文章目录
  1. 1. 主要代码
    1. 1.1. 1. 前端ajax请求
    2. 1.2. 2. 后端代码:Test.java 模拟业务执行
    3. 1.3. 3. 后端代码 跨域支持
    4. 1.4. 4. 拦截器代码,根据日志确认是否同一个跨域请求发送了两次
  2. 2. 执行代码
  3. 3. 解释
  4. 4. 小结
Fork me on GitHub