HTTP跨域请求OPTION解析
跨域请求过程中,出现一个请求发送两次的现象,第一次是以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请求,执行了真正的业务逻辑。
解释
那么为什么会出现这种现象呢,传统的请求不都是一次性执行成功的吗?为何这种跨域请求会执行两次呢?
其实这里是因为浏览器对简单跨域请求和复杂跨域请求的处理区别。
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进行试验,相信实践之后更能加深你的理解。