跨域问题的产生&解决方案

2023年 7月 18日 50.6k 0

跨域报错信息

跨域问题分为两种:

(1) 请求直接被浏览器拦截

(2) 请求到了后台服务

请求直接被浏览器拦截

报错信息:

The request client is not a secure context and the resource is in more-private address space `private` 
“Failed to load resource: net::ERR_FAILED” 这个错误提示意味着请求在
离开浏览器时由于浏览器设置了 CORS 限制而被拦截。在浏览器请求的时候,如
果发现请求的是跨域资源,浏览器会检查服务器响应的 CORS 头部信息是否允许
本次跨域请求。如果不允许,浏览器就会拦截该请求,并显示这个错误信息。

报错原因

在web安全方面,存在一个叫做“安全上下文”的限制,这个限制规定了哪些环境下的请求才是安全的,简单来说,只有在HTTPS下的页面才是安全的,此时浏览器才会信任这个页面,并允许这个页面的脚本访问其他资源。

如果在一个HTTP的前端页面中向受保护且位于更私有地址空间的后端资源发送请求(例如本地网络或者局域网内的服务器 - 这些都是根据DNS解析到的ip来判断的),由于页面不符合安全上下文的要求,浏览器会出现类似于上面的报错

解决方案:

  • 使用合适的协议
    浏览器对于非安全的http://请求可能会直接拦截,尝试将前端应用程序迁移到使用安全的https://协议来发送请求,以避免被拦截
  • 配置前端代理
    在前端服务器配置代理(nginx),进行请求转发,避免被浏览器拦截
  • JSONP发送请求
    如果目标服务器支持JSONP,可以尝试使用JSONP发送跨域请求
  • WebSocket
    如果跨域请求是为了实现实时通信或长连接等特殊需求,可以考虑使用webSocket,webSocket建立与服务器的长期连接,不收同源策略的限制
  • 将服务部署到合适的服务器上
    基于错误产生的原因,可以将服务部署到不会产生这类问题的网络环境中
  • 请求到了后台服务

    在涉及到CORS的时候,浏览器的报错信息通常有以下几种:

    Response to preflight request doesn't pass access control check:
     No 'Access-Control-Allow-Origin' header is present on the 
    requested resource.:这是因为请求没有通过预检请求的校验,
    此时服务器没有设置跨域访问允许(Access-Control-Allow-Origin)头。
    
    Response to preflight request doesn't pass access control check:
     The 'Access-Control-Allow-Origin' header has a value 'xxx' that
     is not equal to the supplied origin. Origin 'xxx' is therefore
     not allowed access.:这是因为响应设置了允许跨域访问的头,但是请求的
    来源和允许的来源不一致,导致请求被拒绝。
    
    Cross-Origin Read Blocking (CORB) blocked cross-origin response
     xxx with MIME type xxx.:这是因为浏览器的安全策略检测到响应的 MIME
     类型不在浏览器预定义的类型范围内而出现的提示,可以通过设置新的 MIME
     类型进行解决。
    
    Cross-Origin Read Blocking (CORB) blocked cross-origin response
     xxx with X-Content-Type-Options: nosniff.:这是因为浏览器的安全策
    略检测到响应的 MIME 类型与响应头中设置的 MIME 类型不一致,可以通过设置
    新的 MIME 类型进行解决。
    
    Blocked by CORS policy: Request header field xxx is not allowed
     by Access-Control-Allow-Headers in preflight response.:这是因为
    请求头中包含了服务器不支持的参数,可以通过添加新的允许参数进行解决。
    

    跨域请求流程

    对于跨域请求,浏览器首先会发送一个OPTIONS 请求,用来判断后台服务器是否支持前端的域名进行跨域请求

    跨域问题的由来

    相信很多人都或多或少了解过跨域问题,尤其在现如今前后端分离大行其道的时候。

    你在本地开发一个前端项目,这个项目是通过 node 运行的,端口是9528,而服务端是通过 spring boot 提供的,端口号是7001。

    当你调用一个服务端接口时,很可能得到类似下面这样的一个错误:

    image.png
    然后你在发送请求的地方debug,在出现异常的地方你将得到这样的结果:

    image.png

    异常对象很诡异,返回的 response 是 undefined 的,并且 message 消息中只有一个"Network Error"。

    看到这里你应该要知道,你遇到跨域问题了。

    但是你需要明确的一点是,这个请求已经发出去了,服务端也接收到并处理了,但是返回的响应结果不是浏览器想要的结果,所以浏览器将这个响应的结果给拦截了,这就是为什么你看到的response是undefined。

    浏览器的同源策略

    那浏览器为什么会将服务端返回的结果拦截掉呢?

    这就需要我们了解浏览器基于安全方面的考虑,而引入的 同源策略(same-origin policy) 了。

    早在1995年,Netscape 公司就在浏览器中引入了“同源策略”。

    最初的 “同源策略”,主要是限制Cookie的访问,A网页设置的 Cookie,B网页无法访问,除非B网页和A网页是“同源”的。

    那么怎么确定两个网页是不是“同源”呢,所谓“同源”就是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
    image.png

    没有同源策略的保护

    那么为什么要做这个同源的限制呢?因为如果没有同源策略的保护,浏览器将没有任何安全可言。

    老李是一个钓鱼爱好者,经常在 我要买(51mai.com) 的网站上买各种钓鱼的工具,并且通过 银行(yinhang.com) 以账号密码的方式直接支付。

    这天老李又在 51mai.com 上买了一根鱼竿,输入银行账号密码支付成功后,在支付成功页看到一个叫 钓鱼(diaoyu.com) 的网站投放的一个"免费领取鱼饵"的广告。

    老李什么都没想就点击了这个广告,跳转到了钓鱼的网站,殊不知这真是一个 “钓鱼” 网站,老李银行账户里面钱全部被转走了。
    image.png
    以上就是老李的钱被盗走的过程:

    1.老李购买鱼竿,并登录了银行的网站输入账号密码进行了支付,浏览器在本地缓存了银行的Cookie

    2.老李点击钓鱼网站,钓鱼网站使用老李登录银行之后的Cookie,伪造成自己是老李进行了转账操作。

    这个过程就是著名的CSRF(Cross Site Request Forgery),跨站请求伪造,正是由于可能存在的伪造请求,导致了浏览器的不安全。

    同源策略限制哪些行为

    上面说了 **同源策略 **是一个安全机制,他本质是限制了从一个源加载的文档或脚本如何与来自另一个源的资源进行交互,这是一个用于隔离潜在恶意文件的重要安全机制。

    随着互联网的发展,"同源策略"越来越严格,不仅限于Cookie的读取。目前,如果非同源,共有三种行为受到限制。

    (1) Cookie、LocalStorage 和 IndexDB 无法读取。
    (2) DOM 无法获得。
    (3) 请求的响应被拦截。
    

    虽然这些限制是必要的,但是有时很不方便,合理的用途也会受到影响,所以为了能够获取非“同源”的资源,就有了跨域资源共享。

    跨域资源共享

    看到这里你应该明白,为什么文章开头的请求会被拦截了,原因就是请求的源和服务端的源不是“同源”,而服务端又没有设置允许的跨域资源共享,所以请求的响应被浏览器给拦截掉了。

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

    CORS实现机制

    那跨域资源共享机制是怎样实现的呢?

    当一个资源(origin)通过脚本向另一个资源(host)发起请求,而被请求的资源(host)和请求源(origin)是不同的源时(协议、域名、端口不全部相同),浏览器就会发起一个 跨域 HTTP 请求 ,并且浏览器会自动将当前资源的域添加在请求头中一个叫 Origin 的 Header 中。

    当然了,有三个标签本身就是允许跨域加载资源的:

    • img
    • link
    • script

    出于安全原因,浏览器限制从脚本内发起的跨域 HTTP 请求。 例如,XMLHttpRequest 和 Fetch API 遵循同源策略。 也就是说使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文中包含了正确 CORS 响应头。

    通过在响应报文中设置额外的 HTTP 响应头来告诉浏览器,运行在某个 origin 上的 Web 应用被准许访问来自不同源服务器上的资源,此时浏览器就不会将该响应拦截掉了。

    那这些额外的 HTTP 响应头是什么呢?

    响应头是否必须含义
    Access-Control-Allow-Origin是该字段表示,服务端接收哪些来源的域的请求
    Access-Control-Allow-Credentials否是否可以向服务端发送Cookie,默认是 false
    Access-Control-Expose-Headers否可以向请求额外暴露的响应头

    其中只有
    Access-Control-Allow-Origin 是必须的,该响应头的值可以是请求的 Origin 的值,也可以是 * ,表示服务端接收所有来源的请求。

    当浏览器发起 CORS 请求时,默认只能获得6个响应头的值:

    Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma

    如果还需要返回其他的响应头给前端,则可以通过在
    Access-Control-Expose-Headers 中指定。

    CORS的两种请求类型

    CORS有两种类型的请求,分别是:简单请求(simple request)和非简单请求(not-so-simple request)

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

    • (1) 请求方法是以下三种方法之一:
      HEAD GET POST
    • (2) HTTP的头信息不超出以下几种字段:
      AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:只限于三个值
      application/x-www-form-urlencoded 、multipart/form-data、text/plain

    凡是不同时满足上面两个条件,就属于非简单请求,浏览器对这两种请求的处理,是不一样的。

    为什么会有两种不同类型的请求呢?

    CORS 规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。

    服务器确认允许之后,浏览器才能发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关的数据)。

    非简单请求就要求浏览器先发送一个预检请求,预检通过后再发送实际的请求。

    怎样实现CORS

    知道了CORS的实现机制之后,我们就可以解决遇到的CORS的问题了。

    1. 通过JSONP

    利用 "script" 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。

    JSONP 和 AJAX 相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但 AJAX 属于同源策略,JSONP 属于非同源策略(支持跨域请求)。JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持 GET 方法具有局限性,不安全可能会遭受XSS攻击。

    2. 利用反向代理服务器

    同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略
    所以通过反向代理服务器可以有效的解决跨域问题,代理服务器需要做以下几个步骤:

    1.接受客户端的请求

    2.将请求转发给实际的服务器

    3.将服务器的响应结果返回给客户端

    Nginx就是类似的反向代理服务器,可以通过配置Nginx代理来解决跨域问题。

    3.服务端支持CORS

    最安全的还是服务端来设置允许哪些来源的请求,即服务端在接收到请求之后,对允许的请求源设置
    Access-Control-Allow-Origin 的响应头。

    通过@CrossOrigin注解

    这里以 Spring Boot 为例,可以通过 @CrossOrigin 注解来指定哪些类或者方法支持跨越,如下列代码所示:

    /**
     * 在类上加注解
     */
    @CrossOrigin({"http://127.0.0.1:9528", "http://localhost:9528"})
    @RestController
    public class UserController {
        
    }
    
    @RestController
    public class UserController {
        @Resource
        private UserFacade userFacade;
        /**
         * 在方法上加注解
         */
        @GetMapping(ApiConstant.Urls.GET_USER_INFO)
        @CrossOrigin({"http://127.0.0.1:9528", "http://localhost:9528"})
        public PojoResult getUserInfo() {
            return userFacade.getUserInfo();
        }
    }
    

    通过Filter设置全局跨域配置

    import org.springframework.core.Ordered;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.*;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Component
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class CosFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {
            HttpServletResponse response = (HttpServletResponse) res;
            HttpServletRequest reqs = (HttpServletRequest) req;
            response.setHeader("Access-Control-Allow-Origin", reqs.getHeader("Origin"));
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
            response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type,X-Requested-With");
            response.setHeader("Access-Control-Max-Age", "3600");
            if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
                response.setStatus(HttpServletResponse.SC_OK);
            } else {
                filterChain.doFilter(req, res);
            }
        }
    
        @Override
        public void destroy() {
    
        }
    }
    

    通过实现WebMvcConfigurer

    @Slf4j
    @Configuration
    @EnableWebMvc
    public class VendorWebMvcConfigurer implements WebMvcConfigurer {
    
    
        /**
         * 跨域配置
         */
        @Autowired
        private CorsConfig corsConfig;
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
       
            registry.addMapping("/**")
                    .allowedOrigins(corsConfig.getOrigin().split(","))
                    .allowedMethods(corsConfig.getMethod().split(","))
                    .allowedHeaders("*")
                    .allowCredentials(true)
                    .maxAge(corsConfig.getMaxAge());
        }
    
    }
    

    最好是使用过滤器而非拦截器进行CORS设置,如果你有一个全局的拦截器用来检测用户的登录态,例如下面的简易代码:

    public class AuthenticationInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
            // 从 http 请求头中取出 token
            String token = httpServletRequest.getHeader("token");
            // 检查是否登录
            if (token == null) {
                throw new InvalidTokenException(ResultCode.INVALID_TOKEN.getCode(), "登录态失效,请重新登录");
            }
            return true;
        }
    }
    

    当自定义拦截器返回true时,一切正常,但是当拦截器抛出异常(或者返回false)时,后续的CORS设置将不会生效。

    为什么拦截器抛出异常时,CORS不生效呢?可以看下这个issue:

    when interceptor preHandler throw exception, the cors is broken

    有个人提交了一个issue,说明如果在自定义拦截器的preHandler方法中抛出异常的话,通过 CorsRegistry 设置的全局 CORS 配置就失效了,但是Spring Boot 的成员不认为这是一个Bug。

    然后提交者举了个具体的例子:

    他先定义了CorsRegistry,并添加了一个自定义的拦截器,拦截器中抛出了异常

    image.png

    然后他发现AbstractHandlerMapping在添加CorsInterceptor的时候,是将 Cors 的拦截器加在拦截器链的最后:

    image.png

    那就会造成上面说的问题,在自定义拦截器中抛出异常之后,CorsInterceptor 拦截器就没有机会执行向 response 中设置 CORS 相关响应头了。

    issue的提交者也给出了解决的方案,就是将用来处理 Cors 的拦截器 CorsInterceptor 加在拦截器链的第一个位置:

    image.png
    这样的话请求来了之后,第一个就会为 response 设置相应的 CORS 响应头,后续如果其他自定义拦截器抛出异常,也不会有影响了。

    感觉是一个可行的解决方案,但是 Spring Boot 的成员认为这不是 Spring Boot 的Bug,而是 Spring Framework 的 Bug,所以将这个issue关闭了。

    所以推荐使用Filter并且将cors的优先级提升到最高

    因为过滤器依赖于 Servlet 容器,基于函数回调,它可以对几乎所有请求进行过滤。而拦截器是依赖于 Web 框架(如Spring MVC框架),基于反射通过AOP的方式实现的。

    在触发顺序上如下图所示:

    image.png
    因为过滤器在触发上是先于拦截器的,但是如果有多个过滤器的话,也需要将 CorsFilter 设置为第一个过滤器才行

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论