跨域问题详解

201 阅读10分钟

本文已参与「博主入驻有礼」活动, 一起开启掘金创作之路。

本文从web开发者角度,浅谈跨域原理,总结处理方法。

为什么会有跨域问题?

简单来说,浏览器不允许访问除当前页面所在源之外的其他源。

协议、域名、端口组成同一源(origin)

在前后端不分离的单体应用中,我们访问的前端页面和他的后端接口通常处于同一个端口下,因此可以直接访问;

大多数开发者第一次接触跨域请求应该是在学习前后端分离的B/S应用时。

即由于浏览器的安全性限制,不允许 AJAX 访问协议不同、域名不同、端口号不同的数据接口,否则会出报 No 'Access-Control-Allow-Origin' header is present on the requested resource.错误。它是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。

同源策略控制不同源之间的交互,例如在使用XMLHttpRequest<img>标签时则会受到同源策略的约束。通常允许跨源写和资源嵌入(如<img>嵌入其他源的图片),但不允许跨源读。

简单来说,浏览器会禁止当前访问的页面请求其他源的资源。我们可以认为当前源是安全的(默认当前源是用户主动要访问的),但此时由程序发起请求访问的其他源却不是用户主动进行的行为,而这可能存在风险。

不论是请求的源还是响应的源,都不欢迎跨域。

用户的cookies信息只在当前源下有用,同源策略可以阻止一个页面上的恶意脚本通过页面的DOM对象获得访问另一个页面上敏感信息的权限,如获取cookie。

如图,像cookie中存储了当前源的信息。如果当前文档随意访问其他源的资源并带回来了不好的东西(脚本之类),会有危险。

image-20220316214643361

当我们访问了一个恶意网站 如果没有同源策略 那么这个网站就能通过js 访问document.cookie 得到用户关于的各个网站的sessionID ,可以对服务器发送CSRF攻击。

api跨域:

有些公共Api也没有设置跨域访问。此时有三个源:我们访问的前端项目、后端项目、公共api。可以通过代理的方式,让浏览器(一直在访问前端)一直访问代理,代理做跨域配置

虽然一直在访问nginx,但是代理服务器如果只做转发的话,http请求中的数据没有动,浏览器解析时还是会当作跨域请求(检查header?)

使用 CORS(跨资源共享)解决跨域问题

之前我们讨论了“为什么浏览器要阻止跨域”,但事实上很多应用都需要跨域来实现功能。因此现在大多浏览器都支持CORS,只需设置服务端也支持CORS即可。而支持CORS使浏览器请求某个资源的过程多了几个步骤。

原理浅析

CORS 是一个 W3C 标准,全称是"跨源(域)资源共享"(Cross-origin resource sharing)。它由一系列传输的HTTP头(CORS头,包含于HTTP头之中)组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。

  • Ajax基于XMLHttpRequset对象实现的,而各种请求库又是对Ajax技术的实现、封装
  • Fetch API
  • Failed to fetch version info for CodeFarmer1999/img_cloud.
    

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于前端开发者来说,CORS 通信与同源的 平时没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

跨源资源共享(CORS) - HTTP | MDN (mozilla.org)

简单来说,现在的浏览器允许发出跨院请求对于收到的response,

CORS有两种处理方式,一种是只在请求头和响应头增加CORS头的信息,另一种是发送预检请求

  • 增加Header信息(示例中省去部分请求头和相应头信息)

    在简单模式下的跨域请求(详情见官方文档)不需要发送预检请求,只需单纯的增加Header信息即可。

    满足简单请求的3个条件:

    • 请求方法为:GET/POST/HEAD 之一;
    • 当请求方法为POST时,Content-Type是application/x-www-form-urlencoded,multipart/form-data或text/plain之一;
    • 没有自定义请求头;
  • 不使用cookie;

    以下是浏览器发送给服务器的请求报文:

    GET /resources/public-data/ HTTP/1.1
    Host: bar.other
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Origin: https://foo.example
    

    请求首部字段 Origin 表明该请求来源于 http://foo.example

    HTTP/1.1 200 OK
    Date: Mon, 01 Dec 2008 00:23:53 GMT
    Server: Apache/2
    Access-Control-Allow-Origin: *
    
    [XML Data]
    
    
    本例中,服务端返回的 `Access-Control-Allow-Origin: *` 表明,该资源可以被 任意 外域访问。
    
    `Access-Control-Allow-Origin` 响应头指定了该响应的资源是否被允许与给定的origin共享。
    
