跨域问题是怎样形成的

跨域问题的由来

相信不少人都或多或少了解过跨域问题,尤为在现现在先后端分离大行其道的时候。css

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

当你调用一个服务端接口时,极可能获得相似下面这样的一个错误:前端

request-blocked-by-cors

而后你在发送请求的地方debug,在出现异常的地方你将获得这样的结果:java

blocked-error

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

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

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

浏览器的同源策略

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

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

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

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

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

the-compose-of-domain

没有同源策略的保护

那么为何要作这个同源的限制呢?由于若是没有同源策略的保护,浏览器将没有任何安全可言。

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

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

老李什么都没想就点击了这个广告,跳转到了钓鱼的网站,却不知这真是一个 “钓鱼” 网站,老李银行帐户里面钱所有被转走了。

no-same-origin-policy

以上就是老李的钱被盗走的过程:

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

2.老李点击钓鱼网站,钓鱼网站使用老李登陆银行以后的Cookie,伪形成本身是老李进行了转帐操做。

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

那么如何防止CSRF攻击呢,能够参考这篇文章:如何防止CSRF攻击?

同源策略限制哪些行为

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

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

(1) Cookie、LocalStorage 和 IndexDB 没法读取。

(2) DOM 没法得到。

(3) 请求的响应被拦截。

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

跨域资源共享

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

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

CORS实现机制

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

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

固然了,有三个标签自己就是容许跨域加载资源的:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

好比某个网站的首页 http://domain-a.com/index.html 经过 <img src="http://domain-b.com/image.jpg" /> 来加载其余域上的图片,除此以外还有诸如经过 CDN 节点引入css和js文件的方式。

出于安全缘由,浏览器限制从脚本内发起的跨域 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-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma

若是还须要返回其余的响应头给前端,则能够经过在 Access-Control-Expose-Headers 中指定。

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

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

为何会有两种不一样类型的请求呢?

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<UserDTO> getUserInfo() {
        return userFacade.getUserInfo();
    }
}

经过CorsRegistry设置全局跨域配置

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
            .allowedOrigins("http://127.0.0.1:9528", "http://localhost:9528");
	}
}

若是你使用的是 Spring Boot,推荐的作法是只定义一个 WebMvcConfigurer 的Bean:

@Configuration
public class MyConfiguration {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                    .allowedOrigins("http://127.0.0.1:9528", "http://localhost:9528");
            }
        };
    }
}

以上两种方式在没有定义拦截器(Interceptor)的时候,使用一切正常,可是若是你有一个全局的拦截器用来检测用户的登陆态,例以下面的简易代码:

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,并添加了一个自定义的拦截器,拦截器中抛出了异常

cors-registry-1

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

cors-registry-2

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

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

cors-registry-3

这样的话请求来了以后,第一个就会为 response 设置相应的 CORS 响应头,后续若是其余自定义拦截器抛出异常,也不会有影响了。

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

经过CorsFilter设置全局跨域配置

既然经过拦截器设置全局跨域配置会有问题,那咱们还有另一种方案,经过过滤器 CorsFilter 的方式来设置,代码以下:

@Configuration
public class MyConfiguration {
	@Bean
	public FilterRegistrationBean corsFilter() {
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		CorsConfiguration config = new CorsConfiguration();
		config.setAllowCredentials(true);
		config.addAllowedOrigin("http://127.0.0.1:9528");
        config.addAllowedOrigin("http://localhost:9528");
		config.addAllowedHeader("*");
		config.addAllowedMethod("*");
		source.registerCorsConfiguration("/**", config);
		FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
		bean.setOrder(0);
		return bean;
	}
}

为何过滤器能够而拦截器不行呢?

由于过滤器依赖于 Servlet 容器,基于函数回调,它能够对几乎全部请求进行过滤。而拦截器是依赖于 Web 框架(如Spring MVC框架),基于反射经过AOP的方式实现的。

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

filter-interceptor-order

由于过滤器在触发上是先于拦截器的,可是若是有多个过滤器的话,也须要将 CorsFilter 设置为第一个过滤器才行。

参考文献

浏览器的同源策略

浏览器同源政策及其规避方法

HTTP访问控制(CORS)

跨域资源共享 CORS 详解

如何防止CSRF攻击?

逐码,专一于原创分享,用通俗易懂的图文描述源码及原理

CodeChaaaser

相关文章
相关标签/搜索