Cors跨域(一):深入理解跨域请求概念及其根因

Talk is cheap. Show me the money.

前言

你好,我是YourBatman

做Web开发的小伙伴对“跨域”定并不陌生,像狗皮膏药一样粘着几乎每位同学,对它可谓既爱又恨。跨域请求之于创业、小型公司来讲是个头疼的问题,因为这类企业还未沉淀出一套行之有效的、统一的解决方案。

让人担忧的是,据我了解不少程序员同学(不乏有高级开发)碰到跨域问题大都一头雾水:
在这里插入图片描述
然后很自然的 用谷歌去百度一下搜索答案,但相关文章可能参差不齐、鱼龙混杂。短则半天长则一天(包含改代码、部署等流程)此问题才得以解决,一个“小小跨域”问题成功偷走你的宝贵时间。

既然跨域是个如此常见(特别是当下前后端分离的开发模式),因此深入理解CORS变得就异常的重要了(反倒前端工程师不用太了解),因此早在2019年我刚开始写博客那会就有过较为详细的系列文章:
Cors跨域(一):深入理解跨域请求概念及其根因_第1张图片
现在把它搬到公众号形成技术专栏,并且加点料,让它更深、更全面、更系统的帮助到你,希望可以助你从此不再怕Cors跨域资源共享问题。

本文提纲

Cors跨域(一):深入理解跨域请求概念及其根因_第2张图片

版本约定

  • JDK:8
  • Servlet:4.x

正文

文章遵循一贯的风格,本文将采用概念 + 代码示例的方式,层层递进的进行展开叙述。那么上菜,先来个示例预览,模拟一下跨域请求,后面的一些的概念示例将以此作为抓手。

模拟跨域请求

要模拟跨域请求的根本是需要两个源:让请求的来源和目标源不一样。这里我就使用IDEA作为静态Web服务器(63342),Tomcat作为后端动态Servlet服务器(8080)。

说明:服务器都在本机,端口不一样即可

前端代码

index.html


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CORS跨域请求title>
    
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js">script>
head>
<body>
<button id="btn">跨域从服务端获取内容button>
<div id="content">div>

<script>
    $("#btn").click(function () {
        // 跨域请求
        $.get("http://localhost:8080/cors", function (result) {
            $("#content").append(result).append("
"
); }); // 同域请求 $.get("http://localhost:63342"); $.post("http://localhost:63342"); });
script> body> html>

使用IDEA作为静态web服务器,浏览器输入地址即可访问(注:端口号为63342):
Cors跨域(一):深入理解跨域请求概念及其根因_第3张图片

后端代码

后端写个Servlet来接收cors请求

/**
 * 在此处添加备注信息
 *
 * @author YourBatman. Send email to me
 * @site https://yourbatman.cn
 * @date 2021/6/9 10:36
 * @since 0.0.1
 */
@Slf4j
@WebServlet(urlPatterns = "/cors")
public class CorsServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = req.getRequestURI();
        String method = req.getMethod();
        String originHeader = req.getHeader("Origin");

        log.info("收到请求:{},方法:{}, Origin头:{}", requestURI, method, originHeader);
        resp.getWriter().write("hello cors...");
    }
}

启动后端服务器,点击页面上的按钮,结果如下:
Cors跨域(一):深入理解跨域请求概念及其根因_第4张图片
服务端控制台输出:

... INFO  c.y.cors.servlet.CorsServlet - 收到请求:/cors,方法:GET, Origin头:http://localhost:63342

服务端输出日志,说明即使前端的Http Status是error,但服务端还是收到并处理了这个请求的

下面以此代码示例为基础,普及一下和Cors跨域资源共享相关的概念。

Host、Referer、Origin的区别

