同源策略

Same Origin Policy,同源策略,它是一种约定,也是浏览器最核心最基本的安全功能。如果缺少了同源策略,浏览器的正常功能会受到影响。
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
同源指的是以下三项必须相同:

  • 协议相同
  • 域名相同
  • 端口相同
    比如对网站 https://www.google.com 来说,它的同源情况如下。
https://www.google.com/abc/123.html:同源
https://google.com/abc/123.html:不同源(域名不同)
http://www.google.com/abc/123.html:不同源(协议不同)
https://www.google.com:81/abc/123.html:不同源(端口不同)

如果非同源,下面三种行为受到限制。

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 无法获得
  • Ajax 请求不能发送

跨域

Ajax 是最常用的前端与后端的交互技术,很多开源前端框架都使用了此技术,因此必须跨过同源政策才能正常使用。
通过三种方式可以规避这个同源政策对 Ajax 的限制。

  • JSONP
  • WebSocket
  • CORS
    通常我们使用第三种方式—— CORS (Cross-Origin Resource Sharing),即跨域资源共享。

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

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

CORS 分类

浏览器将 CORS 请求分成两类:简单请求和复杂请求。对于简单请求和复杂请求,浏览器的处理方式是不同的。
简单来说,符合以下条件的属于简单请求。

  • 请求方式是 HEAD / GET / POST 中的一种
  • HTTP 请求头信息不超过以下字段
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type:且仅限于值为 application/x-www-form-urlencoded、multipart/form-data、text/plain
    • Last-Event-ID
      只要不满足上述两个条件的都属于复杂请求。

CORS 简单请求

对于简单请求,浏览器直接发出 CORS 请求,在头信息之中,增加一个 Origin 字段。此字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果 Origin 指定的域名在许可范围内,服务器返回的响应会多几个头信息字段。

  • Access-Control-Allow-Origin:* 表示接受任何域名的请求
  • Access-Control-Allow-Credentials:boolean 值,表示是否允许发送 Cookie
  • Access-Control-Expose-Headers

如果不在服务端指定域名许可范围内,服务器会返回一个正常的 HTTP 回应。这个回应的头信息没有包含Access-Control-Allow-Origin 字段,从而抛出一个错误。

通过一个 Demo 代码,分别在 8085、8086 两个端口部署,8085 系统使用 Ajax 访问 8086 系统的接口,从而出现报错。

错误示例如下:
20201224202807.png

前端示例代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script>
        function loadXMLDoc() {
            var xmlHttp = new XMLHttpRequest();
            xmlHttp.onreadystatechange = function () {
                if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
                    document.getElementById("myDiv").innerHTML = xmlHttp.responseText;
                }
            };
            xmlHttp.open("GET", "http://localhost:8086/test", true);
            xmlHttp.send();
        }
    </script>
</head>
<body>
<div id="myDiv"><h2>Test</h2></div>
<button type="button" onclick="loadXMLDoc()">请求</button>
</body>
</html>

后台示例代码:
当在 header 中放入 Access-Control-Allow-Origin: * 时,浏览器不再抛错。

@RestController
public class TestController {

    @GetMapping(value = "/test")
    public Object test(HttpServletRequest req, HttpServletResponse rep) {
        //重点语句
        rep.setHeader("Access-Control-Allow-Origin", "*");
        return "123";
    }
}

CORS 复杂请求

POST json 请求或 PUT 请求都属于复杂请求。复杂请求会在正式通信之前,增加一次 HTTP 查询请求,称为 预检 请求。
预检 请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面的关键字段是 Origin 和两个额外字段。

  • Access-Control-Request-Method,表示要请求的方法
  • Access-Control-Request-Headers,表示要额外发送的头信息字段
    服务器收到预检请求后,如果校验通过,会在返回的头信息中包含如下字段:
  • Access-Control-Allow-Origin:表示允许的请求来源域名,* 表示接受任何域名的请求
  • Access-Control-Allow-Methods:表示服务端允许的请求方法
  • Access-Control-Allow-Headers,
  • Access-Control-Allow-Credentials:boolean 值,表示是否允许发送 Cookie。
  • Access-Control-Max-Age:表示本次预检请求的有效期,这段时间范围内不需要再次发送预检请求。
    如果返回的头信息中没有以上字段,则浏览器会认为校验不通过,返回一个错误。
    当通过预检后,后续的请求和简单请求流程就一样了,

CORS 过滤器

上述 Demo 代码里手动在请求返回的 header 中增加了返回字段,正常我们在项目中加个过滤器处理就可以了。Spring 提供了 CorsConfiguration 供我们使用,简单设置就可以实现跨域访问。

@Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // 设置你要允许的网站域名,如果全允许则设为 *
        config.addAllowedOrigin("*");
        // 如果要限制 HEADER 或 METHOD 请自行更改
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(3600L);
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }

总结

限制跨域访问是为了安全,但同时引起了一些必要跨域访问的不便,因此需要通过一些方式来允许这种特殊的请求方式。