为什么出现 OPTIONS?SpringBoot 接口跨域解决方案

本贴最后更新于 1996 天前,其中的信息可能已经天翻地覆

原文地址:https://xeblog.cn/articles/12

引言

前后端分离的项目虽然降低了耦合度,但是引发的各种问题也随之而来。后端项目由 Tomcat 部署(监听 8080 端口),前端项目则部署在 Nginx 上(监听 80、443 等非 8080 端口),前端页面加载速度大大提高了,而当 ajax 请求后端接口的时候却报错了。

image

同源策略

同源策略,它是由 Netscape 提出的一个著名的安全策略。现在所有支持 JavaScript 的浏览器都会使用这个策略。所谓同源是指,域名,协议,端口相同。

前端地址:
http://127.0.0.1:63344

后端地址:
http://127.0.0.1:8080

这两个地址虽然 ip 地址和协议都一样,但是端口不一样,所以并不满足同源,这样就造成了跨域的问题。

解决方案

配置 addCorsMappings

新增一个类实现 WebMvcConfigurer 接口,然后给这个类加上 @Configuration 注解,最后实现 addCorsMappings 方法就 ok 了。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
	@Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
                .maxAge(3600)
                .allowCredentials(true);
    }
}

请求正常

image

这就完事了?这么简单的吗?

登陆后台管理看看

image

wtf?

image

为何是 OPTIONS 请求?

按照我的逻辑,上面那个报错的接口应该是一个 GET 请求,可为什么发的是 OPTIONS 请求?翻阅各种资料,各种百度 google,各种倒腾,各种头疼,最后。。。我掀开了被子,惊奇的发现。。。被窝可真暖和啊!(先睡一觉再说)

image

image

原因

原来浏览器会在发送真正请求之前,先发送一个方法为 OPTIONS 的预检请求 Preflighted requests 这个请求是用来验证本次请求是否安全的,而且并不是所有请求都会发送,需要符合以下条件:

  • 请求方法不是 GET/HEAD/POST
  • POST 请求的 Content-Type 并非 application/x-www-form-urlencoded, multipart/form-data, 或 text/plain
  • 请求设置了自定义的 header 字段

对于管理端的接口,我有对接口进行权限校验,每次请求需要在 header 中携带自定义的字段(token),所以浏览器会多发送一个 OPTIONS 请求。

那为什么 OPTIONS 请求报错了。。。

经过 debug 发现,OPTIONS 请求只会携带自定义的字段,并不会将相应的值带入进去,而后台校验 token 字段时 token 为 NULL,所以验证不通过,抛出了一个异常。

image

放行 OPTIONS

我换了一种解决跨域的方法,使用拦截器解决跨域问题,并且针对 OPTIONS 请求做放行处理。

新增一个拦截器类 CorsInterceptor 实现 HandlerInterceptor 接口

@Component
public class CorsInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
        response.setHeader("Access-Control-Max-Age", "86400");
        response.setHeader("Access-Control-Allow-Headers", "*");

        // 如果是OPTIONS则结束请求
        if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
            response.setStatus(HttpStatus.NO_CONTENT.value());
            return false;
        }

        return true;
    }
}

需将跨域拦截器放在校验拦截器之上

我把原来的跨域配置 addCorsMappings 删除了

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Resource
    private LoginInterceptor loginInterceptor;
    @Resource
    private CorsInterceptor corsInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
		// 跨域拦截器需放在最上面
        registry.addInterceptor(corsInterceptor).addPathPatterns("/**");
		// 校验token的拦截器
        registry.addInterceptor(loginInterceptor).addPathPatterns("/admin/**");
    }

}

OPTIOINS 请求没问题了

image

也相继发送了 GET 请求

image

总结

晚安!

image

补充说明

Access-Control-Allow-Credentials 响应头表示是否可以将对请求的响应暴露给页面。返回 true 则可以,其他值均不可以。Credentials 可以是 cookies, authorization headersTLS client certificates。如果被设置为 true,服务器不得设置 Access-Control-Allow-Origin 的值为 *,需要指定域名,否则当需要发送 Cookie 到服务器时,浏览器响应时将会报错。

测试

Ajax 请求开启 withCredentials 属性

xhrFields: {
	withCredentials: true
}

后端跨域配置不变时,浏览器请求结果报错

image

指定 Access-Control-Allow-Origin 的值为 http://127.0.0.1:63344,再次请求后正常

image

最终配置

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
		// 此处配置的是允许任意域名跨域请求,可根据需求指定
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
        response.setHeader("Access-Control-Max-Age", "86400");
        response.setHeader("Access-Control-Allow-Headers", "*");

        // 如果是OPTIONS则结束请求
        if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
            response.setStatus(HttpStatus.NO_CONTENT.value());
            return false;
        }

        return true;
    }

参考

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3186 引用 • 8212 回帖
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    942 引用 • 1459 回帖 • 31 关注
  • 跨域
    18 引用 • 95 回帖
2 操作
anlingyi 在 2019-05-19 12:41:09 更新了该帖
anlingyi 在 2019-05-19 12:38:59 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • lizhongyue248 1 赞同

    哈哈哈哈,早上看着楼主这些表情包忍不住想笑。一看楼主就是个逗比 😅

    1 回复
  • anlingyi
    作者

    好眼力trollface