-   **发送预检请求 Preflight request**
​
    一个 CORS 预检请求是用于检查服务器是否支持 [CORS](https://developer.mozilla.org/zh-CN/docs/Glossary/CORS) 即跨域资源共享。
​
    它一般是用了以下几个 HTTP 请求首部的 [`OPTIONS`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/OPTIONS) 请求:[`Access-Control-Request-Method`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Request-Method) 和 [`Access-Control-Request-Headers`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Request-Headers),以及一个 [`Origin`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Origin) 首部。
​
    当有必要的时候,浏览器会自动发出一个预检请求;所以在正常情况下,前端开发者不需要自己去发这样的请求。
​
    举个例子,一个客户端可能会在实际发送一个 `DELETE` 请求之前,先向服务器发起一个预检请求,用于询问是否可以向服务器发起一个 [`DELETE`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/DELETE) 请求:
​
    ```http
    OPTIONS /resource/foo
    Access-Control-Request-Method: DELETE
    Access-Control-Request-Headers: origin, x-requested-with
    Origin: https://foo.bar.org

如果服务器允许,那么服务器就会响应这个预检请求。并且其响应首部 Access-Control-Allow-Methods 会将 DELETE 包含在其中: ​

HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400

​ 同时满足下列以下条件,就属于简单请求,否则属于非简单请求(参考HTTP访问控制(CORS))

1.请求方式只能是:GET、POST、HEAD

2.HTTP请求头限制这几种字段(不得人为设置该集合之外的其他首部字段): Accept、Accept-Language、Content-Language、Content-Type(需要注意额外的限制)、DPR、Downlink、Save-Data、Viewport-Width、Width

3.Content-type只能取:application/x-www-form-urlencoded、multipart/form-data、text/plain

4.请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

5.请求中没有使用 ReadableStream 对象。

预检请求的结果可能被缓存

若后端设置Access-Control-Allow-Origin:*,当前端携带Credentials发来请求时,可能会遇到withCredentials问题(前后端都可能遇到报错)(毕竟公交车不在意别人身份证)。此时可以设置后端只允许部分域名访问(推荐,因为安全),或是设置请求头中不携带凭证。

Access-Control-Allow-Credentials:

  • 唯一有效值为true。如果不需要credentials,相比将其设为false,MDN更建议忽视这个头。当然,很多后端代码实现可以提供设置为false的方法

  • 作为普通响应时:表示是否可以将对请求的响应暴露给页面。返回true则可以,其他值均不可以。

    • 如Get请求时,浏览器不会发送预检请求。如果服务端回来的响应头中没有该项或不为true,则不会显示响应内容。
  • 最为预检请求的响应时:表示是否真正的请求可以使用credentials。

    • 如果不允许对资源的请求头携带凭证(如Access-Control-Allow-Origin:*默认不允许),假如请求头真的带了凭证,反而会报错。就好像问公交车需不需要提供身份证,人家会觉得你太正经。
    • Credentials可以是 cookies, authorization headers (token)或 TLS client certificates。

下面展示开发过程中如何解决跨域问题。注意不论是通过代码、配置文件进行配置,大多都是基于CORS标准诞生的解决方法。

采用一种方法解决问题即可!我们的目的是在响应头中添加允许跨域的信息!

利用代理服务器配置

IIS配置:

只需要在IIS添加HTTP响应标头即可

Access-Control-Allow-Headers:Content-Type, api_key, Authorization
Access-Control-Allow-Origin:*

Apache配置:修改http.conf

<Directory "/Users/cindy/dev">
AllowOverride ALL
Header set Access-Control-Allow-Origin *
</Directory>

或者,修改Apache伪静态规则文件.htaccess

Nginx(推荐使用)

修改nginx.conf,用add_header来为响应头添加信息

location ~* .(eot|ttf|woff|svg|otf)$ {
    add_header Access-Control-Allow-Origin *;
}
  • 为什么nginx确定是添加到响应了啊喂

前端开发时代理服务器

vue代理

proxyTable

dev{
    proxyTable: {
      '/api': {
        target: 'http://192.168.0.1:200', // 要代理的域名
        changeOrigin: true,//允许跨域
        pathRewrite: {
          '^/api': '' // 这个是定义要访问的路径,名字随便写
        }
   }
}

配置好之后由http-proxy-middleware中间件做跨域

springboot

注解

直接在需要跨域访问的类或方法上配置@CrossOrigin注解即可。

扩展:@CrossOrigin 注解还支持更加丰富的参数配置:

value:表示支持的域。这里表示来自 http://localhost:8081 域的请求是支持跨域的。默认为 *,表示所有域都可以。
maxAge:表示探测请求的有效期(先进行判断是否有效)。探测请求不用每次都发送,可以配置一个有效期,有效期过了之后才会发送探测请求。默认为 1800 秒,即 30 分钟。
allowedHeaders:表示允许的请求头。默认为 *,表示该域中的所有的请求都被允许。
@CrossOrigin(value = "http://localhost:8081", maxAge = 1800, allowedHeaders ="*")

全局配置:

创建webMvc配置类

全局配置需要添加自定义类继承WebMvcConfigurer类,然后实现接口中的 addCorsMappings 方法。下面是一个简单的样例代码,请根据需要配置:

从上往下依次为:
 addMapping: 表示对哪种格式的请求路径进行处理。
 allowedHeaders: 表示允许的请求头,默认允许所有的请求头信息。也可以在这里配置请求方法。
 allowedMethods: 表示允许的请求方法,默认是 GET、POST  HEAD。这里配置为 * 表示支持所有的请求方法。
 maxAge: 最大响应时间
 allowedOrigins: 该域发出的请求允许跨域
 .allowCredentials(true): 允许携带token
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/controller/**")
                .allowedHeaders("*")
                .allowedMethods("*")
                .maxAge(1800)
                .allowedOrigins("http://localhost:8081")
                .allowCredentials(true);
    }
}

配置专门的跨域配置类

import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
 
/**
 * 解决跨越配置类
 */
@Configuration
public class CorsConfig {
 
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        return corsConfiguration;
    }
 
    /**
     * 跨域过滤器
     */
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}

过滤器

import javax.servlet.*;
​
@Component
public class CorsFilter implements Filter {
 
    // 只有在列表中的才允许访问
    private final List<String> allowedOrigins = Arrays.asList("http://localhost:8089");
 
    public void destroy() {
 
    }
 
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        // Lets make sure that we are working with HTTP (that is, against HttpServletRequest and HttpServletResponse objects)
        if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
 
            // Access-Control-Allow-Origin
            String origin = request.getHeader("Origin");
            response.setHeader("Access-Control-Allow-Origin", allowedOrigins.contains(origin) ? origin : "*");
            response.setHeader("Vary", "Origin");
 
            // Access-Control-Max-Age
            response.setHeader("Access-Control-Max-Age", "3600");
 
            // Access-Control-Allow-Credentials
            response.setHeader("Access-Control-Allow-Credentials", "true");
 
            // Access-Control-Allow-Methods
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
 
            // Access-Control-Allow-Headers
            response.setHeader("Access-Control-Allow-Headers",
                    "Origin, X-Requested-With, Content-Type, Accept, " + "X-CSRF-TOKEN");
        }
 
        chain.doFilter(req, res);
    }
 
    public void init(FilterConfig filterConfig) {
    }
}

Spring Cloud Gateway网关配置

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOriginPatterns: "*"
            allowed-methods: "*"
            allowed-headers: "*"
            allow-credentials: true
            exposedHeaders: "Content-Disposition,Content-Type,Cache-Control"