跨域问题详解

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

为什么会有跨域问题?

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

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

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

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

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

一些例子:

http://www.tablerows.top --> http://admin.tablerows.top 跨域
http://www.tablerows.top --> http://www.tablerows.top 非跨域
http://www.tablerows.top --> http://www.tablerows.top:8080 跨域
http://www.tablerows.top --> https://www.tablerows.top 跨域(协议改变)

同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容

  • DOM 节点

  • AJAX 请求发送后,结果被浏览器拦截了

但是有三个标签是允许跨域加载资源:

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

拓展:

https://www.cnblogs.com/cbs-writing/p/10544203.html

浏览器发起请求后都发生了什么(计算机网络视角)

http是无状态无连接的,但为了收到回应需要连接,所以先用tcp建立连接,最后再断开

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

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

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

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

跨域问题详解_第1张图片

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

api跨域:

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

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

跨域攻击实战

比如在B站看到的油猴脚本获取表单内容、获取token等(当然社工学要起作用比如让你掉线)

所谓其他域的恶意脚本是否就是这个?

就算是html文档,也有js?

防跨域是为了自己域的东西都有保证,,请求的其他域不一定是自己设计的,,,默认用户主动访问的这个域是安全的

虽然现在接口经常返回json数据,但之前响应内同是文档(html等万维网文档)

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

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

原理浅析

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

  • Ajax基于XMLHttpRequset对象实现的,而各种请求库又是对Ajax技术的实现、封装
  • Fetch API

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`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) 会将 `DELETE` 包含在其中:

```http
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标准诞生的解决方法。

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

post发送两次请求,第一次返回码100?不是rfc文档要求的,是curl有这个实现

利用代理服务器配置

####IIS配置:

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

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

####Apache配置:修改http.conf


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中间件做跨域

webpack

https://blog.csdn.net/weixin_34355715/article/details/91413583

node的serve

https://blog.csdn.net/echo008/article/details/78307841

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:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOriginPatterns: "*"
            allowed-methods: "*"
            allowed-headers: "*"
            allow-credentials: true
            exposedHeaders: "Content-Disposition,Content-Type,Cache-Control"

其他

websocket再配置类中也有自己的方法。

  • 待实验
  • 如何允许多个域?
  • 配置了webmvc跨域,还需要配websocket吗?(会覆盖么?)
  • 协议升级?

jsonp

Jsonp(JSON with Padding) 是 json 的一种"使用模式",可以让网页从别的域名(网站)那获取资料,即跨域读取数据。

Padding这里我们理解为填充。pad有软垫的意思,ipad在刚推出时曾被吐槽过女性卫生用品(#iTampon),,

<script type="text/javascript" src="http://localhost:20002/test.js">script>

程序A中sample的部分代码:

<script type="text/javascript">
//回调函数
function callback(data) {
    alert(data.message);
}
script>
<script type="text/javascript" src="http://localhost:20002/test.js">script>

程序B中test.js的代码:

//调用callback函数,并以json数据形式作为阐述传递,完成回调

callback({message:"success"});

这其实就是JSONP的简单实现模式,或者说是JSONP的原型:创建一个回调函数,然后在远程服务上调用这个函数并且将JSON 数据形式作为参数传递,完成回调。

将JSON数据填充进回调函数,这就是JSONP的JSON+Padding的含义吧。

标签的src属性并不被同源策略所约束,所以可以获取任何服务器上脚本并执行。五、jQuery对JSONP的实现

jQuery框架也当然支持JSONP,可以使用$.getJSON(url,[data],[callback])方法(详细可以参考JQ实现jsonp)。

那我们就来修改下程序A的代码,改用jQuery的getJSON方法来实现(下面的例子没用用到向服务传参,所以只写了getJSON(url,[callback])):

<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js">script>
<script type="text/javascript">
    $.getJSON("http://localhost:20002/MyService.ashx?callback=?",function(data){
        alert(data.name + " is a a" + data.sex);
    });
script>

结果是一样的,要注意的是在url的后面必须添加一个callback参数,这样getJSON方法才会知道是用JSONP方式去访问服务,callback后面的那个问号是内部自动生成的一个回调函数名。这个函数名大家可以debug一下看看,比如jQuery17207481773362960666_1332575486681。

当然,加入说我们想指定自己的回调函数名,或者说服务上规定了固定回调函数名该怎么办呢?我们可以使用$.ajax方法来实现(参数较多,详细可以参考http://api.jquery.com/jQuery.ajax)。先来看看如何实现吧:

<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js">script>
<script type="text/javascript">
   $.ajax({
        url:"http://localhost:20002/MyService.ashx?callback=?",   
        dataType:"jsonp",
        jsonpCallback:"person",
        success:function(data){
            alert(data.name + " is a a" + data.sex);
        }
   });
script>

没错,jsonpCallback就是可以指定我们自己的回调方法名person,远程服务接受callback参数的值就不再是自动生成的回调名,而是person。dataType是指定按照JSOPN方式访问远程服务。

或者简单的模拟一下?

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

这种方式又称为隐藏跨域,浏览器一直认为访问的资源没有跨域,实际是服务器做了处理。我们开发的时候经常使用,比如:在vue.config.js或是webpack.config.js中配置devServer的相关参数来实现代理,或者是使用nginx做代理

看看浏览器源码怎么做跨域、预检请求

等等,,有后面i请求头了还要预检干什么,,

不管我们有没有返回header,浏览器都会向我们这个服务发送这个请求。浏览器在发送这个请求的时候,并不知道这个服务是不是跨域。

Access to XMLHttpRequest at ‘file:///C:/Users/hp/Desktop/trIndex-wallpaper/undefinedv3/?id=undefined’ from origin ‘null’ has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome-extension, edge, https, chrome-untrusted.

另外cookie是和域名绑定在一起的,其他网站并不能访问这个网站储存的cookie。不过前提是储存cookie的网站未做跨域处理。
比如google账号,登录一次就可以免登录访问所有谷歌旗下的网站,这就是做了处理的。

你可能感兴趣的:(javascript,servlet,开发语言)