这哥三看起来很是相似,下面对概念作出区分。
Cors跨域(一):深入理解跨域请求概念及其根因_第5张图片

  • Host:去哪里。域名+端口。值为客户端将要访问的远程主机,浏览器在发送Http请求时会带有此Header
  • Referer:来自哪里。协议+域名+端口+路径+参数。当前请求的来源页面的地址,服务端一般使用 Referer 首部识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等
    • 常见应用场景:百度的搜索广告就会分析Referer来判断打开站点是从百度搜索跳转的,还是直接URL输入地址的
    • 一般情况下浏览器会带有此Header,但这些case不会带有Referer这个头
      • 来源页面协议为File或者Data URI(如页面从本地打开的)
      • 来源页面是Https,而目标URL是http
      • 浏览器地址栏直接输入网址访问,或者通过浏览器的书签直接访问
      • 使用JS的location.href跳转
  • Origin:来自哪里(跨域)。协议+域名+端口。它用于Cors请求和同域POST请求

可以看到Referer与Origin功能相似,前者一般用于统计和阻止盗链,后者用于CORS请求。 但是还是有几点不同:

  1. 只有跨域请求,或者同域时发送post请求,才会携带Origin请求头;而Referer只要浏览器能获取到都会携带(除了上面说明的几种case外)
    Cors跨域(一):深入理解跨域请求概念及其根因_第6张图片
    Cors跨域(一):深入理解跨域请求概念及其根因_第7张图片
    在这里插入图片描述
    Cors跨域(一):深入理解跨域请求概念及其根因_第8张图片

  2. 若浏览器不能获取到请求源页面地址(如上面的几种case),Referer头不会发送,但Origin依旧会发送,只是值是null而已(注:虽然值为null,但此请求依旧属于Cors请求哦),如下图所示:
    在这里插入图片描述
    Cors跨域(一):深入理解跨域请求概念及其根因_第9张图片

  3. Origin的值只包括协议、域名和端口,而Rerferer不但包括协议、域名、端口还包括路径,参数,注意不包括hash值

浏览器的同源策略

浏览器的职责是展示/渲染document、css、script脚本等,但是这些资源(将document、css、script统一称为资源)可能来自不同的地方,如本地、远程服务器、甚至黑客的服务器…浏览器作为万维网的入口,是我们接入互联网最重要的软件之一(甚至没有之一),因此它的安全性显得尤为重要,这就出现了浏览器的同源策略。

同源策略是浏览器一个重要的安全策略,它用于限制一个origin源的document或者它加载的脚本如何能与另一个origin源的资源进行交互。它能帮助阻隔恶意文档,减少(并不是杜绝)可能被攻击的媒介。

方便和安全往往是相悖的:安全性增高了,方便性就会有所降低

那么问题来了,什么才算同源?

同源的定义

URL被称作:统一资源定位符,同源是针对URL而言的。一个完整的URL各部分如下图所示:
Cors跨域(一):深入理解跨域请求概念及其根因_第10张图片

Tips:域名和host是等同的概念,域名+端口号 = host+端口号(大部分情况下你看到域名并没有端口号,那是采用了默认端口号80而已)

同源:只和上图的前两部分(protocol + domain)有关,规则为:全部相同则为同源。这个定义不难理解,但有几点需要再强调一下:

  • 两部分必须完全一样才算同源
  • 这里的domain包含port端口号,所以总共是两部分而非三部分
    • 当然也有说三部分的(协议+host+port),理解其含义就成

下面通过举例来彻底了解下。譬如,我的源URL为:http://www.baidu.com/api/user,下面表格描述了不同URL的各类情况:

URL 是否同源 原因说明
http://www.baidu.com/account 前两部分相同,path路径不一样而已
http://www.baidu.com/account?name=a 前两部分相同,path路径、参数不同而已
https://www.baidu.com/api/user 协议不同
http://www.baidu.com:8080/api/user 端口不同(domain不同)
http://map.baidu.com/api/user host不同(domain不同)

不同源的网络访问

浏览器同源策略的存在,限制了不同源之间的交互,实为不便。但是浏览器也开了一些“绿灯”,让其不受同源策略的约束。此种情况一般可分为如下三类:

  1. 跨域写操作(Cross-origin writes):一般是被允许的。如链接(如a标签)、重定向以及表单提交(如form表单的提交)
  2. 跨域资源嵌入(Cross-origin embedding):一般是允许的。比如下面这些例子:
    1. 标签嵌入js脚本
    2. 标签嵌入CSS
    3. 展示的图片
    4. 媒体资源
    5. 嵌入的插件
    6. CSS中使用@font-face引入字体
    7. 通过