当前位置 : 首页 » 文章分类 :  开发  »  同源策略和CORS跨域

同源策略和CORS跨域

同源策略和 CORS 跨域解决方案总结

测试浏览器是否支持跨域
https://webbrowsertools.com/test-cors/


同源策略

同源策略(Same Origin Policy)在 web 应用的安全模型中是一个重要概念。在这个策略下,web 浏览器允许第一个页面的脚本访问第二个页面里的数据,但是也只有在两个页面有相同的源时。源是由URI,主机名,端口号组合而成的。这个策略可以阻止一个页面上的恶意脚本通过页面的 DOM 对象获得访问另一个页面上敏感信息的权限。

什么情况下算是同源?

所谓同源是指:域名、协议、端口相同。
URL 由协议、域名、端口和路径组成,如果两个 URL 的协议、域名和端口相同,则表示他们同源。相反,只要协议,域名,端口有任何一个的不同,就被当作是跨域。


URL构成

是否跨域

例如,我从我博客 madaimeng.com 访问我的后台api服务 api.devgou.com,被同源策略限制时后台提示如下:


页面访问非同源api被浏览器block

不受同源策略限制的元素

1、页面中的链接,重定向以及表单提交是不会受到同源策略限制的。
2、跨域资源的引入是可以的。但是js不能读写加载的内容。如嵌入到页面中的 <script src="..."></script><img><link><iframe> 等。

什么是跨域?

受前面所讲的浏览器同源策略的影响,不是同源的脚本不能操作其他源下面的对象。想要操作另一个源下的对象是就需要跨域。

跨域的实现方式

关于跨域,你想知道的全在这里
https://zhuanlan.zhihu.com/p/25778815

关于前端跨域的整理
https://zhuanlan.zhihu.com/p/24198444

九种跨域方式实现原理(完整版)
https://www.jianshu.com/p/cedc8b1cd84c

JSONP

JSONP 全称为:JSON with Padding,可用于解决主流浏览器的跨域数据访问的问题。
由于包含 src 属性的 <script> 标签可以加载跨域资源。 JSONP 就是利用 <script> 标签的跨域能力实现跨域数据的访问。

Web 页面上调用 js 文件不受浏览器同源策略的影响,所以通过 Script 便签可以进行跨域的请求:
1、首先前端先设置好回调函数,并将其作为 url 的参数。
2、服务端接收到请求后,通过该参数获得回调函数名,并将数据放在参数中将其返回
3、收到结果后因为是 script 标签,所以浏览器会当做是脚本进行运行,从而达到跨域获取数据的目的。

JSONP 的缺点
JSONP 只支持 GET 请求。

window.name

window 对象有个 name 属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。并且可以支持非常长的 name 值(2MB)。

通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

document.domain

对于主域相同而子域不同的情况下,可以通过设置 document.domain 的办法来解决,具体做法是可以在 http://www.example.com/a.htmlhttp://sub.example.com/b.html 两个文件分别加上 document.domain = “a.com”;然后通过 a.html 文件创建一个 iframe,去控制 iframe 的 window,从而进行交互,当然这种方法只能解决主域相同而二级域名不同的情况。

CORS 跨域资源共享

CORS(Cross-Origin Resource Sharing,跨域资源共享)是一个 W3C 标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。

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

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。


CORS 详解与实例

其实 CORS 很好理解,服务端在 HTTP 响应中添加一个 Header Access-Control-Allow-Origin:http://www.masikkk.com header 值是允许的源,浏览器看到自己的源和服务端返回的能匹配上,就允许跨域。

浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

简单请求和复杂请求

只要同时满足以下两大条件,就属于简单请求。
一、请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

二、HTTP 的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain

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

简单请求 CORS 处理过程

对于简单的跨域请求,浏览器会自动在请求的头信息加上 Origin 字段,表示本次请求来自哪个源(协议 + 域名 + 端口),服务端应该获取到这个值,然后判断是否同意这次请求并返回。

简单请求处理过程:
1、浏览器自动在跨域请求中附加一个的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。例如:Origin: http://www.masikkk.com
2、如果服务器认为这个请求可以接受,就在 http 响应中添加 header Access-Control-Allow-Origin,其值设为和请求中 Origin 相同的源信息(如果是公共资源,可以设置 * 表示接受任意域名的请求 )。例如:Access-Control-Allow-Origin:http://www.masikkk.com
3、没有这个头部或者有这个头部但源信息不匹配,浏览器就会驳回请求。注意,请求和响应都不包含 cookie 信息。
4、如果需要包含 cookie 信息,ajax 请求需要设置 XMLHttpRequest 的属性 withCredentialstrue,服务器需要设置响应头部 Access-Control-Allow-Credentials: true

复杂请求 CORS 处理过程(先发 OPTION 请求)

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT 或 DELETE,或者 Content-Type 字段的类型是 application/json。
复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 **”预检”请求(preflight),预检请求使用的 http 方法 OPTIONS**,通过该请求来知道服务端是否允许跨域请求。

