前言
你好,我是YourBatman。
做Web开发的小伙伴对“跨域”定并不陌生,像狗皮膏药一样粘着几乎每位同学,对它可谓既爱又恨。跨域请求之于创业、小型公司来讲是个头疼的问题,因为这类企业还未沉淀出一套行之有效的、统一的解决方案。
让人担忧的是,据我了解不少程序员同学(不乏有高级开发)碰到跨域问题大都一头雾水:
然后很自然的 用谷歌去百度一下搜索答案,但相关文章可能参差不齐、鱼龙混杂。短则半天长则一天(包含改代码、部署等流程)此问题才得以解决,一个“小小跨域”问题成功偷走你的宝贵时间。
既然跨域是个如此常见(特别是当下前后端分离的开发模式),因此深入理解CORS变得就异常的重要了(反倒前端工程师不用太了解),因此早在2019年我刚开始写博客那会就有过较为详细的系列文章:
本文提纲
版本约定
- JDK:8
- Servlet:4.x
正文
文章遵循一贯的风格,本文将采用概念 + 代码示例的方式,层层递进的进行展开叙述。那么上菜,先来个示例预览,模拟一下跨域请求,后面的一些的概念示例将以此作为抓手。
模拟跨域请求
要模拟跨域请求的根本是需要两个源:让请求的来源和目标源不一样。这里我就使用IDEA作为静态Web服务器(63342),Tomcat作为后端动态Servlet服务器(8080)。
说明:服务器都在本机,端口不一样即可
前端代码
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CORS跨域请求</title> <!--导入Jquery--> <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("<br/>"); }); // 同域请求 $.get("http://localhost:63342"); $.post("http://localhost:63342"); }); </script> </body> </html>
使用IDEA作为静态web服务器,浏览器输入地址即可访问(注:端口号为63342):
后端代码
后端写个Servlet来接收cors请求
/** * 在此处添加备注信息 * * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> * @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..."); } }
启动后端服务器,点击页面上的按钮,结果如下:
服务端控制台输出:
... INFO c.y.cors.servlet.CorsServlet - 收到请求:/cors,方法:GET, Origin头:http://loc
服务端输出日志,说明即使前端的Http Status是error,但服务端还是收到并处理了这个请求的
下面以此代码示例为基础,普及一下和Cors跨域资源共享相关的概念。
Host、Referer、Origin的区别
这哥三看起来很是相似,下面对概念作出区分。
- 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请求。 但是还是有几点不同:
- 只有跨域请求,或者同域时发送post请求,才会携带Origin请求头;而Referer只要浏览器能获取到都会携带(除了上面说明的几种case外)
2.若浏览器不能获取到请求源页面地址(如上面的几种case),Referer头不会发送,但Origin依旧会发送,只是值是null而已(注:虽然值为null,但此请求依旧属于Cors请求哦),如下图所示:
Origin的值只包括协议、域名和端口,而Rerferer不但包括协议、域名、端口还包括路径,参数,注意不包括hash值
浏览器的同源策略
浏览器的职责是展示/渲染document、css、script脚本等,但是这些资源(将document、css、script统一称为资源)可能来自不同的地方,如本地、远程服务器、甚至黑客的服务器…浏览器作为万维网的入口,是我们接入互联网最重要的软件之一(甚至没有之一),因此它的安全性显得尤为重要,这就出现了浏览器的同源策略。
同源策略是浏览器一个重要的安全策略,它用于限制一个origin源的document或者它加载的脚本如何能与另一个origin源的资源进行交互。它能帮助阻隔恶意文档,减少(并不是杜绝)可能被攻击的媒介。
方便和安全往往是相悖的:安全性增高了,方便性就会有所降低
那么问题来了,什么才算同源?
同源的定义
URL被称作:统一资源定位符,同源是针对URL而言的。一个完整的URL各部分如下图所示:
Tips:域名和host是等同的概念,域名+端口号 = host+端口号(大部分情况下你看到域名并没有端口号,那是采用了默认端口号80而已)
同源:只和上图的前两部分(protocol + domain)有关,规则为:全部相同则为同源。这个定义不难理解,但有几点需要再强调一下:
- 两部分必须完全一样才算同源
- 这里的domain包含port端口号,所以总共是两部分而非三部分
- 当然也有说三部分的(协议+host+port),理解其含义就成
下面通过举例来彻底了解下。譬如,我的源URL为:http://www.baidu.com/api/user,下面表格描述了不同URL的各类情况:
不同源的网络访问
浏览器同源策略的存在,限制了不同源之间的交互,实为不便。但是浏览器也开了一些“绿灯”,让其不受同源策略的约束。此种情况一般可分为如下三类:
1.跨域写操作(Cross-origin writes):一般是被允许的。如链接(如a标签)、重定向以及表单提交(如form表单的提交)
2.跨域资源嵌入(Cross-origin embedding):一般是允许的。比如下面这些例子:
- <script src="..."></script>标签嵌入js脚本
- <link rel="stylesheet" href="...">标签嵌入CSS
- <img>展示的图片
- <video>和<audio>媒体资源
- <object>、 <embed> 、<applet>嵌入的插件
- CSS中使用@font-face引入字体
- 通过<iframe>载入资源
3.跨域读操作(Cross-origin reads):一般是不被允许的。比如我们的http接口请求等都属于此范畴,也是本专栏关注的焦点
简单总结成一句话:浏览器自己是可以发起跨域请求的(比如a标签、img标签、form表单等),但是Javascript是不能去跨域获取资源(如ajax)。
如何允许不同源的网络访问
上面说到的第三种情况:跨域读操作一般是不允许跨域访问的,而这种情况是我们开发过程中最关心、最常见的case,因此必须解决。
Tips:这里的读指的是广义上的读,指的是从服务器获取资源(有response)的都叫读操作,而和具体是什么Http Method无关。换句话讲,所有的Http API接口请求都在这里都指的是读操作
可以使用 CORS 来允许跨源访问。CORS 是 HTTP 的一部分,它允许服务端来指定哪些主机可以从这个服务端加载资源。