Talk is cheap. Show me the money.

前言

你好,我是YourBatman

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

让人担忧的是,据我了解不少程序员同学(不乏有高级开发)碰到跨域问题大都一头雾水:

然后很自然的 用谷歌去百度一下搜索答案,但相关文章可能参差不齐、鱼龙混杂。短则半天长则一天(包含改代码、部署等流程)此问题才得以解决,一个“小小跨域”问题成功偷走你的宝贵时间。

既然跨域是个如此常见(特别是当下前后端分离的开发模式),因此深入理解CORS变得就异常的重要了(反倒前端工程师不用太了解),因此早在2019年我刚开始写博客那会就有过较为详细的系列文章:

现在把它搬到公众号形成技术专栏,并且加点料,让它更深、更全面、更系统的帮助到你,希望可以助你从此不再怕Cors跨域资源共享问题。

本文提纲

版本约定

  • 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 {@Overrideprotected 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://localhost:63342

服务端输出日志,说明即使前端的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请求。 但是还是有几点不同:

  1. 只有跨域请求,或者同域时发送post请求,才会携带Origin请求头;而Referer只要浏览器能获取到都会携带(除了上面说明的几种case外)



  2. 若浏览器不能获取到请求源页面地址(如上面的几种case),Referer头不会发送,但Origin依旧会发送,只是值是null而已(注:虽然值为null,但此请求依旧属于Cors请求哦),如下图所示:

  3. 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的各类情况:

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. <script src="..."></script>标签嵌入js脚本
    2. <link rel="stylesheet" href="...">标签嵌入CSS
    3. <img>展示的图片
    4. <video><audio>媒体资源
    5. <object>、 <embed> 、<applet>嵌入的插件
    6. CSS中使用@font-face引入字体
    7. 通过<iframe>载入资源
  3. 跨域读操作(Cross-origin reads):一般是不被允许的。比如我们的http接口请求等都属于此范畴,也是本专栏关注的焦点

简单总结成一句话:浏览器自己是可以发起跨域请求的(比如a标签、img标签、form表单等),但是Javascript是不能去跨域获取资源(如ajax)。

如何允许不同源的网络访问

上面说到的第三种情况:跨域读操作一般是不允许跨域访问的,而这种情况是我们开发过程中最关心、最常见的case,因此必须解决。

Tips:这里的读指的是广义上的读,指的是从服务器获取资源(有response)的都叫读操作,而和具体是什么Http Method无关。换句话讲,所有的Http API接口请求都在这里都指的是读操作

可以使用 CORS 来允许跨源访问。CORS 是 HTTP 的一部分,它允许服务端来指定哪些主机可以从这个服务端加载资源。

什么是Cors跨域

Cors(Cross-origin resource sharing):跨域资源共享,它是浏览器的一个技术规范,由W3C规定,规范的wiki地址在此:https://www.w3.org/wiki/CORS_Enabled#What_is_CORS_about.3F

话外音:它是浏览器的一种(自我保护)行为,并且已形成规范。也就是说:backend请求backend是不存在此现象的喽

若想实现Cors机制的跨域请求,是需要浏览器和服务器同时支持的。关于浏览器对CORS的支持情况:现在都2021年了,so可以认为100%的浏览器都是支持的,再加上CORS的整个过程都由浏览器自动完成,前端无需做任何设置,所以前端工程师的ajax原来怎么用现在还是怎么用,它对前段开发人员是完全透明的。

为何需要Cors跨域访问?

浏览器费尽心思的搞个同源策略来保护我们的安全,但为何又需要跨域来打破这种安全策略呢?其实啊,这一切都和互联网的快速发展有关~

随着Web开放的程度越来越高,页面的内容也是越来越丰富。因此页面上出现的元素也就越来越多:图片、视频、各种文字内容等。为了分而治之,一个页面的内容可能来自不同地方,也就是不同的domain域,因此通过API跨域访问成了必然

浏览器作为进入Internet最大的入口,很长时间它是个大互联公司的必争之地,因此市面上并存的浏览器种类繁多且鱼龙混扎:IE 7、8、9、10,Chrome、Safari、火狐,每个浏览器对跨域的实现可能都不一样。因此对开发者而言亟待需要一个规范的、统一方案,它就是Cors

CORS(Cross-Origin Resource Sharing)由W3C组织于2009-03-17编写工作草案,直到2014-01-16才正式毕业成为行业规范,所有浏览器得以遵守。至此,程序员同学们在解决跨域问题上,只需按照Cors规范实施即可。

Cors的工作原理

Web资源涉及到两个角色:浏览器(消费者)和服务器(提供者),面向这两个角色来了解Cors的原理非常简单,如下图所示:

  1. 若浏览器发送的是个跨域请求,http请求中就会携带一个名为Origin的头表明自己的“位置”,如Origin: http://localhost:5432
  2. 服务端接到请求后,就可以根据传过来的Origin头做逻辑,决定是否要将资源共享给这个源喽。而这个决定通过响应头Access-Control-Allow-Origin来承载,它的value值可以是任意值,有如下情况:
    1. 无此头:不共享给此origin
    2. 有此头:值有如下可能情况
      1. 值为*,通配符,允许所有的Origin共享此资源
      2. 值为http://localhost:5432(也就是和Origin相同),共享给此Origin
      3. 值为非http://localhost:5432(也就是和Origin不相同),不共享给此Origin
  3. 浏览器接收到Response响应后,会去提取Access-Control-Allow-Origin这个头。然后根据上述规则来决定要接收此响应内容还是拒绝

Tips:Access-Control-Allow-Origin响应头只能有1个,且value值就是个字符串。另外,value值即使写为http://aa.com,http://bb.com这种也属于一个而非两个值

Cors细粒度控制:授权响应头

在Cors规范中,除了可以通过Access-Control-Allow-Origin响应头来对主体资源(URL级别)进行授权外,还提供了针对于具体响应头更细粒度的控制,这个响应头就是:Access-Control-Expose-Headers。换句话讲,该头用于规定哪些响应头(们)可以暴露给前端,默认情况下这6个响应头无需特别的显示指定就支持:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

若不在此值里面的头将不会返回给前端(其实返回了,只是浏览器让其对前端不可见了而已,对JavaScript也不可见哦)。

但是,但是,但是,这种细粒度控制header的机制对简单请求是无效的,只针对于非简单请求(也叫复杂请求)。由此可见,将哪些类型的跨域资源请求划分为简单请求的范畴就显得特备重要了。

何为简单请求

Cors规范定义简单请求的原则是:请求不是以更新(添加、修改和删除)资源为目的,服务端对请求的处理不会导致自身维护资源的改变。对于简单跨域资源请求来说,浏览器将两个步骤(取得授权和获取资源)合二为一,由于不涉及到资源的改变,所以不会带来任何副作用。

对于一个请求,必须同时符合如下要求才被划为简单请求:

  1. Http Method只能为其一:

    1. GET
    2. POST
    3. HEAD
  2. 请求头只能在如下范围:
    1. Accept
    2. Accept-Language
    3. Content-Language
    4. Content-Type,其中它的值必须如下其一:
      1. application/x-www-form-urlencoded
      2. multipart/form-data
      3. text/plain

除此之外的请求都为非简单请求(也可称为复杂请求)。非简单请求可能对服务端资源改变,因此Cors规定浏览器在发出此类请求之前必须有一个“预检(Preflight)”机制,这也就是我们熟悉的OPTIONS请求。

什么是Preflight预检机制

顾名思义,它表示在浏览器发出真正请求之前,先发送一个预检请求,这个在Http里就是OPTIONS请求方式。这个请求很特殊,它不包含主体(无请求参数、请求体等),主要就是将一些凭证、授权相关的辅助信息放在请求头里交给服务器去做决策。因此它除了携带Origin请求头外,还会额外携带如下两个请求头

  • Access-Control-Request-Method:真正请求的方法
  • Access-Control-Request-Headers:真正请求的自定义请求头(若没有自定义的就是空呗)

服务端在接收到此类请求后,就可以根据其值做逻辑决策啦。如果允许预检请求通过,返回个200即可,否则返回400或者403呗。

如果预检成功,在响应里应该包含上文提到的响应头Access-Control-Allow-OriginAccess-Control-Expose-Headers,除此之外,服务端还可以做更精细化的控制,这些精细化控制的响应头为:

  • Access-Control-Allow-Methods:允许实际请求的Http方法(们)
  • Access-Control-Allow-Headers:允许实际请求的请求头(们)
  • Access-Control-Max-Age:允许浏览器缓存此结果多久,单位:。有了缓存,以后就不用每次请求都发送预检请求啦

说明:以上响应头并不是必须的。若没有此响应头,代表接受所有


预检请求完成后,有个关键点,便是浏览器拿到预检请求的响应后的处理逻辑,这里描述如下:

  1. 先通过自己的Origin匹配预检响应中的Access-Control-Allow-Origin的值,若不匹配就结束请求,若匹配就继续下一步验证

    1. 关于Access-Control-Allow-Origin的验证逻辑,请参考文上描述
  2. 拿到预检响应中的Access-Control-Allow-Methods头。若此头不存在,则进行下一步,若存在则校验预检请求头Access-Control-Request-Method的值是否在此列表中,在其内继续下一步,否则失败
  3. 拿到预检响应中的Access-Control-Request-Headers头。同请求头中的Access-Control-Allow-Headers值记性比较,全部包含在内则匹配成功,否则失败

以上全部匹配成功,就代表预检成功,可以开始发送正式请求了。值得一提的事,Access-Control-Max-Age控制预检结果的浏览器缓存,若缓存还生效的话,是不用单独再发送OPTIONS请求的,匹配成功直接发送目标真实即可。

Access-Control-Max-Age使用细节

Access-Control-Max-Age用于控制浏览器缓存预检请求结果的时间,这里存在一些使用细节你需要注意:

  1. 若浏览器禁用了缓存,也就是勾选了Disable cache,那么此属性无效。也就说每次都还得发送OPTIONS请求
  2. 判断此缓存结果的因素有两个:
    1. 必须是同一URL(也就是Origin相同才会去找对应的缓存)
    2. header变化了,也会重新去发OPTIONS请求(当然若去掉一些header编程简单请求了,就另当别论喽)

跨域请求代码示例

正所谓说再多,也抵不上跑几个case,毕竟show me your code才是最重要。 下面就针对跨域情况的简单请求、非简单请求(预检通过、预检不通过)等case分别用代码(基于文首代码)说明。

简单请求

简单请求正如其名,是最简单的请求方式。

// 跨域请求
$.get("http://localhost:8080/cors", function (result) {$("#content").append(result).append("<br/>");
});

服务端结果:

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

浏览器结果:


若想让请求正常,只需在服务端响应头里“加点料”就成:

...
resp.setHeader("Access-Control-Allow-Origin","http://localhost:63342");
resp.getWriter().write("hello cors...");
...

再次请求,结果成功:

对于简单请求来讲,服务端只需要设置Access-Control-Allow-Origin这个一个头即可,一个即可。

非简单请求

非简单请求的模拟非常简单,随便打破一个简单请求的约束即可。比如我们先在上面get请求的基础上自定义个请求头:

$.ajax({type: "get",url: "http://localhost:8080/cors",headers: {secret:"kkjtjnbgjlfrfgv",token: "abc123"}
});

服务端代码:

/*** 在此处添加备注信息** @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 {@Overrideprotected 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.setHeader("Access-Control-Allow-Origin","http://localhost:63342");resp.setHeader("Access-Control-Expose-Headers","token,secret");resp.setHeader("Access-Control-Allow-Headers","token,secret"); // 一般来讲,让此头的值是上面那个的【子集】(或相同)resp.getWriter().write("hello cors...");}
}

点击按钮,浏览器发送请求,结果为:

服务端没有任何日志输出,也就是说浏览器并未把实际请求发出去。什么原因?查看OPTIONS请求的返回一看便知:

根本原因为:OPTIONS的响应头里并未含有任何跨域相关信息,虽然预检通过(注意:这个预检是通过的哟,预检不通过的场景就不用额外演示了吧~),但预检的结果经浏览器判断此跨域实际请求不能发出,所以给拦下来了。

从代码层面问题就出现在resp.setHeader(xxx,xxx)放在了处理实际方法的Get方法上,显然不对嘛,应该放在doOptions()方法里才行:

@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {super.doOptions(req, resp);resp.setHeader("Access-Control-Allow-Origin","http://localhost:63342");resp.setHeader("Access-Control-Expose-Headers","token,secret");resp.setHeader("Access-Control-Allow-Headers","token,secret"); // 一般来讲,让此头的值是上面那个的【子集】(或相同)
}

在此运行,一切正常:

值得特别注意的是:设置跨域的响应头这块代码,在处理真实请求的doGet里也必须得有,否则服务端处理了,浏览器“不认”也是会出跨域错误的。

另外就是,Access-Control-Allow-Headers/Access-Control-Expose-Headers这两个头里必须包含你的请求的自定义的Header(标准的header不需要包含),否则依旧跨域失败哦~

在实际生产场景中,Http请求的Content-type大都是application/json并非简单请求的头,所以有个现实情况是:实际的跨域请求中,几乎100%的情况下我们发的都是非简单请求

Cros跨域使用展望

如上代码示例,处理简单请求尚且简单,但对于非简单请求来说,我们在doOptions和doGet都写了一段setHeader的代码,是否觉得麻烦呢?

另外,对于Access-Control-Allow-Origin若我需要允许多个源怎么办呢?

Tips:Access-Control-Allow-Origin头只允许一个,且Access-Control-Allow-Origin:a.com,b.com依旧算作一个源的,它没有逗号分隔的“特性”。从命名的艺术你也可看出,它并非是xxx-Origins而是xxx-Origin

既然实际场景中几乎100%都是非简单请求,那么对于控制非简单请求的Access-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Max-Age这些都都改如何赋值?是否有最佳实践?

现在我们大都在Spring Framework/Spring Boot场景下开发应用,框架层面是否提供一些优雅的解决方案?

作为一名后端开发工程师(编程语言不限),也许你从未处理过跨域问题,那么到底是谁默默的帮你解决了这一切呢?是否想知其所以然?

如果这些问题也是你在使用过程中的疑问,或者希望了解的知识点,那么请关注专栏吧。

总结

本文用很长的篇幅介绍了Cors跨域资源共享的相关知识,并且用代码做了示范,希望能助你通关Cors这个狗皮膏药一样粘着我们的硬核知识点。本文文字叙述较多,介绍了同源、跨域、Cors的几乎所有概念,虽然略显难啃,但这些是指导我们实践的说明书。

革命尚未统一,带着

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

  1. Cors跨域(二):实现跨域Cookie共享的三要素

    高考不努力,工地里当兄弟 前言 你好,我是YourBatman. 上篇文章(Cors跨域(一):深入理解跨域请求概念及其根因)用超万字的篇幅把Cors几乎所有概念都扫盲了,接下来将逐步提出解决方案等实 ...

  2. 企业品牌私域化运营,私域流量只是起步

    文 | 公关之家 作者:Leon 私域是既增长之后备受关注的品牌概念性关键词,同时也是品牌全生命周期中各部门协同工作的重要目标. 企业品牌私域化运营,私域流量只是起步 品牌公关在企业公共关系对社会.媒 ...

  3. 2021-10-13企业品牌私域化运营,私域流量只是起步

    企业品牌私域化运营,私域流量只是起步 私域是既增长之后备受关注的品牌概念性关键词,同时也是品牌全生命周期中各部门协同工作的重要目标.品牌公关在企业公共关系对社会.媒体.公众的关系处理过程中,不仅需要充 ...

  4. 关于CORS跨域问题的理解

    起因 因为这段时间一个项目前后端分别部署在不同服务器的需要,抽空学习了一下CORS问题,不足之处,欢迎指教. 什么是CORS CORS是一个w3c标准,全称是"跨域资源共享"(Cr ...

  5. java单点登录跨域_深入浅出让你理解跨域与SSO单点登录原理与技术

    原标题:深入浅出让你理解跨域与SSO单点登录原理与技术 一:SSO体系结构 SSO SSO英文全称Single Sign On,单点登录.SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互 ...

  6. asp 退出登录修改cookie能进入后台_深入浅出让你理解跨域与SSO单点登录原理与技术...

    一:SSO体系结构 SSO SSO英文全称Single Sign On,单点登录.SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统.它包括可以将这次主要的登录映射到其他应用 ...

  7. 跨域问题深入理解以及解决办法

    跨域问题深入理解以及解决办法 参考文章: (1)跨域问题深入理解以及解决办法 (2)https://www.cnblogs.com/mhtss/p/9355956.html (3)https://ww ...

  8. 跨域的另一种解决方案——CORS(Cross-Origin Resource Sharing)跨域资源共享

    跨域的另一种解决方案--CORS(Cross-Origin Resource Sharing)跨域资源共享 参考文章: (1)跨域的另一种解决方案--CORS(Cross-Origin Resourc ...

  9. 跨域的另一种解决方案CORS(CrossOrigin Resource Sharing)跨域资源共享

    在我们日常的项目开发时使用AJAX,传统的Ajax请求只能获取在同一个域名下面的资源,但是HTML5打破了这个限制,允许Ajax发起跨域的请求.浏览器是可以发起跨域请求的,比如你可以外链一个外域的图片 ...

最新文章

  1. NV21数据 镜像 旋转
  2. WinCE 系统刚启动时运行应用,在应用启动时偶尔出现异常
  3. python字典序列化是什么意思_如何将Python字典序列化为字符串,然后回到字典?...
  4. http/https面试总结
  5. LINQ系列:LINQ to SQL Join连接
  6. java web开发小记(6)将int类型的list插入到mysql数据库
  7. [XSY4220] 九万步(结论)
  8. 光纤收发器长距离的传输过程出现死机的解决方案
  9. Go语言web框架 gin
  10. RedisTemplate常用集合使用说明-opsForZSet(六)
  11. python pow和**_第005篇:Python中的数字
  12. 炒菜机器人放食材的顺序_2年要开1000家 碧桂园旗下千玺机器人餐厅开业
  13. Open3d之颜色映射优化
  14. node cluster 数据共享_深入理解Node.js 中的进程与线程
  15. Java使用zip4j库 压缩文件工具类(自适应压缩包编码格式)
  16. 怎样制作CHM格式的电子书?
  17. js+php+mysql写的rpg网页游戏引擎源码-DotK改进版
  18. realize与recognize辨析
  19. 华为p8 root android6,华为P8root工具
  20. 福清龙华职业中专计算机应用学校什么,福建省福清龙华职业中专学校招生专业|福建省福清龙华职业中专学校有哪些专业...

热门文章

  1. easyexcel 列头合并_2020-05-19:EasyExcel自定义合并单元格
  2. Soul源码总结-01-20
  3. 图片文字转换为文本怎么做?图片转文本的简单方法介绍
  4. git更新项目失败报错 protocol error: bad line length character: Acti
  5. 离职半年了,老东家又发 offer,回不回?
  6. 2008年4月28日A股市场存在筑底异样
  7. 供给云saas平台小程序赋能门店增加销售额
  8. mysql 代理 mycat_使用MyCAT代理MySQL数据库
  9. authconfig‐tui_openldap(3)linux 、ldap、sssd 认证
  10. GB/T 10707 橡胶燃烧性能