复杂请求处理过程:
1、浏览器发送预检请求
浏览器在发送真正的请求之前,先发送一个 Preflight 预检请求给服务器,这种请求使用 OPTIONS 方法,调用的也是同一个 API,发送下列头部:
Origin 必须,与简单的请求相同,值为请求页面的源信息(协议、域名和端口)
Access-Control-Request-Method 必须,请求自身使用的 http 方法,例如 PUT。
Access-Control-Request-Headers 可选,自定义的头部信息,多个头部以逗号分隔。
预检请求示例:

OPTIONS /cors HTTP/1.1
Origin: https://masikkk.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header

2、服务端返回预检响应
收到这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通:
Access-Control-Allow-Origin 必须,与简单的请求相同,允许访问的源,接受任意域就返回 *
Access-Control-Allow-Methods 必须,允许的方法,多个方法以逗号分隔。表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
Access-Control-Allow-Headers 如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。
Access-Control-Max-Age 可选,用来指定本次预检请求的有效期,单位为秒。或者说应该将这个 Preflight 请求缓存多长时间。
预检请求响应示例:

Access-Control-Allow-Origin: http://devgou.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000

3、浏览器的正常请求和回应
一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。

浏览器同源策略及跨域的解决方法
http://www.cnblogs.com/laixiangran/p/9064769.html

关于前端跨域的整理
https://zhuanlan.zhihu.com/p/24198444


Nginx CORS 配置实例

前端发送AJAX请求

<html>
<head>
<script type="text/javascript">
  function loadXMLDoc() {
  var xmlhttp;
  if (window.XMLHttpRequest){
    // code for IE7+, Firefox, Chrome, Opera, Safari
    xmlhttp = new XMLHttpRequest();
  } else {
    // code for IE6, IE5
    xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
  }
  xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
      document.getElementById("myDiv").innerHTML = xmlhttp.responseText;
      }
  }
  xmlhttp.open("GET","http://api.devgou.com/user",true);
  xmlhttp.send();
  }
</script>
</head>
<body>

<h2>AJAX CORS</h2>
<button type="button" onclick="loadXMLDoc()">请求数据</button>
<div id="myDiv"></div>

</body>
</html>

这跟一次正常的异步 ajax 请求没有什么区别,关键是在服务端收到请求后的处理:
关键是在于后端设置响应头中的 Access-Control-Allow-Origin,该值要与请求头中 Origin 一致才能生效,否则将跨域失败。

关于跨域,你想知道的全在这里
https://zhuanlan.zhihu.com/p/25778815

Nginx 代理配置

比如服务端是Spring后台接口,用nginx做请求转发

需在 nginx 中配置的几个参数:
Access-Control-Allow-Origin:必含,允许的域名,只能填通配符或者单域名
Access-Control-Allow-Methods:必含,允许跨域请求的 http 方法
Access-Control-Allow-Headers:返回支持的 http 请求头
Access-Control-Allow-Credentials:可选,标志着当前请求是否包含 cookies 信息,布尔值。只有一个可选值:true,如果不包含 cookies,请略去该项,而不是填 false。这一项与XmlHttpRequest 对象当中的 withCredentials 属性应保持一致,即 withCredentials 为true时该项也为 true;withCredentials为false时,省略该项不写。反之则导致请求失败。
Access-Control-Max-Age:可选,以秒为单位的缓存时间,用于缓存预检请求。

如果只响应简单请求,且不使用cookie,只设置 Access-Control-Allow-Origin为 前端的域名即可
比如只允许 http://devgou.com 访问,可如下配置:

add_header 'Access-Control-Allow-Origin' 'http://devgou.com';

如果需要允许来自任何域的访问,可以这样配置

add_header Access-Control-Allow-Origin *;

Nginx 多域名 CORS 配置

如果不想允许所有,但是又需要允许多个域名,那么就需要用到 nginx 的 map 指令
比如想允许如下 map 中的四个域名访问:

http {
    map $http_origin $corsHost {
        default 0;
        "~http://madaimeng.com" http://madaimeng.com;
        "~http://devgou.com" http://devgou.com;
        "~http://masikkk.com" http://masikkk.com;
        "~http://localhost:4000" http://localhost:4000;
    }

    server {
        listen       80;
        server_name  api.masikkk.com api.madaimeng.com api.devgou.com;

        location / {
          add_header 'Access-Control-Allow-Origin' $corsHost;
          proxy_pass http://localhost:8001;
        }
    }
}

nginx CORS 的配置
https://blog.csdn.net/hukfei/article/details/72832428

nginx通过CORS实现跨域
https://www.cnblogs.com/sunmmi/articles/5956554.html


SpringBoot 接口跨域配置

Controller 上加 @CrossOrigin 注解

@Controller
@RequestMapping("/comment")
@CrossOrigin
public class SysLogController {
 
}

配置 WebMvcConfigurer

@Configuration
public class WebMvcConfg implements WebMvcConfigurer {
    static final String ORIGINS[] = new String[] { "GET", "POST", "PUT", "DELETE" };
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //设置允许跨域的路径
        registry.addMapping("/**")
                //设置允许跨域请求的域名,* 表示全部
                .allowedOrigins("http://localhost:8001", "http://masikkk.com", "http://madaimeng.com")
                //是否允许证书 不再默认开启 一旦使用 allowCredentials(true) 方法,则 allowedOrigins("*") 需要指明特定的域,而不能是 *
                .allowCredentials(true)
                //设置允许的方法
                .allowedMethods("*")
                //.allowedMethods(ORIGINS)
                //跨域允许时间
                .maxAge(3600);
    }
}

配置 Servlet Filter

@Component
public class CorsFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin","*"); // 允许的来源
        response.setHeader("Access-Control-Allow-Methods", "*"); // 允许的请求方式
        response.setHeader("Access-Control-Allow-Headers", "*"); // 允许的 header
        response.setHeader("Access-Control-Allow-Credentials", "true"); // 是否允许证书
        response.setHeader("Access-Control-Max-Age", "3600"); // 预检请求的有效期
        chain.doFilter(req, res);
    }
}

暴露一个 CorsFilter Bean

spring-web 中有个 org.springframework.web.filter.CorsFilter 暴露一个这个 Bean 就可配置 CORS 跨域,内部也是 Servlet Filter 实现

import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*");
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setMaxAge(3600L);
        config.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config);
        return new CorsFilter(configSource);
    }
}

HttpServletResponse 中手动设置响应 header

@RequestMapping("/test")
@ResponseBody
public String test(HttpServletResponse response){
  response.addHeader("Access-Control-Allow-Origin", "http://localhost:8080");
  return "success";
}

SpringBoot 拦截器导致跨域失败

问题:
Controller 接口上有 @CrossOrigin 跨域注解,且有全局 HandlerInterceptor 拦截器时,报错如下且跨域失败。

java.lang.ClassCastException: org.springframework.web.servlet.handler.AbstractHandlerMapping$PreFlightHandler cannot be cast to org.springframework.web.method.HandlerMethod
    at com.masikkk.common.web.LogInterceptor.preHandle(LogInterceptor.java:22) ~[classes/:na]

原因:
拦截器代码如下:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    log.info("拦截器 {} {} {} {} {} {}", request.getMethod(), request.getRequestURL(), request.getQueryString(), request.getRemoteAddr(),
            handlerMethod.getBeanType().getName(), handlerMethod.getMethod().getName());
    return true;
}

CORS 处理复杂请求会先给同一个 API 发送 OPTION 请求探测是否可跨域,也就是 Preflight 请求,此时 handler 是 PreFlightHandler,强转为 HandlerMethod 时报错。
所以无论是通过 @CrossOrigin 注解跨域,还是通过其他 Filter 等方式配置跨域,都会有这个问题。

解决:
方法1,发现请求方法是 OPTIONS 时直接跳过拦截器:

if("OPTIONS".equalsIgnoreCase(request.getMethod())) {
    return true;
}

方法2,判断下 handler 的类型

if (!(handler instanceof HandlerMethod)) {
        return true;
}

Springboot 拦截器导致 @CrossOrigin 跨域失效的解决方案
https://cloud.tencent.com/developer/article/1768352

Spring Boot(十四):教你四招实现 CORS 跨域资源共享
https://juejin.cn/post/7053753080176705573


Nginx 和 SpringBoot 同时设置跨域导致跨域失败

浏览器跨域调接口报错:
Access to XMLHttpRequest at ‘http://api.masikkk.com/statistic/createPageView' from origin ‘http://masikkk.com' has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: The ‘Access-Control-Allow-Origin’ header contains multiple values ‘*, http://masikkk.com', but only one is allowed.

原因:
Nginx 和 SpringBoot 同时设置跨域响应 header Access-Control-Allow-Origin 导致跨域失败

解决:
去掉 nginx 或 SpringBoot 任一跨域配置即可,考虑到本地联调前端时也需要跨域访问本地启动的 SpringBoot 服务,所以保留 SpringBoot 里的跨域配置,去掉 nginx 的。


CORS 优缺点

优点
CORS 通信与同源的 AJAX 通信没有差别,代码完全一样,容易维护。
支持所有类型的 HTTP 请求。

缺点
存在兼容性问题,特别是 IE10 以下的浏览器。
第一次发送非简单请求时会多一次请求。

九种跨域方式实现原理(完整版)
https://www.jianshu.com/p/cedc8b1cd84c


上一篇 领域驱动设计

下一篇 Hexo博客(24)VPS中部署Hexo

阅读
评论
4.2k
阅读预计17分钟
创建日期 2019-02-17
修改日期 2022-11-29
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论