diff --git a/404.html b/404.html index 731a5e66f..56e39719c 100644 --- a/404.html +++ b/404.html @@ -31,11 +31,11 @@ ChenSino - - + + -
跳至主要內容
- +
跳至主要內容
+ diff --git a/article/index.html b/article/index.html index 2514f1bd4..bbf222498 100644 --- a/article/index.html +++ b/article/index.html @@ -31,11 +31,11 @@ 文章 | ChenSino - - + + -
跳至主要內容
+ diff --git a/assets/01.html-BqSw6zV7.js b/assets/01.html-DTbWNLK7.js similarity index 99% rename from assets/01.html-BqSw6zV7.js rename to assets/01.html-DTbWNLK7.js index 74a7dff20..b1a698951 100644 --- a/assets/01.html-BqSw6zV7.js +++ b/assets/01.html-DTbWNLK7.js @@ -1 +1 @@ -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as p,a,o as n}from"./app-LRWZkDAq.js";const o={};function i(h,t){return n(),p("div",null,t[0]||(t[0]=[a('

你好,我是王新栋。 在课程正式开始之前,我想先问你个问题。第一次使用极客时间 App 的时候,你是直接使用了第三方帐号(比如微信、微博)登录,还是选择了重新注册新用户?如果你选择了重新注册用户,那你还得上传头像、输入用户名等信息。但如果你选择了使用第三方帐号微信来登录,那极客时间会直接使用你微信的这些信息作为基础信息,你就能省心很多。

到这里,我估计你会问,这是怎么实现的?微信把我的个人信息给了极客时间,它又是怎么保证我的数据安全的呢?

其实,微信这一系列授权背后的原理都可以归到一个词上,那就是 OAuth 2.0。今天这节课,我们就来看看 OAuth 2.0 到底是什么、能干什么以及它是怎么干的。

OAuth 2.0 是什么?

用一句话总结来说,OAuth 2.0 就是一种授权协议。那如何理解这里的“授权”呢?

我举个咱们生活中的例子。假如你是一名销售人员,你想去百度拜访你的大客户王总。到了百度的大楼之后,保安拦住了你,问你要工牌。你说:“保安大哥啊,我是来拜访王总的,哪里有什么工牌”。保安大哥说:“那你要去前台做个登记”。

然后你就赶紧来到前台,前台美女问你是不是做了登记。你说王总秘书昨天有要你的手机号,说是已经做过预约。小姐姐确认之后往你的手机发了个验证码,你把验证码告诉了前台小姐姐之后,她给了你一张门禁卡,于是你就可以开心地去见王总了。

你看,这个例子里面就有一次授权。本来你是没有权限进入百度大楼的,但是经过前台小姐姐一系列的验证之后,她发现你确实是来拜访客户的,于是给了你一张临时工牌。这整个过程就是授权。

我再举一个电商的场景,你估计更有感觉。假如你是一个卖家,在京东商城开了一个店铺,日常运营中你要将订单打印出来以便给用户发货。但打印这事儿也挺繁琐的,之前你总是手工操作,后来发现有个叫“小兔”的第三方软件,它可以帮你高效率地处理这事。

但你想想,小兔是怎么访问到这些订单数据的呢?其实是这样,京东商城提供了开放平台,小兔通过京东商家开放平台的 API 就能访问到用户的订单数据。只要你在软件里点击同意,小兔就可以拿到一个访问令牌,通过访问令牌来获取到你的订单数据帮你干活儿了。你看,这里也是有一次授权。你要是不同意,平台肯定不敢把这些数据给到第三方软件。

为什么用 OAuth 2.0?

基于上面两种场景的解决方案,关于授权我们最容易想到的方案就是提供钥匙。比如,你要去百度拜访王总,那前台小姐姐就给你张百度的工牌;小兔要获取你的订单信息,那你就把你的用户名和密码给它。但稍微有些安全意识,我们都不会这样做。

因为你有了百度工牌,那以后都可以随时自由地进出了,这显然不是百度想要的。所以,百度有一套完整的机制,通过给你一张临时工牌,实现在保证安全的情况下,还能让你去大楼里面见到王总。相应地,小兔软件请求访问你的订单数据的过程,也会涉及这样一套授权机制,那就是 OAuth 2.0。它通过给小兔软件一个访问令牌,而不是让小兔软件拿着你的用户名和密码,去获取你的订单数据帮你干活儿。

其实,除了小兔软件这个场景,在如今的互联网世界里用到 OAuth 2.0 的地方非常多,只是因为它隐藏了实现细节,需要我们多做分析才能发现它。比如,当你使用微信登录其他网站或者 App 的时候,当你开始使用某个小程序的时候,你都在无感知的情况下用到了 OAuth 2.0。

那总结来说, OAuth 2.0 这种授权协议,就是保证第三方(软件)只有在获得授权之后,才可以进一步访问授权者的数据。 因此,我们常常还会听到一种说法,OAuth 2.0 是一种安全协议。现在你知道了,这种说法也是正确的。

现在访问授权者的数据主要是通过 Web API,所以凡是要保护这种对外的 API 时,都需要这样授权的方式。而 OAuth 2.0 的这种颁发访问令牌的机制,是再合适不过的方法了。同时,这样的 Web API 还在持续增加,所以 OAuth 2.0 是目前 Web 上重要的安全手段之一了。

OAuth 2.0 是怎样运转的?

现在,我相信你已经对 OAuth 2.0 有了一个整体印象,接下来咱们再看看它是怎么运转的。

我们还是来看上面提到的小兔打单软件的例子吧。假如小明在京东上面开了一个店铺,小明要管理他的店铺里面的订单,于是选择了使用小兔软件。

现在,让我们把“小明”“小兔软件”“京东商家开放平台”放到一个对话里面,看看“他们”是怎么沟通的吧。

小明:“你好,小兔软件。我正在 Google 浏览器上面,需要访问你来帮我处理我在京东商城店铺的订单。

”小兔软件:“好的,小明,我需要你给我授权。现在我把你引导到京东商家开放平台上,你在那里给我授权吧。

”京东商家开放平台:“你好,小明。我收到了小兔软件跳转过来的请求,现在已经准备好了一个授权页面。你登录并确认后,点击授权页面上面的授权按钮即可。

小明:“好的,京东商家开放平台。我看到了这个授权页面,已经点授权按钮啦😄”

京东商家开放平台:“你好,小兔打单软件。我收到了小明的授权,现在要给你生成一个授权码 code 值,我通过浏览器重定向到你的回调 URL 地址上面了。

”小兔软件:“好的,京东商家开放平台。我现在从浏览器上拿到了授权码,现在就用这个授权码来请求你,请给我一个访问令牌 access_token 吧。”

京东商家开放平台:“好的,小兔打单软件,访问令牌已经发送给你了。”

小兔打单软件:“太好了,我现在就可以使用访问令牌来获取小明店铺的订单了。”

小明:“我已经能够看到我的订单了,现在就开始打单操作了。”下

面,为了帮助你理解,我再用一张图来描述整个过程:

20221108112829
20221108112829

再分析下这个流程,我们不难发现小兔软件最终的目的,是要获取一个叫做“访问令牌”的东西。从最后一步也能够看出来,在小兔软件获取到访问令牌之后,才有足够的 “能力” 去请求小明的店铺的订单,也就是才能够帮助小明打印订单。

那么,小兔软件是怎么获取访问令牌的值的呢?我们会发现还有一个叫做“授权码”的东西,也就是说小兔软件是拿授权码换取的访问令牌。

小兔软件又是怎么拿到授权码的呢?从图中流程刚开始的那一步,我们就会发现,是在小明授权之后,才产生的授权码,上面流程中后续的一切动作,实际上都是在小明对小兔软件授权发生以后才产生的。其中主要的动作,就是生成授权码–> 生成访问令牌–> 使用访问令牌。

到这里,我们不难发现,OAuth 2.0 授权的核心就是颁发访问令牌、使用访问令牌,而且不管是哪种类型的授权流程都是这样。你一定要理解,或者记住这句话,它是整个流程的核心。你也可以再回想下,去百度拜访王总的例子。如果你是百度这套机制的设计者的话,会怎么设计这套授权机制呢。想清楚了这个问题,你再去理解令牌、授权码啥的也就简单了。

在小兔软件这个例子中呢,我们使用的就是授权码许可(Authorization Code)类型。它是 OAuth 2.0 中最经典、最完备、最安全、应用最广泛的许可类型。除了授权码许可类型外,OAuth 2.0 针对不同的使用场景,还有 3 种基础的许可类型,分别是隐式许可(Implicit)、客户端凭据许可(Client Credentials)、资源拥有者凭据许可(Resource Owner Password Credentials)。相对而言,这 3 种授权许可类型的流程,在流程复杂度和安全性上都有所减弱(我会在第 6 讲,与你详细分析)。

因此,在这个课程中,我会频繁用授权码许可类型来举例。至于为什么称它为授权码许可,为什么有两次重定向,以及这种许可类型更详细的通信流程又是怎样的,我会在第 2 讲给你深入分析,你可以先不用关注。

总结

好了,今天这节课就到这里。这节课咱们知识点不多,我来回给你举例子,其实就是希望你能理解 OAuth 到底是什么,为什么需要它,以及它大概的运行逻辑是怎样的。总结来说,我需要你记住以下这 3 个关键点:

  1. OAuth 2.0 的核心是授权许可,更进一步说就是令牌机制。也就是说,像小兔软件这样的第三方软件只有拿到了京东商家开放平台颁发的访问令牌,也就是得到了授权许可,然后才可以代表用户访问他们的数据。
  2. 互联网中所有的受保护资源,几乎都是以 Web API 的形式来提供访问的,比如极客时间 App 要获取用户的头像、昵称,小兔软件要获取用户的店铺订单,我们说 OAuth 2.0 与安全相关,是用来保护 Web API 的。另外,第三方软件通过 OAuth 2.0 取得访问权限之后,用户便把这些权限委托给了第三方软件,我们说 OAuth 2.0 是一种委托协议,也没问题。
  3. 也正因为像小兔这样的第三方软件,每次都是用访问令牌而不是用户名和密码来请求用户的数据,才大大减少了安全风险上的“攻击面”。不然,我们试想一下,每次都带着用户名和密码来访问数量众多的 Web API ,是不是增加了这个“攻击面”。因此,我们说 OAuth 2.0 的核心,就是颁发访问令牌和使用访问令牌。

思考题

好了,今天这一讲我们马上要结束了,我给你留个思考题。你可以再花时间想下小兔软件获取用户订单信息的那个场景,如果让你来设计整个的授权流程,你会怎么设计?还有没有更好的方式?欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

',42)]))}const c=e(o,[["render",i],["__file","01.html.vue"]]),s=JSON.parse('{"path":"/other/oauth2/01.html","title":"01 | OAuth 2.0是要通过什么方式解决什么问题?","lang":"zh-CN","frontmatter":{"title":"01 | OAuth 2.0是要通过什么方式解决什么问题?","date":"2021-11-08T00:00:00.000Z","author":"王新栋","publish":true,"description":"你好,我是王新栋。 在课程正式开始之前,我想先问你个问题。第一次使用极客时间 App 的时候,你是直接使用了第三方帐号(比如微信、微博)登录,还是选择了重新注册新用户?如果你选择了重新注册用户,那你还得上传头像、输入用户名等信息。但如果你选择了使用第三方帐号微信来登录,那极客时间会直接使用你微信的这些信息作为基础信息,你就能省心很多。 到这里,我估计你...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/01.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"01 | OAuth 2.0是要通过什么方式解决什么问题?"}],["meta",{"property":"og:description","content":"你好,我是王新栋。 在课程正式开始之前,我想先问你个问题。第一次使用极客时间 App 的时候,你是直接使用了第三方帐号(比如微信、微博)登录,还是选择了重新注册新用户?如果你选择了重新注册用户,那你还得上传头像、输入用户名等信息。但如果你选择了使用第三方帐号微信来登录,那极客时间会直接使用你微信的这些信息作为基础信息,你就能省心很多。 到这里,我估计你..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221108112829.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"王新栋"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"01 | OAuth 2.0是要通过什么方式解决什么问题?\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108112829.png\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"王新栋\\"}]}"]]},"headers":[{"level":3,"title":"OAuth 2.0 是什么?","slug":"oauth-2-0-是什么","link":"#oauth-2-0-是什么","children":[]},{"level":3,"title":"为什么用 OAuth 2.0?","slug":"为什么用-oauth-2-0","link":"#为什么用-oauth-2-0","children":[]},{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"思考题","slug":"思考题","link":"#思考题","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":10.36,"words":3109},"filePathRelative":"other/oauth2/01.md","localizedDate":"2021年11月8日","excerpt":"

你好,我是王新栋。\\n在课程正式开始之前,我想先问你个问题。第一次使用极客时间 App 的时候,你是直接使用了第三方帐号(比如微信、微博)登录,还是选择了重新注册新用户?如果你选择了重新注册用户,那你还得上传头像、输入用户名等信息。但如果你选择了使用第三方帐号微信来登录,那极客时间会直接使用你微信的这些信息作为基础信息,你就能省心很多。

\\n

到这里,我估计你会问,这是怎么实现的?微信把我的个人信息给了极客时间,它又是怎么保证我的数据安全的呢?

\\n

其实,微信这一系列授权背后的原理都可以归到一个词上,那就是 OAuth 2.0。今天这节课,我们就来看看 OAuth 2.0 到底是什么、能干什么以及它是怎么干的。

","autoDesc":true}');export{c as comp,s as data}; +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as p,a,o as n}from"./app-BIdVr4mq.js";const o={};function i(h,t){return n(),p("div",null,t[0]||(t[0]=[a('

你好,我是王新栋。 在课程正式开始之前,我想先问你个问题。第一次使用极客时间 App 的时候,你是直接使用了第三方帐号(比如微信、微博)登录,还是选择了重新注册新用户?如果你选择了重新注册用户,那你还得上传头像、输入用户名等信息。但如果你选择了使用第三方帐号微信来登录,那极客时间会直接使用你微信的这些信息作为基础信息,你就能省心很多。

到这里,我估计你会问,这是怎么实现的?微信把我的个人信息给了极客时间,它又是怎么保证我的数据安全的呢?

其实,微信这一系列授权背后的原理都可以归到一个词上,那就是 OAuth 2.0。今天这节课,我们就来看看 OAuth 2.0 到底是什么、能干什么以及它是怎么干的。

OAuth 2.0 是什么?

用一句话总结来说,OAuth 2.0 就是一种授权协议。那如何理解这里的“授权”呢?

我举个咱们生活中的例子。假如你是一名销售人员,你想去百度拜访你的大客户王总。到了百度的大楼之后,保安拦住了你,问你要工牌。你说:“保安大哥啊,我是来拜访王总的,哪里有什么工牌”。保安大哥说:“那你要去前台做个登记”。

然后你就赶紧来到前台,前台美女问你是不是做了登记。你说王总秘书昨天有要你的手机号,说是已经做过预约。小姐姐确认之后往你的手机发了个验证码,你把验证码告诉了前台小姐姐之后,她给了你一张门禁卡,于是你就可以开心地去见王总了。

你看,这个例子里面就有一次授权。本来你是没有权限进入百度大楼的,但是经过前台小姐姐一系列的验证之后,她发现你确实是来拜访客户的,于是给了你一张临时工牌。这整个过程就是授权。

我再举一个电商的场景,你估计更有感觉。假如你是一个卖家,在京东商城开了一个店铺,日常运营中你要将订单打印出来以便给用户发货。但打印这事儿也挺繁琐的,之前你总是手工操作,后来发现有个叫“小兔”的第三方软件,它可以帮你高效率地处理这事。

但你想想,小兔是怎么访问到这些订单数据的呢?其实是这样,京东商城提供了开放平台,小兔通过京东商家开放平台的 API 就能访问到用户的订单数据。只要你在软件里点击同意,小兔就可以拿到一个访问令牌,通过访问令牌来获取到你的订单数据帮你干活儿了。你看,这里也是有一次授权。你要是不同意,平台肯定不敢把这些数据给到第三方软件。

为什么用 OAuth 2.0?

基于上面两种场景的解决方案,关于授权我们最容易想到的方案就是提供钥匙。比如,你要去百度拜访王总,那前台小姐姐就给你张百度的工牌;小兔要获取你的订单信息,那你就把你的用户名和密码给它。但稍微有些安全意识,我们都不会这样做。

因为你有了百度工牌,那以后都可以随时自由地进出了,这显然不是百度想要的。所以,百度有一套完整的机制,通过给你一张临时工牌,实现在保证安全的情况下,还能让你去大楼里面见到王总。相应地,小兔软件请求访问你的订单数据的过程,也会涉及这样一套授权机制,那就是 OAuth 2.0。它通过给小兔软件一个访问令牌,而不是让小兔软件拿着你的用户名和密码,去获取你的订单数据帮你干活儿。

其实,除了小兔软件这个场景,在如今的互联网世界里用到 OAuth 2.0 的地方非常多,只是因为它隐藏了实现细节,需要我们多做分析才能发现它。比如,当你使用微信登录其他网站或者 App 的时候,当你开始使用某个小程序的时候,你都在无感知的情况下用到了 OAuth 2.0。

那总结来说, OAuth 2.0 这种授权协议,就是保证第三方(软件)只有在获得授权之后,才可以进一步访问授权者的数据。 因此,我们常常还会听到一种说法,OAuth 2.0 是一种安全协议。现在你知道了,这种说法也是正确的。

现在访问授权者的数据主要是通过 Web API,所以凡是要保护这种对外的 API 时,都需要这样授权的方式。而 OAuth 2.0 的这种颁发访问令牌的机制,是再合适不过的方法了。同时,这样的 Web API 还在持续增加,所以 OAuth 2.0 是目前 Web 上重要的安全手段之一了。

OAuth 2.0 是怎样运转的?

现在,我相信你已经对 OAuth 2.0 有了一个整体印象,接下来咱们再看看它是怎么运转的。

我们还是来看上面提到的小兔打单软件的例子吧。假如小明在京东上面开了一个店铺,小明要管理他的店铺里面的订单,于是选择了使用小兔软件。

现在,让我们把“小明”“小兔软件”“京东商家开放平台”放到一个对话里面,看看“他们”是怎么沟通的吧。

小明:“你好,小兔软件。我正在 Google 浏览器上面,需要访问你来帮我处理我在京东商城店铺的订单。

”小兔软件:“好的,小明,我需要你给我授权。现在我把你引导到京东商家开放平台上,你在那里给我授权吧。

”京东商家开放平台:“你好,小明。我收到了小兔软件跳转过来的请求,现在已经准备好了一个授权页面。你登录并确认后,点击授权页面上面的授权按钮即可。

小明:“好的,京东商家开放平台。我看到了这个授权页面,已经点授权按钮啦😄”

京东商家开放平台:“你好,小兔打单软件。我收到了小明的授权,现在要给你生成一个授权码 code 值,我通过浏览器重定向到你的回调 URL 地址上面了。

”小兔软件:“好的,京东商家开放平台。我现在从浏览器上拿到了授权码,现在就用这个授权码来请求你,请给我一个访问令牌 access_token 吧。”

京东商家开放平台:“好的,小兔打单软件,访问令牌已经发送给你了。”

小兔打单软件:“太好了,我现在就可以使用访问令牌来获取小明店铺的订单了。”

小明:“我已经能够看到我的订单了,现在就开始打单操作了。”下

面,为了帮助你理解,我再用一张图来描述整个过程:

20221108112829
20221108112829

再分析下这个流程,我们不难发现小兔软件最终的目的,是要获取一个叫做“访问令牌”的东西。从最后一步也能够看出来,在小兔软件获取到访问令牌之后,才有足够的 “能力” 去请求小明的店铺的订单,也就是才能够帮助小明打印订单。

那么,小兔软件是怎么获取访问令牌的值的呢?我们会发现还有一个叫做“授权码”的东西,也就是说小兔软件是拿授权码换取的访问令牌。

小兔软件又是怎么拿到授权码的呢?从图中流程刚开始的那一步,我们就会发现,是在小明授权之后,才产生的授权码,上面流程中后续的一切动作,实际上都是在小明对小兔软件授权发生以后才产生的。其中主要的动作,就是生成授权码–> 生成访问令牌–> 使用访问令牌。

到这里,我们不难发现,OAuth 2.0 授权的核心就是颁发访问令牌、使用访问令牌,而且不管是哪种类型的授权流程都是这样。你一定要理解,或者记住这句话,它是整个流程的核心。你也可以再回想下,去百度拜访王总的例子。如果你是百度这套机制的设计者的话,会怎么设计这套授权机制呢。想清楚了这个问题,你再去理解令牌、授权码啥的也就简单了。

在小兔软件这个例子中呢,我们使用的就是授权码许可(Authorization Code)类型。它是 OAuth 2.0 中最经典、最完备、最安全、应用最广泛的许可类型。除了授权码许可类型外,OAuth 2.0 针对不同的使用场景,还有 3 种基础的许可类型,分别是隐式许可(Implicit)、客户端凭据许可(Client Credentials)、资源拥有者凭据许可(Resource Owner Password Credentials)。相对而言,这 3 种授权许可类型的流程,在流程复杂度和安全性上都有所减弱(我会在第 6 讲,与你详细分析)。

因此,在这个课程中,我会频繁用授权码许可类型来举例。至于为什么称它为授权码许可,为什么有两次重定向,以及这种许可类型更详细的通信流程又是怎样的,我会在第 2 讲给你深入分析,你可以先不用关注。

总结

好了,今天这节课就到这里。这节课咱们知识点不多,我来回给你举例子,其实就是希望你能理解 OAuth 到底是什么,为什么需要它,以及它大概的运行逻辑是怎样的。总结来说,我需要你记住以下这 3 个关键点:

  1. OAuth 2.0 的核心是授权许可,更进一步说就是令牌机制。也就是说,像小兔软件这样的第三方软件只有拿到了京东商家开放平台颁发的访问令牌,也就是得到了授权许可,然后才可以代表用户访问他们的数据。
  2. 互联网中所有的受保护资源,几乎都是以 Web API 的形式来提供访问的,比如极客时间 App 要获取用户的头像、昵称,小兔软件要获取用户的店铺订单,我们说 OAuth 2.0 与安全相关,是用来保护 Web API 的。另外,第三方软件通过 OAuth 2.0 取得访问权限之后,用户便把这些权限委托给了第三方软件,我们说 OAuth 2.0 是一种委托协议,也没问题。
  3. 也正因为像小兔这样的第三方软件,每次都是用访问令牌而不是用户名和密码来请求用户的数据,才大大减少了安全风险上的“攻击面”。不然,我们试想一下,每次都带着用户名和密码来访问数量众多的 Web API ,是不是增加了这个“攻击面”。因此,我们说 OAuth 2.0 的核心,就是颁发访问令牌和使用访问令牌。

思考题

好了,今天这一讲我们马上要结束了,我给你留个思考题。你可以再花时间想下小兔软件获取用户订单信息的那个场景,如果让你来设计整个的授权流程,你会怎么设计?还有没有更好的方式?欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

',42)]))}const c=e(o,[["render",i],["__file","01.html.vue"]]),s=JSON.parse('{"path":"/other/oauth2/01.html","title":"01 | OAuth 2.0是要通过什么方式解决什么问题?","lang":"zh-CN","frontmatter":{"title":"01 | OAuth 2.0是要通过什么方式解决什么问题?","date":"2021-11-08T00:00:00.000Z","author":"王新栋","publish":true,"description":"你好,我是王新栋。 在课程正式开始之前,我想先问你个问题。第一次使用极客时间 App 的时候,你是直接使用了第三方帐号(比如微信、微博)登录,还是选择了重新注册新用户?如果你选择了重新注册用户,那你还得上传头像、输入用户名等信息。但如果你选择了使用第三方帐号微信来登录,那极客时间会直接使用你微信的这些信息作为基础信息,你就能省心很多。 到这里,我估计你...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/01.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"01 | OAuth 2.0是要通过什么方式解决什么问题?"}],["meta",{"property":"og:description","content":"你好,我是王新栋。 在课程正式开始之前,我想先问你个问题。第一次使用极客时间 App 的时候,你是直接使用了第三方帐号(比如微信、微博)登录,还是选择了重新注册新用户?如果你选择了重新注册用户,那你还得上传头像、输入用户名等信息。但如果你选择了使用第三方帐号微信来登录,那极客时间会直接使用你微信的这些信息作为基础信息,你就能省心很多。 到这里,我估计你..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221108112829.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"王新栋"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"01 | OAuth 2.0是要通过什么方式解决什么问题?\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108112829.png\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"王新栋\\"}]}"]]},"headers":[{"level":3,"title":"OAuth 2.0 是什么?","slug":"oauth-2-0-是什么","link":"#oauth-2-0-是什么","children":[]},{"level":3,"title":"为什么用 OAuth 2.0?","slug":"为什么用-oauth-2-0","link":"#为什么用-oauth-2-0","children":[]},{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"思考题","slug":"思考题","link":"#思考题","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":10.36,"words":3109},"filePathRelative":"other/oauth2/01.md","localizedDate":"2021年11月8日","excerpt":"

你好,我是王新栋。\\n在课程正式开始之前,我想先问你个问题。第一次使用极客时间 App 的时候,你是直接使用了第三方帐号(比如微信、微博)登录,还是选择了重新注册新用户?如果你选择了重新注册用户,那你还得上传头像、输入用户名等信息。但如果你选择了使用第三方帐号微信来登录,那极客时间会直接使用你微信的这些信息作为基础信息,你就能省心很多。

\\n

到这里,我估计你会问,这是怎么实现的?微信把我的个人信息给了极客时间,它又是怎么保证我的数据安全的呢?

\\n

其实,微信这一系列授权背后的原理都可以归到一个词上,那就是 OAuth 2.0。今天这节课,我们就来看看 OAuth 2.0 到底是什么、能干什么以及它是怎么干的。

","autoDesc":true}');export{c as comp,s as data}; diff --git a/assets/02.html-hTc330ny.js b/assets/02.html-0inoDdd1.js similarity index 99% rename from assets/02.html-hTc330ny.js rename to assets/02.html-0inoDdd1.js index f1253d00c..16930383c 100644 --- a/assets/02.html-hTc330ny.js +++ b/assets/02.html-0inoDdd1.js @@ -1 +1 @@ -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a,o as n}from"./app-LRWZkDAq.js";const p={};function s(h,e){return n(),i("div",null,e[0]||(e[0]=[a('

你好,我是王新栋。

在上一讲,我提到了 OAuth 2.0 的授权码许可类型,在小兔打单软件的例子里面,小兔最终是通过访问令牌请求到小明的店铺里的订单数据。同时呢,我还提到了,这个访问令牌是通过授权码换来的。到这里估计你会问了,为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢?

你可以先停下来想想这个问题。今天咱们这节课,我会带着你深入探究下其中的逻辑。

为什么需要授权码?

在讲这个问题之前,我先要和你同步下,在 OAuth 2.0 的体系里面有 4 种角色,按照官方的称呼它们分别是资源拥有者、客户端、授权服务和受保护资源。不过,这里的客户端,我更愿意称其为第三方软件,而且在咱们这个课程中,都是以第三方软件在举例子。所以,在后续的讲解中我统一把它称为第三方软件。

所以,你在看官方资料的时候,可以自己对应下。为了便于你理解,我还是拿小兔软件来举例子,将官方的称呼 “照进现实”,对应关系就是,资源拥有者 -> 小明,第三方软件 -> 小兔软件,授权服务 -> 京东商家开放平台的授权服务,受保护资源 -> 小明店铺在京东上面的订单。

在理解了这些概念以后,让我们继续。

你知道,OAuth 诞生之初就是为了解决 Web 浏览器场景下的授权问题,所以我基于浏览器的场景,在上一讲的小明使用小兔软件打印订单的整体流程的基础上,画了一个授权码许可类型的序列图。

当然了,这里还是有小兔软件来继续陪伴着我们,不过这次为了能够更好地表述授权码许可流程,我会把小兔软件的前端和后端分开展示,并把京东商家开放平台的系统按照 OAuth 2.0 的组件拆分成了授权服务和受保护资源服务。如下图所示: 图1 以小兔软件为例,授权码许可类型的序列图

突然看到这个序列图增加了这么多步骤的时候,你是不是有些紧张?那如果我告诉你再细分的话步骤还要更多,你是不是就更困惑了?

不过,别紧张,这没啥关系。一方面,咱们这一讲的重点就是跟授权码相关的流程,你只需关注这里的重点步骤,也就是两次重定向相关的步骤就够了。在下一讲中,我再教你如何将这些步骤进一步拆解。另一方面,我接下来还会用另一种视角来帮助你分析这个流程。

我们继续来看这张序列图。从图中看到,在第 4 步授权服务生成了授权码 code,按照一开始我们提出来的问题,如果不要授权码,这一步实际上就可以直接返回访问令牌 access_token 了。

按着这个没有授权码的思路继续想,如果这里直接返回访问令牌,那我们肯定不能使用重定向的方式。因为这样会把安全保密性要求极高的访问令牌暴露在浏览器上,从而将会面临访问令牌失窃的安全风险。显然,这是不能被允许的。

也就是说,如果没有授权码的话,我们就只能把访问令牌发送给第三方软件小兔的后端服务。按照这样的逻辑,上面的流程图就会变成下面这样: 图2 如果没有授权码,直接把访问令牌发送给第三方软件小兔的后端服务

到这里,看起来天衣无缝。小明访问小兔软件,小兔软件说要打单你得给我授权,不然京东不干,然后小兔软件就引导小明跳转到了京东的授权服务。到授权服务之后,京东商家开放平台验证了小兔的合法性以及小明的登录状态后,生成了授权页面。紧接着,小明赶紧点击同意授权,这时候,京东商家开放平台知道可以把小明的订单数据给小兔软件。

于是,京东商家开放平台没含糊,赶紧生成访问令牌 access_token,并且通过后端服务的方式返回给了小兔软件。这时候,小兔软件就能正常工作了。

这样,问题就来了,什么问题呢?当小明被浏览器重定向到授权服务上之后,小明跟小兔软件之间的 “连接” 就断了,相当于此时此刻小明跟授权服务建立了“连接”后,将一直“停留在授权服务的页面上”。你会看到图 2 中问号处的时序上,小明再也没有重新“连接”到小兔软件。

但是,这个时候小兔软件已经拿到了小明授权之后的访问令牌,也使用访问令牌获取到了小明店铺里的订单数据。这时,考虑到“小明的感受”,小兔软件应该要通知到小明,但是如何做呢?现在“连接断了”,这事儿恐怕就没那么容易了。

OK,为了让小兔软件能很容易地通知到小明,还必须让小明跟小兔软件重新建立起 “连接”。这就是我们看到的第二次重定向,小明授权之后,又重新重定向回到了小兔软件的地址上,这样小明就跟小兔软件有了新的 “连接”。

到这里,你就能理解在授权码许可的流程中,为什么需要两次重定向了吧。

为了重新建立起这样的一次连接,我们又不能让访问令牌暴露出去,就有了这样一个临时的、间接的凭证:授权码。因为小兔软件最终要拿到的是安全保密性要求极高的访问令牌,并不是授权码,而授权码是可以暴露在浏览器上面的。这样有了授权码的参与,访问令牌可以在后端服务之间传输,同时呢还可以重新建立小明与小兔软件之间的“连接”。这样通过一个授权码,既“照顾”到了小明的体验,又“照顾”了通信的安全。

这下,你就知道为什么要有授权码了吧。

那么,在执行授权码流程的时候,授权码和访问令牌在小兔软件和授权服务之间到底是怎么流转的呢?要回答这个问题,就需要继续分析一下授权码许可类型的通信过程了。

授权码许可类型的通信过程

图 1 的通信过程中标识出来的步骤就有 9 个,一步步地去分析看似会很复杂,所以我会用另一个维度来分析以帮助你理解,也就是从直接通信和间接通信的维度来分析。这里所谓的间接通信就是指获取授权码的交互,而直接通信就是指通过授权码换取访问令牌的交互。

接下来,我们就一起分析下吧,看看哪些是间接通信,哪些又是直接通信。

间接通信我们先分析下为什么是“间接”。我们把图 1 中获取授权码 code 的流程 “放大”,并换个角度来看一看,也就是将浏览器这个代理放到第三方软件小兔和授权服务中间。于是,我们来到了下面这张图: 图3 获取授权码的交互过程

这个过程,仿佛有这样的一段对话。

小明:“你好,小兔软件,我要访问你了。 ”小兔软件:“好的,我把你引到授权服务那里,我需要授权服务给我一个授权码。” 授权服务:“小兔软件,我把授权码发给浏览器了。” 小兔软件:“好的,我从浏览器拿到了授权码。”

不知道你注意到没有,第三方软件小兔和授权服务之间,并没有发生直接的通信,而是 通过浏览器这个“中间人” 来 “搭线”的。 因此,我们说这是一个间接通信的方式。

直接通信

那我们再分析下,授权码换取访问令牌的交互,为什么是“直接”的。我们再把图 1 中获取访问令牌的流程“放大”,就得到了下面的图示: 图4 授权码换取访问令牌的交互过程

相比获取授权码过程的间接通信,获取访问令牌的直接通信就比较容易理解了,就是第三方软件小兔获取到授权码 code 值后,向授权服务发起获取访问令牌 access_token 的通信请求。这个请求是第三方软件服务器跟授权服务的服务器之间的通信,都是在后端服务器之间的请求和响应,因此也叫作后端通信。

两个 “一伙”

了解了上面的通信方式之后,不知道你有没有意识到,OAuth 2.0 中的 4 个角色是 “两两站队” 的:资源拥有者和第三方软件“站在一起”,因为第三方软件要代表资源拥有者去访问受保护资源;授权服务和受保护资源“站在一起”,因为授权服务负责颁发访问令牌,受保护资源负责接收并验证访问令牌。 图5 OAuth 2.0 中的4个角色是“两两站队”

讲到这里的时候,你会发现在这一讲,介绍授权码流程的时候我都是以浏览器参与的场景来讲的,那么浏览器一定要参与到这个流程中吗?其实,授权码许可流程,不一定要有浏览器的参与。接下来,我们就继续分析下其中的逻辑。

一定要有浏览器吗?

OAuth 2.0 发展之初,开放生态环境相对单薄,以浏览器为代理的 Web 应用居多,授权码许可类型 “理所当然” 地被应用到了通过浏览器才能访问的 Web 应用中。

但实际上,OAuth 2.0 是一个授权理念,或者说是一种授权思维。它的授权码模式的思维可以移植到很多场景中,比如微信小程序。在开发微信小程序应用时,我们通过授权码模式获取用户登录信息,官方文档的地址示例中给出的 grant_type=authorization_code ,就没有用到浏览器。

根据微信官方文档描述,开发者获取用户登录态信息的过程正是一个授权码的许可流程:

你可以看到,这个过程并没有使用到浏览器,但确实按照授权码许可的思想走了一个完整的授权码许可流程。也就是说,先通过小程序前端获取到 code 值,再通过小程序的后端服务使用 code 值换取 session_key 等信息,只不过是访问令牌 access_token 的值被换成了 session_key。

GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

你看,这整个过程体现的就是授权码许可流程的思想。

总结

这节课又接近尾声了,我再带你回顾下重点内容。

今天,我从为什么需要授权码这个问题开始讲起,并通过授权码把授权码许可流程整体的通信过程串了一遍,提到了授权码这种方式解决的问题,也提到了授权码流程的通信方式。总结来说,我需要你记住以下两点。

  1. 授权码许可流程有两种通信方式。一种是前端通信,因为它通过浏览器促成了授权码的交互流程,比如京东商家开放平台的授权服务生成授权码发送到浏览器,第三方软件小兔从浏览器获取授权码。正因为获取授权码的时候小兔软件和授权服务并没有发生直接的联系,也叫做间接通信。另外一种是后端通信,在小兔软件获取到授权码之后,在后端服务直接发起换取访问令牌的请求,也叫做直接通信。
  2. 在 OAuth 2.0 中,访问令牌被要求有极高的安全保密性,因此我们不能让它暴露在浏览器上面,只能通过第三方软件(比如小兔)的后端服务来获取和使用,以最大限度地保障访问令牌的安全性。正因为访问令牌的这种安全要求特性,当需要前端通信,比如浏览器上面的流转的时候,OAuth 2.0 才又提供了一个临时的凭证:授权码。通过授权码的方式,可以让用户小明在授权服务上给小兔授权之后,还能重新回到小兔的操作页面上。这样,在保障安全性的情况下,提升了小明在小兔上的体验。

从授权码许可流程中就可以看出来,它完美地将 OAuth 2.0 的 4 个角色组织了起来,并保证了它们之间的顺畅通信。它提出的这种结构和思想都可以被迁移到其他环境或者协议上,比如在微信小程序中使用授权码许可。

不过,也正是因为有了授权码的参与,才使得授权码许可要比其他授权许可类型,在授权的流程上多出了好多步骤,让授权码许可类型成为了 OAuth 2.0 体系中迄今流程最完备、安全性最高的授权流程。在接下来的两讲中,我还会为你重点讲解授权码许可类型下的授权服务。

思考题

好了,今天这一讲我们马上要结束了,我给你留个思考题。关于不需要浏览器参与的授权码许可流程,你还能列举出更多的应用场景吗?

',52)]))}const r=t(p,[["render",s],["__file","02.html.vue"]]),c=JSON.parse('{"path":"/other/oauth2/02.html","title":"02 | 授权码许可类型中,为什么一定要有授权码?","lang":"zh-CN","frontmatter":{"title":"02 | 授权码许可类型中,为什么一定要有授权码?","date":"2021-11-08T00:00:00.000Z","author":"ChenSino","publish":true,"description":"你好,我是王新栋。 在上一讲,我提到了 OAuth 2.0 的授权码许可类型,在小兔打单软件的例子里面,小兔最终是通过访问令牌请求到小明的店铺里的订单数据。同时呢,我还提到了,这个访问令牌是通过授权码换来的。到这里估计你会问了,为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢? 你可以先停下来想想这个问题。今天咱们这节课,我会带着你深入探究下其中...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/02.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"02 | 授权码许可类型中,为什么一定要有授权码?"}],["meta",{"property":"og:description","content":"你好,我是王新栋。 在上一讲,我提到了 OAuth 2.0 的授权码许可类型,在小兔打单软件的例子里面,小兔最终是通过访问令牌请求到小明的店铺里的订单数据。同时呢,我还提到了,这个访问令牌是通过授权码换来的。到这里估计你会问了,为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢? 你可以先停下来想想这个问题。今天咱们这节课,我会带着你深入探究下其中..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221108113248.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"ChenSino"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"02 | 授权码许可类型中,为什么一定要有授权码?\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108113248.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108113402.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108113557.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108113739.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108113820.png\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\"}]}"]]},"headers":[{"level":3,"title":"为什么需要授权码?","slug":"为什么需要授权码","link":"#为什么需要授权码","children":[]},{"level":3,"title":"授权码许可类型的通信过程","slug":"授权码许可类型的通信过程","link":"#授权码许可类型的通信过程","children":[]},{"level":3,"title":"直接通信","slug":"直接通信","link":"#直接通信","children":[]},{"level":3,"title":"两个 “一伙”","slug":"两个-一伙","link":"#两个-一伙","children":[]},{"level":3,"title":"一定要有浏览器吗?","slug":"一定要有浏览器吗","link":"#一定要有浏览器吗","children":[]},{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"思考题","slug":"思考题","link":"#思考题","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":4},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":12.79,"words":3838},"filePathRelative":"other/oauth2/02.md","localizedDate":"2021年11月8日","excerpt":"

你好,我是王新栋。

\\n

在上一讲,我提到了 OAuth 2.0 的授权码许可类型,在小兔打单软件的例子里面,小兔最终是通过访问令牌请求到小明的店铺里的订单数据。同时呢,我还提到了,这个访问令牌是通过授权码换来的。到这里估计你会问了,为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢?

\\n

你可以先停下来想想这个问题。今天咱们这节课,我会带着你深入探究下其中的逻辑。

\\n

为什么需要授权码?

\\n

在讲这个问题之前,我先要和你同步下,在 OAuth 2.0 的体系里面有 4 种角色,按照官方的称呼它们分别是资源拥有者、客户端、授权服务和受保护资源。不过,这里的客户端,我更愿意称其为第三方软件,而且在咱们这个课程中,都是以第三方软件在举例子。所以,在后续的讲解中我统一把它称为第三方软件。

","autoDesc":true}');export{r as comp,c as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a,o as n}from"./app-BIdVr4mq.js";const p={};function s(h,e){return n(),i("div",null,e[0]||(e[0]=[a('

你好,我是王新栋。

在上一讲,我提到了 OAuth 2.0 的授权码许可类型,在小兔打单软件的例子里面,小兔最终是通过访问令牌请求到小明的店铺里的订单数据。同时呢,我还提到了,这个访问令牌是通过授权码换来的。到这里估计你会问了,为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢?

你可以先停下来想想这个问题。今天咱们这节课,我会带着你深入探究下其中的逻辑。

为什么需要授权码?

在讲这个问题之前,我先要和你同步下,在 OAuth 2.0 的体系里面有 4 种角色,按照官方的称呼它们分别是资源拥有者、客户端、授权服务和受保护资源。不过,这里的客户端,我更愿意称其为第三方软件,而且在咱们这个课程中,都是以第三方软件在举例子。所以,在后续的讲解中我统一把它称为第三方软件。

所以,你在看官方资料的时候,可以自己对应下。为了便于你理解,我还是拿小兔软件来举例子,将官方的称呼 “照进现实”,对应关系就是,资源拥有者 -> 小明,第三方软件 -> 小兔软件,授权服务 -> 京东商家开放平台的授权服务,受保护资源 -> 小明店铺在京东上面的订单。

在理解了这些概念以后,让我们继续。

你知道,OAuth 诞生之初就是为了解决 Web 浏览器场景下的授权问题,所以我基于浏览器的场景,在上一讲的小明使用小兔软件打印订单的整体流程的基础上,画了一个授权码许可类型的序列图。

当然了,这里还是有小兔软件来继续陪伴着我们,不过这次为了能够更好地表述授权码许可流程,我会把小兔软件的前端和后端分开展示,并把京东商家开放平台的系统按照 OAuth 2.0 的组件拆分成了授权服务和受保护资源服务。如下图所示: 图1 以小兔软件为例,授权码许可类型的序列图

突然看到这个序列图增加了这么多步骤的时候,你是不是有些紧张?那如果我告诉你再细分的话步骤还要更多,你是不是就更困惑了?

不过,别紧张,这没啥关系。一方面,咱们这一讲的重点就是跟授权码相关的流程,你只需关注这里的重点步骤,也就是两次重定向相关的步骤就够了。在下一讲中,我再教你如何将这些步骤进一步拆解。另一方面,我接下来还会用另一种视角来帮助你分析这个流程。

我们继续来看这张序列图。从图中看到,在第 4 步授权服务生成了授权码 code,按照一开始我们提出来的问题,如果不要授权码,这一步实际上就可以直接返回访问令牌 access_token 了。

按着这个没有授权码的思路继续想,如果这里直接返回访问令牌,那我们肯定不能使用重定向的方式。因为这样会把安全保密性要求极高的访问令牌暴露在浏览器上,从而将会面临访问令牌失窃的安全风险。显然,这是不能被允许的。

也就是说,如果没有授权码的话,我们就只能把访问令牌发送给第三方软件小兔的后端服务。按照这样的逻辑,上面的流程图就会变成下面这样: 图2 如果没有授权码,直接把访问令牌发送给第三方软件小兔的后端服务

到这里,看起来天衣无缝。小明访问小兔软件,小兔软件说要打单你得给我授权,不然京东不干,然后小兔软件就引导小明跳转到了京东的授权服务。到授权服务之后,京东商家开放平台验证了小兔的合法性以及小明的登录状态后,生成了授权页面。紧接着,小明赶紧点击同意授权,这时候,京东商家开放平台知道可以把小明的订单数据给小兔软件。

于是,京东商家开放平台没含糊,赶紧生成访问令牌 access_token,并且通过后端服务的方式返回给了小兔软件。这时候,小兔软件就能正常工作了。

这样,问题就来了,什么问题呢?当小明被浏览器重定向到授权服务上之后,小明跟小兔软件之间的 “连接” 就断了,相当于此时此刻小明跟授权服务建立了“连接”后,将一直“停留在授权服务的页面上”。你会看到图 2 中问号处的时序上,小明再也没有重新“连接”到小兔软件。

但是,这个时候小兔软件已经拿到了小明授权之后的访问令牌,也使用访问令牌获取到了小明店铺里的订单数据。这时,考虑到“小明的感受”,小兔软件应该要通知到小明,但是如何做呢?现在“连接断了”,这事儿恐怕就没那么容易了。

OK,为了让小兔软件能很容易地通知到小明,还必须让小明跟小兔软件重新建立起 “连接”。这就是我们看到的第二次重定向,小明授权之后,又重新重定向回到了小兔软件的地址上,这样小明就跟小兔软件有了新的 “连接”。

到这里,你就能理解在授权码许可的流程中,为什么需要两次重定向了吧。

为了重新建立起这样的一次连接,我们又不能让访问令牌暴露出去,就有了这样一个临时的、间接的凭证:授权码。因为小兔软件最终要拿到的是安全保密性要求极高的访问令牌,并不是授权码,而授权码是可以暴露在浏览器上面的。这样有了授权码的参与,访问令牌可以在后端服务之间传输,同时呢还可以重新建立小明与小兔软件之间的“连接”。这样通过一个授权码,既“照顾”到了小明的体验,又“照顾”了通信的安全。

这下,你就知道为什么要有授权码了吧。

那么,在执行授权码流程的时候,授权码和访问令牌在小兔软件和授权服务之间到底是怎么流转的呢?要回答这个问题,就需要继续分析一下授权码许可类型的通信过程了。

授权码许可类型的通信过程

图 1 的通信过程中标识出来的步骤就有 9 个,一步步地去分析看似会很复杂,所以我会用另一个维度来分析以帮助你理解,也就是从直接通信和间接通信的维度来分析。这里所谓的间接通信就是指获取授权码的交互,而直接通信就是指通过授权码换取访问令牌的交互。

接下来,我们就一起分析下吧,看看哪些是间接通信,哪些又是直接通信。

间接通信我们先分析下为什么是“间接”。我们把图 1 中获取授权码 code 的流程 “放大”,并换个角度来看一看,也就是将浏览器这个代理放到第三方软件小兔和授权服务中间。于是,我们来到了下面这张图: 图3 获取授权码的交互过程

这个过程,仿佛有这样的一段对话。

小明:“你好,小兔软件,我要访问你了。 ”小兔软件:“好的,我把你引到授权服务那里,我需要授权服务给我一个授权码。” 授权服务:“小兔软件,我把授权码发给浏览器了。” 小兔软件:“好的,我从浏览器拿到了授权码。”

不知道你注意到没有,第三方软件小兔和授权服务之间,并没有发生直接的通信,而是 通过浏览器这个“中间人” 来 “搭线”的。 因此,我们说这是一个间接通信的方式。

直接通信

那我们再分析下,授权码换取访问令牌的交互,为什么是“直接”的。我们再把图 1 中获取访问令牌的流程“放大”,就得到了下面的图示: 图4 授权码换取访问令牌的交互过程

相比获取授权码过程的间接通信,获取访问令牌的直接通信就比较容易理解了,就是第三方软件小兔获取到授权码 code 值后,向授权服务发起获取访问令牌 access_token 的通信请求。这个请求是第三方软件服务器跟授权服务的服务器之间的通信,都是在后端服务器之间的请求和响应,因此也叫作后端通信。

两个 “一伙”

了解了上面的通信方式之后,不知道你有没有意识到,OAuth 2.0 中的 4 个角色是 “两两站队” 的:资源拥有者和第三方软件“站在一起”,因为第三方软件要代表资源拥有者去访问受保护资源;授权服务和受保护资源“站在一起”,因为授权服务负责颁发访问令牌,受保护资源负责接收并验证访问令牌。 图5 OAuth 2.0 中的4个角色是“两两站队”

讲到这里的时候,你会发现在这一讲,介绍授权码流程的时候我都是以浏览器参与的场景来讲的,那么浏览器一定要参与到这个流程中吗?其实,授权码许可流程,不一定要有浏览器的参与。接下来,我们就继续分析下其中的逻辑。

一定要有浏览器吗?

OAuth 2.0 发展之初,开放生态环境相对单薄,以浏览器为代理的 Web 应用居多,授权码许可类型 “理所当然” 地被应用到了通过浏览器才能访问的 Web 应用中。

但实际上,OAuth 2.0 是一个授权理念,或者说是一种授权思维。它的授权码模式的思维可以移植到很多场景中,比如微信小程序。在开发微信小程序应用时,我们通过授权码模式获取用户登录信息,官方文档的地址示例中给出的 grant_type=authorization_code ,就没有用到浏览器。

根据微信官方文档描述,开发者获取用户登录态信息的过程正是一个授权码的许可流程:

你可以看到,这个过程并没有使用到浏览器,但确实按照授权码许可的思想走了一个完整的授权码许可流程。也就是说,先通过小程序前端获取到 code 值,再通过小程序的后端服务使用 code 值换取 session_key 等信息,只不过是访问令牌 access_token 的值被换成了 session_key。

GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

你看,这整个过程体现的就是授权码许可流程的思想。

总结

这节课又接近尾声了,我再带你回顾下重点内容。

今天,我从为什么需要授权码这个问题开始讲起,并通过授权码把授权码许可流程整体的通信过程串了一遍,提到了授权码这种方式解决的问题,也提到了授权码流程的通信方式。总结来说,我需要你记住以下两点。

  1. 授权码许可流程有两种通信方式。一种是前端通信,因为它通过浏览器促成了授权码的交互流程,比如京东商家开放平台的授权服务生成授权码发送到浏览器,第三方软件小兔从浏览器获取授权码。正因为获取授权码的时候小兔软件和授权服务并没有发生直接的联系,也叫做间接通信。另外一种是后端通信,在小兔软件获取到授权码之后,在后端服务直接发起换取访问令牌的请求,也叫做直接通信。
  2. 在 OAuth 2.0 中,访问令牌被要求有极高的安全保密性,因此我们不能让它暴露在浏览器上面,只能通过第三方软件(比如小兔)的后端服务来获取和使用,以最大限度地保障访问令牌的安全性。正因为访问令牌的这种安全要求特性,当需要前端通信,比如浏览器上面的流转的时候,OAuth 2.0 才又提供了一个临时的凭证:授权码。通过授权码的方式,可以让用户小明在授权服务上给小兔授权之后,还能重新回到小兔的操作页面上。这样,在保障安全性的情况下,提升了小明在小兔上的体验。

从授权码许可流程中就可以看出来,它完美地将 OAuth 2.0 的 4 个角色组织了起来,并保证了它们之间的顺畅通信。它提出的这种结构和思想都可以被迁移到其他环境或者协议上,比如在微信小程序中使用授权码许可。

不过,也正是因为有了授权码的参与,才使得授权码许可要比其他授权许可类型,在授权的流程上多出了好多步骤,让授权码许可类型成为了 OAuth 2.0 体系中迄今流程最完备、安全性最高的授权流程。在接下来的两讲中,我还会为你重点讲解授权码许可类型下的授权服务。

思考题

好了,今天这一讲我们马上要结束了,我给你留个思考题。关于不需要浏览器参与的授权码许可流程,你还能列举出更多的应用场景吗?

',52)]))}const r=t(p,[["render",s],["__file","02.html.vue"]]),c=JSON.parse('{"path":"/other/oauth2/02.html","title":"02 | 授权码许可类型中,为什么一定要有授权码?","lang":"zh-CN","frontmatter":{"title":"02 | 授权码许可类型中,为什么一定要有授权码?","date":"2021-11-08T00:00:00.000Z","author":"ChenSino","publish":true,"description":"你好,我是王新栋。 在上一讲,我提到了 OAuth 2.0 的授权码许可类型,在小兔打单软件的例子里面,小兔最终是通过访问令牌请求到小明的店铺里的订单数据。同时呢,我还提到了,这个访问令牌是通过授权码换来的。到这里估计你会问了,为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢? 你可以先停下来想想这个问题。今天咱们这节课,我会带着你深入探究下其中...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/02.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"02 | 授权码许可类型中,为什么一定要有授权码?"}],["meta",{"property":"og:description","content":"你好,我是王新栋。 在上一讲,我提到了 OAuth 2.0 的授权码许可类型,在小兔打单软件的例子里面,小兔最终是通过访问令牌请求到小明的店铺里的订单数据。同时呢,我还提到了,这个访问令牌是通过授权码换来的。到这里估计你会问了,为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢? 你可以先停下来想想这个问题。今天咱们这节课,我会带着你深入探究下其中..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221108113248.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"ChenSino"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"02 | 授权码许可类型中,为什么一定要有授权码?\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108113248.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108113402.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108113557.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108113739.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221108113820.png\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\"}]}"]]},"headers":[{"level":3,"title":"为什么需要授权码?","slug":"为什么需要授权码","link":"#为什么需要授权码","children":[]},{"level":3,"title":"授权码许可类型的通信过程","slug":"授权码许可类型的通信过程","link":"#授权码许可类型的通信过程","children":[]},{"level":3,"title":"直接通信","slug":"直接通信","link":"#直接通信","children":[]},{"level":3,"title":"两个 “一伙”","slug":"两个-一伙","link":"#两个-一伙","children":[]},{"level":3,"title":"一定要有浏览器吗?","slug":"一定要有浏览器吗","link":"#一定要有浏览器吗","children":[]},{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"思考题","slug":"思考题","link":"#思考题","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":4},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":12.79,"words":3838},"filePathRelative":"other/oauth2/02.md","localizedDate":"2021年11月8日","excerpt":"

你好,我是王新栋。

\\n

在上一讲,我提到了 OAuth 2.0 的授权码许可类型,在小兔打单软件的例子里面,小兔最终是通过访问令牌请求到小明的店铺里的订单数据。同时呢,我还提到了,这个访问令牌是通过授权码换来的。到这里估计你会问了,为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢?

\\n

你可以先停下来想想这个问题。今天咱们这节课,我会带着你深入探究下其中的逻辑。

\\n

为什么需要授权码?

\\n

在讲这个问题之前,我先要和你同步下,在 OAuth 2.0 的体系里面有 4 种角色,按照官方的称呼它们分别是资源拥有者、客户端、授权服务和受保护资源。不过,这里的客户端,我更愿意称其为第三方软件,而且在咱们这个课程中,都是以第三方软件在举例子。所以,在后续的讲解中我统一把它称为第三方软件。

","autoDesc":true}');export{r as comp,c as data}; diff --git a/assets/03.html-BlykeApW.js b/assets/03.html-0MOLhHcg.js similarity index 99% rename from assets/03.html-BlykeApW.js rename to assets/03.html-0MOLhHcg.js index 973e3c13c..5680a24b7 100644 --- a/assets/03.html-BlykeApW.js +++ b/assets/03.html-0MOLhHcg.js @@ -1,4 +1,4 @@ -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-LRWZkDAq.js";const t={};function l(e,s){return h(),a("div",null,s[0]||(s[0]=[n(`

你好,我是王新栋。

在上一讲,我从为什么需要授权码这个问题开始,为你串了一遍授权码许可流程整体的通信过程。在接下来的三讲中,我会着重为你讲解关于授权服务的工作流程、授权过程中的令牌,以及如何接入 OAuth 2.0。这样一来,你就可以吃透授权码许可这一最经典、最完备、最常用的授权流程了,以后再处理授权相关的逻辑就更得心应手了。现在呢,让我们开始这一讲。

在介绍授权码许可类型时,我提到了很多次 “授权服务”。一句话概括,授权服务就是负责颁发访问令牌的服务。更进一步地讲,OAuth 2.0 的核心是授权服务,而授权服务的核心

为什么这么说呢?当第三方软件比如小兔,要想获取小明在京东店铺的订单,就必须先从京东商家开放平台的授权服务那里获取访问令牌,进而通过访问令牌来 “代表” 小明去请求小明的订单数据。这不恰恰就是整个 OAuth 2.0 授权体系的核心吗?

那么,授权服务到底是怎么生成访问令牌的,这其中包含了哪些操作呢?还有一个问题是,访问令牌过期了而用户又不在场的情况下,又如何重新生成访问令牌呢?

带着这两个问题,我们就以授权码许可类型为例,一起深入探索下授权服务这个核心组件吧。

授权服务的工作过程

开始之前,你还是要先回想下小明给小兔软件授权订单数据的整个流程。

我们说小兔软件先要让小明去京东商家开放平台那里给它授权数据,那这里是不是你觉得很奇怪?你总不能说,“嘿,京东,你把数据给小兔用吧”,那京东肯定会回复说,“小明,小兔是谁啊,没在咱家备过案,我不能给他,万一是骗子呢?”

对吧,你想想是不是这个逻辑。所以,授权这个大动作的前提,肯定是小兔要去平台那里“备案”,也就是注册。注册完后,京东商家开放平台就会给小兔软件 app_id 和 app_secret 等信息,以方便后面授权时的各种身份校验。

同时,注册的时候,第三方软件也会请求受保护资源的可访问范围。比如,小兔能否获取小明店铺 3 个月以前的订单,能否获取每条订单的所有字段信息等等。这个权限范围,就是 scope。后面呢,我还会详细讲述范围控制。

文字说起来有点抽象,咱们还是直接上代码吧。关于注册后的数据存储,我们使用如下 Java 代码来模拟:


+import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-BIdVr4mq.js";const t={};function l(e,s){return h(),a("div",null,s[0]||(s[0]=[n(`

你好,我是王新栋。

在上一讲,我从为什么需要授权码这个问题开始,为你串了一遍授权码许可流程整体的通信过程。在接下来的三讲中,我会着重为你讲解关于授权服务的工作流程、授权过程中的令牌,以及如何接入 OAuth 2.0。这样一来,你就可以吃透授权码许可这一最经典、最完备、最常用的授权流程了,以后再处理授权相关的逻辑就更得心应手了。现在呢,让我们开始这一讲。

在介绍授权码许可类型时,我提到了很多次 “授权服务”。一句话概括,授权服务就是负责颁发访问令牌的服务。更进一步地讲,OAuth 2.0 的核心是授权服务,而授权服务的核心

为什么这么说呢?当第三方软件比如小兔,要想获取小明在京东店铺的订单,就必须先从京东商家开放平台的授权服务那里获取访问令牌,进而通过访问令牌来 “代表” 小明去请求小明的订单数据。这不恰恰就是整个 OAuth 2.0 授权体系的核心吗?

那么,授权服务到底是怎么生成访问令牌的,这其中包含了哪些操作呢?还有一个问题是,访问令牌过期了而用户又不在场的情况下,又如何重新生成访问令牌呢?

带着这两个问题,我们就以授权码许可类型为例,一起深入探索下授权服务这个核心组件吧。

授权服务的工作过程

开始之前,你还是要先回想下小明给小兔软件授权订单数据的整个流程。

我们说小兔软件先要让小明去京东商家开放平台那里给它授权数据,那这里是不是你觉得很奇怪?你总不能说,“嘿,京东,你把数据给小兔用吧”,那京东肯定会回复说,“小明,小兔是谁啊,没在咱家备过案,我不能给他,万一是骗子呢?”

对吧,你想想是不是这个逻辑。所以,授权这个大动作的前提,肯定是小兔要去平台那里“备案”,也就是注册。注册完后,京东商家开放平台就会给小兔软件 app_id 和 app_secret 等信息,以方便后面授权时的各种身份校验。

同时,注册的时候,第三方软件也会请求受保护资源的可访问范围。比如,小兔能否获取小明店铺 3 个月以前的订单,能否获取每条订单的所有字段信息等等。这个权限范围,就是 scope。后面呢,我还会详细讲述范围控制。

文字说起来有点抽象,咱们还是直接上代码吧。关于注册后的数据存储,我们使用如下 Java 代码来模拟:


 Map<String,String> appMap =  new HashMap<String, String>();//模拟第三方软件注册之后的数据库存储
 
 appMap.put("app_id","APPID_RABBIT");
diff --git a/assets/04.html-Bb3h6Bb4.js b/assets/04.html-CODd576A.js
similarity index 99%
rename from assets/04.html-Bb3h6Bb4.js
rename to assets/04.html-CODd576A.js
index dfb3437f8..b7e361567 100644
--- a/assets/04.html-Bb3h6Bb4.js
+++ b/assets/04.html-CODd576A.js
@@ -1,4 +1,4 @@
-import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as n}from"./app-LRWZkDAq.js";const t={};function l(p,i){return n(),a("div",null,i[0]||(i[0]=[e(`

在上一讲,我们讲到了授权服务的核心就是颁发访问令牌,而 OAuth 2.0 规范并没有约束访问令牌内容的生成规则,只要符合唯一性、不连续性、不可猜性就够了。这就意味着,我们可以灵活选择令牌的形式,既可以是没有内部结构且不包含任何信息含义的随机字符串,也可以是具有内部结构且包含有信息含义的字符串。

随机字符串这样的方式我就不再介绍了,之前课程中我们生成令牌的方式都是默认一个随机字符串。而在结构化令牌这方面,目前用得最多的就是 JWT 令牌了。

接下来,我就要和你详细讲讲,JWT 是什么、原理是怎样的、优势是什么,以及怎么使用,同时我还会讲到令牌生命周期的问题。

JWT 结构化令牌

关于什么是 JWT,官方定义是这样描述的:

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。

这个定义是不是很费解?我们简单理解下,JWT 就是用一种结构化封装的方式来生成 token 的技术。结构化后的 token 可以被赋予非常丰富的含义,这也是它与原先毫无意义的、随机的字符串形式 token 的最大区别。

结构化之后,令牌本身就可以被“塞进”一些有用的信息,比如小明为小兔软件进行了授权的信息、授权的范围信息等。或者,你可以形象地将其理解为这是一种“自编码”的能力,而这些恰恰是无结构化令牌所不具备的。

JWT 这种结构化体可以分为 HEADER(头部)、PAYLOAD(数据体)和 SIGNATURE(签名)三部分。经过签名之后的 JWT 的整体结构,是被句点符号分割的三段内容,结构为 header.payload.signature 。比如下面这个示例:


+import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as n}from"./app-BIdVr4mq.js";const t={};function l(p,i){return n(),a("div",null,i[0]||(i[0]=[e(`

在上一讲,我们讲到了授权服务的核心就是颁发访问令牌,而 OAuth 2.0 规范并没有约束访问令牌内容的生成规则,只要符合唯一性、不连续性、不可猜性就够了。这就意味着,我们可以灵活选择令牌的形式,既可以是没有内部结构且不包含任何信息含义的随机字符串,也可以是具有内部结构且包含有信息含义的字符串。

随机字符串这样的方式我就不再介绍了,之前课程中我们生成令牌的方式都是默认一个随机字符串。而在结构化令牌这方面,目前用得最多的就是 JWT 令牌了。

接下来,我就要和你详细讲讲,JWT 是什么、原理是怎样的、优势是什么,以及怎么使用,同时我还会讲到令牌生命周期的问题。

JWT 结构化令牌

关于什么是 JWT,官方定义是这样描述的:

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。

这个定义是不是很费解?我们简单理解下,JWT 就是用一种结构化封装的方式来生成 token 的技术。结构化后的 token 可以被赋予非常丰富的含义,这也是它与原先毫无意义的、随机的字符串形式 token 的最大区别。

结构化之后,令牌本身就可以被“塞进”一些有用的信息,比如小明为小兔软件进行了授权的信息、授权的范围信息等。或者,你可以形象地将其理解为这是一种“自编码”的能力,而这些恰恰是无结构化令牌所不具备的。

JWT 这种结构化体可以分为 HEADER(头部)、PAYLOAD(数据体)和 SIGNATURE(签名)三部分。经过签名之后的 JWT 的整体结构,是被句点符号分割的三段内容,结构为 header.payload.signature 。比如下面这个示例:


 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
 eyJzdWIiOiJVU0VSVEVTVCIsImV4cCI6MTU4NDEwNTc5MDcwMywiaWF0IjoxNTg0MTA1OTQ4MzcyfQ.
 1HbleXbvJ_2SW8ry30cXOBGR9FW4oSWBd3PWaWKsEXE

注意:JWT 内部没有换行,这里只是为了展示方便,才将其用三行来表示。

你可能会说,这个 JWT 令牌看起来也是毫无意义的、随机的字符串啊。确实,你直接去看这个字符串是没啥意义,但如果你把它拷贝到https://jwt.io/ 网站的在线校验工具中,就可以看到解码之后的数据:

img
img

再看解码后的数据,你是不是发现它跟随机的字符串不一样了呢。很显然,现在呈现出来的就是结构化的内容了。接下来,我就具体和你说说 JWT 的这三部分。

HEADER 表示装载令牌类型和算法等信息,是 JWT 的头部。其中,typ 表示第二部分 PAYLOAD 是 JWT 类型,alg 表示使用 HS256 对称签名的算法。

PAYLOAD 表示是 JWT 的数据体,代表了一组数据。其中,sub(令牌的主体,一般设为资源拥有者的唯一标识)、exp(令牌的过期时间戳)、iat(令牌颁发的时间戳)是 JWT 规范性的声明,代表的是常规性操作。更多的通用声明,你可以参考RFC 7519 开放标准。不过,在一个 JWT 内可以包含一切合法的 JSON 格式的数据,也就是说,PAYLOAD 表示的一组数据允许我们自定义声明。

SIGNATURE 表示对 JWT 信息的签名。那么,它有什么作用呢?我们可能认为,有了 HEADER 和 PAYLOAD 两部分内容后,就可以让令牌携带信息了,似乎就可以在网络中传输了,但是在网络中传输这样的信息体是不安全的,因为你在“裸奔”啊。所以,我们还需要对其进行加密签名处理,而 SIGNATURE 就是对信息的签名结果,当受保护资源接收到第三方软件的签名后需要验证令牌的签名是否合法。

现在,我们知道了 JWT 的结构以及每部分的含义,那么具体到 OAuth 2.0 的授权流程中,JWT 令牌是如何被使用的呢?在讲如何使用之前呢,我先和你说说“令牌内检”

令牌内检

什么是令牌内检呢?授权服务颁发令牌,受保护资源服务就要验证令牌。同时呢,授权服务和受保护资源服务,它俩是“一伙的”,还记得我之前在第 2 课讲过的吧。受保护资源来调用授权服务提供的检验令牌的服务,我们把这种校验令牌的方式称为令牌内检。

有时候授权服务依赖一个数据库,然后受保护资源服务也依赖这个数据库,也就是我们说的“共享数据库”。不过,在如今已经成熟的分布式以及微服务的环境下,不同的系统之间是依靠服务而不是数据库来通信了,比如授权服务给受保护资源服务提供一个 RPC 服务。如下图所示。

img
img

那么,在有了 JWT 令牌之后,我们就多了一种选择,因为 JWT 令牌本身就包含了之前所要依赖数据库或者依赖 RPC 服务才能拿到的信息,比如我上面提到的哪个用户为哪个软件进行了授权等信息。接下来就让我们看看有了 JWT 令牌之后,整体的内检流程会变成什么样子。

JWT 是如何被使用的?

有了 JWT 令牌之后的通信方式,就如下面的图 3 所展示的那样了,授权服务“扔出”一个令牌,受保护资源服务“接住”这个令牌,然后自己开始解析令牌本身所包含的信息就可以了,而不需要再去查询数据库或者请求 RPC 服务。这样也实现了我们上面说的令牌内检。

img
img

在上面这幅图中呢,为了更能突出 JWT 令牌的位置,我简化了逻辑关系。实际上,授权服务颁发了 JWT 令牌后给到了小兔软件,小兔软件拿着 JWT 令牌来请求受保护资源服务,也就是小明在京东店铺的订单。很显然,JWT 令牌需要在公网上做传输。所以在传输过程中,JWT 令牌需要进行 Base64 编码以防止乱码,同时还需要进行签名及加密处理来防止数据信息泄露。

如果是我们自己处理这些编码、加密等工作的话,就会增加额外的编码负担。好在,我们可以借助一些开源的工具来帮助我们处理这些工作。比如,我在下面的 Demo 中,给出了开源 JJWT(Java JWT)的使用方法。

JJWT 是目前 Java 开源的、比较方便的 JWT 工具,封装了 Base64URL 编码和对称 HMAC、非对称 RSA 的一系列签名算法。使用 JJWT,我们只关注上层的业务逻辑实现,而无需关注编解码和签名算法的具体实现,这类开源工具可以做到“开箱即用”。

这个 Demo 的代码如下,使用 JJWT 可以很方便地生成一个经过签名的 JWT 令牌,以及解析一个 JWT 令牌。


diff --git a/assets/05.html-Cxt8rLap.js b/assets/05.html-BlGcSftZ.js
similarity index 99%
rename from assets/05.html-Cxt8rLap.js
rename to assets/05.html-BlGcSftZ.js
index d69ca6aa8..e07230d63 100644
--- a/assets/05.html-Cxt8rLap.js
+++ b/assets/05.html-BlGcSftZ.js
@@ -1,4 +1,4 @@
-import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,b as t,d as s,e as l,f as h,a as p,r as k,o as r}from"./app-LRWZkDAq.js";const d={};function g(A,i){const a=k("RouteLink");return r(),e("div",null,[t("p",null,[i[1]||(i[1]=s("在")),l(a,{to:"/other/oauth2/03.html"},{default:h(()=>i[0]||(i[0]=[s("第 3 讲")])),_:1}),i[2]||(i[2]=s(",我已经讲了授权服务的流程,如果你还记得的话,当时我特意强调了一点,就是授权服务将 OAuth 2.0 的复杂性都揽在了自己身上,这也是授权服务为什么是 OAuth 2.0 体系的核心的原因之一。"))]),i[3]||(i[3]=p(`

虽然授权服务做了大部分工作,但是呢,在 OAuth 2.0 的体系里面,除了资源拥有者是作为用户参与,还有另外两个系统角色,也就是第三方软件和受保护资源服务。那么今天这一讲,我们就站在这两个角色的角度,看看它们应该做哪些工作,才能接入到 OAuth 2.0 的体系里面呢?

现在,就让我们来看看,作为第三方软件的小兔和京东的受保护资源服务,具体需要着重处理哪些工作吧。

注:另外说明一点,为了脱敏的需要,在下面的讲述中,我只是把京东商家开放平台作为一个角色使用,以便有场景感,来帮助你理解。

构建第三方软件应用

我们先来思考一下:如果要基于京东商家开放平台构建一个小兔打单软件的应用,小兔软件的研发人员应该做哪些工作?

是不是要到京东商家开放平台申请注册为开发者,在成为开发者以后再创建一个应用,之后我们就开始开发了,对吧?没错,一定是这样的流程。那么,开发第三方软件应用的过程中,我们需要重点关注哪些内容呢?

我先来和你总结下,这些内容包括 4 部分,分别是:注册信息、引导授权、使用访问令牌、使用刷新令牌。

图1 开发第三方软件应用,应该关注的内容
图1 开发第三方软件应用,应该关注的内容

第一点,注册信息。

首先,小兔软件只有先有了身份,才可以参与到 OAuth 2.0 的流程中去。也就是说,小兔软件需要先拥有自己的 app_id 和 app_serect 等信息,同时还要填写自己的回调地址 redirect_uri、申请权限等信息。这种方式的注册呢,我们有时候也称它为静态注册,也就是小兔软件的研发人员提前登录到京东商家开放平台进行手动注册,以便后续使用这些注册的相关信息来请求访问令牌。

第二点,引导授权。

当用户需要使用第三方软件,来操作其在受保护资源上的数据,就需要第三方软件来引导授权。比如,小明要使用小兔打单软件来对店铺里面的订单发货打印,那小明首先访问的一定是小兔软件(原则上是直接访问第三方软件,不过我们在后面讲到服务市场这种场景的时候,会有稍微不同),不会是授权服务,更不会是受保护资源服务。

但是呢,小兔软件需要小明的授权,只有授权服务才能允许小明这样做。所以呢,小兔软件需要 “配合” 小明做的第一件事儿,就是将小明引导至授权服务,如下面代码所示。那去做什么呢?其实就是让用户为第三方软件授权,得到了授权之后,第三方软件才可以代表用户去访问数据。也就是说,小兔打单软件获得授权之后,才能够代表小明处理其在京东店铺上的订单数据。


+import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,b as t,d as s,e as l,f as h,a as p,r as k,o as r}from"./app-BIdVr4mq.js";const d={};function g(A,i){const a=k("RouteLink");return r(),e("div",null,[t("p",null,[i[1]||(i[1]=s("在")),l(a,{to:"/other/oauth2/03.html"},{default:h(()=>i[0]||(i[0]=[s("第 3 讲")])),_:1}),i[2]||(i[2]=s(",我已经讲了授权服务的流程,如果你还记得的话,当时我特意强调了一点,就是授权服务将 OAuth 2.0 的复杂性都揽在了自己身上,这也是授权服务为什么是 OAuth 2.0 体系的核心的原因之一。"))]),i[3]||(i[3]=p(`

虽然授权服务做了大部分工作,但是呢,在 OAuth 2.0 的体系里面,除了资源拥有者是作为用户参与,还有另外两个系统角色,也就是第三方软件和受保护资源服务。那么今天这一讲,我们就站在这两个角色的角度,看看它们应该做哪些工作,才能接入到 OAuth 2.0 的体系里面呢?

现在,就让我们来看看,作为第三方软件的小兔和京东的受保护资源服务,具体需要着重处理哪些工作吧。

注:另外说明一点,为了脱敏的需要,在下面的讲述中,我只是把京东商家开放平台作为一个角色使用,以便有场景感,来帮助你理解。

构建第三方软件应用

我们先来思考一下:如果要基于京东商家开放平台构建一个小兔打单软件的应用,小兔软件的研发人员应该做哪些工作?

是不是要到京东商家开放平台申请注册为开发者,在成为开发者以后再创建一个应用,之后我们就开始开发了,对吧?没错,一定是这样的流程。那么,开发第三方软件应用的过程中,我们需要重点关注哪些内容呢?

我先来和你总结下,这些内容包括 4 部分,分别是:注册信息、引导授权、使用访问令牌、使用刷新令牌。

图1 开发第三方软件应用,应该关注的内容
图1 开发第三方软件应用,应该关注的内容

第一点,注册信息。

首先,小兔软件只有先有了身份,才可以参与到 OAuth 2.0 的流程中去。也就是说,小兔软件需要先拥有自己的 app_id 和 app_serect 等信息,同时还要填写自己的回调地址 redirect_uri、申请权限等信息。这种方式的注册呢,我们有时候也称它为静态注册,也就是小兔软件的研发人员提前登录到京东商家开放平台进行手动注册,以便后续使用这些注册的相关信息来请求访问令牌。

第二点,引导授权。

当用户需要使用第三方软件,来操作其在受保护资源上的数据,就需要第三方软件来引导授权。比如,小明要使用小兔打单软件来对店铺里面的订单发货打印,那小明首先访问的一定是小兔软件(原则上是直接访问第三方软件,不过我们在后面讲到服务市场这种场景的时候,会有稍微不同),不会是授权服务,更不会是受保护资源服务。

但是呢,小兔软件需要小明的授权,只有授权服务才能允许小明这样做。所以呢,小兔软件需要 “配合” 小明做的第一件事儿,就是将小明引导至授权服务,如下面代码所示。那去做什么呢?其实就是让用户为第三方软件授权,得到了授权之后,第三方软件才可以代表用户去访问数据。也就是说,小兔打单软件获得授权之后,才能够代表小明处理其在京东店铺上的订单数据。


 String oauthUrl = "http://localhost:8081/OauthServlet-ch03?reqType=oauth";
 
 response.sendRedirect(toOauthUrl);

第三点,使用访问令牌。

拿到令牌后去使用令牌,才是第三方软件的最终目的。然后我们看看如何使用令牌。目前 OAuth 2.0 的令牌只支持一种类型,那就是 bearer 令牌,也就是我之前讲到的可以是任意字符串格式的令牌。

官方规范给出的使用访问令牌请求的方式,有三种,分别是:

  • Form-Encoded Body Parameter(表单参数)

diff --git a/assets/06.html-Bcc4cjIC.js b/assets/06.html-FuByxaV8.js
similarity index 99%
rename from assets/06.html-Bcc4cjIC.js
rename to assets/06.html-FuByxaV8.js
index f5ffd29ad..47fc1aab6 100644
--- a/assets/06.html-Bcc4cjIC.js
+++ b/assets/06.html-FuByxaV8.js
@@ -1,4 +1,4 @@
-import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-LRWZkDAq.js";const t={};function l(k,s){return h(),a("div",null,s[0]||(s[0]=[n(`

在前面几讲学习授权码许可类型的原理与工作流程时,不知道你是不是一直有这样一个疑问:授权码许可的流程最完备、最安全没错儿,但它适合所有的授权场景吗?在有些场景下使用授权码许可授权,是不是过于复杂了,是不是根本就没必要这样?

比如,小兔打单软件是京东官方开发的一款软件,那么小明在使用小兔的时候,还需要小兔再走一遍授权码许可类型的流程吗?估计你也猜到答案了,肯定是不需要了。

你还记得授权码许可流程的特点么?它通过授权码这种临时的中间值,让小明这样的用户参与进来,从而让小兔软件和京东之间建立联系,进而让小兔代表小明去访问他在京东店铺的订单数据。

现在小兔被“招安”了,是京东自家的了,是被京东充分信任的,没有“第三方软件”的概念了。同时,小明也是京东店铺的商家,也就是说软件和用户都是京东的资产。这时,显然没有必要再使用授权码许可类型进行授权了。但是呢,小兔依然要通过互联网访问订单数据的 Web API,来提供为小明打单的功能。

于是,为了保护这些场景下的 Web API,又为了让 OAuth 2.0 更好地适应现实世界的更多场景,来解决比如上述小兔软件这样的案例,OAuth 2.0 体系中还提供了资源拥有者凭据许可类型。

资源拥有者凭据许可

从“资源拥有者凭据许可”这个命名上,你可能就已经理解它的含义了。没错,资源拥有者的凭据,就是用户的凭据,就是用户名和密码。可见,这是最糟糕的一种方式。那为什么 OAuth 2.0 还支持这种许可类型,而且编入了 OAuth 2.0 的规范呢?

我们先来思考一下。正如上面我提到的,小兔此时就是京东官方出品的一款软件,小明也是京东的用户,那么小明其实是可以使用用户名和密码来直接使用小兔这款软件的。原因很简单,那就是这里不再有“第三方”的概念了。

但是呢,如果每次小兔都是拿着小明的用户名和密码来通过调用 Web API 的方式,来访问小明店铺的订单数据,甚至还有商品信息等,在调用这么多 API 的情况下,无疑增加了用户名和密码等敏感信息的攻击面。

如果是使用了 token 来代替这些“满天飞”的敏感信息,不就能很大程度上保护敏感信息数据了吗?这样,小兔软件只需要使用一次用户名和密码数据来换回一个 token,进而通过 token 来访问小明店铺的数据,以后就不会再使用用户名和密码了。

接下来,我们一起看下这种许可类型的流程,如下图所示:

图1 资源拥有者凭据许可类型的流程
图1 资源拥有者凭据许可类型的流程

步骤 1:当用户访问第三方软件小兔时,会提示输入用户名和密码。索要用户名和密码,就是资源拥有者凭据许可类型的特点。

步骤 2:这里的 grant_type 的值为 password,告诉授权服务使用资源拥有者凭据许可凭据的方式去请求访问。


+import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-BIdVr4mq.js";const t={};function l(k,s){return h(),a("div",null,s[0]||(s[0]=[n(`

在前面几讲学习授权码许可类型的原理与工作流程时,不知道你是不是一直有这样一个疑问:授权码许可的流程最完备、最安全没错儿,但它适合所有的授权场景吗?在有些场景下使用授权码许可授权,是不是过于复杂了,是不是根本就没必要这样?

比如,小兔打单软件是京东官方开发的一款软件,那么小明在使用小兔的时候,还需要小兔再走一遍授权码许可类型的流程吗?估计你也猜到答案了,肯定是不需要了。

你还记得授权码许可流程的特点么?它通过授权码这种临时的中间值,让小明这样的用户参与进来,从而让小兔软件和京东之间建立联系,进而让小兔代表小明去访问他在京东店铺的订单数据。

现在小兔被“招安”了,是京东自家的了,是被京东充分信任的,没有“第三方软件”的概念了。同时,小明也是京东店铺的商家,也就是说软件和用户都是京东的资产。这时,显然没有必要再使用授权码许可类型进行授权了。但是呢,小兔依然要通过互联网访问订单数据的 Web API,来提供为小明打单的功能。

于是,为了保护这些场景下的 Web API,又为了让 OAuth 2.0 更好地适应现实世界的更多场景,来解决比如上述小兔软件这样的案例,OAuth 2.0 体系中还提供了资源拥有者凭据许可类型。

资源拥有者凭据许可

从“资源拥有者凭据许可”这个命名上,你可能就已经理解它的含义了。没错,资源拥有者的凭据,就是用户的凭据,就是用户名和密码。可见,这是最糟糕的一种方式。那为什么 OAuth 2.0 还支持这种许可类型,而且编入了 OAuth 2.0 的规范呢?

我们先来思考一下。正如上面我提到的,小兔此时就是京东官方出品的一款软件,小明也是京东的用户,那么小明其实是可以使用用户名和密码来直接使用小兔这款软件的。原因很简单,那就是这里不再有“第三方”的概念了。

但是呢,如果每次小兔都是拿着小明的用户名和密码来通过调用 Web API 的方式,来访问小明店铺的订单数据,甚至还有商品信息等,在调用这么多 API 的情况下,无疑增加了用户名和密码等敏感信息的攻击面。

如果是使用了 token 来代替这些“满天飞”的敏感信息,不就能很大程度上保护敏感信息数据了吗?这样,小兔软件只需要使用一次用户名和密码数据来换回一个 token,进而通过 token 来访问小明店铺的数据,以后就不会再使用用户名和密码了。

接下来,我们一起看下这种许可类型的流程,如下图所示:

图1 资源拥有者凭据许可类型的流程
图1 资源拥有者凭据许可类型的流程

步骤 1:当用户访问第三方软件小兔时,会提示输入用户名和密码。索要用户名和密码,就是资源拥有者凭据许可类型的特点。

步骤 2:这里的 grant_type 的值为 password,告诉授权服务使用资源拥有者凭据许可凭据的方式去请求访问。


 Map<String, String> params = new HashMap<String, String>();
 params.put("grant_type","password");
 params.put("app_id","APPIDTEST");
diff --git a/assets/07.html-CN-w1qxn.js b/assets/07.html-Cg_BlXZ8.js
similarity index 99%
rename from assets/07.html-CN-w1qxn.js
rename to assets/07.html-Cg_BlXZ8.js
index dc198d72e..950e130b1 100644
--- a/assets/07.html-CN-w1qxn.js
+++ b/assets/07.html-Cg_BlXZ8.js
@@ -1,4 +1,4 @@
-import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as s,a,o as p}from"./app-LRWZkDAq.js";const t={};function n(r,e){return p(),s("div",null,e[0]||(e[0]=[a(`

在前面几讲中,我都是基于 Web 应用的场景来讲解的 OAuth 2.0。除了 Web 应用外,现实环境中还有非常多的移动 App。那么,在移动 App 中,能不能使用 OAuth 2.0 ,又该如何使用 OAuth 2.0 呢?

没错,OAuth 2.0 最初的应用场景确实是 Web 应用,但是它的伟大之处就在于,它把自己的核心协议定位成了一个框架而不是单个的协议。这样做的好处是,我们可以基于这个基本的框架协议,在一些特定的领域进行扩展。

因此,到了桌面或者移动的场景下,OAuth 2.0 的协议一样适用。考虑到授权码许可是最完备、最安全的许可类型,所以我在讲移动 App 如何使用 OAuth 2.0 的时候,依然会用授权码许可来讲解,毕竟“要用就用最好的”。

当我们开发一款移动 App 的时候,可以选择没有 Server 端的 “纯 App” 架构,比如这款 App 不需要跟自己的 Server 端通信,或者可以调用其它开放的 HTTP 接口;当然也可以选择有服务端的架构,比如这款 App 还想把用户的操作日志记录下来并保存到 Server 端的数据库中。

那总结下来呢,移动 App 可以分为两类,一类是没有 Server 端的 App 应用,一类是有 Server 端的 App 应用。

图1 两类移动App
图1 两类移动App

这两类 App 在使用 OAuth 2.0 时的最大区别,在于获取访问令牌的方式:

  • 如果有 Server 端,就建议通过 Server 端和授权服务做交互来换取访问令牌;
  • 如果没有 Server 端,那么只能通过前端通信来跟授权服务做交互,比如在上一讲中提到的隐式许可授权类型。当然,这种方式的安全性就降低了很多。

有些时候,我们可能觉得自己开发一个 App 不需要一个 Server 端。那好,就让我们先来看看没有 Server 端的 App 应用如何使用授权码许可类型。

没有 Server 端的 App

在一个没有 Server 端支持的纯 App 应用中,我们首先想到的是,如何可以像 Web 服务那样,让请求和响应“来去自如”呢。

你可能会想,我是不是可以将一个“迷你”的 Web 服务器嵌入到 App 里面去,这样不就可以

这样的 App 通过监听运行在 localhost 上的 Web 服务器 URI,就可以做到跟普通的 Web 应用一样的通信机制。但这种方式不是我们这次要讲的重点,如果你想深入了解可以去查些资料。因为当使用这种方式的时候,请求访问令牌时需要的 app_secret 就只能保存在用户本地设备上,而这并不是我们所建议的。

到这里,你应该猜到了,问题的关键在于如何保存 app_secret,因为 App 会被安装在成千上万个终端设备上,app_secret 一旦被破解,就将会造成灾难性的后果。这时,有的同学突发奇想,如果不用 app_secret,也能在授权码流程里换回访问令牌 access_token,不就可以了吗?

确实可以,但新的问题也来了。在授权码许可类型的流程中,如果没有了 app_secret 这一层的保护,那么通过授权码 code 换取访问令牌的时候,就只有授权码 code 在“冲锋陷阵”了。这时,授权码 code 一旦失窃,就会带来严重的安全问题。那么,我既不使用 app_secret,还要防止授权码 code 失窃,有什么好的方法吗?

有,OAuth 2.0 里面就有这样的指导方法。这个方法就是我们将要介绍的 PKCE 协议,全称是 Proof Key for Code Exchange by OAuth Public Clients。

在下面的流程图中,为了突出第三方软件使用 PKCE 协议时与授权服务之间的通信过程,我省略了受保护资源服务和资源拥有者的角色:

图2 使用PKCE协议的流程图
图2 使用PKCE协议的流程图

我来和你分析下这个流程中的重点。

首先,App 自己要生成一个随机的、长度在 43~128 字符之间的、参数为 code_verifier 的字符串验证码;接着,我们再利用这个 code_verifier,来生成一个被称为“挑战码”的参数code_challenge。

那怎么生成这个 code_challenge 的值呢?OAuth 2.0 规范里面给出了两种方法,就是看 code_challenge_method 这个参数的值:

  • 一种 code_challenge_method=plain,此时 code_verifier 的值就是 code_challenge 的值;
  • 另外一种 code_challenge_method=S256,就是将 code_verifier 值进行 ASCII 编码之后再进行哈希,然后再将哈希之后的值进行 BASE64-URL 编码,如下代码所示。
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

好了,我知道有这样两个值,也知道它们的生成方法了,但这两个值跟我们的授权码流程有什么关系呢,又怎么利用它们呢?不用着急,我们接着讲。

授权码流程简单概括起来不是有两步吗,第一步是获取授权码 code,第二步是用 app_id+app_secret+code 获取访问令牌 access_token。刚才我们的“梦想”不是设想不使用 app_secret,但同时又能保证授权码流程的安全性么?

没错。code_verifier 和 code_challenge 这两个参数,就是来帮我们实现这个“梦想”的。

在第一步获取授权码 code 的时候,我们使用 code_challenge 参数。需要注意的是,我们要同时将 code_challenge_method 参数也传过去,目的是让授权服务知道生成 code_challenge 值的方法是 plain 还是 S256。https://authorization-server.com/auth?response_type=code&app_id=APP_ID&redirect_uri=REDIRECT_URI&code_challenge=CODE_CHALLENGE&code_challenge_method=S256


+import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as s,a,o as p}from"./app-BIdVr4mq.js";const t={};function n(r,e){return p(),s("div",null,e[0]||(e[0]=[a(`

在前面几讲中,我都是基于 Web 应用的场景来讲解的 OAuth 2.0。除了 Web 应用外,现实环境中还有非常多的移动 App。那么,在移动 App 中,能不能使用 OAuth 2.0 ,又该如何使用 OAuth 2.0 呢?

没错,OAuth 2.0 最初的应用场景确实是 Web 应用,但是它的伟大之处就在于,它把自己的核心协议定位成了一个框架而不是单个的协议。这样做的好处是,我们可以基于这个基本的框架协议,在一些特定的领域进行扩展。

因此,到了桌面或者移动的场景下,OAuth 2.0 的协议一样适用。考虑到授权码许可是最完备、最安全的许可类型,所以我在讲移动 App 如何使用 OAuth 2.0 的时候,依然会用授权码许可来讲解,毕竟“要用就用最好的”。

当我们开发一款移动 App 的时候,可以选择没有 Server 端的 “纯 App” 架构,比如这款 App 不需要跟自己的 Server 端通信,或者可以调用其它开放的 HTTP 接口;当然也可以选择有服务端的架构,比如这款 App 还想把用户的操作日志记录下来并保存到 Server 端的数据库中。

那总结下来呢,移动 App 可以分为两类,一类是没有 Server 端的 App 应用,一类是有 Server 端的 App 应用。

图1 两类移动App
图1 两类移动App

这两类 App 在使用 OAuth 2.0 时的最大区别,在于获取访问令牌的方式:

  • 如果有 Server 端,就建议通过 Server 端和授权服务做交互来换取访问令牌;
  • 如果没有 Server 端,那么只能通过前端通信来跟授权服务做交互,比如在上一讲中提到的隐式许可授权类型。当然,这种方式的安全性就降低了很多。

有些时候,我们可能觉得自己开发一个 App 不需要一个 Server 端。那好,就让我们先来看看没有 Server 端的 App 应用如何使用授权码许可类型。

没有 Server 端的 App

在一个没有 Server 端支持的纯 App 应用中,我们首先想到的是,如何可以像 Web 服务那样,让请求和响应“来去自如”呢。

你可能会想,我是不是可以将一个“迷你”的 Web 服务器嵌入到 App 里面去,这样不就可以

这样的 App 通过监听运行在 localhost 上的 Web 服务器 URI,就可以做到跟普通的 Web 应用一样的通信机制。但这种方式不是我们这次要讲的重点,如果你想深入了解可以去查些资料。因为当使用这种方式的时候,请求访问令牌时需要的 app_secret 就只能保存在用户本地设备上,而这并不是我们所建议的。

到这里,你应该猜到了,问题的关键在于如何保存 app_secret,因为 App 会被安装在成千上万个终端设备上,app_secret 一旦被破解,就将会造成灾难性的后果。这时,有的同学突发奇想,如果不用 app_secret,也能在授权码流程里换回访问令牌 access_token,不就可以了吗?

确实可以,但新的问题也来了。在授权码许可类型的流程中,如果没有了 app_secret 这一层的保护,那么通过授权码 code 换取访问令牌的时候,就只有授权码 code 在“冲锋陷阵”了。这时,授权码 code 一旦失窃,就会带来严重的安全问题。那么,我既不使用 app_secret,还要防止授权码 code 失窃,有什么好的方法吗?

有,OAuth 2.0 里面就有这样的指导方法。这个方法就是我们将要介绍的 PKCE 协议,全称是 Proof Key for Code Exchange by OAuth Public Clients。

在下面的流程图中,为了突出第三方软件使用 PKCE 协议时与授权服务之间的通信过程,我省略了受保护资源服务和资源拥有者的角色:

图2 使用PKCE协议的流程图
图2 使用PKCE协议的流程图

我来和你分析下这个流程中的重点。

首先,App 自己要生成一个随机的、长度在 43~128 字符之间的、参数为 code_verifier 的字符串验证码;接着,我们再利用这个 code_verifier,来生成一个被称为“挑战码”的参数code_challenge。

那怎么生成这个 code_challenge 的值呢?OAuth 2.0 规范里面给出了两种方法,就是看 code_challenge_method 这个参数的值:

  • 一种 code_challenge_method=plain,此时 code_verifier 的值就是 code_challenge 的值;
  • 另外一种 code_challenge_method=S256,就是将 code_verifier 值进行 ASCII 编码之后再进行哈希,然后再将哈希之后的值进行 BASE64-URL 编码,如下代码所示。
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

好了,我知道有这样两个值,也知道它们的生成方法了,但这两个值跟我们的授权码流程有什么关系呢,又怎么利用它们呢?不用着急,我们接着讲。

授权码流程简单概括起来不是有两步吗,第一步是获取授权码 code,第二步是用 app_id+app_secret+code 获取访问令牌 access_token。刚才我们的“梦想”不是设想不使用 app_secret,但同时又能保证授权码流程的安全性么?

没错。code_verifier 和 code_challenge 这两个参数,就是来帮我们实现这个“梦想”的。

在第一步获取授权码 code 的时候,我们使用 code_challenge 参数。需要注意的是,我们要同时将 code_challenge_method 参数也传过去,目的是让授权服务知道生成 code_challenge 值的方法是 plain 还是 S256。https://authorization-server.com/auth?response_type=code&app_id=APP_ID&redirect_uri=REDIRECT_URI&code_challenge=CODE_CHALLENGE&code_challenge_method=S256


 https://authorization-server.com/auth?
 response_type=code&
 app_id=APP_ID&
diff --git a/assets/08.html-BdpbItx3.js b/assets/08.html-C2-4HV62.js
similarity index 99%
rename from assets/08.html-BdpbItx3.js
rename to assets/08.html-C2-4HV62.js
index 12e4b8eb5..ccff0e10b 100644
--- a/assets/08.html-BdpbItx3.js
+++ b/assets/08.html-C2-4HV62.js
@@ -1,4 +1,4 @@
-import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a,o as s}from"./app-LRWZkDAq.js";const n={};function p(h,e){return s(),i("div",null,e[0]||(e[0]=[a(`

当知道这一讲的主题是 OAuth 2.0 的安全漏洞时,你可能要问了:“OAuth 2.0 不是一种安全协议吗,不是保护 Web API 的吗?为啥 OAuth 2.0 自己还有安全的问题了呢?”

首先,OAuth 2.0 的确是一种安全协议。这没啥问题,但是它有很多使用规范,比如授权码是一个临时凭据只能被使用一次,要对重定向 URI 做校验等。那么,如果使用的时候你没有按照这样的规范来实施,就会有安全漏洞了。

其次,OAuth 2.0 既然是“生长”在互联网这个大环境中,就一样会面对互联网上常见安全风险的攻击,比如跨站请求伪造(Cross-site request forgery,CSRF)、跨站脚本攻击(Cross Site Scripting,XSS)。最后,除了这些常见攻击类型外,OAuth 2.0 自身也有可被利用的安全漏洞,比如授权码失窃、重定向 URI 伪造。

所以,我们在实践 OAuth 2.0 的过程中,安全问题一定是重中之重。接下来,我挑选了 5 个典型的安全问题,其中 CSRF、XSS、水平越权这三种是互联网环境下常见的安全风险,授权码失窃和重定向 URI 被篡改属于 OAuth2.0“专属”的安全风险。接下来,我就和你一起看看这些安全风险的由来,以及如何应对吧。

CSRF 攻击

对于 CSRF 的定义,《OAuth 2 in Action》这本书里的解释,是我目前看到的最为贴切的解释:恶意软件让浏览器向已完成用户身份认证的网站发起请求,并执行有害的操作,就是跨站请求伪造攻击。

它是互联网上最为常见的攻击之一。我们在实践 OAuth2.0 的过程,其实就是在构建一次互联网的应用。因此,OAuth 2.0 同样也会面临这个攻击。接下来,我通过一个案例和你说明这个攻击类型。

有一个软件 A,我们让它来扮演攻击者,让它的开发者按照正常的流程使用极客时间。当该攻击者授权后,拿到授权码的值 codeA 之后,“立即按下了暂停键”,不继续往下走了。那它想干啥呢,我们继续往下看。

这时,有一个第三方软件 B,比如咱们的 Web 版极客时间,来扮演受害者吧。当然最终的受害者是用户,这里是用 Web 版极客时间来作为被软件 A 攻击的对象。

极客时间用于接收授权码的回调地址为 https://time.geekbang.org/callback。有一个用户 G 已经在极客时间的平台登录,且对极客时间进行了授权,也就是用户 G 已经在极客时间平台上有登录态了。如果此时攻击者软件 A,在自己的网站上构造了一个恶意页面:


+import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a,o as s}from"./app-BIdVr4mq.js";const n={};function p(h,e){return s(),i("div",null,e[0]||(e[0]=[a(`

当知道这一讲的主题是 OAuth 2.0 的安全漏洞时,你可能要问了:“OAuth 2.0 不是一种安全协议吗,不是保护 Web API 的吗?为啥 OAuth 2.0 自己还有安全的问题了呢?”

首先,OAuth 2.0 的确是一种安全协议。这没啥问题,但是它有很多使用规范,比如授权码是一个临时凭据只能被使用一次,要对重定向 URI 做校验等。那么,如果使用的时候你没有按照这样的规范来实施,就会有安全漏洞了。

其次,OAuth 2.0 既然是“生长”在互联网这个大环境中,就一样会面对互联网上常见安全风险的攻击,比如跨站请求伪造(Cross-site request forgery,CSRF)、跨站脚本攻击(Cross Site Scripting,XSS)。最后,除了这些常见攻击类型外,OAuth 2.0 自身也有可被利用的安全漏洞,比如授权码失窃、重定向 URI 伪造。

所以,我们在实践 OAuth 2.0 的过程中,安全问题一定是重中之重。接下来,我挑选了 5 个典型的安全问题,其中 CSRF、XSS、水平越权这三种是互联网环境下常见的安全风险,授权码失窃和重定向 URI 被篡改属于 OAuth2.0“专属”的安全风险。接下来,我就和你一起看看这些安全风险的由来,以及如何应对吧。

CSRF 攻击

对于 CSRF 的定义,《OAuth 2 in Action》这本书里的解释,是我目前看到的最为贴切的解释:恶意软件让浏览器向已完成用户身份认证的网站发起请求,并执行有害的操作,就是跨站请求伪造攻击。

它是互联网上最为常见的攻击之一。我们在实践 OAuth2.0 的过程,其实就是在构建一次互联网的应用。因此,OAuth 2.0 同样也会面临这个攻击。接下来,我通过一个案例和你说明这个攻击类型。

有一个软件 A,我们让它来扮演攻击者,让它的开发者按照正常的流程使用极客时间。当该攻击者授权后,拿到授权码的值 codeA 之后,“立即按下了暂停键”,不继续往下走了。那它想干啥呢,我们继续往下看。

这时,有一个第三方软件 B,比如咱们的 Web 版极客时间,来扮演受害者吧。当然最终的受害者是用户,这里是用 Web 版极客时间来作为被软件 A 攻击的对象。

极客时间用于接收授权码的回调地址为 https://time.geekbang.org/callback。有一个用户 G 已经在极客时间的平台登录,且对极客时间进行了授权,也就是用户 G 已经在极客时间平台上有登录态了。如果此时攻击者软件 A,在自己的网站上构造了一个恶意页面:


 <html>
 <img src ="https://time.geekbang.org/callback?code=codeA">
 </html>

如果这个时候用户 G 被攻击者软件 A 诱导而点击了这个恶意页面,那结果就是,极客时间使用 codeA 值去继续 OAuth 2.0 的流程了。这其实就走完了一个 CSRF 攻击的过程,如下图所示:

图1 CSRF攻击过程
图1 CSRF攻击过程

如果我们将 OAuth 2.0 用于了身份认证,那么就会造成严重的后果,因为用户 G 使用的极客时间的授权上下文环境跟攻击者软件 A 的授权上下文环境绑定在了一起。为了解释两个上下文环境绑定在一起可能带来的危害,我们还是拿极客时间来举例。

假如,极客时间提供了用户账号和微信账号做绑定的功能,也就是说用户先用自己的极客时间的账号登录,然后可以绑定微信账号,以便后续可以使用微信账号来登录。在绑定微信账号的时候,微信会咨询你是否给极客时间授权,让它获取你在微信上的个人信息。这时候,就需要用到 OAuth 2.0 的授权流程。

如果攻击者软件 A,通过自己的极客时间账号事先做了上面的绑定操作,也就是说攻击者已经可以使用自己的微信账号来登录极客时间了。那有一天,软件 A 想要“搞事情”了,便在发起了一个授权请求后构造了一个攻击页面,里面包含的模拟代码正如我在上面描述的那样,来诱导用户 G 点击。

而用户 G 已经用极客时间的账号登录了极客时间,此时正要去做跟微信账号的绑定。如果这个时候他刚好点击了攻击者 A“种下”的这个恶意页面,那么后面换取授权的访问令牌 access_token,以及通过 accces_token 获取的信息就都是攻击者软件 A 的了。

这就相当于,用户 G 将自己的极客时间的账号跟攻击者软件 A 的微信账号绑定在了一起。这样一来,后续攻击者软件 A 就能够通过自己的微信账号,来登录用户 G 的极客时间了。

那如何避免这种攻击呢?方法也很简单,实际上 OAuth 2.0 中也有这样的建议,就是使用 state 参数,它是一个随机值的参数。

还是以上面的场景为例,当极客时间请求授权码的时候附带一个自己生成 state 参数值,同时授权服务也要按照规则将这个随机的 state 值跟授权码 code 一起返回给极客时间。这样,当极客时间接收到授权码的时候,就要在极客时间这一侧做一个 state 参数值的比对校验,如果相同就继续流程,否则直接拒绝后续流程。

在这样的情况下,软件 A 要想再发起 CSRF 攻击,就必须另外构造一个 state 值,而这个 state 没那么容易被伪造。这本就是一个随机的数值,而且在生成时就遵从了被“猜中”的概率要极小的建议。比如,生成一个 6 位字母和数字的组合值,显然要比生成一个 6 位纯数字值被“猜中”的概率要小。所以,软件 B 通过使用 state 参数,就实现了一个基本的防跨站请求伪造保护。

我们再来总结下,这个攻击过程本质上就是,软件 A(攻击者)用自己的授权码 codeA 的值,通过 CSRF 攻击,“替换”了软件 B 的授权码的值。

XSS 攻击

XSS 攻击的主要手段是将恶意脚本注入到请求的输入中,攻击者可以通过注入的恶意脚本来进行攻击行为,比如搜集数据等。截止到 2020 年 6 月 23 日,在 OWASP(一个开源的 Web 应用安全项目)上查看安全漏洞排名的话,它依然在TOP10榜单上面,可谓“大名鼎鼎”。

网络上有很多关于 XSS 的介绍了,我推荐你看看《XSS 攻击原理分析与防御技术》这篇文章,它很清晰地分析了 XSS 的原理以及防御方法。今天,我们主要看看它是怎么在 OAuth 2.0 的流程中“发挥”的。

当请求抵达受保护资源服务时,系统需要做校验,比如第三方软件身份合法性校验、访问令牌 access_token 的校验,如果这些信息都不能被校验通过,受保护资源服务就会返回错误的信息。

图2 XSS攻击过程
图2 XSS攻击过程

大多数情况下,受保护资源都是把输入的内容,比如 app_id invalid、access_token invalid ,再回显一遍,这时就会被 XSS 攻击者捕获到机会。试想下,如果攻击者传入了一些恶意的、搜集用户数据的 JavaScript 代码,受保护资源服务直接原路返回到用户的页面上,那么当用户触发到这些代码的时候就会遭受到攻击。

因此,受保护资源服务就需要对这类 XSS 漏洞做修复,而具体的修复方法跟其它网站防御 XSS 类似,最简单的方法就是对此类非法信息做转义过滤,比如对包含<script>、<img>、<a>等标签的信息进行转义过滤。

CSRF 攻击、XSS 攻击是我从 OWASP 网站上挑选的两个最为熟知的两种攻击类型,它们应该是所有 Web 系统都需要共同防范的。我们在实施 OAuth 2.0 架构的时候,也一定要考虑到这层防护,否则就会给用户造成伤害。接下来,我再带着你了解一下水平越权攻击。

水平越权

水平越权是指,在请求受保护资源服务数据的时候,服务端应用程序未校验这条数据是否归属于当前授权的请求用户。这样不法者用自己获得的授权来访问受保护资源服务的时候,就有可能获取其他用户的数据,导致水平越权漏洞问题的发生。攻击者可越权的操作有增加、删除、修改和查询,无论更新操作还是查询操

这么说可能有些抽象,我们看一个具体的例子。

还是以我们的“小兔打单软件”为例,第三方开发者开发了这款打单软件,目前有两个商家 A 和商家 B 购买并使用。现在小兔打单软件上面提供了根据订单 ID 查询订单数据的功能,如下图所示。

img
img

商家 A 和商家 B 分别给小兔打单软件应用做了授权,也就是说,小兔打单软件可以获取商家 A 和商家 B 的订单数据。此时没有任何问题,那么商家 A 可以获取商家 B 的订单数据吗?答案是,极有可能的。

在开放平台环境下,授权关系的校验是由一般由开放网关这一层来处理,因为受保护资源服务会散落在各个业务支持部门。请求数据通过开放网关之后由访问令牌 access_token 获取了用户的身份,比如商家 ID,就会透传到受保护资源服务,也就是上游接口提供方的系统。

此时,如果受保护资源服务没有对商家 ID 和订单 ID 做归属判断,就有可能发生商家 A 获取商家 B 订单数据的问题,造成水平越权问题。

img
img

发生水平越权问题的根本原因,还是开发人员的认知与意识不够。如果认知与意识跟得上,那在设计之初增加归属关系判断,比如上面提到的订单 ID 和商家 ID 的归属关系判断,就能在很大程度上避免这个漏洞。

同时,在开放平台环境下,由于开放网关和数据接口提供方来自不同的业务部门,防止水平校验的逻辑处理很容易被遗漏:

  • 一方面,开放网关的作用是将用户授权之后的访问令牌 access_token 信息转换成真实的用户信息,比如上面提到的商家 ID,然后传递到接口提供方,数据归属判断逻辑只能在接口提供方内部处理;
  • 另一方面,数据提供方往往会认为开放出的接口是被“跟自己一个公司的系统所调用的”,容易忽略水平校验的逻辑处理。

所以,在开放平台环境下,我们就要更加重视与防范数据的越权问题。

以上,CSRF 攻击、XSS 攻击、水平越权这三种攻击类型,它们都属于 OAuth 2.0 面临的互联网非常常见的通用攻击类型。而对于其他的互联网攻击类型,如果你想深入了解的话,可以看一下这篇安全案例回顾的文章。接下来,我们再看两种 OAuth 2.0 专有的安全攻击,分别是授权码失窃、重定向 URI 被篡改。

接下来,我们再看两种 OAuth 2.0 专有的安全攻击,分别是授权码失窃、重定向 URI 被篡改。

授权码失窃

我们举个例子,先来学习授权码失窃这个场景。

如果第三方软件 A 有合法的 app_id 和 app_secret,那么当它去请求访问令牌的时候,也是合法的。这个时候没有任何问题,让我们继续。

如果有一个用户 G 对第三方软件 B,比如极客时间,进行授权并产生了一个授权码 codeB,但并没有对攻击者软件 A 授权。此时,软件 A 是不能访问用户 G 的所有数据的。但这时,如果软件 A 获取了这个 codeB,是不是就能够在没有获得用户 G 授权的情况下访问用户 G 的数据了?整个过程如下图所示。

img
img

这时问题的根源就在于两点:

  • 授权服务在进行授权码校验的时候,没有校验 app_id_B;
  • 软件 B(也就是极客时间)使用过一次 codeB 的值之后,授权服务没有删除这个 codeB;

看到这里,通过校验 app_id_B,并删除掉使用过一次的授权码及其对应的访问令牌,就可以从根本上来杜绝授权码失窃带来的危害了。

说到这里,你不禁要问了,授权码到底是怎么失窃的呢?接下来,我要介绍的就是授权码失窃的可能的方法之一,这也是 OAuth 2.0 中因重定向 URI 校验方法不当而遭受到的一种危害。这种安全攻击类型,就是重定向 URI 被篡改。

重定向 URI 被篡改

有的时候,授权服务提供方并没有对第三方软件的回调 URI 做完整性要求和完整性校验。比如,第三软件 B 极客时间的详细回调 URI 是https://time.geekbang.org/callback,那么在完整性校验缺失的情况下,只要以https://time.geekbang.org开始的回调 URI 地址,都会被认为是合法的。此时,如果黑客在https://time.geekbang.org/page/下,创建了一个页面 hacker.html。这个页面的内容可以很简单,其目的就是让请求能够抵达攻击者的服务。


diff --git a/assets/09.html-FHq4Vqms.js b/assets/09.html-CYHAg00J.js
similarity index 99%
rename from assets/09.html-FHq4Vqms.js
rename to assets/09.html-CYHAg00J.js
index a4a88967e..89e170b3e 100644
--- a/assets/09.html-FHq4Vqms.js
+++ b/assets/09.html-CYHAg00J.js
@@ -1,4 +1,4 @@
-import{_ as h}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a,b as l,d as s,e as p,f as k,r as e,o as d}from"./app-LRWZkDAq.js";const r={};function g(A,i){const n=e("RouteLink");return d(),t("div",null,[i[3]||(i[3]=a(`

如果你是一个第三方软件开发者,在实现用户登录的逻辑时,除了可以让用户新注册一个账号再登录外,还可以接入微信、微博等平台,让用户使用自己的微信、微博账号去登录。同时,如果你的应用下面又有多个子应用,还可以让用户只登录一次就能访问所有的子应用,来提升用户体验。这就是联合登录和单点登录了。再继续深究,它们其实都是 OpenID Connect(简称 OIDC)的应用场景的实现。那 OIDC 又是什么呢?今天,我们就来学习下 OIDC 和 OAuth 2.0 的关系,以及如何用 OAuth 2.0 来实现一个 OIDC 用户身份认证协议。

OIDC 是什么?

OIDC 其实就是一种用户身份认证的开放标准。使用微信账号登录极客时间的场景,就是这种开放标准的实践。

说到这里,你可能要发问了:“不对呀,使用微信登录第三方 App 用的不是 OAuth 2.0 开放协议吗,怎么又扯上 OIDC 了呢?”

没错,用微信登录某第三方软件,确实使用的是 OAuth 2.0。但 OAuth2.0 是一种授权协议,而不是身份认证协议。OIDC 才是身份认证协议,而且是基于 OAuth 2.0 来执行用户身份认证的互通协议。更概括地说,OIDC 就是直接基于 OAuth 2.0 构建的身份认证框架协议。

换种表述方式,OIDC= 授权协议 + 身份认证,是 OAuth 2.0 的超集。为方便理解,我们可以把 OAuth 2.0 理解为面粉,把 OIDC 理解为面包。这下,你是不是就理解它们的关系了?因此,我们说“第三方 App 使用微信登录用到了 OAuth 2.0”没有错,说“使用到了 OIDC”更没有错。

考虑到单点登录、联合登录,都遵循的是 OIDC 的标准流程,因此今天我们就讲讲如何利用 OAuth2.0 来实现一个 OIDC,“高屋建瓴” 地去看问题。掌握了这一点,我们再去做单点登录、联合登录的场景,以及其他更多关于身份认证的场景,就都不再是问题了。

OIDC 和 OAuth 2.0 的角色对应关系

说到“如何利用 OAuth 2.0 来构建 OIDC 这样的认证协议”,我们可以想到一个切入点,这个切入点就是 OAuth 2.0 的四种角色。

OAuth 2.0 的授权码许可流程的运转,需要资源拥有者、第三方软件、授权服务、受保护资源这 4 个角色间的顺畅通信、配合才能够完成。如果我们要想在 OAuth 2.0 的授权码许可类型的基础上,来构建 OIDC 的话,这 4 个角色仍然要继续发挥 “它们的价值”。那么,这 4 个角色又是怎么对应到 OIDC 中的参与方的呢?

那么,我们就先想想一个关于身份认证的协议框架,应该有什么角色。你可能已经想出来了,它需要一个登录第三方软件的最终用户、一个第三方软件,以及一个认证服务来为这个用户提供身份证明的验证判断。

没错,这就是 OIDC 的三个主要角色了。在 OIDC 的官方标准框架中,这三个角色的名字是:

  • EU(End User),代表最终用户。
  • RP(Relying Party),代表认证服务的依赖方,就是上面我提到的第三方软件。
  • OP(OpenID Provider),代表提供身份认证服务方。

EU、RP 和 OP 这三个角色对于 OIDC 非常重要,我后面也会时常使用简称来描述,希望你能先记住。

现在很多 App 都接入了微信登录,那么微信登录就是一个大的身份认证服务(OP)。一旦我们有了微信账号,就可以登录所有接入了微信登录体系的 App(RP),这就是我们常说的联合登录。

现在,我们就借助极客时间的例子,来看一下 OAuth 2.0 的 4 个角色和 OIDC 的 3 个角色之间的对应关系:

图1 OAuth 2.0和OIDC的角色对应关系
图1 OAuth 2.0和OIDC的角色对应关系

OIDC 和 OAuth 2.0 的关键区别

看到这张角色对应关系图,你是不是有点 “恍然大悟” 的感觉:要实现一个 OIDC 协议,不就是直接实现一个 OAuth 2.0 协议吗。没错,我在这一讲的开始也说了,OIDC 就是基于 OAuth 2.0 来实现的一个身份认证协议框架。

我再继续给你画一张 OIDC 的通信流程图,你就更清楚 OIDC 和 OAuth 2.0 的关系了:

图2 基于授权码流程的OIDC通信流程
图2 基于授权码流程的OIDC通信流程

可以发现,一个基于授权码流程的 OIDC 协议流程,跟 OAuth 2.0 中的授权码许可的流程几乎完全一致,唯一的区别就是多返回了一个 ID_TOKEN,我们称之为 ID 令牌。这个令牌是身份认证的关键。所以,接下来我就着重和你讲一下这个令牌,而不再细讲 OIDC 的整个流程。

OIDC 中的 ID 令牌生成和解析方法

在图 2 的 OIDC 通信流程的第 6 步,我们可以看到 ID 令牌(ID_TOKEN)和访问令牌(ACCESS_TOKEN)是一起返回的。关于为什么要同时返回两个令牌,我后面再和你分析。我们先把焦点放在 ID 令牌上。

我们知道,访问令牌不需要被第三方软件解析,因为它对第三方软件来说是不透明的。但 ID 令牌需要能够被第三方软件解析出来,因为第三方软件需要获取 ID 令牌里面的内容,来处理用户的登录

那 ID 令牌的内容是什么呢?

首先,ID 令牌是一个 JWT 格式的令牌。你可以到第 4 讲中复习下 JWT 的相关内容。这里需要强调的是,虽然 JWT 令牌是一种自包含信息体的令牌,为将其作为 ID 令牌带来了方便性,但是因为 ID 令牌需要能够标识出用户、失效时间等属性来达到身份认证的目的,所以要将其作为 OIDC 的 ID 令牌时,下面这 5 个 JWT 声明参数也是必须要有的。

  • iss,令牌的颁发者,其值就是身份认证服务(OP)的 URL。
  • sub,令牌的主题,其值是一个能够代表最终用户(EU)的全局唯一标识符。
  • aud,令牌的目标受众,其值是三方软件(RP)的 app_id。
  • exp,令牌的到期时间戳,所有的 ID 令牌都会有一个过期时间。
  • iat,颁发令牌的时间戳。

生成 ID 令牌这部分的示例代码如下:


+import{_ as h}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a,b as l,d as s,e as p,f as k,r as e,o as d}from"./app-BIdVr4mq.js";const r={};function g(A,i){const n=e("RouteLink");return d(),t("div",null,[i[3]||(i[3]=a(`

如果你是一个第三方软件开发者,在实现用户登录的逻辑时,除了可以让用户新注册一个账号再登录外,还可以接入微信、微博等平台,让用户使用自己的微信、微博账号去登录。同时,如果你的应用下面又有多个子应用,还可以让用户只登录一次就能访问所有的子应用,来提升用户体验。这就是联合登录和单点登录了。再继续深究,它们其实都是 OpenID Connect(简称 OIDC)的应用场景的实现。那 OIDC 又是什么呢?今天,我们就来学习下 OIDC 和 OAuth 2.0 的关系,以及如何用 OAuth 2.0 来实现一个 OIDC 用户身份认证协议。

OIDC 是什么?

OIDC 其实就是一种用户身份认证的开放标准。使用微信账号登录极客时间的场景,就是这种开放标准的实践。

说到这里,你可能要发问了:“不对呀,使用微信登录第三方 App 用的不是 OAuth 2.0 开放协议吗,怎么又扯上 OIDC 了呢?”

没错,用微信登录某第三方软件,确实使用的是 OAuth 2.0。但 OAuth2.0 是一种授权协议,而不是身份认证协议。OIDC 才是身份认证协议,而且是基于 OAuth 2.0 来执行用户身份认证的互通协议。更概括地说,OIDC 就是直接基于 OAuth 2.0 构建的身份认证框架协议。

换种表述方式,OIDC= 授权协议 + 身份认证,是 OAuth 2.0 的超集。为方便理解,我们可以把 OAuth 2.0 理解为面粉,把 OIDC 理解为面包。这下,你是不是就理解它们的关系了?因此,我们说“第三方 App 使用微信登录用到了 OAuth 2.0”没有错,说“使用到了 OIDC”更没有错。

考虑到单点登录、联合登录,都遵循的是 OIDC 的标准流程,因此今天我们就讲讲如何利用 OAuth2.0 来实现一个 OIDC,“高屋建瓴” 地去看问题。掌握了这一点,我们再去做单点登录、联合登录的场景,以及其他更多关于身份认证的场景,就都不再是问题了。

OIDC 和 OAuth 2.0 的角色对应关系

说到“如何利用 OAuth 2.0 来构建 OIDC 这样的认证协议”,我们可以想到一个切入点,这个切入点就是 OAuth 2.0 的四种角色。

OAuth 2.0 的授权码许可流程的运转,需要资源拥有者、第三方软件、授权服务、受保护资源这 4 个角色间的顺畅通信、配合才能够完成。如果我们要想在 OAuth 2.0 的授权码许可类型的基础上,来构建 OIDC 的话,这 4 个角色仍然要继续发挥 “它们的价值”。那么,这 4 个角色又是怎么对应到 OIDC 中的参与方的呢?

那么,我们就先想想一个关于身份认证的协议框架,应该有什么角色。你可能已经想出来了,它需要一个登录第三方软件的最终用户、一个第三方软件,以及一个认证服务来为这个用户提供身份证明的验证判断。

没错,这就是 OIDC 的三个主要角色了。在 OIDC 的官方标准框架中,这三个角色的名字是:

  • EU(End User),代表最终用户。
  • RP(Relying Party),代表认证服务的依赖方,就是上面我提到的第三方软件。
  • OP(OpenID Provider),代表提供身份认证服务方。

EU、RP 和 OP 这三个角色对于 OIDC 非常重要,我后面也会时常使用简称来描述,希望你能先记住。

现在很多 App 都接入了微信登录,那么微信登录就是一个大的身份认证服务(OP)。一旦我们有了微信账号,就可以登录所有接入了微信登录体系的 App(RP),这就是我们常说的联合登录。

现在,我们就借助极客时间的例子,来看一下 OAuth 2.0 的 4 个角色和 OIDC 的 3 个角色之间的对应关系:

图1 OAuth 2.0和OIDC的角色对应关系
图1 OAuth 2.0和OIDC的角色对应关系

OIDC 和 OAuth 2.0 的关键区别

看到这张角色对应关系图,你是不是有点 “恍然大悟” 的感觉:要实现一个 OIDC 协议,不就是直接实现一个 OAuth 2.0 协议吗。没错,我在这一讲的开始也说了,OIDC 就是基于 OAuth 2.0 来实现的一个身份认证协议框架。

我再继续给你画一张 OIDC 的通信流程图,你就更清楚 OIDC 和 OAuth 2.0 的关系了:

图2 基于授权码流程的OIDC通信流程
图2 基于授权码流程的OIDC通信流程

可以发现,一个基于授权码流程的 OIDC 协议流程,跟 OAuth 2.0 中的授权码许可的流程几乎完全一致,唯一的区别就是多返回了一个 ID_TOKEN,我们称之为 ID 令牌。这个令牌是身份认证的关键。所以,接下来我就着重和你讲一下这个令牌,而不再细讲 OIDC 的整个流程。

OIDC 中的 ID 令牌生成和解析方法

在图 2 的 OIDC 通信流程的第 6 步,我们可以看到 ID 令牌(ID_TOKEN)和访问令牌(ACCESS_TOKEN)是一起返回的。关于为什么要同时返回两个令牌,我后面再和你分析。我们先把焦点放在 ID 令牌上。

我们知道,访问令牌不需要被第三方软件解析,因为它对第三方软件来说是不透明的。但 ID 令牌需要能够被第三方软件解析出来,因为第三方软件需要获取 ID 令牌里面的内容,来处理用户的登录

那 ID 令牌的内容是什么呢?

首先,ID 令牌是一个 JWT 格式的令牌。你可以到第 4 讲中复习下 JWT 的相关内容。这里需要强调的是,虽然 JWT 令牌是一种自包含信息体的令牌,为将其作为 ID 令牌带来了方便性,但是因为 ID 令牌需要能够标识出用户、失效时间等属性来达到身份认证的目的,所以要将其作为 OIDC 的 ID 令牌时,下面这 5 个 JWT 声明参数也是必须要有的。

  • iss,令牌的颁发者,其值就是身份认证服务(OP)的 URL。
  • sub,令牌的主题,其值是一个能够代表最终用户(EU)的全局唯一标识符。
  • aud,令牌的目标受众,其值是三方软件(RP)的 app_id。
  • exp,令牌的到期时间戳,所有的 ID 令牌都会有一个过期时间。
  • iat,颁发令牌的时间戳。

生成 ID 令牌这部分的示例代码如下:


 //GENATE ID TOKEN
 String id_token=genrateIdToken(appId,user);
 
diff --git a/assets/1.html-CERq7o11.js b/assets/1.html-BKAh0zdH.js
similarity index 99%
rename from assets/1.html-CERq7o11.js
rename to assets/1.html-BKAh0zdH.js
index 9ea523d18..3e43dc07e 100644
--- a/assets/1.html-CERq7o11.js
+++ b/assets/1.html-BKAh0zdH.js
@@ -1 +1 @@
-import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a as i,o as c}from"./app-LRWZkDAq.js";const n={};function p(l,e){return c(),t("div",null,e[0]||(e[0]=[i('

文章来源:https://zhuanlan.zhihu.com/p/111110992

gcc

它是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)。

我们的程序只有一个源文件时,直接就可以用gcc命令编译它。

可是,如果我们的程序包含很多个源文件时,该咋整?用gcc命令逐个去编译时,就发现很容易混乱而且工作量大,所以出现了下面make工具。

gcc命令在链接时默认使用C的库,只有添加了-lstdc++选项才会使用 C++ 的库。 不过 GCC 中还有一个g++命令,它专门用来编译 C++ 程序,广大 C++ 开发人员也都使用这个命令。g++命令和gcc命令的用法是一样的。

make

make工具可以看成是一个智能的批处理工具,它本身并没有编译和链接的功能,而是用类似于批处理的方式—通过调用makefile文件中用户指定的命令来进行编译和链接的。

makefile

这个是啥东西?

简单的说就像一首歌的乐谱,make工具就像指挥家,指挥家根据乐谱指挥整个乐团怎么样演奏,make工具就根据makefile中的命令进行编译和链接的。makefile命令中就包含了调用gcc(也可以是别的编译器)去编译某个源文件的命令。

makefile在一些简单的工程完全可以人工拿下,但是当工程非常大的时候,手写makefile也是非常麻烦的,如果换了个平台makefile又要重新修改,这时候就出现了下面的Cmake这个工具。

cmake

cmake就可以更加简单的生成makefile文件给上面那个make用。当然cmake还有其他更牛X功能,就是可以跨平台生成对应平台能用的makefile,我们就不用再自己去修改了。

可是cmake根据什么生成makefile呢?它又要根据一个叫CMakeLists.txt文件(学名:组态档)去生成makefile。

CMakeList.txt

到最后CMakeLists.txt文件谁写啊?亲,是你自己手写的。

nmake[1]

nmake又是啥东西?

nmake是Microsoft Visual Studio中的附带命令,需要安装VS,实际上可以说相当于linux的make,明白了么?

20230113135511
20230113135511

总结一下大体流程[2] 1.用编辑器编写源代码,如.c文件。

2.用编译器编译代码生成目标文件,如.o。

3.用链接器连接目标代码生成可执行文件,如.exe。

但如果源文件太多,一个一个编译那得多麻烦啊?于是人们想到,为啥不设计一种类似批处理的程序,来批处理编译源文件呢?

于是就有了make工具,它是一个自动化编译工具,你可以使用一条命令实现完全编译。但是你需要编写一个规则文件,make依据它来批处理编译,这个文件就是makefile,所以编写makefile文件也是一个程序员所必备的技能。

对于一个大工程,编写makefile实在是件复杂的事,于是人们又想,为什么不设计一个工具,读入所有源文件之后,自动生成makefile呢,于是就出现了cmake工具,它能够输出各种各样的makefile或者project文件,从而帮助程序员减轻负担。但是随之而来也就是编写cmakelist文件,它是cmake所依据的规则。(cmake中有很多设置库的,此时还不是可执行文件,而make生成后才是二进制可执行文件。)

',27)]))}const r=a(n,[["render",p],["__file","1.html.vue"]]),s=JSON.parse('{"path":"/cpp/other/1.html","title":"c++中使用的编译工具介绍","lang":"zh-CN","frontmatter":{"title":"c++中使用的编译工具介绍","date":"2022-01-13T00:00:00.000Z","description":"文章来源:https://zhuanlan.zhihu.com/p/111110992 gcc 它是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)。 我们的程序只有一个源文件时,直接就可以用gcc命令编译它。 ...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/cpp/other/1.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"c++中使用的编译工具介绍"}],["meta",{"property":"og:description","content":"文章来源:https://zhuanlan.zhihu.com/p/111110992 gcc 它是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)。 我们的程序只有一个源文件时,直接就可以用gcc命令编译它。 ..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20230113135511.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-01-13T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"c++中使用的编译工具介绍\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20230113135511.png\\"],\\"datePublished\\":\\"2022-01-13T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":3,"title":"gcc","slug":"gcc","link":"#gcc","children":[]},{"level":3,"title":"make","slug":"make","link":"#make","children":[]},{"level":3,"title":"makefile","slug":"makefile","link":"#makefile","children":[]},{"level":3,"title":"cmake","slug":"cmake","link":"#cmake","children":[]},{"level":3,"title":"CMakeList.txt","slug":"cmakelist-txt","link":"#cmakelist-txt","children":[]},{"level":3,"title":"nmake[1]","slug":"nmake-1","link":"#nmake-1","children":[]}],"git":{"createdTime":1673589476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2}]},"readingTime":{"minutes":3.17,"words":951},"filePathRelative":"cpp/other/1.md","localizedDate":"2022年1月13日","excerpt":"

文章来源:https://zhuanlan.zhihu.com/p/111110992

\\n

gcc

\\n

它是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)。

\\n

我们的程序只有一个源文件时,直接就可以用gcc命令编译它。

\\n

可是,如果我们的程序包含很多个源文件时,该咋整?用gcc命令逐个去编译时,就发现很容易混乱而且工作量大,所以出现了下面make工具。

\\n

gcc命令在链接时默认使用C的库,只有添加了-lstdc++选项才会使用 C++ 的库。\\n不过 GCC 中还有一个g++命令,它专门用来编译 C++ 程序,广大 C++ 开发人员也都使用这个命令。g++命令和gcc命令的用法是一样的。

","autoDesc":true}');export{r as comp,s as data}; +import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a as i,o as c}from"./app-BIdVr4mq.js";const n={};function p(l,e){return c(),t("div",null,e[0]||(e[0]=[i('

文章来源:https://zhuanlan.zhihu.com/p/111110992

gcc

它是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)。

我们的程序只有一个源文件时,直接就可以用gcc命令编译它。

可是,如果我们的程序包含很多个源文件时,该咋整?用gcc命令逐个去编译时,就发现很容易混乱而且工作量大,所以出现了下面make工具。

gcc命令在链接时默认使用C的库,只有添加了-lstdc++选项才会使用 C++ 的库。 不过 GCC 中还有一个g++命令,它专门用来编译 C++ 程序,广大 C++ 开发人员也都使用这个命令。g++命令和gcc命令的用法是一样的。

make

make工具可以看成是一个智能的批处理工具,它本身并没有编译和链接的功能,而是用类似于批处理的方式—通过调用makefile文件中用户指定的命令来进行编译和链接的。

makefile

这个是啥东西?

简单的说就像一首歌的乐谱,make工具就像指挥家,指挥家根据乐谱指挥整个乐团怎么样演奏,make工具就根据makefile中的命令进行编译和链接的。makefile命令中就包含了调用gcc(也可以是别的编译器)去编译某个源文件的命令。

makefile在一些简单的工程完全可以人工拿下,但是当工程非常大的时候,手写makefile也是非常麻烦的,如果换了个平台makefile又要重新修改,这时候就出现了下面的Cmake这个工具。

cmake

cmake就可以更加简单的生成makefile文件给上面那个make用。当然cmake还有其他更牛X功能,就是可以跨平台生成对应平台能用的makefile,我们就不用再自己去修改了。

可是cmake根据什么生成makefile呢?它又要根据一个叫CMakeLists.txt文件(学名:组态档)去生成makefile。

CMakeList.txt

到最后CMakeLists.txt文件谁写啊?亲,是你自己手写的。

nmake[1]

nmake又是啥东西?

nmake是Microsoft Visual Studio中的附带命令,需要安装VS,实际上可以说相当于linux的make,明白了么?

20230113135511
20230113135511

总结一下大体流程[2] 1.用编辑器编写源代码,如.c文件。

2.用编译器编译代码生成目标文件,如.o。

3.用链接器连接目标代码生成可执行文件,如.exe。

但如果源文件太多,一个一个编译那得多麻烦啊?于是人们想到,为啥不设计一种类似批处理的程序,来批处理编译源文件呢?

于是就有了make工具,它是一个自动化编译工具,你可以使用一条命令实现完全编译。但是你需要编写一个规则文件,make依据它来批处理编译,这个文件就是makefile,所以编写makefile文件也是一个程序员所必备的技能。

对于一个大工程,编写makefile实在是件复杂的事,于是人们又想,为什么不设计一个工具,读入所有源文件之后,自动生成makefile呢,于是就出现了cmake工具,它能够输出各种各样的makefile或者project文件,从而帮助程序员减轻负担。但是随之而来也就是编写cmakelist文件,它是cmake所依据的规则。(cmake中有很多设置库的,此时还不是可执行文件,而make生成后才是二进制可执行文件。)

',27)]))}const r=a(n,[["render",p],["__file","1.html.vue"]]),s=JSON.parse('{"path":"/cpp/other/1.html","title":"c++中使用的编译工具介绍","lang":"zh-CN","frontmatter":{"title":"c++中使用的编译工具介绍","date":"2022-01-13T00:00:00.000Z","description":"文章来源:https://zhuanlan.zhihu.com/p/111110992 gcc 它是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)。 我们的程序只有一个源文件时,直接就可以用gcc命令编译它。 ...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/cpp/other/1.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"c++中使用的编译工具介绍"}],["meta",{"property":"og:description","content":"文章来源:https://zhuanlan.zhihu.com/p/111110992 gcc 它是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)。 我们的程序只有一个源文件时,直接就可以用gcc命令编译它。 ..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20230113135511.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-01-13T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"c++中使用的编译工具介绍\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20230113135511.png\\"],\\"datePublished\\":\\"2022-01-13T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":3,"title":"gcc","slug":"gcc","link":"#gcc","children":[]},{"level":3,"title":"make","slug":"make","link":"#make","children":[]},{"level":3,"title":"makefile","slug":"makefile","link":"#makefile","children":[]},{"level":3,"title":"cmake","slug":"cmake","link":"#cmake","children":[]},{"level":3,"title":"CMakeList.txt","slug":"cmakelist-txt","link":"#cmakelist-txt","children":[]},{"level":3,"title":"nmake[1]","slug":"nmake-1","link":"#nmake-1","children":[]}],"git":{"createdTime":1673589476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2}]},"readingTime":{"minutes":3.17,"words":951},"filePathRelative":"cpp/other/1.md","localizedDate":"2022年1月13日","excerpt":"

文章来源:https://zhuanlan.zhihu.com/p/111110992

\\n

gcc

\\n

它是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)。

\\n

我们的程序只有一个源文件时,直接就可以用gcc命令编译它。

\\n

可是,如果我们的程序包含很多个源文件时,该咋整?用gcc命令逐个去编译时,就发现很容易混乱而且工作量大,所以出现了下面make工具。

\\n

gcc命令在链接时默认使用C的库,只有添加了-lstdc++选项才会使用 C++ 的库。\\n不过 GCC 中还有一个g++命令,它专门用来编译 C++ 程序,广大 C++ 开发人员也都使用这个命令。g++命令和gcc命令的用法是一样的。

","autoDesc":true}');export{r as comp,s as data}; diff --git a/assets/10.html-CxJyQyMC.js b/assets/10.html-N4UaRZCw.js similarity index 99% rename from assets/10.html-CxJyQyMC.js rename to assets/10.html-N4UaRZCw.js index f8e4f0b2a..6357420a6 100644 --- a/assets/10.html-CxJyQyMC.js +++ b/assets/10.html-N4UaRZCw.js @@ -1 +1 @@ -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as p,o as h}from"./app-LRWZkDAq.js";const n={};function i(o,t){return h(),a("div",null,t[0]||(t[0]=[p('

今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0 的整个知识体系。

当然了,我也会在这个过程中,与你分享我在实践 OAuth 2.0 的过程中,积累的最值得分享的经验。

好,接下来就让我们先串一串 OAuth 2.0 的工作流程吧。

OAuth 2.0 工作流程串讲

img
img

我们一直在讲 OAuth 2.0 是一种授权协议,这种协议可以让第三方软件代表用户去执行被允许的操作。那么,第三方软件就需要向用户索取授权来获得那个令牌。

我们回想下第 1 讲拜访百度王总的例子。只有拿到前台小姐姐给你的门禁卡,你才能够进入百度大楼。这个过程就相当于前台小姐姐给你做了一次授权,而这个授权的凭证就是门禁卡。对应到我们的系统中,门禁卡便相当于访问令牌。

通过“代表”“授权”这样的关键词,我们可以认识到,OAuth 2.0 是一个授权协议,也是一个安全协议。那么,如果我说它也是一种委托协议,你也不要吃惊。

试想一下,用户在微信平台上有修改昵称、修改头像、修改个人兴趣的权限,当第三方软件请求让自己代表用户来操作这些权限的时候,就是第三方软件请求用户把这些权限委托给自己,用户在批准了委托请求之后,才可以代表用户去执行这些操作。

这时,我们细想一下,委托才是 OAuth 2.0 授权概念的根基,因为没有“委托”之意就不会有“代表”行为的发生。

在整个课程讲述授权的过程中,我频繁举例和强调的就是授权码许可流程。在学习授权码流程的时候,你最困惑的一点恐怕莫过于 “为什么要多此一举,非得通过一个授权码 code 来换取访问令牌 access_token”了吧。这个问题我在讲授权码许可的整体流程时也做过分析了,你现在回想起来应该不会再为此“痛苦不堪” 了吧。

我们再来分析下,第三方软件要获取访问令牌,只能通过两个渠道:

  • 一个渠道是第三方软件的前端页面。但是,如果直接返回到前端页面上,访问令牌是很容易被通过浏览器截获的,所以显然不可取。
  • 另外一个渠道是通过后端传输。第三方软件的后端和授权服务的后端之间通信,这样就可以避免令牌被直接暴露的问题。

再往深了想,第三方软件的后端总不能向授权服务的后端 “硬要” 吧,总要告诉授权服务是要哪个用户的 access_token 吧,所以还需要用户的参与。

用户一旦参与进来,访问的第一个页面是第三方软件,用户要授权,第三方软件就需要把用户引导到授权服务页面。但这个时候,用户就跟第三方软件之间没有任何“通信连接”了。如果授权服务通过后端通信直接将令牌给了第三方软件的后端,那第三方软件该如何通知用户呢,恐怕就不太好实现了。

这种情况下就很巧妙地引入了授权码 code:先把 code 通过重定向返回到第三方软件的页面;第三方软件通过浏览器获取到 code 后,再通过后端通信换取 access_token;待拿到 token 之后,由于此时用户已经在第三方软件的服务上,所以可以很容易地通知到用户。

以上,就是授权码许可的整体工作流程了。我们说,这是 OAuth 2.0 授权体系中最完备的流程,其他的授权许可类型,比如资源拥有者凭据许可、客户端凭据许可、隐式许可,都是以此为基础。因此,只要你能理解授权码许可的流程,也就掌握了整个 OAuth 2.0 中所有许可类型的运转机制,在实际工作场景中用上 OAuth 2.0 将不再是问题。

OAuth 2.0 安全问题串讲

但是,到这里并没有万事大吉,我们只是解决了 OAuth 2.0 的基础使用的问题。要想用好、用对这个协议,成长为这个协议的应用专家,我们还必须关注 OAu

我们在实践 OAuth 2.0 的过程中,还必须按照规范建议来执行,否则便会引发一系列的安全问题。这也往往导致有的同学会发出这样的疑问,OAuth 2.0 不是安全的吗?它不是一直在保护着互联网上成千上万个 Web API 吗,我们不也说它是一种安全协议吗?

首先我们说 OAuth 2.0 是安全协议没问题,但如果使用不当也会引起安全上的问题。比如,我们在第 8 讲中提到了一个很广泛的跨站请求伪造问题。之所以出现这样的安全问题,就是因为我们没有遵循 OAuth 2.0 的使用建议,比如没有使用 state 这样的参数来做请求的校验,或者是没有遵循授权码 code 值只能使用一次,并且还要清除使用过的 code 值跟 token 值之间的绑定关系的建议。

在安全问题上,其实我们一直都没有特别说明一点,那就是在使用 OAuth 2.0 的流程中,我们的 HTTP 通信要使用 HTTPS 协议来保护数据传输的安全性。这是因为 OAuth 2.0 支持的 bearer 令牌类型,也就是任意字符串格式的令牌,并没有提供且没有要求使用信息签名的机制。

你可能会说,JWT 令牌有这样的加密机制啊。但其实,这也正说明了 OAuth 2.0 是一个没有约束普通令牌的规则,所以才有了 JWT 这样对 OAuth 2.0 的额外补充。

实际上,JWT 跟 OAuth 2.0 并没有直接关系,它只是一种结构化的信息存储,可以被用在除了 OAuth 2.0 以外的任何地方。比如,重置密码的时候,会给你的邮箱发送一个链接,这个链接就需要能够标识出用户是谁、不能篡改、有效期 5 分钟,这些特征都跟 JWT 相符合。也就是说,JWT 并不是 OAuth 2.0 协议规范所涵盖的内容。

OAuth 2.0 似乎没有自己的规则约束机制,或者说只有比较弱的约束,但其实不是不约束,而是它就致力于做好授权框架这一件事儿。通过我们前面的学习,也可以验证出它的确很好地胜任了这项工作。

也许正是因为 OAuth 2.0 可以支持类似 OIDC 这样的身份认证协议,导致我们总是“坚持”认为 OAuth 2.0 是一种身份认证协议。当然了,OAuth 2.0 并不是身份认证协议,我在第 9 讲中用“面粉”和“面包”来类比 OAuth 2.0 和 OIDC 的关系。

这里我再解释一下。究竟是什么原因导致了我们对 OAuth 2.0 有这样的 “误解” 呢?我想大概原因是,OAuth 2.0 中确实包含了身份认证的内容,即授权服务需要让用户登录以后才可以进行用户确认授权的操作。

但这样的流程,仅仅是 OAuth 2.0 涉及到了身份认证的行为,还不足以让 OAuth 2.0 成为一个真正的用户身份认证协议。因为 OAuth 2.0 关心的只有两点,颁发令牌和使用令牌,并且令牌对第三方软件是不透明的;同时,受保护资源服务也不关心是哪个用户来请求,只要有合法的令牌 “递” 过来,就会给出正确的响应,把数据返回给第三方软件。

以上,就是与 OAuth 2.0 安全问题息息相关的内容了。讲到这里,希望你可以按照自己的理解,融会贯通 OAuth 2.0 的这些核心知识了。接下来,我再和你分享一个我在实践 OAuth 2.0 过程中感触最深的一个问题吧。

再强调都不为过的安全意识

根据我在开放平台上这些年的工作经验,安全意识是实践 OAuth 2.0 过程中,再怎么强调都不为过的问题。

因为总结起来,要说使用 OAuth 2.0 的过程中如果能有哪个机会让你“栽个大跟头”的话,那这个机会一定是在安全上:OAuth 2.0 本就是致力于保护开放的 Web API,保护用户在平台上的资源,如果因为 OAuth 2.0 使用不当而造成安全问题,确实是一件非常 “丢人” 的事情。

而 OAuth2.0 的流程里面能够为安全做贡献的只有两方,一方是第三方软件,一方是平台方。在安全性这个问题上,第三方软件开发者的安全意识参差不齐。那针对这一点,就需要平台方在其官方文档上重笔描述,并给出常见安全漏洞相应的解决方案。同时,作为平台方的内部开发人员,对安全的问题同样不能忽视,而且要有更高的安全意识和认知。

只有第三方软件开发者和平台方的研发人员共同保有较高的安全意识,才能让“安全的墙”垒得越来越高,让攻击者的成本越来越高。因为安全的本质就是成本问题。

你看,我花了这么大的篇幅来和你讲解 OAuth 2.0 的安全问题,并单独分析了安全意识,是不是足以凸显安全性这个问题的重要程度了。没错儿,这也是你能用好 OAuth 2.0 的一个关键标志。

总结

好了,以上就是我们今天的主要内容了。我希望你能记住以下三点:

  1. OAuth 2.0 是一个授权协议,它通过访问令牌来表示这种授权。第三软件拿到访问令牌之后,就可以使用访问令牌来代表用户去访问用户的数据了。所以,我们说授权的核心就是获取访问令牌和使用访问令牌。

  2. OAuth 2.0 是一个安全协议,但是如果你使用不当,它并不能保证一定是安全的。如果你不按照 OAuth 2.0 规范中的建议来实施,就会有安全风险。比如,你没有遵循授权服务中的授权码只能使用一次、第三方软件的重定向 URL 要精确匹配等建议。

  3. 安全防护的过程一直都是“魔高一尺道高一丈”,相互攀升的过程。因此,在使用 OAuth 2.0 的过程中,第三方软件和平台方都要有足够的安全意识,来把“安全的墙”筑得更高。

    最后我想说的是,无论你使用 OAuth 2.0 目的是保护 API,还是作为用户身份认证的基础,OAuth 2.0 都只是解决这些问题的一种工具。而掌握 OAuth 2.0 这种工具的原理及其使用场景,将会帮助你更高效、更优雅地解决这些问题。

思考题

如果你是一名第三方软件的开发人员,你觉得应该如何提高自己的安全意识呢?

',40)]))}const r=e(n,[["render",i],["__file","10.html.vue"]]),l=JSON.parse('{"path":"/other/oauth2/10.html","title":"10 | 串讲:OAuth 2.0的工作流程与安全问题","lang":"zh-CN","frontmatter":{"title":"10 | 串讲:OAuth 2.0的工作流程与安全问题","date":"2021-11-08T00:00:00.000Z","author":"ChenSino","publish":true,"description":"今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0 的整个知识体系。 当然了,我也会在这个过程中,与你分享我在实践 OAuth 2.0 的过程中,积累的最值得分享的经验。 好,接下来就让我们先串一串 OAuth 2.0 的工作流程吧。 OAuth ...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/10.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"10 | 串讲:OAuth 2.0的工作流程与安全问题"}],["meta",{"property":"og:description","content":"今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0 的整个知识体系。 当然了,我也会在这个过程中,与你分享我在实践 OAuth 2.0 的过程中,积累的最值得分享的经验。 好,接下来就让我们先串一串 OAuth 2.0 的工作流程吧。 OAuth ..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/beb02a5baf3654c5025238552cd26a2a.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"ChenSino"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"10 | 串讲:OAuth 2.0的工作流程与安全问题\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/beb02a5baf3654c5025238552cd26a2a.jpg\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\"}]}"]]},"headers":[{"level":3,"title":"OAuth 2.0 安全问题串讲","slug":"oauth-2-0-安全问题串讲","link":"#oauth-2-0-安全问题串讲","children":[]},{"level":3,"title":"再强调都不为过的安全意识","slug":"再强调都不为过的安全意识","link":"#再强调都不为过的安全意识","children":[]},{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"思考题","slug":"思考题","link":"#思考题","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":10.17,"words":3052},"filePathRelative":"other/oauth2/10.md","localizedDate":"2021年11月8日","excerpt":"

今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0 的整个知识体系。

\\n

当然了,我也会在这个过程中,与你分享我在实践 OAuth 2.0 的过程中,积累的最值得分享的经验。

\\n

好,接下来就让我们先串一串 OAuth 2.0 的工作流程吧。

\\n

OAuth 2.0 工作流程串讲

\\n
\\"img\\"
img
","autoDesc":true}');export{r as comp,l as data}; +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as p,o as h}from"./app-BIdVr4mq.js";const n={};function i(o,t){return h(),a("div",null,t[0]||(t[0]=[p('

今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0 的整个知识体系。

当然了,我也会在这个过程中,与你分享我在实践 OAuth 2.0 的过程中,积累的最值得分享的经验。

好,接下来就让我们先串一串 OAuth 2.0 的工作流程吧。

OAuth 2.0 工作流程串讲

img
img

我们一直在讲 OAuth 2.0 是一种授权协议,这种协议可以让第三方软件代表用户去执行被允许的操作。那么,第三方软件就需要向用户索取授权来获得那个令牌。

我们回想下第 1 讲拜访百度王总的例子。只有拿到前台小姐姐给你的门禁卡,你才能够进入百度大楼。这个过程就相当于前台小姐姐给你做了一次授权,而这个授权的凭证就是门禁卡。对应到我们的系统中,门禁卡便相当于访问令牌。

通过“代表”“授权”这样的关键词,我们可以认识到,OAuth 2.0 是一个授权协议,也是一个安全协议。那么,如果我说它也是一种委托协议,你也不要吃惊。

试想一下,用户在微信平台上有修改昵称、修改头像、修改个人兴趣的权限,当第三方软件请求让自己代表用户来操作这些权限的时候,就是第三方软件请求用户把这些权限委托给自己,用户在批准了委托请求之后,才可以代表用户去执行这些操作。

这时,我们细想一下,委托才是 OAuth 2.0 授权概念的根基,因为没有“委托”之意就不会有“代表”行为的发生。

在整个课程讲述授权的过程中,我频繁举例和强调的就是授权码许可流程。在学习授权码流程的时候,你最困惑的一点恐怕莫过于 “为什么要多此一举,非得通过一个授权码 code 来换取访问令牌 access_token”了吧。这个问题我在讲授权码许可的整体流程时也做过分析了,你现在回想起来应该不会再为此“痛苦不堪” 了吧。

我们再来分析下,第三方软件要获取访问令牌,只能通过两个渠道:

  • 一个渠道是第三方软件的前端页面。但是,如果直接返回到前端页面上,访问令牌是很容易被通过浏览器截获的,所以显然不可取。
  • 另外一个渠道是通过后端传输。第三方软件的后端和授权服务的后端之间通信,这样就可以避免令牌被直接暴露的问题。

再往深了想,第三方软件的后端总不能向授权服务的后端 “硬要” 吧,总要告诉授权服务是要哪个用户的 access_token 吧,所以还需要用户的参与。

用户一旦参与进来,访问的第一个页面是第三方软件,用户要授权,第三方软件就需要把用户引导到授权服务页面。但这个时候,用户就跟第三方软件之间没有任何“通信连接”了。如果授权服务通过后端通信直接将令牌给了第三方软件的后端,那第三方软件该如何通知用户呢,恐怕就不太好实现了。

这种情况下就很巧妙地引入了授权码 code:先把 code 通过重定向返回到第三方软件的页面;第三方软件通过浏览器获取到 code 后,再通过后端通信换取 access_token;待拿到 token 之后,由于此时用户已经在第三方软件的服务上,所以可以很容易地通知到用户。

以上,就是授权码许可的整体工作流程了。我们说,这是 OAuth 2.0 授权体系中最完备的流程,其他的授权许可类型,比如资源拥有者凭据许可、客户端凭据许可、隐式许可,都是以此为基础。因此,只要你能理解授权码许可的流程,也就掌握了整个 OAuth 2.0 中所有许可类型的运转机制,在实际工作场景中用上 OAuth 2.0 将不再是问题。

OAuth 2.0 安全问题串讲

但是,到这里并没有万事大吉,我们只是解决了 OAuth 2.0 的基础使用的问题。要想用好、用对这个协议,成长为这个协议的应用专家,我们还必须关注 OAu

我们在实践 OAuth 2.0 的过程中,还必须按照规范建议来执行,否则便会引发一系列的安全问题。这也往往导致有的同学会发出这样的疑问,OAuth 2.0 不是安全的吗?它不是一直在保护着互联网上成千上万个 Web API 吗,我们不也说它是一种安全协议吗?

首先我们说 OAuth 2.0 是安全协议没问题,但如果使用不当也会引起安全上的问题。比如,我们在第 8 讲中提到了一个很广泛的跨站请求伪造问题。之所以出现这样的安全问题,就是因为我们没有遵循 OAuth 2.0 的使用建议,比如没有使用 state 这样的参数来做请求的校验,或者是没有遵循授权码 code 值只能使用一次,并且还要清除使用过的 code 值跟 token 值之间的绑定关系的建议。

在安全问题上,其实我们一直都没有特别说明一点,那就是在使用 OAuth 2.0 的流程中,我们的 HTTP 通信要使用 HTTPS 协议来保护数据传输的安全性。这是因为 OAuth 2.0 支持的 bearer 令牌类型,也就是任意字符串格式的令牌,并没有提供且没有要求使用信息签名的机制。

你可能会说,JWT 令牌有这样的加密机制啊。但其实,这也正说明了 OAuth 2.0 是一个没有约束普通令牌的规则,所以才有了 JWT 这样对 OAuth 2.0 的额外补充。

实际上,JWT 跟 OAuth 2.0 并没有直接关系,它只是一种结构化的信息存储,可以被用在除了 OAuth 2.0 以外的任何地方。比如,重置密码的时候,会给你的邮箱发送一个链接,这个链接就需要能够标识出用户是谁、不能篡改、有效期 5 分钟,这些特征都跟 JWT 相符合。也就是说,JWT 并不是 OAuth 2.0 协议规范所涵盖的内容。

OAuth 2.0 似乎没有自己的规则约束机制,或者说只有比较弱的约束,但其实不是不约束,而是它就致力于做好授权框架这一件事儿。通过我们前面的学习,也可以验证出它的确很好地胜任了这项工作。

也许正是因为 OAuth 2.0 可以支持类似 OIDC 这样的身份认证协议,导致我们总是“坚持”认为 OAuth 2.0 是一种身份认证协议。当然了,OAuth 2.0 并不是身份认证协议,我在第 9 讲中用“面粉”和“面包”来类比 OAuth 2.0 和 OIDC 的关系。

这里我再解释一下。究竟是什么原因导致了我们对 OAuth 2.0 有这样的 “误解” 呢?我想大概原因是,OAuth 2.0 中确实包含了身份认证的内容,即授权服务需要让用户登录以后才可以进行用户确认授权的操作。

但这样的流程,仅仅是 OAuth 2.0 涉及到了身份认证的行为,还不足以让 OAuth 2.0 成为一个真正的用户身份认证协议。因为 OAuth 2.0 关心的只有两点,颁发令牌和使用令牌,并且令牌对第三方软件是不透明的;同时,受保护资源服务也不关心是哪个用户来请求,只要有合法的令牌 “递” 过来,就会给出正确的响应,把数据返回给第三方软件。

以上,就是与 OAuth 2.0 安全问题息息相关的内容了。讲到这里,希望你可以按照自己的理解,融会贯通 OAuth 2.0 的这些核心知识了。接下来,我再和你分享一个我在实践 OAuth 2.0 过程中感触最深的一个问题吧。

再强调都不为过的安全意识

根据我在开放平台上这些年的工作经验,安全意识是实践 OAuth 2.0 过程中,再怎么强调都不为过的问题。

因为总结起来,要说使用 OAuth 2.0 的过程中如果能有哪个机会让你“栽个大跟头”的话,那这个机会一定是在安全上:OAuth 2.0 本就是致力于保护开放的 Web API,保护用户在平台上的资源,如果因为 OAuth 2.0 使用不当而造成安全问题,确实是一件非常 “丢人” 的事情。

而 OAuth2.0 的流程里面能够为安全做贡献的只有两方,一方是第三方软件,一方是平台方。在安全性这个问题上,第三方软件开发者的安全意识参差不齐。那针对这一点,就需要平台方在其官方文档上重笔描述,并给出常见安全漏洞相应的解决方案。同时,作为平台方的内部开发人员,对安全的问题同样不能忽视,而且要有更高的安全意识和认知。

只有第三方软件开发者和平台方的研发人员共同保有较高的安全意识,才能让“安全的墙”垒得越来越高,让攻击者的成本越来越高。因为安全的本质就是成本问题。

你看,我花了这么大的篇幅来和你讲解 OAuth 2.0 的安全问题,并单独分析了安全意识,是不是足以凸显安全性这个问题的重要程度了。没错儿,这也是你能用好 OAuth 2.0 的一个关键标志。

总结

好了,以上就是我们今天的主要内容了。我希望你能记住以下三点:

  1. OAuth 2.0 是一个授权协议,它通过访问令牌来表示这种授权。第三软件拿到访问令牌之后,就可以使用访问令牌来代表用户去访问用户的数据了。所以,我们说授权的核心就是获取访问令牌和使用访问令牌。

  2. OAuth 2.0 是一个安全协议,但是如果你使用不当,它并不能保证一定是安全的。如果你不按照 OAuth 2.0 规范中的建议来实施,就会有安全风险。比如,你没有遵循授权服务中的授权码只能使用一次、第三方软件的重定向 URL 要精确匹配等建议。

  3. 安全防护的过程一直都是“魔高一尺道高一丈”,相互攀升的过程。因此,在使用 OAuth 2.0 的过程中,第三方软件和平台方都要有足够的安全意识,来把“安全的墙”筑得更高。

    最后我想说的是,无论你使用 OAuth 2.0 目的是保护 API,还是作为用户身份认证的基础,OAuth 2.0 都只是解决这些问题的一种工具。而掌握 OAuth 2.0 这种工具的原理及其使用场景,将会帮助你更高效、更优雅地解决这些问题。

思考题

如果你是一名第三方软件的开发人员,你觉得应该如何提高自己的安全意识呢?

',40)]))}const r=e(n,[["render",i],["__file","10.html.vue"]]),l=JSON.parse('{"path":"/other/oauth2/10.html","title":"10 | 串讲:OAuth 2.0的工作流程与安全问题","lang":"zh-CN","frontmatter":{"title":"10 | 串讲:OAuth 2.0的工作流程与安全问题","date":"2021-11-08T00:00:00.000Z","author":"ChenSino","publish":true,"description":"今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0 的整个知识体系。 当然了,我也会在这个过程中,与你分享我在实践 OAuth 2.0 的过程中,积累的最值得分享的经验。 好,接下来就让我们先串一串 OAuth 2.0 的工作流程吧。 OAuth ...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/10.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"10 | 串讲:OAuth 2.0的工作流程与安全问题"}],["meta",{"property":"og:description","content":"今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0 的整个知识体系。 当然了,我也会在这个过程中,与你分享我在实践 OAuth 2.0 的过程中,积累的最值得分享的经验。 好,接下来就让我们先串一串 OAuth 2.0 的工作流程吧。 OAuth ..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/beb02a5baf3654c5025238552cd26a2a.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"ChenSino"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"10 | 串讲:OAuth 2.0的工作流程与安全问题\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/beb02a5baf3654c5025238552cd26a2a.jpg\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\"}]}"]]},"headers":[{"level":3,"title":"OAuth 2.0 安全问题串讲","slug":"oauth-2-0-安全问题串讲","link":"#oauth-2-0-安全问题串讲","children":[]},{"level":3,"title":"再强调都不为过的安全意识","slug":"再强调都不为过的安全意识","link":"#再强调都不为过的安全意识","children":[]},{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"思考题","slug":"思考题","link":"#思考题","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":10.17,"words":3052},"filePathRelative":"other/oauth2/10.md","localizedDate":"2021年11月8日","excerpt":"

今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0 的整个知识体系。

\\n

当然了,我也会在这个过程中,与你分享我在实践 OAuth 2.0 的过程中,积累的最值得分享的经验。

\\n

好,接下来就让我们先串一串 OAuth 2.0 的工作流程吧。

\\n

OAuth 2.0 工作流程串讲

\\n
\\"img\\"
img
","autoDesc":true}');export{r as comp,l as data}; diff --git a/assets/11.html-C-Hi5p0c.js b/assets/11.html-q32x2bnY.js similarity index 99% rename from assets/11.html-C-Hi5p0c.js rename to assets/11.html-q32x2bnY.js index 66e55f8e3..e88137088 100644 --- a/assets/11.html-C-Hi5p0c.js +++ b/assets/11.html-q32x2bnY.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const t={};function h(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

你好,我朱晔,是《Java 业务开发常见错误 100 例》专栏课程的作者。

《OAuth 2.0 实战课》上线之后,我也第一时间关注了这门课。在开篇词中,我看到有一些同学留言问道:“如何使用 Spring Security 来实现 OAuth 2.0?”这时,我想到之前自己写过一篇相关的文章,于是就直接在开篇词下留了言。后面我很快收到了不少用户的点赞和肯定,紧接着极客时间编辑也邀请我从自己的角度为专栏写篇加餐。好吧,功不唐捐,于是我就将之前我写的那篇老文章再次迭代、整理为今天的这一讲内容,希望可以帮助你掌握 OAuth 2.0。

如果你熟悉 Spring Security 的话,肯定知道它因为功能多、组件抽象程度高、配置方式多样,导致了强大且复杂的特性。也因此,Spring Security 的学习成本几乎是 Spring 家族中最高的。但不仅于此,在结合实际的复杂业务场景使用 Spring Security 时,我们还要去理解一些组件的工作原理和流程,不然需要自定义和扩展框架的时候就会手足无措。这就让使用 Spring Security 的门槛更高了。

因此,在决定使用 Spring Security 搭建整套安全体系(授权、认证、权限、审计)之前,我们还需要考虑的是:将来我们的业务会多复杂,徒手写一套安全体系来得划算,还是使用 Spring Security 更好?我相信,这也是王老师给出课程配套代码中,并没有使用 Spring Security 来演示 OAuth 2.0 流程的原因之一。

反过来说,如果你的应用已经使用了 Spring Security 来做鉴权、认证和权限管理的话,那么仍然使用 Spring Security 来实现 OAuth 的成本是很低的。而且,在学习了 OAuth 2.0 的流程打下扎实的基础之后,我们再使用 Spring Security 来配置 OAuth 2.0 就不会那么迷茫了。这也是我在工作中使用 Spring Security 来实现 OAuth 2.0 的直观感受。

所以,我就结合自己的实践和积累,带你使用 Spring Security 来一步一步地搭建一套基于 JWT 的 OAuth 2.0 授权体系。这些内容会涉及 OAuth 2.0 的三角色(客户端、授权服务、受保护资源),以及资源拥有者凭据许可、客户端凭据许可和授权码许可这三种常用的授权许可类型(隐式许可类型,不太安全也不太常用)。同时,我还会演示 OAuth 2.0 的权限控制,以及使用 OAuth 2.0 实现 SSO 单点登录体系。

这样一来,今天这一讲涉及到的流程就会比较多,内容也会很长。不过不用担心,我会手把手带你从零开始,完成整个程序的搭建,并给出所有流程的演示。

项目准备工作

实战之前,我们先来搭建项目父依赖和初始化数据库结构,为后面具体的编码做准备。

首先,我们来创建一个父 POM,内含三个模块:

  • springsecurity101-cloud-oauth2-client,用来扮演客户端角色;

  • springsecurity101-cloud-oauth2-server,用来扮演授权服务器角色;

  • springsecurity101-cloud-oauth2-userservice,是用户服务,用来扮演资源提供者角色。

    
    +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const t={};function h(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

    你好,我朱晔,是《Java 业务开发常见错误 100 例》专栏课程的作者。

    《OAuth 2.0 实战课》上线之后,我也第一时间关注了这门课。在开篇词中,我看到有一些同学留言问道:“如何使用 Spring Security 来实现 OAuth 2.0?”这时,我想到之前自己写过一篇相关的文章,于是就直接在开篇词下留了言。后面我很快收到了不少用户的点赞和肯定,紧接着极客时间编辑也邀请我从自己的角度为专栏写篇加餐。好吧,功不唐捐,于是我就将之前我写的那篇老文章再次迭代、整理为今天的这一讲内容,希望可以帮助你掌握 OAuth 2.0。

    如果你熟悉 Spring Security 的话,肯定知道它因为功能多、组件抽象程度高、配置方式多样,导致了强大且复杂的特性。也因此,Spring Security 的学习成本几乎是 Spring 家族中最高的。但不仅于此,在结合实际的复杂业务场景使用 Spring Security 时,我们还要去理解一些组件的工作原理和流程,不然需要自定义和扩展框架的时候就会手足无措。这就让使用 Spring Security 的门槛更高了。

    因此,在决定使用 Spring Security 搭建整套安全体系(授权、认证、权限、审计)之前,我们还需要考虑的是:将来我们的业务会多复杂,徒手写一套安全体系来得划算,还是使用 Spring Security 更好?我相信,这也是王老师给出课程配套代码中,并没有使用 Spring Security 来演示 OAuth 2.0 流程的原因之一。

    反过来说,如果你的应用已经使用了 Spring Security 来做鉴权、认证和权限管理的话,那么仍然使用 Spring Security 来实现 OAuth 的成本是很低的。而且,在学习了 OAuth 2.0 的流程打下扎实的基础之后,我们再使用 Spring Security 来配置 OAuth 2.0 就不会那么迷茫了。这也是我在工作中使用 Spring Security 来实现 OAuth 2.0 的直观感受。

    所以,我就结合自己的实践和积累,带你使用 Spring Security 来一步一步地搭建一套基于 JWT 的 OAuth 2.0 授权体系。这些内容会涉及 OAuth 2.0 的三角色(客户端、授权服务、受保护资源),以及资源拥有者凭据许可、客户端凭据许可和授权码许可这三种常用的授权许可类型(隐式许可类型,不太安全也不太常用)。同时,我还会演示 OAuth 2.0 的权限控制,以及使用 OAuth 2.0 实现 SSO 单点登录体系。

    这样一来,今天这一讲涉及到的流程就会比较多,内容也会很长。不过不用担心,我会手把手带你从零开始,完成整个程序的搭建,并给出所有流程的演示。

    项目准备工作

    实战之前,我们先来搭建项目父依赖和初始化数据库结构,为后面具体的编码做准备。

    首先,我们来创建一个父 POM,内含三个模块:

    • springsecurity101-cloud-oauth2-client,用来扮演客户端角色;

    • springsecurity101-cloud-oauth2-server,用来扮演授权服务器角色;

    • springsecurity101-cloud-oauth2-userservice,是用户服务,用来扮演资源提供者角色。

      
       <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xmlns="http://maven.apache.org/POM/4.0.0"
                xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      diff --git a/assets/12.html-BNyGNDL2.js b/assets/12.html-DGRJGE6K.js
      similarity index 99%
      rename from assets/12.html-BNyGNDL2.js
      rename to assets/12.html-DGRJGE6K.js
      index 16fcc84ce..48f0b0128 100644
      --- a/assets/12.html-BNyGNDL2.js
      +++ b/assets/12.html-DGRJGE6K.js
      @@ -1 +1 @@
      -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a,o as n}from"./app-LRWZkDAq.js";const l={};function p(o,e){return n(),i("div",null,e[0]||(e[0]=[a('

      在前面几讲,我们一起学习了 OAuth 2.0 在开放环境中的使用过程。那么 OAuth 2.0 不仅仅可以用在开放的场景中,它可以应用到我们任何需要授权 / 鉴权的地方,包括微服务。

      因此今天,我特别邀请了我的朋友杨波,来和你分享一个基于 OAuth 2.0/JWT 的微服务参考架构。杨波,曾先后担任过携程框架部的研发总监和拍拍贷基础架构部的研发总监,在微服务和 OAuth 2.0 有非常丰富的实践经验。

      其中,在携程工作期间,他负责过携程的 API 网关产品的研发工作,包括它和携程的令牌服务的集成;在拍拍贷工作期间,他负责过拍拍贷的令牌服务的研发和运维工作。这两家公司的令牌服务和 OAuth 2.0 类似,但要更简单些。

      接下来,我们就开始学习杨波老师给我们带来的内容吧。

      你好,我是杨波。

      从单体到微服务架构的演进,是当前企业数字化转型的一大趋势。OAuth 2.0是当前业界标准的授权协议,它的核心是若干个针对不同场景的令牌颁发和管理流程;而JWT是一种轻量级、自包含的令牌,可用于在微服务间安全地传递用户信息。

      据我目前了解到的情况,虽然有不少企业已经部分或全部转型到微服务架构,但是在授权认证机制方面,它们一般都是定制自研的,比方说携程和拍拍贷的令牌服务。之所以定制自研,主要原因在于标准的 OAuth 2.0 协议相对比较复杂,门槛也比较高。定制自研固然可以暂时解决企业的问题,但是不具备通用性,也可能有很多潜在的安全风险。

      那么,到底应该如何将行业标准的 OAuth 2.0/JWT 和微服务集成起来呢,又有没有可落地的参考架构呢?

      针对这个问题,今天我就和你分享一种可落地的参考架构。不过,我要提前说明的是,这个架构的思想源于MICRO-SERVICES ARCHITECTURE WITH OAUTH2 AND JWT – PART 1 – OVERVIEW这篇文章。

      根据原作者 Thijs 的描述,他提出的架构已经在企业落地架构了。如果你还想获得关于原架构的更多细节,建议进一步参考“What is PKCE?”这篇文章。我认为,Thijs 给出的架构确实具有可落地性和参考价值,但是他的架构里面对某些微服务层次的命名,例如 BFF 和 Facade 层,和目前主流的微服务架构不符,还有他的架构应该是手绘,不够清晰,也不容易理解。为此,我专门用今天这一讲,来改进 Thijs 给出的架构,并补充针对不同场景的流程。

      为了方便理解,在接下来的讲述中,我会假定有这样一家叫 ACME 的新零售公司,它已经实现了数字化转型,微服务电商平台是支持业务运作的核心基础设施。

      在业务架构方面,ACME 有近千家线下门店,这些门店通过 POS 系统和电商平台对接。公司还有一些物流发货中心,拣选(Order Picking)系统也要和电商平台对接。另外,公司还有很多送货司机,通过 App 和电商平台对接。当然,ACME 还有一些电商网站,做线上营销和销售,这些网站是电商平台的主要流量源。

      虽然支持 ACME 公司业务运作的技术平台很复杂,但是它的核心可以用一个简化的微服务架构图来描述:

      img
      img

      可以看出,这个微服务架构是运行在 Kubernetes 集群中的。当然了,这个架构实际上并不一定需要 Kubernetes 环境,用传统数据中心也可以。另外,它的整体认证授权架构是基于 OAuth 2.0/JWT 实现的。

      接下来,我按这个微服务架构的分层方式,依次和你分析下它的每一层,以及应用认证 / 授权和服务调用的相关流程。这样,你不仅可以理解一个典型的微服务架构该如何分层,还可以弄清楚 OAuth 2.0/JWT 该如何与微服务进行集成。

      微服务分层架构

      ACME 公司的微服务架构,大致可以分为 Nginx 反向代理层、Web 应用层、Gateway 网关层、BEF 层和领域服务层,还包括一个 IDP 服务。总体上讲,这是一种目前主流的微服务架构分层方式,每一层职责单一、清晰。

      接下来,我们具体看看每一层的主要功能。

      Nginx 反向代理层

      首先,Nginx 集群是整个平台的流量入口。Nginx 是 7 层 HTTP 反向代理,主要功能是实现反向路由,也就是将外部流量根据 HOST 主机头或者 PATH,路由到不同的后端,比方说路由到 Web 应用,或者直接到网关 Gateway。在 Kubernetes 体系中,Nginx 是和 Ingress Controller(入口控制器)配合工作的(总称为 Nginx Ingress),Ingress Controller 支持通过 Ingress Rules,配置 Nginx 的路由规则。

      Web 应用层

      这一层主要是一些 Web 应用,html/css/js 等资源就住在这一层。Web 服务层通常采用传统的 Web MVC + 模版引擎方式处理,可以实现服务器端渲染,也可以采用单页 SPA 方式。这一层主要由公司的前端团队负责,通常会使用 Node.js 技术栈来实现,也可以采用 Spring MVC 技术栈实现。具体怎么实现,要看公司的前端团队更擅长哪种技术。当这一层需要后台数据时,可以通过网关调用后台服务获取数据。

      Gateway 网关层

      这一层是微服务调用流量的入口。网关的主要职责是反向路由,也就是将前端请求根据 HOST 主机头、或者 PATH、或者查询参数,路由到后端目标微服务(比如,图中的 IDP/BFF 或者直接到领域服务)。

      另外,网关还承担两个重要的安全职责:

      • 一个是令牌的校验和转换,将前端传递过来的 OAuth 2.0 访问令牌,通过调用 IDP 进行校验,并转换为包含用户和权限信息的 JWT 令牌,再将 JWT 令牌向后台微服务传递。
      • 另外一个是权限校验,网关的路由表可以和 OAuth 2.0 的 Scope 进行关联。这样,网关根据请求令牌中的权限范围 Scope,就可以判断请求是否具有调用后台服务的权限。

      关于安全相关的场景和流程,我会在下一章节做进一步解释。另外,网关还需承担集中式限流、日志监控,以及支持 CORS 等功能。对于网关层的技术选型,当前主流的 API 网关产品,像 Netflix 开源的 Zuul、Spring Cloud Gateway 等,都可以考虑。

      IDP 服务

      IDP 是 Identity Provider 的简称,主要负责 OAuth 2.0 授权协议处理,OAuth 2.0 和 JWT 令牌颁发和管理,以及用户认证等功能。IDP 使用后台的 Login-Service 进行用户认证。对于 IDP 的技术选型,当前主流的 Spring Security OAuth,或者 RedHat 开源的 KeyCloak,都可以考虑。其中,Spring Security OAuth 是一个 OAuth 2.0 的开发框架,适合企业定制。KeyCloak 则是一个开箱即用的 OAuth 2.0/OIDC 产品。

      BFF 层

      BFF 是 Backend for Frontend 的简称,主要实现对后台领域服务的聚合(Aggregation,有点类似数据库的 Join)功能,同时为不同的前端体验(PC/Mobile/ 开放平台等)提供更友好的 API 和数据格式。

      BFF 中可以包含一些业务逻辑,甚至还可以有自己的数据库存储。通常,BFF 要调用两个或两个以上的领域服务,甚至还可能调用其它的 BFF(当然一般并不建议这样调用,因为这样会让调用关系变得错综复杂,无法理解)。

      如果 BFF 需要获取调用用户或者 OAuth 2.0 Scope 相关信息,它可以从传递过来的 JWT 令牌中直接获取。

      BFF 服务可以用 Node.js 开发,也可以用 Java/Spring 等框架开发。

      领域服务层

      领域服务层在整个微服务架构的底层。这些服务包含业务逻辑,通常有自己独立的数据库存储,还可以根据需要调用外部的服务。根据微服务分层原则,领域服务禁止调用其它的领域服务,更不允许反向调用 BFF 服务。这样做是为了保持微服务职责单一(Single Responsibility)和有界上下文(Bounded Context),避免复杂的领域依赖。领域服务是独立的开发、测试和发布单位。在电商领域,常见的领域服务有用户服务、商品服务、订单服务和支付服务等。和 BFF 一样,如果领域服务需要获取调用用户或者 OAuth 2.0 Scope 相关信息,它可以从传递过来的 JWT 令牌中直接获取。可以看到,领域服务和 BFF 服务都是无状态的,它们本身并不存储用户状态,而是通过传递过来的 JWT 数据获取用户信息。所以在整个架构中,微服务都是无状态、可以按需水平扩展的,状态要么存在用户端(浏览器或者手机 App 中),要么存在集中的数据库中。

      OAuth 2.0/JWT 如何与微服务进行集成?

      以上,就是 ACME 公司的整个微服务架构的层次了。这个分层架构,对于大部分的互联网业务系统场景都适用。因此,如果你是一家企业的架构师,需要设计一套微服务架构,完全可以参考它来设计。接下来,我再演示几个典型的应用认证场景,以及相应的服务调用流程,来帮助你理解 OAuth 2.0/JWT 是如何和微服务进行集成的。

      场景 1:第一方 Web 应用 + 资源拥有者凭据模式

      这个场景是用户访问 ACME 公司自己的电商网站,假设这个电商网站是用 Spring MVC 开发的。考虑到这是一个第一方场景(也就是公司自己开发的网站应用),我们可以选 OAuth 2.0 的资源拥有者凭据许可(Resource Owner Password Credentials Grant),也可以选更安全的授权码许可(Authorization Code Grant)。因为这里没有第三方的概念,所以我们就选相对简单的资源拥有者凭据许可。下面是一个认证授权流程样例。注意,这个只是突出了关键步骤,实际生产的话,还有很多需要完善和优化的地方。另外,为描述简单,这里假定一个成功流程。

      img
      img

      在上面的图中,用户对应 OAuth 2.0 中的资源拥有者,ACME IDP 对应 OAuth 2.0 中的授权服务。另外,前面架构图中的后台微服务(包括 BFF 和基础领域服务),对应 OAuth 2.0 中的受保护资源。下面是流程说明:

      1. 用户通过浏览器访问 ACME 公司的电商网站,点击登录链接。
      2. Web 应用返回登录界面(这个登录页可以是网站自己定制开发)。
      3. 用户输入用户名、密码进行认证。
      4. Web 应用将用户名、密码,通过网关转发到 IDP 的令牌获取端点(POST /oauth2/token,grant_type=password)。
      5. IDP 通过 Login Service 对用户进行认证。
      6. IDP 认证通过,返回有效访问令牌(根据需要也可以返回刷新令牌)。
      7. Web 应用接收到访问令牌,创建用户 Session,并将 OAuth 2.0 令牌保存其中,然后返回登录成功到用户端。
      8. 用户浏览器中记录 Session Cookie,登录成功。

      那接下来,我们再来看看认证授权之后的服务调用流程。同样,这里也只是突出了关键步骤,并假定是一个成功流程。

      img
      img
      1. 用户登录后,在网站上点击查看自己的购物历史记录。
      2. Web 应用通过网关调用后台 API(查询用户的购物历史记录),请求 HTTP header 中带上 OAuth 2.0 令牌(来自用户 Session)。
      3. 网关截取 OAuth 2.0 令牌,去 IDP 进行校验。
      4. IDP 校验令牌通过,再通过令牌查询用户和 Scope 信息,构建 JWT 令牌,返回。
      5. 网关获得 JWT 令牌,校验 Scope 是否有权限调用 API,如果有就转发到后台 API 进行调用。
      6. 后台 BFF(或者领域服务)通过传递过来的 JWT 获取用户信息,根据用户 ID 查询购物历史记录,返回。
      7. Web 应用获得用户的购物历史数据,可以根据需要缓存在 Session 中,再返回用户端。
      8. 购物历史数据返回到用户浏览器端。

      注意,这个服务调用流程,也可以应用在其他场景中,比如我们接下来要学习的“第一方移动应用 + 授权码许可模式”和“第三方 Web 应用 + 授权码许可模式”。基本上只要你理解了这个流程原理,就可以根据实际场景灵活套用。

      场景 2:第一方移动应用 + 授权码许可模式

      第二个场景是用户通过手机访问 ACME 公司自己的电商 App。这是第一方的原生应用(Native App)场景,通常考虑选用 OAuth 2.0 的用户名密码模式,但是并不安全(参考MICRO-SERVICES ARCHITECTURE WITH OAUTH2 AND JWT – PART 3 – IDP的 Security Consideration 部分),所以业界建议采用授权码模式,而且是要支持PKCE扩展的授权码模式。那接下来,我们来看看这个认证授权的流程。同样,这里只是突出了关键步骤,并假定是一个成功流程。

      img
      img
      1. 用户访问电商 App,点击登录。
      2. App 生成 PKCE 相关的 code verifier + challenge。
      3. App 以内嵌方式启动手机浏览器,访问 IDP 的统一认证页 (GET /authorize),请求带上 PKCE 的 code challenge 相关参数。
      4. IDP 返回统一认证页。
      5. 用户认证和授权。
      6. IDP 通过 Login Service 对用户进行认证。
      7. IDP 返回授权码到 App 浏览器。
      8. App 截取浏览器带回的授权码,将授权码 +PKCE code verifer,通过网关转发到 IDP 的令牌获取端点(POST /oauth2/token, grant_type=authorization-code)。
      9. IDP 校验 PKCE 和授权码,校验通过则返回有效访问令牌。
      10. App 获取令牌,本地存储,登录成功。

      之后,App 如果需要和后台交互,可直接通过网关调用后台微服务,请求 HTTP header 中带上 OAuth 2.0 访问令牌即可。后续的服务调用流程,和“第一方应用 + 资源拥有者凭据模式”类似。

      场景 3:第三方 Web 应用 + 授权码模式

      第三个场景是某第三方合作厂商开发了一个 Web 网站,要访问 ACME 公司的电商开放平台 API。这是一个第三方 Web 应用场景,通常选用 OAuth 2.0 的授权码许可模式。那接下来,我们来看看这个认证授权的流程。同样,这里只是突出了关键步骤,并假设是一个成功流程。

      img
      img
      1. 用户访问这个第三方 Web 应用,点击登录链接。
      2. Web 应用后台向 ACME 公司的 IDP 服务发送申请授权码请求(GET /authorize)。
      3. 用户被重定向到 ACME 公司的 IDP 统一登录页面。
      4. 用户进行认证和授权。
      5. IDP 通过 Login Service 对用户进行认证。
      6. 认证和授权通过,IDP 返回授权码。
      7. Web 应用获得授权码,再向 IDP 服务的令牌获取端点发起请求(POST /oauth2/token, grant_type=authorization-code)。
      8. IDP 校验授权码,校验通过则返回有效 OAuth 2.0 令牌(根据需要也可以返回刷新令牌)。
      9. Web 应用创建用户 Session,将 OAuth 2.0 令牌保存在 Session 中,然后返回登录成功到用户端。
      10. 用户浏览器中记录 Session Cookie,登录成功。

      之后,第三方 Web 应用如果需要和 ACME 电商平台交互,可直接通过网关调用微服务,请求 HTTP header 中带上 OAuth 2.0 访问令牌即可。后续的服务调用流程,和前面的“第一方应用 + 资源拥有者凭据模式”类似。

      额外说明

      除了上面的三个主要场景和流程,我还要和你分享 6 点。这 6 点是对上面基本流程的补充,也是企业级的 OAuth 2.0 应用要额外考虑的。

      第一点是,IDP 的 API 要支持从 OAuth 2.0 访问令牌到 JWT 令牌的互转。 今天我们提到的集成架构采用 OAuth 2.0 访问令牌 + JWT 令牌的混合模式,中间需要实现 OAuth 2.0 访问令牌到 JWT 令牌的互转。这个互转 API 并非 OAuth 2.0 的标准,有些 IDP 产品(比方 Spring Security OAuth)可能并不支持,因此需要用户定制扩展。

      第二点是,关于单页 SPA 应用场景。 关于单页 SPA 应用场景,简单做法是采用隐式许可,但是这个模式是 OAuth 2.0 中比较不安全的,所以一般不建议采用。对于纯单页 SPA 应用,业界推荐的做法是:

      • 如果浏览器支持 Web Crypto for PKCE,则可以考虑使用类似“第一方移动应用”场景下的授权码许可 +PKCE 扩展流程;
      • 否则,考虑 SPA+ 传统 Web 混合(hybrid)模式,前端页面可以住在客户浏览器端中,但登录认证还是由后台 Web 站点配合实现,走类似“第一方 Web 应用”场景的资源拥有者凭据模式,或者“第三方 Web 应用”场景下的授权码许可模式。

      第三点是,关于 SSO 单点登录场景。 为了简化描述,上面的流程没有考虑 SSO 单点登录场景。如果要支持 Web SSO,那么各种应用场景都必须通过浏览器 +IDP 登录页集中登录,并且 IDP 要支持 Session,用于维护登录态。如果 IDP 以集群方式部署的话,还要考虑粘性 Sticky Session 或者集中式 Session。

      这样,当用户通过一个 Web 应用登录后,后续如果再用其它 Web 应用登录的话,只要 IDP 上的 Session 还存在,那么这个登录就可以自动完成,相当于单点登录。

      当然,如果要支持 SSO,IDP 的 Session Cookie 要种在 Web 应用的根域上,也就是说不同 Web 应用的根域必须相同,否则会有跨域问题。

      第四点是关于 IDP 和网关的部署方式。 前面的几张架构图中,IDP 虽然躲在网关后面,但实际上 IDP 可以直接通过 Nginx 对外暴露,不经过网关。或者,IDP 的登录授权页面,可以通过 Nginx 直接暴露,API 接口则走网关。

      第五点是关于刷新令牌。 为了简化描述,上面的流程没有详细说明刷新令牌的集成方式。企业根据场景需要,可以启用刷新令牌,来延长用户的登录时间,具体的集成方式需要考虑安全性的需求。

      第六点是关于 Web Session。 为了简化描述,在上面的流程中,Web 应用登录成功后假设启用 Web Session,也就是服务器端 Session。在实际场景中,Web Session 并非唯一选择,也可以采用简单的客户端 Session 方式,也称无状态 Session,也就是在客户端浏览器 Cookie 中保存 OAuth 2.0 访问令牌。

      小结

      好了,以上就是今天的主要内容了。今天,我和你分享了如何将行业标准的 OAuth 2.0/JWT 和微服务集成起来,你需要记住以下四点。

      第一,目前主流的微服务架构大致可以分为 5 层,分别是:Nginx 流量接入层 ->Web 应用层 ->API 网关层 ->BFF 聚合层 -> 领域服务层。这个架构可以住在云原生的 Kubernetes 环境中,也可以住在传统数据中心里头。

      第二,API 网关是微服务调用的入口,承担重要的安全认证和鉴权功能。主要的安全操作包括:一,通过 IDP 校验 OAuth 2.0 访问令牌,并获取带用户和权限信息的 JWT 令牌;二,基于 OAuth 2.0 的 Scope 对 API 调用进行鉴权。

      第三,在微服务架构体系下,通常需要一个集中的 IDP 服务,它相当于一个 Authentication & Authorization as a Service 角色,负责令牌颁发 / 校验 / 管理,还有用户认证。

      第四,在今天这一讲提出的架构中,Web 应用层(网关之前)的安全机制主要基于 OAuth 2.0 访问令牌实现(它是一种透明令牌或者称引用令牌),微服务层(网关之后)的安全机制主要基于 JWT 令牌实现(它是一种不透明的自包含令牌)。网关层在中间实现两种令牌的转换。这是一种 OAuth 2.0 访问令牌 +JWT 令牌的混合模式。

      之所以这样设计,是因为 Web 层靠近用户端,如果采用 JWT 令牌,会暴露用户信息,有一定的安全风险,所以采用 OAuth 2.0 访问令牌,它是一个无意义随机字符串。而在网关之后,安全风险相对低,同时很多服务需要用户信息,所以采用自包含用户信息的 JWT 令牌更合适。当然,如果企业内网没有特别的安全考量,也可以直接传递完全透明的用户信息(例如使用 JSON 格式)。

      思考题

      1. 除了今天我们讲到的 OAuth 2.0 访问令牌 +JWT 令牌的混合模式,实践中也可以全程采用 OAuth 2.0 访问令牌,或者全程采用 JWT 令牌。对比混合模式,如果全程采用 OAuth 2.0 访问令牌,或者全程采用 JWT 令牌,你觉得有哪些利弊呢?
      2. 你可以说说自己对基于传统 Web 应用的认证授权机制的理解吗?并对比今天讲到的现代微服务的认证授权机制,你可以说说它们之间的本质差异和相似点吗?
      ',78)]))}const s=t(l,[["render",p],["__file","12.html.vue"]]),c=JSON.parse('{"path":"/other/oauth2/12.html","title":"12 | 架构案例:基于OAuth 2.0/JWT的微服务参考架构","lang":"zh-CN","frontmatter":{"title":"12 | 架构案例:基于OAuth 2.0/JWT的微服务参考架构","date":"2021-11-08T00:00:00.000Z","author":"ChenSino","publish":true,"description":"在前面几讲,我们一起学习了 OAuth 2.0 在开放环境中的使用过程。那么 OAuth 2.0 不仅仅可以用在开放的场景中,它可以应用到我们任何需要授权 / 鉴权的地方,包括微服务。 因此今天,我特别邀请了我的朋友杨波,来和你分享一个基于 OAuth 2.0/JWT 的微服务参考架构。杨波,曾先后担任过携程框架部的研发总监和拍拍贷基础架构部的研发总监...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/12.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"12 | 架构案例:基于OAuth 2.0/JWT的微服务参考架构"}],["meta",{"property":"og:description","content":"在前面几讲,我们一起学习了 OAuth 2.0 在开放环境中的使用过程。那么 OAuth 2.0 不仅仅可以用在开放的场景中,它可以应用到我们任何需要授权 / 鉴权的地方,包括微服务。 因此今天,我特别邀请了我的朋友杨波,来和你分享一个基于 OAuth 2.0/JWT 的微服务参考架构。杨波,曾先后担任过携程框架部的研发总监和拍拍贷基础架构部的研发总监..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/228199yya6051f1f62f23547a88be4ff.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"ChenSino"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"12 | 架构案例:基于OAuth 2.0/JWT的微服务参考架构\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/228199yya6051f1f62f23547a88be4ff.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/b658befe1da937fa3685b55522487dbd.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/c88e46dd26deb76d6yy8f42f83066f4a.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/443dab973274d8d13c76b2ef4cd1d393.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/02affbdf32f005af65454f3acc4cd957.jpg\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\"}]}"]]},"headers":[{"level":2,"title":"微服务分层架构","slug":"微服务分层架构","link":"#微服务分层架构","children":[{"level":3,"title":"Nginx 反向代理层","slug":"nginx-反向代理层","link":"#nginx-反向代理层","children":[]},{"level":3,"title":"Web 应用层","slug":"web-应用层","link":"#web-应用层","children":[]},{"level":3,"title":"Gateway 网关层","slug":"gateway-网关层","link":"#gateway-网关层","children":[]},{"level":3,"title":"IDP 服务","slug":"idp-服务","link":"#idp-服务","children":[]},{"level":3,"title":"BFF 层","slug":"bff-层","link":"#bff-层","children":[]},{"level":3,"title":"领域服务层","slug":"领域服务层","link":"#领域服务层","children":[]}]},{"level":2,"title":"OAuth 2.0/JWT 如何与微服务进行集成?","slug":"oauth-2-0-jwt-如何与微服务进行集成","link":"#oauth-2-0-jwt-如何与微服务进行集成","children":[{"level":3,"title":"场景 1:第一方 Web 应用 + 资源拥有者凭据模式","slug":"场景-1-第一方-web-应用-资源拥有者凭据模式","link":"#场景-1-第一方-web-应用-资源拥有者凭据模式","children":[]},{"level":3,"title":"场景 2:第一方移动应用 + 授权码许可模式","slug":"场景-2-第一方移动应用-授权码许可模式","link":"#场景-2-第一方移动应用-授权码许可模式","children":[]},{"level":3,"title":"场景 3:第三方 Web 应用 + 授权码模式","slug":"场景-3-第三方-web-应用-授权码模式","link":"#场景-3-第三方-web-应用-授权码模式","children":[]}]},{"level":2,"title":"小结","slug":"小结","link":"#小结","children":[]},{"level":2,"title":"思考题","slug":"思考题","link":"#思考题","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":19.48,"words":5844},"filePathRelative":"other/oauth2/12.md","localizedDate":"2021年11月8日","excerpt":"
      \\n

      在前面几讲,我们一起学习了 OAuth 2.0 在开放环境中的使用过程。那么 OAuth 2.0 不仅仅可以用在开放的场景中,它可以应用到我们任何需要授权 / 鉴权的地方,包括微服务。

      \\n
      \\n
      \\n

      因此今天,我特别邀请了我的朋友杨波,来和你分享一个基于 OAuth 2.0/JWT 的微服务参考架构。杨波,曾先后担任过携程框架部的研发总监和拍拍贷基础架构部的研发总监,在微服务和 OAuth 2.0 有非常丰富的实践经验。

      \\n
      \\n
      \\n

      其中,在携程工作期间,他负责过携程的 API 网关产品的研发工作,包括它和携程的令牌服务的集成;在拍拍贷工作期间,他负责过拍拍贷的令牌服务的研发和运维工作。这两家公司的令牌服务和 OAuth 2.0 类似,但要更简单些。

      \\n
      ","autoDesc":true}');export{s as comp,c as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a,o as n}from"./app-BIdVr4mq.js";const l={};function p(o,e){return n(),i("div",null,e[0]||(e[0]=[a('

      在前面几讲,我们一起学习了 OAuth 2.0 在开放环境中的使用过程。那么 OAuth 2.0 不仅仅可以用在开放的场景中,它可以应用到我们任何需要授权 / 鉴权的地方,包括微服务。

      因此今天,我特别邀请了我的朋友杨波,来和你分享一个基于 OAuth 2.0/JWT 的微服务参考架构。杨波,曾先后担任过携程框架部的研发总监和拍拍贷基础架构部的研发总监,在微服务和 OAuth 2.0 有非常丰富的实践经验。

      其中,在携程工作期间,他负责过携程的 API 网关产品的研发工作,包括它和携程的令牌服务的集成;在拍拍贷工作期间,他负责过拍拍贷的令牌服务的研发和运维工作。这两家公司的令牌服务和 OAuth 2.0 类似,但要更简单些。

      接下来,我们就开始学习杨波老师给我们带来的内容吧。

      你好,我是杨波。

      从单体到微服务架构的演进,是当前企业数字化转型的一大趋势。OAuth 2.0是当前业界标准的授权协议,它的核心是若干个针对不同场景的令牌颁发和管理流程;而JWT是一种轻量级、自包含的令牌,可用于在微服务间安全地传递用户信息。

      据我目前了解到的情况,虽然有不少企业已经部分或全部转型到微服务架构,但是在授权认证机制方面,它们一般都是定制自研的,比方说携程和拍拍贷的令牌服务。之所以定制自研,主要原因在于标准的 OAuth 2.0 协议相对比较复杂,门槛也比较高。定制自研固然可以暂时解决企业的问题,但是不具备通用性,也可能有很多潜在的安全风险。

      那么,到底应该如何将行业标准的 OAuth 2.0/JWT 和微服务集成起来呢,又有没有可落地的参考架构呢?

      针对这个问题,今天我就和你分享一种可落地的参考架构。不过,我要提前说明的是,这个架构的思想源于MICRO-SERVICES ARCHITECTURE WITH OAUTH2 AND JWT – PART 1 – OVERVIEW这篇文章。

      根据原作者 Thijs 的描述,他提出的架构已经在企业落地架构了。如果你还想获得关于原架构的更多细节,建议进一步参考“What is PKCE?”这篇文章。我认为,Thijs 给出的架构确实具有可落地性和参考价值,但是他的架构里面对某些微服务层次的命名,例如 BFF 和 Facade 层,和目前主流的微服务架构不符,还有他的架构应该是手绘,不够清晰,也不容易理解。为此,我专门用今天这一讲,来改进 Thijs 给出的架构,并补充针对不同场景的流程。

      为了方便理解,在接下来的讲述中,我会假定有这样一家叫 ACME 的新零售公司,它已经实现了数字化转型,微服务电商平台是支持业务运作的核心基础设施。

      在业务架构方面,ACME 有近千家线下门店,这些门店通过 POS 系统和电商平台对接。公司还有一些物流发货中心,拣选(Order Picking)系统也要和电商平台对接。另外,公司还有很多送货司机,通过 App 和电商平台对接。当然,ACME 还有一些电商网站,做线上营销和销售,这些网站是电商平台的主要流量源。

      虽然支持 ACME 公司业务运作的技术平台很复杂,但是它的核心可以用一个简化的微服务架构图来描述:

      img
      img

      可以看出,这个微服务架构是运行在 Kubernetes 集群中的。当然了,这个架构实际上并不一定需要 Kubernetes 环境,用传统数据中心也可以。另外,它的整体认证授权架构是基于 OAuth 2.0/JWT 实现的。

      接下来,我按这个微服务架构的分层方式,依次和你分析下它的每一层,以及应用认证 / 授权和服务调用的相关流程。这样,你不仅可以理解一个典型的微服务架构该如何分层,还可以弄清楚 OAuth 2.0/JWT 该如何与微服务进行集成。

      微服务分层架构

      ACME 公司的微服务架构,大致可以分为 Nginx 反向代理层、Web 应用层、Gateway 网关层、BEF 层和领域服务层,还包括一个 IDP 服务。总体上讲,这是一种目前主流的微服务架构分层方式,每一层职责单一、清晰。

      接下来,我们具体看看每一层的主要功能。

      Nginx 反向代理层

      首先,Nginx 集群是整个平台的流量入口。Nginx 是 7 层 HTTP 反向代理,主要功能是实现反向路由,也就是将外部流量根据 HOST 主机头或者 PATH,路由到不同的后端,比方说路由到 Web 应用,或者直接到网关 Gateway。在 Kubernetes 体系中,Nginx 是和 Ingress Controller(入口控制器)配合工作的(总称为 Nginx Ingress),Ingress Controller 支持通过 Ingress Rules,配置 Nginx 的路由规则。

      Web 应用层

      这一层主要是一些 Web 应用,html/css/js 等资源就住在这一层。Web 服务层通常采用传统的 Web MVC + 模版引擎方式处理,可以实现服务器端渲染,也可以采用单页 SPA 方式。这一层主要由公司的前端团队负责,通常会使用 Node.js 技术栈来实现,也可以采用 Spring MVC 技术栈实现。具体怎么实现,要看公司的前端团队更擅长哪种技术。当这一层需要后台数据时,可以通过网关调用后台服务获取数据。

      Gateway 网关层

      这一层是微服务调用流量的入口。网关的主要职责是反向路由,也就是将前端请求根据 HOST 主机头、或者 PATH、或者查询参数,路由到后端目标微服务(比如,图中的 IDP/BFF 或者直接到领域服务)。

      另外,网关还承担两个重要的安全职责:

      • 一个是令牌的校验和转换,将前端传递过来的 OAuth 2.0 访问令牌,通过调用 IDP 进行校验,并转换为包含用户和权限信息的 JWT 令牌,再将 JWT 令牌向后台微服务传递。
      • 另外一个是权限校验,网关的路由表可以和 OAuth 2.0 的 Scope 进行关联。这样,网关根据请求令牌中的权限范围 Scope,就可以判断请求是否具有调用后台服务的权限。

      关于安全相关的场景和流程,我会在下一章节做进一步解释。另外,网关还需承担集中式限流、日志监控,以及支持 CORS 等功能。对于网关层的技术选型,当前主流的 API 网关产品,像 Netflix 开源的 Zuul、Spring Cloud Gateway 等,都可以考虑。

      IDP 服务

      IDP 是 Identity Provider 的简称,主要负责 OAuth 2.0 授权协议处理,OAuth 2.0 和 JWT 令牌颁发和管理,以及用户认证等功能。IDP 使用后台的 Login-Service 进行用户认证。对于 IDP 的技术选型,当前主流的 Spring Security OAuth,或者 RedHat 开源的 KeyCloak,都可以考虑。其中,Spring Security OAuth 是一个 OAuth 2.0 的开发框架,适合企业定制。KeyCloak 则是一个开箱即用的 OAuth 2.0/OIDC 产品。

      BFF 层

      BFF 是 Backend for Frontend 的简称,主要实现对后台领域服务的聚合(Aggregation,有点类似数据库的 Join)功能,同时为不同的前端体验(PC/Mobile/ 开放平台等)提供更友好的 API 和数据格式。

      BFF 中可以包含一些业务逻辑,甚至还可以有自己的数据库存储。通常,BFF 要调用两个或两个以上的领域服务,甚至还可能调用其它的 BFF(当然一般并不建议这样调用,因为这样会让调用关系变得错综复杂,无法理解)。

      如果 BFF 需要获取调用用户或者 OAuth 2.0 Scope 相关信息,它可以从传递过来的 JWT 令牌中直接获取。

      BFF 服务可以用 Node.js 开发,也可以用 Java/Spring 等框架开发。

      领域服务层

      领域服务层在整个微服务架构的底层。这些服务包含业务逻辑,通常有自己独立的数据库存储,还可以根据需要调用外部的服务。根据微服务分层原则,领域服务禁止调用其它的领域服务,更不允许反向调用 BFF 服务。这样做是为了保持微服务职责单一(Single Responsibility)和有界上下文(Bounded Context),避免复杂的领域依赖。领域服务是独立的开发、测试和发布单位。在电商领域,常见的领域服务有用户服务、商品服务、订单服务和支付服务等。和 BFF 一样,如果领域服务需要获取调用用户或者 OAuth 2.0 Scope 相关信息,它可以从传递过来的 JWT 令牌中直接获取。可以看到,领域服务和 BFF 服务都是无状态的,它们本身并不存储用户状态,而是通过传递过来的 JWT 数据获取用户信息。所以在整个架构中,微服务都是无状态、可以按需水平扩展的,状态要么存在用户端(浏览器或者手机 App 中),要么存在集中的数据库中。

      OAuth 2.0/JWT 如何与微服务进行集成?

      以上,就是 ACME 公司的整个微服务架构的层次了。这个分层架构,对于大部分的互联网业务系统场景都适用。因此,如果你是一家企业的架构师,需要设计一套微服务架构,完全可以参考它来设计。接下来,我再演示几个典型的应用认证场景,以及相应的服务调用流程,来帮助你理解 OAuth 2.0/JWT 是如何和微服务进行集成的。

      场景 1:第一方 Web 应用 + 资源拥有者凭据模式

      这个场景是用户访问 ACME 公司自己的电商网站,假设这个电商网站是用 Spring MVC 开发的。考虑到这是一个第一方场景(也就是公司自己开发的网站应用),我们可以选 OAuth 2.0 的资源拥有者凭据许可(Resource Owner Password Credentials Grant),也可以选更安全的授权码许可(Authorization Code Grant)。因为这里没有第三方的概念,所以我们就选相对简单的资源拥有者凭据许可。下面是一个认证授权流程样例。注意,这个只是突出了关键步骤,实际生产的话,还有很多需要完善和优化的地方。另外,为描述简单,这里假定一个成功流程。

      img
      img

      在上面的图中,用户对应 OAuth 2.0 中的资源拥有者,ACME IDP 对应 OAuth 2.0 中的授权服务。另外,前面架构图中的后台微服务(包括 BFF 和基础领域服务),对应 OAuth 2.0 中的受保护资源。下面是流程说明:

      1. 用户通过浏览器访问 ACME 公司的电商网站,点击登录链接。
      2. Web 应用返回登录界面(这个登录页可以是网站自己定制开发)。
      3. 用户输入用户名、密码进行认证。
      4. Web 应用将用户名、密码,通过网关转发到 IDP 的令牌获取端点(POST /oauth2/token,grant_type=password)。
      5. IDP 通过 Login Service 对用户进行认证。
      6. IDP 认证通过,返回有效访问令牌(根据需要也可以返回刷新令牌)。
      7. Web 应用接收到访问令牌,创建用户 Session,并将 OAuth 2.0 令牌保存其中,然后返回登录成功到用户端。
      8. 用户浏览器中记录 Session Cookie,登录成功。

      那接下来,我们再来看看认证授权之后的服务调用流程。同样,这里也只是突出了关键步骤,并假定是一个成功流程。

      img
      img
      1. 用户登录后,在网站上点击查看自己的购物历史记录。
      2. Web 应用通过网关调用后台 API(查询用户的购物历史记录),请求 HTTP header 中带上 OAuth 2.0 令牌(来自用户 Session)。
      3. 网关截取 OAuth 2.0 令牌,去 IDP 进行校验。
      4. IDP 校验令牌通过,再通过令牌查询用户和 Scope 信息,构建 JWT 令牌,返回。
      5. 网关获得 JWT 令牌,校验 Scope 是否有权限调用 API,如果有就转发到后台 API 进行调用。
      6. 后台 BFF(或者领域服务)通过传递过来的 JWT 获取用户信息,根据用户 ID 查询购物历史记录,返回。
      7. Web 应用获得用户的购物历史数据,可以根据需要缓存在 Session 中,再返回用户端。
      8. 购物历史数据返回到用户浏览器端。

      注意,这个服务调用流程,也可以应用在其他场景中,比如我们接下来要学习的“第一方移动应用 + 授权码许可模式”和“第三方 Web 应用 + 授权码许可模式”。基本上只要你理解了这个流程原理,就可以根据实际场景灵活套用。

      场景 2:第一方移动应用 + 授权码许可模式

      第二个场景是用户通过手机访问 ACME 公司自己的电商 App。这是第一方的原生应用(Native App)场景,通常考虑选用 OAuth 2.0 的用户名密码模式,但是并不安全(参考MICRO-SERVICES ARCHITECTURE WITH OAUTH2 AND JWT – PART 3 – IDP的 Security Consideration 部分),所以业界建议采用授权码模式,而且是要支持PKCE扩展的授权码模式。那接下来,我们来看看这个认证授权的流程。同样,这里只是突出了关键步骤,并假定是一个成功流程。

      img
      img
      1. 用户访问电商 App,点击登录。
      2. App 生成 PKCE 相关的 code verifier + challenge。
      3. App 以内嵌方式启动手机浏览器,访问 IDP 的统一认证页 (GET /authorize),请求带上 PKCE 的 code challenge 相关参数。
      4. IDP 返回统一认证页。
      5. 用户认证和授权。
      6. IDP 通过 Login Service 对用户进行认证。
      7. IDP 返回授权码到 App 浏览器。
      8. App 截取浏览器带回的授权码,将授权码 +PKCE code verifer,通过网关转发到 IDP 的令牌获取端点(POST /oauth2/token, grant_type=authorization-code)。
      9. IDP 校验 PKCE 和授权码,校验通过则返回有效访问令牌。
      10. App 获取令牌,本地存储,登录成功。

      之后,App 如果需要和后台交互,可直接通过网关调用后台微服务,请求 HTTP header 中带上 OAuth 2.0 访问令牌即可。后续的服务调用流程,和“第一方应用 + 资源拥有者凭据模式”类似。

      场景 3:第三方 Web 应用 + 授权码模式

      第三个场景是某第三方合作厂商开发了一个 Web 网站,要访问 ACME 公司的电商开放平台 API。这是一个第三方 Web 应用场景,通常选用 OAuth 2.0 的授权码许可模式。那接下来,我们来看看这个认证授权的流程。同样,这里只是突出了关键步骤,并假设是一个成功流程。

      img
      img
      1. 用户访问这个第三方 Web 应用,点击登录链接。
      2. Web 应用后台向 ACME 公司的 IDP 服务发送申请授权码请求(GET /authorize)。
      3. 用户被重定向到 ACME 公司的 IDP 统一登录页面。
      4. 用户进行认证和授权。
      5. IDP 通过 Login Service 对用户进行认证。
      6. 认证和授权通过,IDP 返回授权码。
      7. Web 应用获得授权码,再向 IDP 服务的令牌获取端点发起请求(POST /oauth2/token, grant_type=authorization-code)。
      8. IDP 校验授权码,校验通过则返回有效 OAuth 2.0 令牌(根据需要也可以返回刷新令牌)。
      9. Web 应用创建用户 Session,将 OAuth 2.0 令牌保存在 Session 中,然后返回登录成功到用户端。
      10. 用户浏览器中记录 Session Cookie,登录成功。

      之后,第三方 Web 应用如果需要和 ACME 电商平台交互,可直接通过网关调用微服务,请求 HTTP header 中带上 OAuth 2.0 访问令牌即可。后续的服务调用流程,和前面的“第一方应用 + 资源拥有者凭据模式”类似。

      额外说明

      除了上面的三个主要场景和流程,我还要和你分享 6 点。这 6 点是对上面基本流程的补充,也是企业级的 OAuth 2.0 应用要额外考虑的。

      第一点是,IDP 的 API 要支持从 OAuth 2.0 访问令牌到 JWT 令牌的互转。 今天我们提到的集成架构采用 OAuth 2.0 访问令牌 + JWT 令牌的混合模式,中间需要实现 OAuth 2.0 访问令牌到 JWT 令牌的互转。这个互转 API 并非 OAuth 2.0 的标准,有些 IDP 产品(比方 Spring Security OAuth)可能并不支持,因此需要用户定制扩展。

      第二点是,关于单页 SPA 应用场景。 关于单页 SPA 应用场景,简单做法是采用隐式许可,但是这个模式是 OAuth 2.0 中比较不安全的,所以一般不建议采用。对于纯单页 SPA 应用,业界推荐的做法是:

      • 如果浏览器支持 Web Crypto for PKCE,则可以考虑使用类似“第一方移动应用”场景下的授权码许可 +PKCE 扩展流程;
      • 否则,考虑 SPA+ 传统 Web 混合(hybrid)模式,前端页面可以住在客户浏览器端中,但登录认证还是由后台 Web 站点配合实现,走类似“第一方 Web 应用”场景的资源拥有者凭据模式,或者“第三方 Web 应用”场景下的授权码许可模式。

      第三点是,关于 SSO 单点登录场景。 为了简化描述,上面的流程没有考虑 SSO 单点登录场景。如果要支持 Web SSO,那么各种应用场景都必须通过浏览器 +IDP 登录页集中登录,并且 IDP 要支持 Session,用于维护登录态。如果 IDP 以集群方式部署的话,还要考虑粘性 Sticky Session 或者集中式 Session。

      这样,当用户通过一个 Web 应用登录后,后续如果再用其它 Web 应用登录的话,只要 IDP 上的 Session 还存在,那么这个登录就可以自动完成,相当于单点登录。

      当然,如果要支持 SSO,IDP 的 Session Cookie 要种在 Web 应用的根域上,也就是说不同 Web 应用的根域必须相同,否则会有跨域问题。

      第四点是关于 IDP 和网关的部署方式。 前面的几张架构图中,IDP 虽然躲在网关后面,但实际上 IDP 可以直接通过 Nginx 对外暴露,不经过网关。或者,IDP 的登录授权页面,可以通过 Nginx 直接暴露,API 接口则走网关。

      第五点是关于刷新令牌。 为了简化描述,上面的流程没有详细说明刷新令牌的集成方式。企业根据场景需要,可以启用刷新令牌,来延长用户的登录时间,具体的集成方式需要考虑安全性的需求。

      第六点是关于 Web Session。 为了简化描述,在上面的流程中,Web 应用登录成功后假设启用 Web Session,也就是服务器端 Session。在实际场景中,Web Session 并非唯一选择,也可以采用简单的客户端 Session 方式,也称无状态 Session,也就是在客户端浏览器 Cookie 中保存 OAuth 2.0 访问令牌。

      小结

      好了,以上就是今天的主要内容了。今天,我和你分享了如何将行业标准的 OAuth 2.0/JWT 和微服务集成起来,你需要记住以下四点。

      第一,目前主流的微服务架构大致可以分为 5 层,分别是:Nginx 流量接入层 ->Web 应用层 ->API 网关层 ->BFF 聚合层 -> 领域服务层。这个架构可以住在云原生的 Kubernetes 环境中,也可以住在传统数据中心里头。

      第二,API 网关是微服务调用的入口,承担重要的安全认证和鉴权功能。主要的安全操作包括:一,通过 IDP 校验 OAuth 2.0 访问令牌,并获取带用户和权限信息的 JWT 令牌;二,基于 OAuth 2.0 的 Scope 对 API 调用进行鉴权。

      第三,在微服务架构体系下,通常需要一个集中的 IDP 服务,它相当于一个 Authentication & Authorization as a Service 角色,负责令牌颁发 / 校验 / 管理,还有用户认证。

      第四,在今天这一讲提出的架构中,Web 应用层(网关之前)的安全机制主要基于 OAuth 2.0 访问令牌实现(它是一种透明令牌或者称引用令牌),微服务层(网关之后)的安全机制主要基于 JWT 令牌实现(它是一种不透明的自包含令牌)。网关层在中间实现两种令牌的转换。这是一种 OAuth 2.0 访问令牌 +JWT 令牌的混合模式。

      之所以这样设计,是因为 Web 层靠近用户端,如果采用 JWT 令牌,会暴露用户信息,有一定的安全风险,所以采用 OAuth 2.0 访问令牌,它是一个无意义随机字符串。而在网关之后,安全风险相对低,同时很多服务需要用户信息,所以采用自包含用户信息的 JWT 令牌更合适。当然,如果企业内网没有特别的安全考量,也可以直接传递完全透明的用户信息(例如使用 JSON 格式)。

      思考题

      1. 除了今天我们讲到的 OAuth 2.0 访问令牌 +JWT 令牌的混合模式,实践中也可以全程采用 OAuth 2.0 访问令牌,或者全程采用 JWT 令牌。对比混合模式,如果全程采用 OAuth 2.0 访问令牌,或者全程采用 JWT 令牌,你觉得有哪些利弊呢?
      2. 你可以说说自己对基于传统 Web 应用的认证授权机制的理解吗?并对比今天讲到的现代微服务的认证授权机制,你可以说说它们之间的本质差异和相似点吗?
      ',78)]))}const s=t(l,[["render",p],["__file","12.html.vue"]]),c=JSON.parse('{"path":"/other/oauth2/12.html","title":"12 | 架构案例:基于OAuth 2.0/JWT的微服务参考架构","lang":"zh-CN","frontmatter":{"title":"12 | 架构案例:基于OAuth 2.0/JWT的微服务参考架构","date":"2021-11-08T00:00:00.000Z","author":"ChenSino","publish":true,"description":"在前面几讲,我们一起学习了 OAuth 2.0 在开放环境中的使用过程。那么 OAuth 2.0 不仅仅可以用在开放的场景中,它可以应用到我们任何需要授权 / 鉴权的地方,包括微服务。 因此今天,我特别邀请了我的朋友杨波,来和你分享一个基于 OAuth 2.0/JWT 的微服务参考架构。杨波,曾先后担任过携程框架部的研发总监和拍拍贷基础架构部的研发总监...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/12.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"12 | 架构案例:基于OAuth 2.0/JWT的微服务参考架构"}],["meta",{"property":"og:description","content":"在前面几讲,我们一起学习了 OAuth 2.0 在开放环境中的使用过程。那么 OAuth 2.0 不仅仅可以用在开放的场景中,它可以应用到我们任何需要授权 / 鉴权的地方,包括微服务。 因此今天,我特别邀请了我的朋友杨波,来和你分享一个基于 OAuth 2.0/JWT 的微服务参考架构。杨波,曾先后担任过携程框架部的研发总监和拍拍贷基础架构部的研发总监..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/228199yya6051f1f62f23547a88be4ff.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"ChenSino"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"12 | 架构案例:基于OAuth 2.0/JWT的微服务参考架构\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/228199yya6051f1f62f23547a88be4ff.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/b658befe1da937fa3685b55522487dbd.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/c88e46dd26deb76d6yy8f42f83066f4a.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/443dab973274d8d13c76b2ef4cd1d393.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/02affbdf32f005af65454f3acc4cd957.jpg\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\"}]}"]]},"headers":[{"level":2,"title":"微服务分层架构","slug":"微服务分层架构","link":"#微服务分层架构","children":[{"level":3,"title":"Nginx 反向代理层","slug":"nginx-反向代理层","link":"#nginx-反向代理层","children":[]},{"level":3,"title":"Web 应用层","slug":"web-应用层","link":"#web-应用层","children":[]},{"level":3,"title":"Gateway 网关层","slug":"gateway-网关层","link":"#gateway-网关层","children":[]},{"level":3,"title":"IDP 服务","slug":"idp-服务","link":"#idp-服务","children":[]},{"level":3,"title":"BFF 层","slug":"bff-层","link":"#bff-层","children":[]},{"level":3,"title":"领域服务层","slug":"领域服务层","link":"#领域服务层","children":[]}]},{"level":2,"title":"OAuth 2.0/JWT 如何与微服务进行集成?","slug":"oauth-2-0-jwt-如何与微服务进行集成","link":"#oauth-2-0-jwt-如何与微服务进行集成","children":[{"level":3,"title":"场景 1:第一方 Web 应用 + 资源拥有者凭据模式","slug":"场景-1-第一方-web-应用-资源拥有者凭据模式","link":"#场景-1-第一方-web-应用-资源拥有者凭据模式","children":[]},{"level":3,"title":"场景 2:第一方移动应用 + 授权码许可模式","slug":"场景-2-第一方移动应用-授权码许可模式","link":"#场景-2-第一方移动应用-授权码许可模式","children":[]},{"level":3,"title":"场景 3:第三方 Web 应用 + 授权码模式","slug":"场景-3-第三方-web-应用-授权码模式","link":"#场景-3-第三方-web-应用-授权码模式","children":[]}]},{"level":2,"title":"小结","slug":"小结","link":"#小结","children":[]},{"level":2,"title":"思考题","slug":"思考题","link":"#思考题","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":19.48,"words":5844},"filePathRelative":"other/oauth2/12.md","localizedDate":"2021年11月8日","excerpt":"
      \\n

      在前面几讲,我们一起学习了 OAuth 2.0 在开放环境中的使用过程。那么 OAuth 2.0 不仅仅可以用在开放的场景中,它可以应用到我们任何需要授权 / 鉴权的地方,包括微服务。

      \\n
      \\n
      \\n

      因此今天,我特别邀请了我的朋友杨波,来和你分享一个基于 OAuth 2.0/JWT 的微服务参考架构。杨波,曾先后担任过携程框架部的研发总监和拍拍贷基础架构部的研发总监,在微服务和 OAuth 2.0 有非常丰富的实践经验。

      \\n
      \\n
      \\n

      其中,在携程工作期间,他负责过携程的 API 网关产品的研发工作,包括它和携程的令牌服务的集成;在拍拍贷工作期间,他负责过拍拍贷的令牌服务的研发和运维工作。这两家公司的令牌服务和 OAuth 2.0 类似,但要更简单些。

      \\n
      ","autoDesc":true}');export{s as comp,c as data}; diff --git a/assets/13.html-DtCYSHOP.js b/assets/13.html-DiPY3dlK.js similarity index 99% rename from assets/13.html-DtCYSHOP.js rename to assets/13.html-DiPY3dlK.js index f1c2834ef..90b2e47bf 100644 --- a/assets/13.html-DtCYSHOP.js +++ b/assets/13.html-DiPY3dlK.js @@ -1 +1 @@ -import{_ as d}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as l,a as o,b as n,d as e,e as i,f as p,r as c,o as r}from"./app-LRWZkDAq.js";const g={};function s(f,t){const a=c("RouteLink");return r(),l("div",null,[t[8]||(t[8]=o('

      在咱们这门课中,我提到了很多次“开放平台”,不难理解,它的作用就是企业把自己的业务能力主要以开放 API 的形式,赋能给外部开发者。而作为第三方开发者或者 ISV(独立软件供应商)在接入这些开放平台的时候,我们最应该关心的就是它们的官方文档,关注接入的流程是怎样的、对应的 API 是什么、每个 API 都传递哪些参数,也就

      到这里,你会发现“开放平台的官方文档”会是一个关键点。不过呢,当你去各大开放平台上面看这些文

      其中的原因也很简单,那就是开放平台为了让已经具备 OAuth 2.0 知识的研发人员去快速地对接平台上面的业务,把各类对接流程做了分类归档。比如,你会发现微信开放平台上有使用授权码获取授权信息的文档,也有获取令牌的文档,但并没有一份整体的、能够串起来的文档说明。从我的角度来看,这其实也就间接提高了使用门槛,因为如果你不懂 OAuth 2.0,基本是

      那么,今天我就借着这个点,和你说说以京东、微信、支付宝、美团为代表的各大开放平台是如何使用 OAuth 2.0 的。理解了这个问题,你以后再对接一个开放平台、再阅读一份官方对接文档时,就更能明白它们的底层逻辑了。

      在正式介绍各大开放平台的使用细节之前,我们先来看看大厂的开放平台全局体系。据我观察,各个开放平台基本的系统结构和授权系统在中间的交互流程,大同小异,都是通过授权服务来授权,通过网关来鉴权。所以接下来,我就以京东商家开放平台为例,来和你说说开放平台的体系到底是什么样子的。

      开放平台体系是什么样子的?

      我们首先来看一下京东商家开放平台全局体系的结构,如下图所示。

      图1 京东商家开放平台体系结构示意图
      图1 京东商家开放平台体系结构示意图

      我们可以把这个架构体系分为三部分来看:

      1. 第三方软件,一般是指第三方开发者或者 ISV 通过对接开放平台来实现的应用软件,比如小兔打单软件。
      2. 京东商家开放平台,包含 API 网关服务、OAuth 2.0 授权服务和第三方软件开发者中心服务。其中,API 网关服务和 OAuth 2.0 授权服务,是开放平台的“两条腿”;第三方软件开发者中心服务,是为开发者提供管理第三方软件应用基本信息的服务,比如 app_id、app_secret 等信息。
      3. 京东内部的各个微服务,比如订单服务、商品服务等。这些微服务,就是我们之前提到的受保护资源服务。

      从图中我们还可以看到这个体系整体的调用关系是:第三方软件通过 HTTP 协议请求到开放平台,更具体地说是开放平台的 API 网关服务,然后由 API 网关通过内部的 RPC 调用到各个微服务。接下来,我们再以用户小明使用小兔打单软件为例,来看看这些系统角色之间具体又是怎样交互的?

      图2 开放平台体系交互示意图
      图2 开放平台体系交互示意图

      到这里,我们可以发现,在开放平台体系中各个系统角色间的交互可以归结为:

      1. 当用户小明访问小兔软件的时候,小兔会首先向开放平台的 OAuth 2.0 授权服务去请求访问令牌,接着小兔拿着访问令牌去请求 API 网关服务;
      2. 在 API 网关服务中,会做最基本的两种校验,一种是访问令牌的合法性校验,比如访问令牌是否过期的校验,另一种是小兔打单软件的基本信息的合法性校验,比如 app_id 和 app_secret 的校验;
      3. 都校验成功之后,API 网关服务会发起最终的数据请求。
      ',14)),n("p",null,[t[1]||(t[1]=e("这里需要说明的是,在")),i(a,{to:"/other/oauth2/05.html"},{default:p(()=>t[0]||(t[0]=[e("第 5")])),_:1}),t[2]||(t[2]=e(" 讲中我们提到,验证访问令牌或者第三方软件应用信息的时候,都是在受保护资源服务中去做的。当有了 API 网关这一层的时候,这些校验工作就会都落到了 API 网关的身上,因为我们不能让很多个受保护资源服务做同样的事情。"))]),t[9]||(t[9]=n("p",null,"我们理解了京东商家开放平台的体系结构后,可以小结下了。依靠开放平台提供的能力,可以说开放平台、用户和开发者实现了三赢:小明因为使用小兔提高了打单效率;小兔的开发者因为小明的订购服务获得了收益;而通过开放出去的 API 让小兔帮助小明能够极快地处理 C 端用户的订单,京东提高了用户的使用体验。",-1)),n("p",null,[t[5]||(t[5]=e("但同时呢,开放也是一把双刃剑。理想状态下,平台、开发者、用户可以实现三赢,但正如我们在")),i(a,{to:"/other/oauth2/08.html"},{default:p(()=>t[3]||(t[3]=[e("第 8 讲")])),_:1}),t[6]||(t[6]=e("和")),i(a,{to:"/other/oauth2/10.html"},{default:p(()=>t[4]||(t[4]=[e("第 10 讲")])),_:1}),t[7]||(t[7]=e("中提到的,安全的问题绝不容忽视,而用户的信息安全又是重中之重。接下来,我就和你分享一个,开放平台体系是如何解决访问令牌安全问题的案例。"))]),t[10]||(t[10]=o('

      我们已经知道,用户给第三方软件授权之后,授权服务就会生成一个访问令牌,而且这个访问令牌是跟用户关联的。比如,小明给小兔打单软件进行了授权,那么此时访问令牌的粒度就是:小兔打单软件 + 小明。

      我们还知道了,小兔打单软件可以拿着这个访问令牌去代表小明访问小明的数据;如果访问令牌过期了,小兔打单软件还可以继续使用刷新令牌来访问,直到刷新令牌也过期了。

      现在问题来了,如果小明注销了账号,或者修改了自己的密码,那他之前为其它第三方软件进行授权的访问令牌就应该立即失效。否则,在刷新令牌过期之前,第三方软件可以一直拿着之前的访问令牌去请求数据。这显然不合理。

      所以在这种情况下,授权服务就要通过 MQ(消息队列)接收用户的注销和修改密码这两类消息,然后对访问令牌进行清理。

      图3 访问令牌的清理
      图3 访问令牌的清理

      其实,这个案例中解决访问令牌安全问题的方式,不仅仅适用于开放平台,还可以为你在企业内构建自己的 OAuth 2.0 授权体系结构时提供借鉴。

      以上就是开放平台整体的结构,以及其中需要重点关注的用户访问令牌的安全性问题了。我们作为第三方软件开发者,在对接到这些开放平台或者浏览它们的网站时,几乎都能看到类似这样的一句话:“所有接口都需要接入 OAuth 授权,经过用户确认授权后才可以调用”,这正是 OAuth 2.0 的根本性作用。

      理解了开放平台的脉络之后,接下来,就让我们通过一组图看一看开放平台是如何使用 OAuth 2.0 授权流程的吧。

      各大开放平台授权流程

      我们以微信支付宝美团为例,看看它们在开放授权上是如何使用 OAuth 2.0 的。我们首先看一下官方的授权流程图:

      图4 微信开放平台授权流程图
      图4 微信开放平台授权流程图
      图5 支付宝开放平台授权流程图
      图5 支付宝开放平台授权流程图
      图6 美团开放平台授权流程图
      图6 美团开放平台授权流程图

      我们可以在这三张授权流程图中看到,都有和授权码 code 相关的文字。这就说明,它们都建议开发者首选授权码流程。所以,你现在更能明白我为啥在这门课里要花这么多篇幅,来和你讲授权码许可相关的内容了吧。

      在这一讲最开始我也提到了,我们作为开发者在对接开放平台的时候,最关心的就是它们提供的官方对接文档了。而这些文档里面,最让人头疼就是那些通信过程中需要传递的参数了。下面我会带着你从我的角度,以京东商家开放平台为例,给你串下这些参数背后的含义,以及关键点。这样你在做具体接入操作的时候,就可以举重若轻了。

      授权码流程中的参数说明

      概括来讲,在京东商家开放平台的授权服务这一侧,提供服务的就是两个端点:负责生成授权码的授权端点以及负责颁发访问令牌的令牌端点。整个授权过程中,虽然看着有很多参数,但你可以围绕这两条线,来对它们做归类。

      接下来,我们继续以小兔打单软件为例,来看一下它在对接京东商家开放平台的时候都用到了哪些参数。

      小明在使用小兔打单软件的时候,首先被小兔通过重定向的方式引导到京东商家开放平台的授权服务上,其实就是引导到了授权服务的授权端点上。这个重定向的过程中用到的参数如下:

      图7 重定向过程用到的参数
      图7 重定向过程用到的参数

      这里需要强调的是,对于 state 参数,现在官方都是“推荐”使用。我们在第 8 讲中说过,OAuth 2.0 官方建议的避免 CSRF 攻击的方式,就是使用 state 参数。所以安全起见,你还是应该使用。

      接着,京东商家开放平台授权服务的授权端点,会向小兔软件做出响应。这个响应的过程用到的基本参数,如下:

      图8 授权端点响应小兔软件用到的参数
      图8 授权端点响应小兔软件用到的参数

      对于授权码 code 的值,一般建议的最长生命周期是 10 分钟。另外,小兔打单软件只能被允许使用一次该授权码的值,如果使用一次之后还用同样的授权码值来请求,授权服务必须拒绝。

      对于这次的 state 值,授权服务每次都是必须要返回给小兔打单软件的。无论小兔打单软件在起初的时候有没有发送该值,都必须返回回去,如果没有就返回空。这样当小兔打单软件日后升级增加该值的时候,京东商家开放平台就不需要改动任何代码逻辑了。

      在拿到授权码 code 的值之后,接下来就是小兔打单软件向京东商家开放平台的授权服务的令牌端点发起请求,申请访问令牌。这个过程中需要传递的基本参数,如下:

      图9 申请访问令牌需要传递的基本参数
      图9 申请访问令牌需要传递的基本参数

      在授权服务接收到小兔打单软件申请访问令牌的请求后,像授权端点一样,令牌端点也需要向小兔打单软件做出响应。这个过程涉及到的基本参数,如下:

      图10 令牌端点响应小兔软件涉及的参数
      图10 令牌端点响应小兔软件涉及的参数

      对于这里返回的 scope 值,我要强调下,其实就是小兔软件被允许的实际的权限范围,因为小明有可能给小兔软件授予了小于它在开放平台注册时申请的权限范围。比如,小兔打单软件申请了查询历史订单、查询当天订单两个 API 的权限,但小明可能只给小兔授权了查询当天订单 API 的权限。

      总结

      好了,这一讲就要结束了。我们一起学习了开放平台体系的整体结构和授权流程,以及第三方软件开发者关心的对接开放平台的通信流程中需要传递的参数。现在,我希望你能记住以下三点内容。

      1. 当有多个受保护资源服务的时候,基本的鉴权工作,包括访问令牌的验证、第三方软件应用信息的验证都应该抽出一个 API 网关层,并把这些基本的工作放到这个 API 网关层。
      2. 各大开放平台都是推荐使用授权码许可流程,无论是网页版的 Web 应用程序,还是移动应用程序。
      3. 对于第三方软件开发者重点关注的参数,可以从授权服务的授权端点和令牌端点来区分,授权端点重点是授权码请求和响应的处理,令牌端点重点是访问令牌请求和响应的处理。
      ',33))])}const u=d(g,[["render",s],["__file","13.html.vue"]]),m=JSON.parse('{"path":"/other/oauth2/13.html","title":"13 | 各大开放平台是如何使用OAuth 2.0的?","lang":"zh-CN","frontmatter":{"title":"13 | 各大开放平台是如何使用OAuth 2.0的?","date":"2021-11-08T00:00:00.000Z","author":"ChenSino","publish":true,"description":"在咱们这门课中,我提到了很多次“开放平台”,不难理解,它的作用就是企业把自己的业务能力主要以开放 API 的形式,赋能给外部开发者。而作为第三方开发者或者 ISV(独立软件供应商)在接入这些开放平台的时候,我们最应该关心的就是它们的官方文档,关注接入的流程是怎样的、对应的 API 是什么、每个 API 都传递哪些参数,也就 到这里,你会发现“开放平台的...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/13.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"13 | 各大开放平台是如何使用OAuth 2.0的?"}],["meta",{"property":"og:description","content":"在咱们这门课中,我提到了很多次“开放平台”,不难理解,它的作用就是企业把自己的业务能力主要以开放 API 的形式,赋能给外部开发者。而作为第三方开发者或者 ISV(独立软件供应商)在接入这些开放平台的时候,我们最应该关心的就是它们的官方文档,关注接入的流程是怎样的、对应的 API 是什么、每个 API 都传递哪些参数,也就 到这里,你会发现“开放平台的..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/5775b5bbc363ba2f94d9f6f8e2d05d56.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"ChenSino"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"13 | 各大开放平台是如何使用OAuth 2.0的?\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/5775b5bbc363ba2f94d9f6f8e2d05d56.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/30f2b2db8c01f0fc60e2d821cd59f94b.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/9d05fa572bccec8da5c895b49a9946ca.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/abc7611d467d45bf39d8e07e0d0267dc.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/e27a6836ef686e23284f9314cc3a25b7.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/e98e9ed5e607561df173262703ca3dea.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/0e8394fedd9205e38c3yyc6e7bae2303.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/b653bc541a98920001385612f2309361.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/18c8245e61ce14c1f7a227a6e713b37f.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/1ac4ded2b7131b475ac71bf4b39c72b5.jpg\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\"}]}"]]},"headers":[{"level":3,"title":"开放平台体系是什么样子的?","slug":"开放平台体系是什么样子的","link":"#开放平台体系是什么样子的","children":[]},{"level":3,"title":"各大开放平台授权流程","slug":"各大开放平台授权流程","link":"#各大开放平台授权流程","children":[]},{"level":3,"title":"授权码流程中的参数说明","slug":"授权码流程中的参数说明","link":"#授权码流程中的参数说明","children":[]},{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2}]},"readingTime":{"minutes":11.53,"words":3458},"filePathRelative":"other/oauth2/13.md","localizedDate":"2021年11月8日","excerpt":"

      在咱们这门课中,我提到了很多次“开放平台”,不难理解,它的作用就是企业把自己的业务能力主要以开放 API 的形式,赋能给外部开发者。而作为第三方开发者或者 ISV(独立软件供应商)在接入这些开放平台的时候,我们最应该关心的就是它们的官方文档,关注接入的流程是怎样的、对应的 API 是什么、每个 API 都传递哪些参数,也就

      \\n

      到这里,你会发现“开放平台的官方文档”会是一个关键点。不过呢,当你去各大开放平台上面看这些文

      \\n

      其中的原因也很简单,那就是开放平台为了让已经具备 OAuth 2.0 知识的研发人员去快速地对接平台上面的业务,把各类对接流程做了分类归档。比如,你会发现微信开放平台上有使用授权码获取授权信息的文档,也有获取令牌的文档,但并没有一份整体的、能够串起来的文档说明。从我的角度来看,这其实也就间接提高了使用门槛,因为如果你不懂 OAuth 2.0,基本是

      ","autoDesc":true}');export{u as comp,m as data}; +import{_ as d}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as l,a as o,b as n,d as e,e as i,f as p,r as c,o as r}from"./app-BIdVr4mq.js";const g={};function s(f,t){const a=c("RouteLink");return r(),l("div",null,[t[8]||(t[8]=o('

      在咱们这门课中,我提到了很多次“开放平台”,不难理解,它的作用就是企业把自己的业务能力主要以开放 API 的形式,赋能给外部开发者。而作为第三方开发者或者 ISV(独立软件供应商)在接入这些开放平台的时候,我们最应该关心的就是它们的官方文档,关注接入的流程是怎样的、对应的 API 是什么、每个 API 都传递哪些参数,也就

      到这里,你会发现“开放平台的官方文档”会是一个关键点。不过呢,当你去各大开放平台上面看这些文

      其中的原因也很简单,那就是开放平台为了让已经具备 OAuth 2.0 知识的研发人员去快速地对接平台上面的业务,把各类对接流程做了分类归档。比如,你会发现微信开放平台上有使用授权码获取授权信息的文档,也有获取令牌的文档,但并没有一份整体的、能够串起来的文档说明。从我的角度来看,这其实也就间接提高了使用门槛,因为如果你不懂 OAuth 2.0,基本是

      那么,今天我就借着这个点,和你说说以京东、微信、支付宝、美团为代表的各大开放平台是如何使用 OAuth 2.0 的。理解了这个问题,你以后再对接一个开放平台、再阅读一份官方对接文档时,就更能明白它们的底层逻辑了。

      在正式介绍各大开放平台的使用细节之前,我们先来看看大厂的开放平台全局体系。据我观察,各个开放平台基本的系统结构和授权系统在中间的交互流程,大同小异,都是通过授权服务来授权,通过网关来鉴权。所以接下来,我就以京东商家开放平台为例,来和你说说开放平台的体系到底是什么样子的。

      开放平台体系是什么样子的?

      我们首先来看一下京东商家开放平台全局体系的结构,如下图所示。

      图1 京东商家开放平台体系结构示意图
      图1 京东商家开放平台体系结构示意图

      我们可以把这个架构体系分为三部分来看:

      1. 第三方软件,一般是指第三方开发者或者 ISV 通过对接开放平台来实现的应用软件,比如小兔打单软件。
      2. 京东商家开放平台,包含 API 网关服务、OAuth 2.0 授权服务和第三方软件开发者中心服务。其中,API 网关服务和 OAuth 2.0 授权服务,是开放平台的“两条腿”;第三方软件开发者中心服务,是为开发者提供管理第三方软件应用基本信息的服务,比如 app_id、app_secret 等信息。
      3. 京东内部的各个微服务,比如订单服务、商品服务等。这些微服务,就是我们之前提到的受保护资源服务。

      从图中我们还可以看到这个体系整体的调用关系是:第三方软件通过 HTTP 协议请求到开放平台,更具体地说是开放平台的 API 网关服务,然后由 API 网关通过内部的 RPC 调用到各个微服务。接下来,我们再以用户小明使用小兔打单软件为例,来看看这些系统角色之间具体又是怎样交互的?

      图2 开放平台体系交互示意图
      图2 开放平台体系交互示意图

      到这里,我们可以发现,在开放平台体系中各个系统角色间的交互可以归结为:

      1. 当用户小明访问小兔软件的时候,小兔会首先向开放平台的 OAuth 2.0 授权服务去请求访问令牌,接着小兔拿着访问令牌去请求 API 网关服务;
      2. 在 API 网关服务中,会做最基本的两种校验,一种是访问令牌的合法性校验,比如访问令牌是否过期的校验,另一种是小兔打单软件的基本信息的合法性校验,比如 app_id 和 app_secret 的校验;
      3. 都校验成功之后,API 网关服务会发起最终的数据请求。
      ',14)),n("p",null,[t[1]||(t[1]=e("这里需要说明的是,在")),i(a,{to:"/other/oauth2/05.html"},{default:p(()=>t[0]||(t[0]=[e("第 5")])),_:1}),t[2]||(t[2]=e(" 讲中我们提到,验证访问令牌或者第三方软件应用信息的时候,都是在受保护资源服务中去做的。当有了 API 网关这一层的时候,这些校验工作就会都落到了 API 网关的身上,因为我们不能让很多个受保护资源服务做同样的事情。"))]),t[9]||(t[9]=n("p",null,"我们理解了京东商家开放平台的体系结构后,可以小结下了。依靠开放平台提供的能力,可以说开放平台、用户和开发者实现了三赢:小明因为使用小兔提高了打单效率;小兔的开发者因为小明的订购服务获得了收益;而通过开放出去的 API 让小兔帮助小明能够极快地处理 C 端用户的订单,京东提高了用户的使用体验。",-1)),n("p",null,[t[5]||(t[5]=e("但同时呢,开放也是一把双刃剑。理想状态下,平台、开发者、用户可以实现三赢,但正如我们在")),i(a,{to:"/other/oauth2/08.html"},{default:p(()=>t[3]||(t[3]=[e("第 8 讲")])),_:1}),t[6]||(t[6]=e("和")),i(a,{to:"/other/oauth2/10.html"},{default:p(()=>t[4]||(t[4]=[e("第 10 讲")])),_:1}),t[7]||(t[7]=e("中提到的,安全的问题绝不容忽视,而用户的信息安全又是重中之重。接下来,我就和你分享一个,开放平台体系是如何解决访问令牌安全问题的案例。"))]),t[10]||(t[10]=o('

      我们已经知道,用户给第三方软件授权之后,授权服务就会生成一个访问令牌,而且这个访问令牌是跟用户关联的。比如,小明给小兔打单软件进行了授权,那么此时访问令牌的粒度就是:小兔打单软件 + 小明。

      我们还知道了,小兔打单软件可以拿着这个访问令牌去代表小明访问小明的数据;如果访问令牌过期了,小兔打单软件还可以继续使用刷新令牌来访问,直到刷新令牌也过期了。

      现在问题来了,如果小明注销了账号,或者修改了自己的密码,那他之前为其它第三方软件进行授权的访问令牌就应该立即失效。否则,在刷新令牌过期之前,第三方软件可以一直拿着之前的访问令牌去请求数据。这显然不合理。

      所以在这种情况下,授权服务就要通过 MQ(消息队列)接收用户的注销和修改密码这两类消息,然后对访问令牌进行清理。

      图3 访问令牌的清理
      图3 访问令牌的清理

      其实,这个案例中解决访问令牌安全问题的方式,不仅仅适用于开放平台,还可以为你在企业内构建自己的 OAuth 2.0 授权体系结构时提供借鉴。

      以上就是开放平台整体的结构,以及其中需要重点关注的用户访问令牌的安全性问题了。我们作为第三方软件开发者,在对接到这些开放平台或者浏览它们的网站时,几乎都能看到类似这样的一句话:“所有接口都需要接入 OAuth 授权,经过用户确认授权后才可以调用”,这正是 OAuth 2.0 的根本性作用。

      理解了开放平台的脉络之后,接下来,就让我们通过一组图看一看开放平台是如何使用 OAuth 2.0 授权流程的吧。

      各大开放平台授权流程

      我们以微信支付宝美团为例,看看它们在开放授权上是如何使用 OAuth 2.0 的。我们首先看一下官方的授权流程图:

      图4 微信开放平台授权流程图
      图4 微信开放平台授权流程图
      图5 支付宝开放平台授权流程图
      图5 支付宝开放平台授权流程图
      图6 美团开放平台授权流程图
      图6 美团开放平台授权流程图

      我们可以在这三张授权流程图中看到,都有和授权码 code 相关的文字。这就说明,它们都建议开发者首选授权码流程。所以,你现在更能明白我为啥在这门课里要花这么多篇幅,来和你讲授权码许可相关的内容了吧。

      在这一讲最开始我也提到了,我们作为开发者在对接开放平台的时候,最关心的就是它们提供的官方对接文档了。而这些文档里面,最让人头疼就是那些通信过程中需要传递的参数了。下面我会带着你从我的角度,以京东商家开放平台为例,给你串下这些参数背后的含义,以及关键点。这样你在做具体接入操作的时候,就可以举重若轻了。

      授权码流程中的参数说明

      概括来讲,在京东商家开放平台的授权服务这一侧,提供服务的就是两个端点:负责生成授权码的授权端点以及负责颁发访问令牌的令牌端点。整个授权过程中,虽然看着有很多参数,但你可以围绕这两条线,来对它们做归类。

      接下来,我们继续以小兔打单软件为例,来看一下它在对接京东商家开放平台的时候都用到了哪些参数。

      小明在使用小兔打单软件的时候,首先被小兔通过重定向的方式引导到京东商家开放平台的授权服务上,其实就是引导到了授权服务的授权端点上。这个重定向的过程中用到的参数如下:

      图7 重定向过程用到的参数
      图7 重定向过程用到的参数

      这里需要强调的是,对于 state 参数,现在官方都是“推荐”使用。我们在第 8 讲中说过,OAuth 2.0 官方建议的避免 CSRF 攻击的方式,就是使用 state 参数。所以安全起见,你还是应该使用。

      接着,京东商家开放平台授权服务的授权端点,会向小兔软件做出响应。这个响应的过程用到的基本参数,如下:

      图8 授权端点响应小兔软件用到的参数
      图8 授权端点响应小兔软件用到的参数

      对于授权码 code 的值,一般建议的最长生命周期是 10 分钟。另外,小兔打单软件只能被允许使用一次该授权码的值,如果使用一次之后还用同样的授权码值来请求,授权服务必须拒绝。

      对于这次的 state 值,授权服务每次都是必须要返回给小兔打单软件的。无论小兔打单软件在起初的时候有没有发送该值,都必须返回回去,如果没有就返回空。这样当小兔打单软件日后升级增加该值的时候,京东商家开放平台就不需要改动任何代码逻辑了。

      在拿到授权码 code 的值之后,接下来就是小兔打单软件向京东商家开放平台的授权服务的令牌端点发起请求,申请访问令牌。这个过程中需要传递的基本参数,如下:

      图9 申请访问令牌需要传递的基本参数
      图9 申请访问令牌需要传递的基本参数

      在授权服务接收到小兔打单软件申请访问令牌的请求后,像授权端点一样,令牌端点也需要向小兔打单软件做出响应。这个过程涉及到的基本参数,如下:

      图10 令牌端点响应小兔软件涉及的参数
      图10 令牌端点响应小兔软件涉及的参数

      对于这里返回的 scope 值,我要强调下,其实就是小兔软件被允许的实际的权限范围,因为小明有可能给小兔软件授予了小于它在开放平台注册时申请的权限范围。比如,小兔打单软件申请了查询历史订单、查询当天订单两个 API 的权限,但小明可能只给小兔授权了查询当天订单 API 的权限。

      总结

      好了,这一讲就要结束了。我们一起学习了开放平台体系的整体结构和授权流程,以及第三方软件开发者关心的对接开放平台的通信流程中需要传递的参数。现在,我希望你能记住以下三点内容。

      1. 当有多个受保护资源服务的时候,基本的鉴权工作,包括访问令牌的验证、第三方软件应用信息的验证都应该抽出一个 API 网关层,并把这些基本的工作放到这个 API 网关层。
      2. 各大开放平台都是推荐使用授权码许可流程,无论是网页版的 Web 应用程序,还是移动应用程序。
      3. 对于第三方软件开发者重点关注的参数,可以从授权服务的授权端点和令牌端点来区分,授权端点重点是授权码请求和响应的处理,令牌端点重点是访问令牌请求和响应的处理。
      ',33))])}const u=d(g,[["render",s],["__file","13.html.vue"]]),m=JSON.parse('{"path":"/other/oauth2/13.html","title":"13 | 各大开放平台是如何使用OAuth 2.0的?","lang":"zh-CN","frontmatter":{"title":"13 | 各大开放平台是如何使用OAuth 2.0的?","date":"2021-11-08T00:00:00.000Z","author":"ChenSino","publish":true,"description":"在咱们这门课中,我提到了很多次“开放平台”,不难理解,它的作用就是企业把自己的业务能力主要以开放 API 的形式,赋能给外部开发者。而作为第三方开发者或者 ISV(独立软件供应商)在接入这些开放平台的时候,我们最应该关心的就是它们的官方文档,关注接入的流程是怎样的、对应的 API 是什么、每个 API 都传递哪些参数,也就 到这里,你会发现“开放平台的...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/13.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"13 | 各大开放平台是如何使用OAuth 2.0的?"}],["meta",{"property":"og:description","content":"在咱们这门课中,我提到了很多次“开放平台”,不难理解,它的作用就是企业把自己的业务能力主要以开放 API 的形式,赋能给外部开发者。而作为第三方开发者或者 ISV(独立软件供应商)在接入这些开放平台的时候,我们最应该关心的就是它们的官方文档,关注接入的流程是怎样的、对应的 API 是什么、每个 API 都传递哪些参数,也就 到这里,你会发现“开放平台的..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/5775b5bbc363ba2f94d9f6f8e2d05d56.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"ChenSino"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"13 | 各大开放平台是如何使用OAuth 2.0的?\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/5775b5bbc363ba2f94d9f6f8e2d05d56.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/30f2b2db8c01f0fc60e2d821cd59f94b.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/9d05fa572bccec8da5c895b49a9946ca.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/abc7611d467d45bf39d8e07e0d0267dc.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/e27a6836ef686e23284f9314cc3a25b7.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/e98e9ed5e607561df173262703ca3dea.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/0e8394fedd9205e38c3yyc6e7bae2303.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/b653bc541a98920001385612f2309361.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/18c8245e61ce14c1f7a227a6e713b37f.jpg\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/1ac4ded2b7131b475ac71bf4b39c72b5.jpg\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\"}]}"]]},"headers":[{"level":3,"title":"开放平台体系是什么样子的?","slug":"开放平台体系是什么样子的","link":"#开放平台体系是什么样子的","children":[]},{"level":3,"title":"各大开放平台授权流程","slug":"各大开放平台授权流程","link":"#各大开放平台授权流程","children":[]},{"level":3,"title":"授权码流程中的参数说明","slug":"授权码流程中的参数说明","link":"#授权码流程中的参数说明","children":[]},{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2}]},"readingTime":{"minutes":11.53,"words":3458},"filePathRelative":"other/oauth2/13.md","localizedDate":"2021年11月8日","excerpt":"

      在咱们这门课中,我提到了很多次“开放平台”,不难理解,它的作用就是企业把自己的业务能力主要以开放 API 的形式,赋能给外部开发者。而作为第三方开发者或者 ISV(独立软件供应商)在接入这些开放平台的时候,我们最应该关心的就是它们的官方文档,关注接入的流程是怎样的、对应的 API 是什么、每个 API 都传递哪些参数,也就

      \\n

      到这里,你会发现“开放平台的官方文档”会是一个关键点。不过呢,当你去各大开放平台上面看这些文

      \\n

      其中的原因也很简单,那就是开放平台为了让已经具备 OAuth 2.0 知识的研发人员去快速地对接平台上面的业务,把各类对接流程做了分类归档。比如,你会发现微信开放平台上有使用授权码获取授权信息的文档,也有获取令牌的文档,但并没有一份整体的、能够串起来的文档说明。从我的角度来看,这其实也就间接提高了使用门槛,因为如果你不懂 OAuth 2.0,基本是

      ","autoDesc":true}');export{u as comp,m as data}; diff --git a/assets/14.html-C7QC-4vS.js b/assets/14.html-Cm6GW1t2.js similarity index 99% rename from assets/14.html-C7QC-4vS.js rename to assets/14.html-Cm6GW1t2.js index 56118e51f..22442e6fa 100644 --- a/assets/14.html-C7QC-4vS.js +++ b/assets/14.html-Cm6GW1t2.js @@ -1 +1 @@ -import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as h,a as i,b as e,d as l,e as p,f as o,r,o as u}from"./app-LRWZkDAq.js";const s={};function d(m,t){const n=r("RouteLink");return u(),h("div",null,[t[6]||(t[6]=i('

      从 6 月 29 日这门课上线,到现在已经过去一个多月了。我看到了很多同学的留言,有思考,也有提出的问题。那我首先,在这里要感谢你对咱们这门课的支持、鼓励和反馈。在回复你们的留言时,我也把你们提出的问题记了下来。在梳理今天这期答疑的时候,我又从头到尾看了一遍这些问题,也进一步思考了每个问题背后的元认知,最后我归纳出了 6 个问题:

      1. 发明 OAuth 的目的到底是什么?

      2. OAuth 2.0 是身份认证协议吗?

      3. 有了刷新令牌,是不是就可以让访问令牌一直有效了?

      4. 使用了 HTTPS,是不是就能确保 JWT 格式令牌的数据安全?

      5. ID 令牌和访问令牌之间有联系吗?

      6. PKCE 协议到底解决的是什么问题?

      接下来,我们就一一看看这些问题吧。

      发明 OAuth 的目的到底是什么?

      OAuth 协议的设计初衷,就是让最终用户也就是资源拥有者(小明),将他们在受保护资源服务器(京东商家开放平台)上的部分权限(查询当天订单)委托给第三方应用(小兔打单软件),使得第三方应用(小兔)能够代表最终用户(小明)执行操作(查询当天订单)。这便是 OAuth 协议设计的目的。在 OAuth 协议中,通过为每个第三方软件和每个用户的组合分别生成对受保护资源具有受限的访问权限的凭据,也就是访问令牌,来代替之前的用户名和密码。而生成访问令牌之前的登录操作,又是在用户跟平台之间进行的,第三方软件根本无从得知用户的任何信息。这样第三方软件的逻辑处理就大大简化了,它今后的动作就变成了请求访问令牌、使用访问令牌、访问受保护资源,同时在第三方软件调用大量 API 的时候,不再传输用户名和密码,从而减少了网络安全的攻击面。从安全的角度来讲,为每个第三方软件和每个用户的组合来生成一个访问令牌的方式,可以减少对平台更多用户造成的危害。因为这样一来,单个第三方软件被攻破而带来的危害,仅仅会让这一个第三方软件的用户受到影响。那么有的同学就要会问了,这样攻击的对象就会转移到授权服务身上。这个想法没错,但保护一个授权服务肯定要比保护成千上万个、由不同研发人员开发的第三方软件容易得多。

      OAuth 2.0 是身份认证协议吗?

      在这门课中,我其实一直在强调,OAuth 2.0 是一种授权协议,“它一心只专注于干好授权这件事儿”,OAuth 2.0 不是身份认证协议。但实际上,我在刚开始学习 OAuth 2.0 的时候,也曾错误地认为它是身份认证协议。因为我当时觉得,有用户参与其中,比如小明在使用小兔打单软件之前,要向授权服务进行登录操作从而进行身份认证 ,那 OAuth 2.0 就应该是一个身份认证协议啊。但是,小明必须登录之后才能进行授权,是一个额外的需求,登录跟授权体系是独立的。虽然登录操作看似“内嵌”在了 OAuth 2.0 的流程中,但生产环境中登录和授权还是两套独立存在的系统。所以说,像这种“内嵌”的身份认证行为,并不是说 OAuth 2.0 自身承担起了身份认证协议的责任。同时,身份认证会告诉第三方软件当前的用户是谁,但实际上 OAuth 2.0 自始至终都没有向第三方软件透露过关于用户的任何信息。这一点,我们在讲发明 OAuth 协议的目的时也提到过。我们可以再想想小兔打单软件的例子,看是不是这样:小兔打单软件永远也不会知道小明的任何信息,它仅仅是请求访问令牌,使用访问令牌并最终调用查询订单的 API。

      有了刷新令牌,是不是就可以让访问令牌一直有效了?

      要回答这个问题,我们先复习下访问令牌和刷新令牌相关的几个知识点吧。

      第一,OAuth 2.0 的核心是授权,授权的核心是令牌,也就是我们说的访问令牌。

      第二,在第 3 讲中我们提到,为了提高用户的体验,OAuth 2.0 提供了刷新令牌的机制,使得访问令牌过期后,第三方软件在无需用户再次授权的情况下,可以重新请求一个访问令牌。

      第三,在使用上,刷新令牌只能用在授权服务上,而访问令牌只能用在受保护资源服务上。

      有了这些知识做基础,我们可以继续分析“有了刷新令牌,是不是就可以让访问令牌一直有效”这个问题了。

      当访问令牌被 “递给” 受保护资源服务的时候,受保护资源服务需要对访问令牌进行验证,还要对访问令牌关联的权限和第三方软件的请求进行权限匹配校验。当访问令牌过期的时候,我们使用刷新令牌请求到的访问令牌,是授权服务重新生成的,而不是延长了原访问令牌的有效期。

      当前的这个刷新令牌被使用之后,授权服务可以自行决定是颁发一个新的刷新令牌,还是仍然给第三方软件返回上一个刷新令牌。安全起见,我们的建议是返回一个新的刷新令牌。这时,你可能就有一个疑问了:第三方软件已经换了一个访问令牌了,刷新令牌又一直存在,那是不是就可以一直使用刷新令牌来获取访问令牌了呢?

      要解决这个疑问,我们要知道的是,刷新令牌也有有效期。尽管生成了新的刷新令牌,但它的有效期不会改变,有效期的时间戳仍然是上一个刷新令牌的。刷新令牌的有效期到了,就不能再继续用它来申请新的访问令牌了。

      使用了 HTTPS,是不是就能确保 JWT 格式令牌的数据安全?

      OAuth 2.0 的使用从来都不应该脱离 HTTPS。因为访问令牌、应用密钥敏感信息要在网络上传输,都离不开 HTTPS 的保护。但是,HTTPS 也只是保证了访问令牌等重要信息在网络传输上的安全。

      在 OAuth 2.0 的规范中,访问令牌对第三方软件是不透明的,从来都不应该被任何第三方软件解析到。由于 JWT 格式的令牌自包含了用户相关的信息,比如用户标识,因此仅仅对它进行签名还不够。要避免第三方软件有机会获取访问令牌所包含的信息,那我们在与第三方软件交互的环境下使用 JWT 格式的令牌时,还要对它进行加密来保障令牌的安全,而不是仅仅依靠 HTTPS。

      ID 令牌和访问令牌之间有联系吗?

      ',20)),e("p",null,[t[1]||(t[1]=l("在")),p(n,{to:"/other/oauth2/09.html"},{default:o(()=>t[0]||(t[0]=[l("第 9 讲")])),_:1}),t[2]||(t[2]=l("中,我们在用 OAuth 2.0 实现一个 OpenID Connect 身份认证协议的时候,讲到了 ID 令牌。在这一讲的后面,有同学还是不太清楚 ID 令牌和访问令牌是啥关系,当时我就在留言区做了回复。现在,我重新整理了思路再和你解释一下,因为认识到 ID 令牌和访问令牌的联系与区别,对我们利用 OAuth 2.0 搭建一个身份认证协议来说太重要了。"))]),t[7]||(t[7]=e("p",null,"我们先来总结下 ID 令牌和访问令牌的作用:",-1)),t[8]||(t[8]=e("ul",null,[e("li",null,"ID 令牌,也就是 ID_TOKEN,代表的是用户身份令牌,可以说是一个单独的身份认证结果,永远不会像访问令牌那样作为一个参数,去传递给其它外部服务;"),e("li",null,"访问令牌,也就是 ACCESS_TOKEN,就是一个令牌,是要被第三方软件用来作为凭证,从而代表用户去请求受保护资源服务的。")],-1)),t[9]||(t[9]=e("p",null,"你看,这两种令牌是截然不同的。接下来,我们就分析下,它们的区别都体现在哪些方面吧。",-1)),t[10]||(t[10]=e("p",null,"第一,ID 令牌是对访问令牌的补充,而不是要替换访问令牌。之所以采用这样双令牌的方式,就是想让早先存在的访问令牌,可以在 OAuth 2.0 中继续保持对第三方软件的不透明性,而让后来新增的 ID 令牌要能够被解析,目的就是方便应用到身份认证协议中。第",-1)),t[11]||(t[11]=e("p",null,"二,ID 令牌和访问令牌有不同的生命周期,ID 令牌的生命周期相对来说更短些。因为 ID 令牌的作用就是代表一个单独的身份认证结果,它的使命就是用来标识用户的。而这个标识并不是用户名,用户登录的时候用的是用户名而不是这个 ID 令牌,所以如果用户注销或者退出了登录,ID 令牌的生命周期就随之结束了。",-1)),t[12]||(t[12]=e("p",null,"访问令牌可以在用户离开后的很长时间内,继续被第三方软件用来请求受保护资源服务。比如,小明使用了小兔打单软件的批量导出订单功能,如果耗时相对比较长,小明不必一直在场。",-1)),t[13]||(t[13]=e("h3",{id:"pkce-协议到底解决的是什么问题",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#pkce-协议到底解决的是什么问题"},[e("span",null,"PKCE 协议到底解决的是什么问题?")])],-1)),e("p",null,[t[4]||(t[4]=l("我们在")),p(n,{to:"/other/oauth2/07.html"},{default:o(()=>t[3]||(t[3]=[l("第 7 讲")])),_:1}),t[5]||(t[5]=l("中学习 PKCE 协议时,我看到了大家对这个协议的很多留言,有的是自己的思考,有的是问题的进一步讨论。我们要理解 PKCE 协议到底解决了什么问题,就要先看一"))]),t[14]||(t[14]=i('

      2012 年 10 月 OAuth 2.0 的正式授权协议框架,也就是官方的 RFC 6749 被正式发布,2015 年 9 月增补了 PKCE 协议,也就是官方的 RFC 7636。从时间上来看,从正式发布 OAuth 2.0 授权协议到增补发布了 PKCE 协议,整整间隔了三年,而这三年恰恰是移动应用蓬勃发展的时期。

      同时,在原生的移动客户端应用保存秘钥又存在特殊的安全问题,使用 OAuth 2.0 授权码许可类型的客户端又容易受到授权码窃听的攻击。

      所以,PKCE 被增补发布的背景是,移动应用大力发展,同时原生客户端使用 OAuth 2.0 面临着安全风险。这样我们就能理解了,发布 PKCE 协议的目的,主要就是缓解针对公开客户端的攻击,提高授权码使用的安全性。

      总结

      1. OAuth 协议被发明的目的,就是用令牌代替用户名和密码。
      2. OAuth 2.0 不能被直接用来“从事”身份认证协议的“工作”。虽然 OAuth2.0 的使用要求是在 HTTPS 的环境下,但这并不能解决 JWT 令牌对第三方软件“不透明”的问题,还需要进行加密。
      3. 有了刷新令牌也不能让访问令牌一直有效下去,因为刷新令牌也有有效期。
      4. ID 令牌是对访问令牌的补充,而不是要替代访问令牌。
      5. PKCE 是 OAuth 2.0 的一个增补协议,主要用来缓解授权码被窃听的安全风险。
      ',5))])}const T=a(s,[["render",d],["__file","14.html.vue"]]),C=JSON.parse('{"path":"/other/oauth2/14.html","title":"14 | 查漏补缺:OAuth 2.0 常见问题答疑","lang":"zh-CN","frontmatter":{"title":"14 | 查漏补缺:OAuth 2.0 常见问题答疑","date":"2021-11-08T00:00:00.000Z","author":"ChenSino","publish":true,"description":"从 6 月 29 日这门课上线,到现在已经过去一个多月了。我看到了很多同学的留言,有思考,也有提出的问题。那我首先,在这里要感谢你对咱们这门课的支持、鼓励和反馈。在回复你们的留言时,我也把你们提出的问题记了下来。在梳理今天这期答疑的时候,我又从头到尾看了一遍这些问题,也进一步思考了每个问题背后的元认知,最后我归纳出了 6 个问题: 发明 OAuth 的...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/14.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"14 | 查漏补缺:OAuth 2.0 常见问题答疑"}],["meta",{"property":"og:description","content":"从 6 月 29 日这门课上线,到现在已经过去一个多月了。我看到了很多同学的留言,有思考,也有提出的问题。那我首先,在这里要感谢你对咱们这门课的支持、鼓励和反馈。在回复你们的留言时,我也把你们提出的问题记了下来。在梳理今天这期答疑的时候,我又从头到尾看了一遍这些问题,也进一步思考了每个问题背后的元认知,最后我归纳出了 6 个问题: 发明 OAuth 的..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"ChenSino"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"14 | 查漏补缺:OAuth 2.0 常见问题答疑\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\"}]}"]]},"headers":[{"level":3,"title":"发明 OAuth 的目的到底是什么?","slug":"发明-oauth-的目的到底是什么","link":"#发明-oauth-的目的到底是什么","children":[]},{"level":3,"title":"OAuth 2.0 是身份认证协议吗?","slug":"oauth-2-0-是身份认证协议吗","link":"#oauth-2-0-是身份认证协议吗","children":[]},{"level":3,"title":"有了刷新令牌,是不是就可以让访问令牌一直有效了?","slug":"有了刷新令牌-是不是就可以让访问令牌一直有效了","link":"#有了刷新令牌-是不是就可以让访问令牌一直有效了","children":[]},{"level":3,"title":"使用了 HTTPS,是不是就能确保 JWT 格式令牌的数据安全?","slug":"使用了-https-是不是就能确保-jwt-格式令牌的数据安全","link":"#使用了-https-是不是就能确保-jwt-格式令牌的数据安全","children":[]},{"level":3,"title":"ID 令牌和访问令牌之间有联系吗?","slug":"id-令牌和访问令牌之间有联系吗","link":"#id-令牌和访问令牌之间有联系吗","children":[]},{"level":3,"title":"PKCE 协议到底解决的是什么问题?","slug":"pkce-协议到底解决的是什么问题","link":"#pkce-协议到底解决的是什么问题","children":[]},{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2},{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":10.29,"words":3086},"filePathRelative":"other/oauth2/14.md","localizedDate":"2021年11月8日","excerpt":"

      从 6 月 29 日这门课上线,到现在已经过去一个多月了。我看到了很多同学的留言,有思考,也有提出的问题。那我首先,在这里要感谢你对咱们这门课的支持、鼓励和反馈。在回复你们的留言时,我也把你们提出的问题记了下来。在梳理今天这期答疑的时候,我又从头到尾看了一遍这些问题,也进一步思考了每个问题背后的元认知,最后我归纳出了 6 个问题:

      \\n
        \\n
      1. \\n

        发明 OAuth 的目的到底是什么?

        \\n
      2. \\n
      3. \\n

        OAuth 2.0 是身份认证协议吗?

        \\n
      4. \\n
      5. \\n

        有了刷新令牌,是不是就可以让访问令牌一直有效了?

        \\n
      6. \\n
      7. \\n

        使用了 HTTPS,是不是就能确保 JWT 格式令牌的数据安全?

        \\n
      8. \\n
      9. \\n

        ID 令牌和访问令牌之间有联系吗?

        \\n
      10. \\n
      11. \\n

        PKCE 协议到底解决的是什么问题?

        \\n
      12. \\n
      ","autoDesc":true}');export{T as comp,C as data}; +import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as h,a as i,b as e,d as l,e as p,f as o,r,o as u}from"./app-BIdVr4mq.js";const s={};function d(m,t){const n=r("RouteLink");return u(),h("div",null,[t[6]||(t[6]=i('

      从 6 月 29 日这门课上线,到现在已经过去一个多月了。我看到了很多同学的留言,有思考,也有提出的问题。那我首先,在这里要感谢你对咱们这门课的支持、鼓励和反馈。在回复你们的留言时,我也把你们提出的问题记了下来。在梳理今天这期答疑的时候,我又从头到尾看了一遍这些问题,也进一步思考了每个问题背后的元认知,最后我归纳出了 6 个问题:

      1. 发明 OAuth 的目的到底是什么?

      2. OAuth 2.0 是身份认证协议吗?

      3. 有了刷新令牌,是不是就可以让访问令牌一直有效了?

      4. 使用了 HTTPS,是不是就能确保 JWT 格式令牌的数据安全?

      5. ID 令牌和访问令牌之间有联系吗?

      6. PKCE 协议到底解决的是什么问题?

      接下来,我们就一一看看这些问题吧。

      发明 OAuth 的目的到底是什么?

      OAuth 协议的设计初衷,就是让最终用户也就是资源拥有者(小明),将他们在受保护资源服务器(京东商家开放平台)上的部分权限(查询当天订单)委托给第三方应用(小兔打单软件),使得第三方应用(小兔)能够代表最终用户(小明)执行操作(查询当天订单)。这便是 OAuth 协议设计的目的。在 OAuth 协议中,通过为每个第三方软件和每个用户的组合分别生成对受保护资源具有受限的访问权限的凭据,也就是访问令牌,来代替之前的用户名和密码。而生成访问令牌之前的登录操作,又是在用户跟平台之间进行的,第三方软件根本无从得知用户的任何信息。这样第三方软件的逻辑处理就大大简化了,它今后的动作就变成了请求访问令牌、使用访问令牌、访问受保护资源,同时在第三方软件调用大量 API 的时候,不再传输用户名和密码,从而减少了网络安全的攻击面。从安全的角度来讲,为每个第三方软件和每个用户的组合来生成一个访问令牌的方式,可以减少对平台更多用户造成的危害。因为这样一来,单个第三方软件被攻破而带来的危害,仅仅会让这一个第三方软件的用户受到影响。那么有的同学就要会问了,这样攻击的对象就会转移到授权服务身上。这个想法没错,但保护一个授权服务肯定要比保护成千上万个、由不同研发人员开发的第三方软件容易得多。

      OAuth 2.0 是身份认证协议吗?

      在这门课中,我其实一直在强调,OAuth 2.0 是一种授权协议,“它一心只专注于干好授权这件事儿”,OAuth 2.0 不是身份认证协议。但实际上,我在刚开始学习 OAuth 2.0 的时候,也曾错误地认为它是身份认证协议。因为我当时觉得,有用户参与其中,比如小明在使用小兔打单软件之前,要向授权服务进行登录操作从而进行身份认证 ,那 OAuth 2.0 就应该是一个身份认证协议啊。但是,小明必须登录之后才能进行授权,是一个额外的需求,登录跟授权体系是独立的。虽然登录操作看似“内嵌”在了 OAuth 2.0 的流程中,但生产环境中登录和授权还是两套独立存在的系统。所以说,像这种“内嵌”的身份认证行为,并不是说 OAuth 2.0 自身承担起了身份认证协议的责任。同时,身份认证会告诉第三方软件当前的用户是谁,但实际上 OAuth 2.0 自始至终都没有向第三方软件透露过关于用户的任何信息。这一点,我们在讲发明 OAuth 协议的目的时也提到过。我们可以再想想小兔打单软件的例子,看是不是这样:小兔打单软件永远也不会知道小明的任何信息,它仅仅是请求访问令牌,使用访问令牌并最终调用查询订单的 API。

      有了刷新令牌,是不是就可以让访问令牌一直有效了?

      要回答这个问题,我们先复习下访问令牌和刷新令牌相关的几个知识点吧。

      第一,OAuth 2.0 的核心是授权,授权的核心是令牌,也就是我们说的访问令牌。

      第二,在第 3 讲中我们提到,为了提高用户的体验,OAuth 2.0 提供了刷新令牌的机制,使得访问令牌过期后,第三方软件在无需用户再次授权的情况下,可以重新请求一个访问令牌。

      第三,在使用上,刷新令牌只能用在授权服务上,而访问令牌只能用在受保护资源服务上。

      有了这些知识做基础,我们可以继续分析“有了刷新令牌,是不是就可以让访问令牌一直有效”这个问题了。

      当访问令牌被 “递给” 受保护资源服务的时候,受保护资源服务需要对访问令牌进行验证,还要对访问令牌关联的权限和第三方软件的请求进行权限匹配校验。当访问令牌过期的时候,我们使用刷新令牌请求到的访问令牌,是授权服务重新生成的,而不是延长了原访问令牌的有效期。

      当前的这个刷新令牌被使用之后,授权服务可以自行决定是颁发一个新的刷新令牌,还是仍然给第三方软件返回上一个刷新令牌。安全起见,我们的建议是返回一个新的刷新令牌。这时,你可能就有一个疑问了:第三方软件已经换了一个访问令牌了,刷新令牌又一直存在,那是不是就可以一直使用刷新令牌来获取访问令牌了呢?

      要解决这个疑问,我们要知道的是,刷新令牌也有有效期。尽管生成了新的刷新令牌,但它的有效期不会改变,有效期的时间戳仍然是上一个刷新令牌的。刷新令牌的有效期到了,就不能再继续用它来申请新的访问令牌了。

      使用了 HTTPS,是不是就能确保 JWT 格式令牌的数据安全?

      OAuth 2.0 的使用从来都不应该脱离 HTTPS。因为访问令牌、应用密钥敏感信息要在网络上传输,都离不开 HTTPS 的保护。但是,HTTPS 也只是保证了访问令牌等重要信息在网络传输上的安全。

      在 OAuth 2.0 的规范中,访问令牌对第三方软件是不透明的,从来都不应该被任何第三方软件解析到。由于 JWT 格式的令牌自包含了用户相关的信息,比如用户标识,因此仅仅对它进行签名还不够。要避免第三方软件有机会获取访问令牌所包含的信息,那我们在与第三方软件交互的环境下使用 JWT 格式的令牌时,还要对它进行加密来保障令牌的安全,而不是仅仅依靠 HTTPS。

      ID 令牌和访问令牌之间有联系吗?

      ',20)),e("p",null,[t[1]||(t[1]=l("在")),p(n,{to:"/other/oauth2/09.html"},{default:o(()=>t[0]||(t[0]=[l("第 9 讲")])),_:1}),t[2]||(t[2]=l("中,我们在用 OAuth 2.0 实现一个 OpenID Connect 身份认证协议的时候,讲到了 ID 令牌。在这一讲的后面,有同学还是不太清楚 ID 令牌和访问令牌是啥关系,当时我就在留言区做了回复。现在,我重新整理了思路再和你解释一下,因为认识到 ID 令牌和访问令牌的联系与区别,对我们利用 OAuth 2.0 搭建一个身份认证协议来说太重要了。"))]),t[7]||(t[7]=e("p",null,"我们先来总结下 ID 令牌和访问令牌的作用:",-1)),t[8]||(t[8]=e("ul",null,[e("li",null,"ID 令牌,也就是 ID_TOKEN,代表的是用户身份令牌,可以说是一个单独的身份认证结果,永远不会像访问令牌那样作为一个参数,去传递给其它外部服务;"),e("li",null,"访问令牌,也就是 ACCESS_TOKEN,就是一个令牌,是要被第三方软件用来作为凭证,从而代表用户去请求受保护资源服务的。")],-1)),t[9]||(t[9]=e("p",null,"你看,这两种令牌是截然不同的。接下来,我们就分析下,它们的区别都体现在哪些方面吧。",-1)),t[10]||(t[10]=e("p",null,"第一,ID 令牌是对访问令牌的补充,而不是要替换访问令牌。之所以采用这样双令牌的方式,就是想让早先存在的访问令牌,可以在 OAuth 2.0 中继续保持对第三方软件的不透明性,而让后来新增的 ID 令牌要能够被解析,目的就是方便应用到身份认证协议中。第",-1)),t[11]||(t[11]=e("p",null,"二,ID 令牌和访问令牌有不同的生命周期,ID 令牌的生命周期相对来说更短些。因为 ID 令牌的作用就是代表一个单独的身份认证结果,它的使命就是用来标识用户的。而这个标识并不是用户名,用户登录的时候用的是用户名而不是这个 ID 令牌,所以如果用户注销或者退出了登录,ID 令牌的生命周期就随之结束了。",-1)),t[12]||(t[12]=e("p",null,"访问令牌可以在用户离开后的很长时间内,继续被第三方软件用来请求受保护资源服务。比如,小明使用了小兔打单软件的批量导出订单功能,如果耗时相对比较长,小明不必一直在场。",-1)),t[13]||(t[13]=e("h3",{id:"pkce-协议到底解决的是什么问题",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#pkce-协议到底解决的是什么问题"},[e("span",null,"PKCE 协议到底解决的是什么问题?")])],-1)),e("p",null,[t[4]||(t[4]=l("我们在")),p(n,{to:"/other/oauth2/07.html"},{default:o(()=>t[3]||(t[3]=[l("第 7 讲")])),_:1}),t[5]||(t[5]=l("中学习 PKCE 协议时,我看到了大家对这个协议的很多留言,有的是自己的思考,有的是问题的进一步讨论。我们要理解 PKCE 协议到底解决了什么问题,就要先看一"))]),t[14]||(t[14]=i('

      2012 年 10 月 OAuth 2.0 的正式授权协议框架,也就是官方的 RFC 6749 被正式发布,2015 年 9 月增补了 PKCE 协议,也就是官方的 RFC 7636。从时间上来看,从正式发布 OAuth 2.0 授权协议到增补发布了 PKCE 协议,整整间隔了三年,而这三年恰恰是移动应用蓬勃发展的时期。

      同时,在原生的移动客户端应用保存秘钥又存在特殊的安全问题,使用 OAuth 2.0 授权码许可类型的客户端又容易受到授权码窃听的攻击。

      所以,PKCE 被增补发布的背景是,移动应用大力发展,同时原生客户端使用 OAuth 2.0 面临着安全风险。这样我们就能理解了,发布 PKCE 协议的目的,主要就是缓解针对公开客户端的攻击,提高授权码使用的安全性。

      总结

      1. OAuth 协议被发明的目的,就是用令牌代替用户名和密码。
      2. OAuth 2.0 不能被直接用来“从事”身份认证协议的“工作”。虽然 OAuth2.0 的使用要求是在 HTTPS 的环境下,但这并不能解决 JWT 令牌对第三方软件“不透明”的问题,还需要进行加密。
      3. 有了刷新令牌也不能让访问令牌一直有效下去,因为刷新令牌也有有效期。
      4. ID 令牌是对访问令牌的补充,而不是要替代访问令牌。
      5. PKCE 是 OAuth 2.0 的一个增补协议,主要用来缓解授权码被窃听的安全风险。
      ',5))])}const T=a(s,[["render",d],["__file","14.html.vue"]]),C=JSON.parse('{"path":"/other/oauth2/14.html","title":"14 | 查漏补缺:OAuth 2.0 常见问题答疑","lang":"zh-CN","frontmatter":{"title":"14 | 查漏补缺:OAuth 2.0 常见问题答疑","date":"2021-11-08T00:00:00.000Z","author":"ChenSino","publish":true,"description":"从 6 月 29 日这门课上线,到现在已经过去一个多月了。我看到了很多同学的留言,有思考,也有提出的问题。那我首先,在这里要感谢你对咱们这门课的支持、鼓励和反馈。在回复你们的留言时,我也把你们提出的问题记了下来。在梳理今天这期答疑的时候,我又从头到尾看了一遍这些问题,也进一步思考了每个问题背后的元认知,最后我归纳出了 6 个问题: 发明 OAuth 的...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/oauth2/14.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"14 | 查漏补缺:OAuth 2.0 常见问题答疑"}],["meta",{"property":"og:description","content":"从 6 月 29 日这门课上线,到现在已经过去一个多月了。我看到了很多同学的留言,有思考,也有提出的问题。那我首先,在这里要感谢你对咱们这门课的支持、鼓励和反馈。在回复你们的留言时,我也把你们提出的问题记了下来。在梳理今天这期答疑的时候,我又从头到尾看了一遍这些问题,也进一步思考了每个问题背后的元认知,最后我归纳出了 6 个问题: 发明 OAuth 的..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"ChenSino"}],["meta",{"property":"article:published_time","content":"2021-11-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"14 | 查漏补缺:OAuth 2.0 常见问题答疑\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-11-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\"}]}"]]},"headers":[{"level":3,"title":"发明 OAuth 的目的到底是什么?","slug":"发明-oauth-的目的到底是什么","link":"#发明-oauth-的目的到底是什么","children":[]},{"level":3,"title":"OAuth 2.0 是身份认证协议吗?","slug":"oauth-2-0-是身份认证协议吗","link":"#oauth-2-0-是身份认证协议吗","children":[]},{"level":3,"title":"有了刷新令牌,是不是就可以让访问令牌一直有效了?","slug":"有了刷新令牌-是不是就可以让访问令牌一直有效了","link":"#有了刷新令牌-是不是就可以让访问令牌一直有效了","children":[]},{"level":3,"title":"使用了 HTTPS,是不是就能确保 JWT 格式令牌的数据安全?","slug":"使用了-https-是不是就能确保-jwt-格式令牌的数据安全","link":"#使用了-https-是不是就能确保-jwt-格式令牌的数据安全","children":[]},{"level":3,"title":"ID 令牌和访问令牌之间有联系吗?","slug":"id-令牌和访问令牌之间有联系吗","link":"#id-令牌和访问令牌之间有联系吗","children":[]},{"level":3,"title":"PKCE 协议到底解决的是什么问题?","slug":"pkce-协议到底解决的是什么问题","link":"#pkce-协议到底解决的是什么问题","children":[]},{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]}],"git":{"createdTime":1667895476000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2},{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":10.29,"words":3086},"filePathRelative":"other/oauth2/14.md","localizedDate":"2021年11月8日","excerpt":"

      从 6 月 29 日这门课上线,到现在已经过去一个多月了。我看到了很多同学的留言,有思考,也有提出的问题。那我首先,在这里要感谢你对咱们这门课的支持、鼓励和反馈。在回复你们的留言时,我也把你们提出的问题记了下来。在梳理今天这期答疑的时候,我又从头到尾看了一遍这些问题,也进一步思考了每个问题背后的元认知,最后我归纳出了 6 个问题:

      \\n
        \\n
      1. \\n

        发明 OAuth 的目的到底是什么?

        \\n
      2. \\n
      3. \\n

        OAuth 2.0 是身份认证协议吗?

        \\n
      4. \\n
      5. \\n

        有了刷新令牌,是不是就可以让访问令牌一直有效了?

        \\n
      6. \\n
      7. \\n

        使用了 HTTPS,是不是就能确保 JWT 格式令牌的数据安全?

        \\n
      8. \\n
      9. \\n

        ID 令牌和访问令牌之间有联系吗?

        \\n
      10. \\n
      11. \\n

        PKCE 协议到底解决的是什么问题?

        \\n
      12. \\n
      ","autoDesc":true}');export{T as comp,C as data}; diff --git a/assets/2.html-CMg5IU4s.js b/assets/2.html-DYUUXpT6.js similarity index 97% rename from assets/2.html-CMg5IU4s.js rename to assets/2.html-DYUUXpT6.js index 50ebbf9f9..af49abee1 100644 --- a/assets/2.html-CMg5IU4s.js +++ b/assets/2.html-DYUUXpT6.js @@ -1 +1 @@ -import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as t,o as i}from"./app-LRWZkDAq.js";const p={};function a(r,e){return i(),o("div",null,e[0]||(e[0]=[t("h2",{id:"占坑",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#占坑"},[t("span",null,"占坑")])],-1),t("p",null,"https://juejin.cn/post/6969389200416178213#heading-11",-1),t("p",null,"https://blog.jetbrains.com/clion/2020/03/openjdk-with-clion/",-1)]))}const s=n(p,[["render",a],["__file","2.html.vue"]]),h=JSON.parse('{"path":"/cpp/other/2.html","title":"使用Clion搭建jdk源码调试环境","lang":"zh-CN","frontmatter":{"title":"使用Clion搭建jdk源码调试环境","date":"2022-01-13T00:00:00.000Z","description":"占坑 https://juejin.cn/post/6969389200416178213#heading-11 https://blog.jetbrains.com/clion/2020/03/openjdk-with-clion/","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/cpp/other/2.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"使用Clion搭建jdk源码调试环境"}],["meta",{"property":"og:description","content":"占坑 https://juejin.cn/post/6969389200416178213#heading-11 https://blog.jetbrains.com/clion/2020/03/openjdk-with-clion/"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-01-13T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"使用Clion搭建jdk源码调试环境\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-01-13T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"占坑","slug":"占坑","link":"#占坑","children":[]}],"git":{"createdTime":1674959709000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":0.09,"words":27},"filePathRelative":"cpp/other/2.md","localizedDate":"2022年1月13日","excerpt":"

      占坑

      \\n

      https://juejin.cn/post/6969389200416178213#heading-11

      \\n

      https://blog.jetbrains.com/clion/2020/03/openjdk-with-clion/

      \\n","autoDesc":true}');export{s as comp,h as data}; +import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as t,o as i}from"./app-BIdVr4mq.js";const p={};function a(r,e){return i(),o("div",null,e[0]||(e[0]=[t("h2",{id:"占坑",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#占坑"},[t("span",null,"占坑")])],-1),t("p",null,"https://juejin.cn/post/6969389200416178213#heading-11",-1),t("p",null,"https://blog.jetbrains.com/clion/2020/03/openjdk-with-clion/",-1)]))}const s=n(p,[["render",a],["__file","2.html.vue"]]),h=JSON.parse('{"path":"/cpp/other/2.html","title":"使用Clion搭建jdk源码调试环境","lang":"zh-CN","frontmatter":{"title":"使用Clion搭建jdk源码调试环境","date":"2022-01-13T00:00:00.000Z","description":"占坑 https://juejin.cn/post/6969389200416178213#heading-11 https://blog.jetbrains.com/clion/2020/03/openjdk-with-clion/","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/cpp/other/2.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"使用Clion搭建jdk源码调试环境"}],["meta",{"property":"og:description","content":"占坑 https://juejin.cn/post/6969389200416178213#heading-11 https://blog.jetbrains.com/clion/2020/03/openjdk-with-clion/"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-01-13T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"使用Clion搭建jdk源码调试环境\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-01-13T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"占坑","slug":"占坑","link":"#占坑","children":[]}],"git":{"createdTime":1674959709000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":0.09,"words":27},"filePathRelative":"cpp/other/2.md","localizedDate":"2022年1月13日","excerpt":"

      占坑

      \\n

      https://juejin.cn/post/6969389200416178213#heading-11

      \\n

      https://blog.jetbrains.com/clion/2020/03/openjdk-with-clion/

      \\n","autoDesc":true}');export{s as comp,h as data}; diff --git a/assets/2022-04-12.html-VXRZFk6l.js b/assets/2022-04-12.html-COB6RKZC.js similarity index 99% rename from assets/2022-04-12.html-VXRZFk6l.js rename to assets/2022-04-12.html-COB6RKZC.js index 0e207b86f..125589dbe 100644 --- a/assets/2022-04-12.html-VXRZFk6l.js +++ b/assets/2022-04-12.html-COB6RKZC.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a,o as t}from"./app-LRWZkDAq.js";const l={};function n(h,i){return t(),e("div",null,i[0]||(i[0]=[a(`

      背景:

      我有一个库A,这个库同时被两个服务使用(serviceA、serviceB),某天,因serviceA业务需要,必须更改数据库结构,导致serviceB无法使用,但是serviceB又不能停机,所以就考虑把数据库克隆一份给serviceB使用。在克隆使用的是mysqldump直接导出,然后再新建另一个库B,在库B导入sql,结果就悲剧了。

      1、库A导出

      # 在服务器把写了个定时备份脚本,这个databaseA是未更新前备份的库
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a,o as t}from"./app-BIdVr4mq.js";const l={};function n(h,i){return t(),e("div",null,i[0]||(i[0]=[a(`

      背景:

      我有一个库A,这个库同时被两个服务使用(serviceA、serviceB),某天,因serviceA业务需要,必须更改数据库结构,导致serviceB无法使用,但是serviceB又不能停机,所以就考虑把数据库克隆一份给serviceB使用。在克隆使用的是mysqldump直接导出,然后再新建另一个库B,在库B导入sql,结果就悲剧了。

      1、库A导出

      # 在服务器把写了个定时备份脚本,这个databaseA是未更新前备份的库
       mysqldump -h localhost -uroot -p123456 databaseA>databaseA.sql

      2、使用source导入

      # 1. 先创建databaseB(此步省略)
       
       # 2. source命令导入
      diff --git a/assets/404.html-BagFMDd7.js b/assets/404.html-Cj93pVFe.js
      similarity index 93%
      rename from assets/404.html-BagFMDd7.js
      rename to assets/404.html-Cj93pVFe.js
      index 666ffafbc..c346d034e 100644
      --- a/assets/404.html-BagFMDd7.js
      +++ b/assets/404.html-Cj93pVFe.js
      @@ -1 +1 @@
      -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as n,o as r}from"./app-LRWZkDAq.js";const a={};function p(i,t){return r(),o("div",null,t[0]||(t[0]=[n("p",null,"404 Not Found",-1)]))}const l=e(a,[["render",p],["__file","404.html.vue"]]),m=JSON.parse('{"path":"/404.html","title":"","lang":"zh-CN","frontmatter":{"layout":"NotFound","description":"404 Not Found","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/404.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:description","content":"404 Not Found"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"\\",\\"description\\":\\"404 Not Found\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0.01,"words":3},"filePathRelative":null,"excerpt":"

      404 Not Found

      \\n","autoDesc":true}');export{l as comp,m as data}; +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as n,o as r}from"./app-BIdVr4mq.js";const a={};function p(i,t){return r(),o("div",null,t[0]||(t[0]=[n("p",null,"404 Not Found",-1)]))}const l=e(a,[["render",p],["__file","404.html.vue"]]),m=JSON.parse('{"path":"/404.html","title":"","lang":"zh-CN","frontmatter":{"layout":"NotFound","description":"404 Not Found","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/404.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:description","content":"404 Not Found"}],["meta",{"property":"og:type","content":"website"}],["meta",{"property":"og:locale","content":"zh-CN"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"WebPage\\",\\"name\\":\\"\\",\\"description\\":\\"404 Not Found\\"}"]]},"headers":[],"git":{},"readingTime":{"minutes":0.01,"words":3},"filePathRelative":null,"excerpt":"

      404 Not Found

      \\n","autoDesc":true}');export{l as comp,m as data}; diff --git a/assets/AOPLog.html-Bm02oOMf.js b/assets/AOPLog.html-oemJ_ITV.js similarity index 99% rename from assets/AOPLog.html-Bm02oOMf.js rename to assets/AOPLog.html-oemJ_ITV.js index 3dc2640a6..0a30d267b 100644 --- a/assets/AOPLog.html-Bm02oOMf.js +++ b/assets/AOPLog.html-oemJ_ITV.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const t={};function h(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

      1. 博客背景

      最近业务提了一个需求,让记录每个用户的每个操作请求到数据库,保证每个操作都可追溯,这个需求很典型,实现起来也不难,一个自定义注解就搞定了。

      2. 实现

      实现思路比较简单,采用AOP,先自定义一个注解,在需要记录的地方就使用注解

      2.1 自定义注解SysLog
      @Target(ElementType.METHOD)
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const t={};function h(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

      1. 博客背景

      最近业务提了一个需求,让记录每个用户的每个操作请求到数据库,保证每个操作都可追溯,这个需求很典型,实现起来也不难,一个自定义注解就搞定了。

      2. 实现

      实现思路比较简单,采用AOP,先自定义一个注解,在需要记录的地方就使用注解

      2.1 自定义注解SysLog
      @Target(ElementType.METHOD)
       @Retention(RetentionPolicy.RUNTIME)
       @Documented
       public @interface SysLog {
      diff --git a/assets/Annotation.html-DEhyElMf.js b/assets/Annotation.html-C9NZ7kgo.js
      similarity index 99%
      rename from assets/Annotation.html-DEhyElMf.js
      rename to assets/Annotation.html-C9NZ7kgo.js
      index 1512a8ddb..0f062d924 100644
      --- a/assets/Annotation.html-DEhyElMf.js
      +++ b/assets/Annotation.html-C9NZ7kgo.js
      @@ -1,4 +1,4 @@
      -import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a as l,b as s,d as a,e as r,f as p,r as h,o as d}from"./app-LRWZkDAq.js";const k={};function o(g,i){const e=h("RouteLink");return d(),t("div",null,[i[3]||(i[3]=l(`

      1、依赖注入

       1. @Autowired
      +import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a as l,b as s,d as a,e as r,f as p,r as h,o as d}from"./app-BIdVr4mq.js";const k={};function o(g,i){const e=h("RouteLink");return d(),t("div",null,[i[3]||(i[3]=l(`

      1、依赖注入

       1. @Autowired
            是Spring中的注解,按照类型注入,此注解可以用于字段属性上、setter方法上、构造函数上,用在字段上则Spring底层会使用反射对字段进行赋值,用成员变量的在setter方法上,则会调用setter方法进行注入。从Spring4.3开始,如果只有一个有参的构造方法,则可以省略构造方法上的@Autowired
        2. @Autowired + @Qualifier 
           按照bean的名字注入
      diff --git a/assets/Arthas.html-CX1s5XJ4.js b/assets/Arthas.html-iIzMcD21.js
      similarity index 99%
      rename from assets/Arthas.html-CX1s5XJ4.js
      rename to assets/Arthas.html-iIzMcD21.js
      index cee772985..da22e67bf 100644
      --- a/assets/Arthas.html-CX1s5XJ4.js
      +++ b/assets/Arthas.html-iIzMcD21.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const t={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

      背景:有一个导出PDF的功能,在本地运行正常,在线上异常,抛出的异常无法直接定位到问题。

      1、使用arthas来跟踪

      1. 进入arths,attach进入项目
      $ as.sh
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const t={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

      背景:有一个导出PDF的功能,在本地运行正常,在线上异常,抛出的异常无法直接定位到问题。

      1、使用arthas来跟踪

      1. 进入arths,attach进入项目
      $ as.sh
       Arthas script version: 3.5.5
       [INFO] JAVA_HOME: /usr/local/jdk1.8.0_72
       [INFO] Process 57135 already using port 3658
      diff --git a/assets/Authorization.html-CaZDVM9e.js b/assets/Authorization.html-J2At3l0r.js
      similarity index 98%
      rename from assets/Authorization.html-CaZDVM9e.js
      rename to assets/Authorization.html-J2At3l0r.js
      index 41f277b52..03bb501bc 100644
      --- a/assets/Authorization.html-CaZDVM9e.js
      +++ b/assets/Authorization.html-J2At3l0r.js
      @@ -1 +1 @@
      -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a as r,o as n}from"./app-LRWZkDAq.js";const o={};function a(c,t){return n(),i("div",null,t[0]||(t[0]=[r('

      1、权限说明

      认证(Authentication):登录操作就是最常见的认证方式,就是提供用户名和密码来证明自己是某个系统的合法用户,当用户没有经过认证去访问一个受保护资源时,应当响应401
      授权(Authorization):授权是检验用户是否有权限访问某个资源,比如普通用户是无法看到管理员界面的,当用户无权访问某个资源,应当响应403

      2、Security中负责权限校验的类结构图

      Security中权限类
      Security中权限类

      如上图,可以看到最底层有两个类,分别是FilterSecurityInterceptorMethodSecurityInterceptor,这两个类都是AbstractSecurityInterceptor的子类。
      其中FilterSecurityInterceptor还实现了Filter接口,它是一个SecurityFilter,是众多SecurityFilterChain过滤器中的一个,它处理认证问题,当用户访问未认证接口 会被此类拦截,抛出异常,返回401。

      MethodSecurityInterceptor是当程序即将调用Controller中方法之前调用,对应的它处理Controller层被使用了@PreAuthorize注解的方法,它用来校验当前用户是否有注解中 包含的权限,当前用户不包含对应权限时,会抛出异常返回403。

      ',6)]))}const l=e(o,[["render",a],["__file","Authorization.html.vue"]]),s=JSON.parse('{"path":"/java/framework/security/Authorization.html","title":"权限校验原理","lang":"zh-CN","frontmatter":{"title":"权限校验原理","date":"2022-12-20T00:00:00.000Z","category":["security"],"description":"1、权限说明 认证(Authentication):登录操作就是最常见的认证方式,就是提供用户名和密码来证明自己是某个系统的合法用户,当用户没有经过认证去访问一个受保护资源时,应当响应401 授权(Authorization):授权是检验用户是否有权限访问某个资源,比如普通用户是无法看到管理员界面的,当用户无权访问某个资源,应当响应403 2、Secu...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/security/Authorization.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"权限校验原理"}],["meta",{"property":"og:description","content":"1、权限说明 认证(Authentication):登录操作就是最常见的认证方式,就是提供用户名和密码来证明自己是某个系统的合法用户,当用户没有经过认证去访问一个受保护资源时,应当响应401 授权(Authorization):授权是检验用户是否有权限访问某个资源,比如普通用户是无法看到管理员界面的,当用户无权访问某个资源,应当响应403 2、Secu..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221220222603.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-01T09:56:25.000Z"}],["meta",{"property":"article:published_time","content":"2022-12-20T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-01T09:56:25.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"权限校验原理\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221220222603.png\\"],\\"datePublished\\":\\"2022-12-20T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-01T09:56:25.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、权限说明","slug":"_1、权限说明","link":"#_1、权限说明","children":[]},{"level":2,"title":"2、Security中负责权限校验的类结构图","slug":"_2、security中负责权限校验的类结构图","link":"#_2、security中负责权限校验的类结构图","children":[]}],"git":{"createdTime":1671546901000,"updatedTime":1730454985000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":1.07,"words":322},"filePathRelative":"java/framework/security/Authorization.md","localizedDate":"2022年12月20日","excerpt":"

      1、权限说明

      \\n

      认证(Authentication):登录操作就是最常见的认证方式,就是提供用户名和密码来证明自己是某个系统的合法用户,当用户没有经过认证去访问一个受保护资源时,应当响应401
      \\n授权(Authorization):授权是检验用户是否有权限访问某个资源,比如普通用户是无法看到管理员界面的,当用户无权访问某个资源,应当响应403

      \\n

      2、Security中负责权限校验的类结构图

      \\n
      \\"Security中权限类\\"
      Security中权限类
      ","autoDesc":true}');export{l as comp,s as data}; +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a as r,o as n}from"./app-BIdVr4mq.js";const o={};function a(c,t){return n(),i("div",null,t[0]||(t[0]=[r('

      1、权限说明

      认证(Authentication):登录操作就是最常见的认证方式,就是提供用户名和密码来证明自己是某个系统的合法用户,当用户没有经过认证去访问一个受保护资源时,应当响应401
      授权(Authorization):授权是检验用户是否有权限访问某个资源,比如普通用户是无法看到管理员界面的,当用户无权访问某个资源,应当响应403

      2、Security中负责权限校验的类结构图

      Security中权限类
      Security中权限类

      如上图,可以看到最底层有两个类,分别是FilterSecurityInterceptorMethodSecurityInterceptor,这两个类都是AbstractSecurityInterceptor的子类。
      其中FilterSecurityInterceptor还实现了Filter接口,它是一个SecurityFilter,是众多SecurityFilterChain过滤器中的一个,它处理认证问题,当用户访问未认证接口 会被此类拦截,抛出异常,返回401。

      MethodSecurityInterceptor是当程序即将调用Controller中方法之前调用,对应的它处理Controller层被使用了@PreAuthorize注解的方法,它用来校验当前用户是否有注解中 包含的权限,当前用户不包含对应权限时,会抛出异常返回403。

      ',6)]))}const l=e(o,[["render",a],["__file","Authorization.html.vue"]]),s=JSON.parse('{"path":"/java/framework/security/Authorization.html","title":"权限校验原理","lang":"zh-CN","frontmatter":{"title":"权限校验原理","date":"2022-12-20T00:00:00.000Z","category":["security"],"description":"1、权限说明 认证(Authentication):登录操作就是最常见的认证方式,就是提供用户名和密码来证明自己是某个系统的合法用户,当用户没有经过认证去访问一个受保护资源时,应当响应401 授权(Authorization):授权是检验用户是否有权限访问某个资源,比如普通用户是无法看到管理员界面的,当用户无权访问某个资源,应当响应403 2、Secu...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/security/Authorization.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"权限校验原理"}],["meta",{"property":"og:description","content":"1、权限说明 认证(Authentication):登录操作就是最常见的认证方式,就是提供用户名和密码来证明自己是某个系统的合法用户,当用户没有经过认证去访问一个受保护资源时,应当响应401 授权(Authorization):授权是检验用户是否有权限访问某个资源,比如普通用户是无法看到管理员界面的,当用户无权访问某个资源,应当响应403 2、Secu..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221220222603.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-01T09:56:25.000Z"}],["meta",{"property":"article:published_time","content":"2022-12-20T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-01T09:56:25.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"权限校验原理\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221220222603.png\\"],\\"datePublished\\":\\"2022-12-20T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-01T09:56:25.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、权限说明","slug":"_1、权限说明","link":"#_1、权限说明","children":[]},{"level":2,"title":"2、Security中负责权限校验的类结构图","slug":"_2、security中负责权限校验的类结构图","link":"#_2、security中负责权限校验的类结构图","children":[]}],"git":{"createdTime":1671546901000,"updatedTime":1730454985000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":1.07,"words":322},"filePathRelative":"java/framework/security/Authorization.md","localizedDate":"2022年12月20日","excerpt":"

      1、权限说明

      \\n

      认证(Authentication):登录操作就是最常见的认证方式,就是提供用户名和密码来证明自己是某个系统的合法用户,当用户没有经过认证去访问一个受保护资源时,应当响应401
      \\n授权(Authorization):授权是检验用户是否有权限访问某个资源,比如普通用户是无法看到管理员界面的,当用户无权访问某个资源,应当响应403

      \\n

      2、Security中负责权限校验的类结构图

      \\n
      \\"Security中权限类\\"
      Security中权限类
      ","autoDesc":true}');export{l as comp,s as data}; diff --git a/assets/BTree.html-CLTFqYVA.js b/assets/BTree.html-B6mC9nUD.js similarity index 97% rename from assets/BTree.html-CLTFqYVA.js rename to assets/BTree.html-B6mC9nUD.js index 4fd0d54ba..50c7a5dff 100644 --- a/assets/BTree.html-CLTFqYVA.js +++ b/assets/BTree.html-B6mC9nUD.js @@ -1 +1 @@ -import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,b as e,o}from"./app-LRWZkDAq.js";const i={};function r(c,t){return o(),a("div",null,t[0]||(t[0]=[e("h2",{id:"二叉树进化图",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#二叉树进化图"},[e("span",null,"二叉树进化图")])],-1),e("figure",null,[e("img",{src:"https://ddns.chensina.cn:29000/afatpig/blog/16181908373514.jpg",alt:"树结构大道",tabindex:"0",loading:"lazy"}),e("figcaption",null,"树结构大道")],-1)]))}const s=n(i,[["render",r],["__file","BTree.html.vue"]]),m=JSON.parse('{"path":"/other/essay/BTree.html","title":"二叉树","lang":"zh-CN","frontmatter":{"title":"二叉树","date":"2020-02-20T00:00:00.000Z","sticky":8,"tags":["数据结构","二叉树"],"description":"二叉树进化图 树结构大道树结构大道","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/essay/BTree.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"二叉树"}],["meta",{"property":"og:description","content":"二叉树进化图 树结构大道树结构大道"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/16181908373514.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:tag","content":"数据结构"}],["meta",{"property":"article:tag","content":"二叉树"}],["meta",{"property":"article:published_time","content":"2020-02-20T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"二叉树\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/16181908373514.jpg\\"],\\"datePublished\\":\\"2020-02-20T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"二叉树进化图","slug":"二叉树进化图","link":"#二叉树进化图","children":[]}],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.11,"words":32},"filePathRelative":"other/essay/BTree.md","localizedDate":"2020年2月20日","excerpt":"

      二叉树进化图

      \\n
      \\"树结构大道\\"
      树结构大道
      \\n","autoDesc":true}');export{s as comp,m as data}; +import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,b as e,o}from"./app-BIdVr4mq.js";const i={};function r(c,t){return o(),a("div",null,t[0]||(t[0]=[e("h2",{id:"二叉树进化图",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#二叉树进化图"},[e("span",null,"二叉树进化图")])],-1),e("figure",null,[e("img",{src:"https://ddns.chensina.cn:29000/afatpig/blog/16181908373514.jpg",alt:"树结构大道",tabindex:"0",loading:"lazy"}),e("figcaption",null,"树结构大道")],-1)]))}const s=n(i,[["render",r],["__file","BTree.html.vue"]]),m=JSON.parse('{"path":"/other/essay/BTree.html","title":"二叉树","lang":"zh-CN","frontmatter":{"title":"二叉树","date":"2020-02-20T00:00:00.000Z","sticky":8,"tags":["数据结构","二叉树"],"description":"二叉树进化图 树结构大道树结构大道","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/essay/BTree.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"二叉树"}],["meta",{"property":"og:description","content":"二叉树进化图 树结构大道树结构大道"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/16181908373514.jpg"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:tag","content":"数据结构"}],["meta",{"property":"article:tag","content":"二叉树"}],["meta",{"property":"article:published_time","content":"2020-02-20T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"二叉树\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/16181908373514.jpg\\"],\\"datePublished\\":\\"2020-02-20T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"二叉树进化图","slug":"二叉树进化图","link":"#二叉树进化图","children":[]}],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.11,"words":32},"filePathRelative":"other/essay/BTree.md","localizedDate":"2020年2月20日","excerpt":"

      二叉树进化图

      \\n
      \\"树结构大道\\"
      树结构大道
      \\n","autoDesc":true}');export{s as comp,m as data}; diff --git a/assets/BatchDeleteGitHubRepo.html-DsXldxOW.js b/assets/BatchDeleteGitHubRepo.html-i5eo4HNC.js similarity index 99% rename from assets/BatchDeleteGitHubRepo.html-DsXldxOW.js rename to assets/BatchDeleteGitHubRepo.html-i5eo4HNC.js index e8f26e671..71acf7bd2 100644 --- a/assets/BatchDeleteGitHubRepo.html-DsXldxOW.js +++ b/assets/BatchDeleteGitHubRepo.html-i5eo4HNC.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as t}from"./app-LRWZkDAq.js";const n={};function l(h,i){return t(),a("div",null,i[0]||(i[0]=[e(`

      背景

      github上fork了很多仓库,但是平时又没看,所以索性删除,一个个删除又很慢,所以搞个脚本批量删除

      方法

      1. 到github配置一个token,https://github.com/settings/tokens
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as t}from"./app-BIdVr4mq.js";const n={};function l(h,i){return t(),a("div",null,i[0]||(i[0]=[e(`

      背景

      github上fork了很多仓库,但是平时又没看,所以索性删除,一个个删除又很慢,所以搞个脚本批量删除

      方法

      1. 到github配置一个token,https://github.com/settings/tokens
       2. 准备一个文件放置待删除的仓库,每行一个仓库名
       3. 用脚本批量删除

      脚本:

      # 将 DELETE_KOKEN 和 GithubName 都替换为自己的
       DELETE_KOKEN="你的token"
      diff --git a/assets/BeanPostProcessor.html-B4kV_I9i.js b/assets/BeanPostProcessor.html-Dp0QSMvM.js
      similarity index 99%
      rename from assets/BeanPostProcessor.html-B4kV_I9i.js
      rename to assets/BeanPostProcessor.html-Dp0QSMvM.js
      index 246fd3605..2862ab596 100644
      --- a/assets/BeanPostProcessor.html-B4kV_I9i.js
      +++ b/assets/BeanPostProcessor.html-Dp0QSMvM.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-LRWZkDAq.js";const l={};function h(e,i){return t(),a("div",null,i[0]||(i[0]=[n(`

      1. BeanPostProcessor介绍

      打开源码里面有两个方法,分别是postProcessBeforeInitialization和postProcessAfterInitialization。

      
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-BIdVr4mq.js";const l={};function h(e,i){return t(),a("div",null,i[0]||(i[0]=[n(`

      1. BeanPostProcessor介绍

      打开源码里面有两个方法,分别是postProcessBeforeInitialization和postProcessAfterInitialization。

      
       public interface BeanPostProcessor {
       	@Nullable
       	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
      diff --git a/assets/BuildWebProject.html-C1h9cPZ9.js b/assets/BuildWebProject.html-iK6Db4et.js
      similarity index 99%
      rename from assets/BuildWebProject.html-C1h9cPZ9.js
      rename to assets/BuildWebProject.html-iK6Db4et.js
      index 471164ace..f95f61386 100644
      --- a/assets/BuildWebProject.html-C1h9cPZ9.js
      +++ b/assets/BuildWebProject.html-iK6Db4et.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const t={};function h(e,i){return l(),a("div",null,i[0]||(i[0]=[n(`

      温馨提示

      项目地址,每个节点的代码使用commitId作为区分,想看某个节点代码,直接还原到对应commitid即可,执行git reset --hard commitId

      1、后端篇

      1.1 初始化springboot项目

      git reset --hard 20e22c237e51fb9c7f01bdfd589a90f47fa73c34

      1.1.1 使用maven聚合模块以及parent依赖的方式初始化好了项目

      问题1
      分模块后,如何读取到其他模块中的bean,比如全局异常处理放在了common模块,在业务模块依赖了common,如何让common中的全局异常拦截生效?
      首先要明白无法common模块的component在core-biz不生效的原因是在biz模块默认扫描的component的包范围是启动类所在的包,也就是com.chensino.core,而全局异常类所在的包是com.chensino.common.security.exception,根本没有被扫描到。

      1
      1

      解决方法有三种
      参考此处文档

      1. 把扫描范围搞大一点
      package com.chensino.core;
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const t={};function h(e,i){return l(),a("div",null,i[0]||(i[0]=[n(`

      温馨提示

      项目地址,每个节点的代码使用commitId作为区分,想看某个节点代码,直接还原到对应commitid即可,执行git reset --hard commitId

      1、后端篇

      1.1 初始化springboot项目

      git reset --hard 20e22c237e51fb9c7f01bdfd589a90f47fa73c34

      1.1.1 使用maven聚合模块以及parent依赖的方式初始化好了项目

      问题1
      分模块后,如何读取到其他模块中的bean,比如全局异常处理放在了common模块,在业务模块依赖了common,如何让common中的全局异常拦截生效?
      首先要明白无法common模块的component在core-biz不生效的原因是在biz模块默认扫描的component的包范围是启动类所在的包,也就是com.chensino.core,而全局异常类所在的包是com.chensino.common.security.exception,根本没有被扫描到。

      1
      1

      解决方法有三种
      参考此处文档

      1. 把扫描范围搞大一点
      package com.chensino.core;
       
       import org.springframework.boot.SpringApplication;
       import org.springframework.boot.autoconfigure.SpringBootApplication;
      diff --git a/assets/CDN.html-BrUklWPJ.js b/assets/CDN.html-DEIMFq2d.js
      similarity index 99%
      rename from assets/CDN.html-BrUklWPJ.js
      rename to assets/CDN.html-DEIMFq2d.js
      index 7f039257c..f4516e926 100644
      --- a/assets/CDN.html-BrUklWPJ.js
      +++ b/assets/CDN.html-DEIMFq2d.js
      @@ -1 +1 @@
      -import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a as t,o as n}from"./app-LRWZkDAq.js";const s={};function p(c,e){return n(),i("div",null,e[0]||(e[0]=[t('

      1、什么是CDN?

      CDN(Content Delivery Network,内容分发网络)是构建在现有互联网基础之上的一层智能虚拟网络,通过在网络各处部署节点服务器,实现将源站内容分发至所有CDN节点,使用户可以就近获得所需的内容。CDN服务缩短了用户查看内容的访问延迟,提高了用户访问网站的响应速度与网站的可用性,解决了网络带宽小、用户访问量大、网点分布不均等问题。

      2、工作原理

      cdn原理

      当用户访问使用CDN服务的网站时,本地DNS服务器通过CNAME方式将最终域名请求重定向到CDN服务。CDN通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的CDN节点IP地址提供给用户,使用户可以以最快的速度获得网站内容。使用CDN后的HTTP请求处理流程如下。

      img
      img

      HTTP请求流程说明:

      1. 用户在浏览器输入要访问的网站域名www.example.com,向本地DNS发起域名解析请求。
      2. 本地DNS检查缓存中是否有www.example.com的IP地址记录。如果有,则直接返回给终端用户;如果没有,则向网站授权DNS查询。
      3. 网站DNS服务器解析发现域名已经解析到了CNAME:www.example.com.c.cdnhwc1.com。
      4. 请求被指向CDN服务。
      5. CDN对域名进行智能解析,将响应速度最快的CDN节点IP地址返回给本地DNS。
      6. 用户获取响应速度最快的CDN节点IP地址。
      7. 浏览器在得到最佳节点的IP地址以后,向CDN节点发出访问请求。
        • 如果该IP地址对应的节点已缓存该资源,节点将数据直接返回给用户,如图中步骤7和8,请求结束。
        • 如果该IP地址对应的节点未缓存该资源,节点回源请求资源。获取资源后,结合用户自定义配置的缓存策略,将资源缓存至节点,如图中的北京节点,并返回给用户,请求结束。

      3、CDN应用场景

      华为云关于CDN应用场景介绍

      3.1 网站加速

      适用于有加速需求的网站,包括门户网站、电商平台、资讯APP、UGC应用(User Generated Content,用户原创内容)等。CDN网络能够对加速域名下的静态内容提供良好的加速服务。支持自定义缓存规则,用户可以根据数据需求设置缓存过期时间,缓存格式包括但不限于zip、exe、wmv、gif、png、bmp、wma、rar、jpeg、jpg等。

      img
      img
      3.2 文件下载加速

      适用于使用http/https文件下载业务的网站、下载工具、游戏客户端、APP商店等。现在越来越多的新业务需要通过网络对客户端软件进行实时更新,包括APP更新,手游更新等,传统的下载类业务也需要支持更多的文件数量和更大的文件,如果所有的请求都通过源站服务器来处理,服务器和网络会成为很大的瓶颈,导致下载体验变差。使用CDN下载加速可以将下载量大的内容分发到各地的CDN节点,有效减轻源站的压力,同时保证了客户端高速下载的需求。

      img
      img
      3.3 点播加速

      适用于提供音视频点播服务的客户。例如:在线教育类网站、在线视频分享网站、互联网电视点播平台、音乐视频点播APP等。传统的点播服务会加大服务器的负载,并消耗巨大的带宽资源,同时又无法保证终端用户访问时需要的高速体验,CDN点播加速可以提供快速、稳定和安全的点播加速服务,通过分布在各个区域的CDN节点,将音视频内容扩展到距离用户较近的地方,随时随地为用户提供高品质的访问体验。

      点击放大
      点击放大
      3.4 全站加速

      适用于各行业动静态内容混合,含较多动态资源请求(如asp、jsp、php等格式的文件)的网站。全站加速融合了动态和静态加速,用户请求资源时,静态内容从边缘节点就近获取,动态内容通过动态加速技术智能选择较优路由回源获取。CDN全站加速有效提升动态页面的加载速度,避开网络拥堵路由,提高访问成功率,实现网站整体加速与实时优化。

      4、CDN加速原理

      当用户访问使用CDN服务的网站时,本地DNS服务器通过CNAME方式将最终域名请求重定向到CDN服务。CDN通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的CDN节点IP地址提供给用户,使用户可以以最快的速度获得网站内容。
      4.1 CDN节点无缓存场景
      CDN节点无缓存场景
      CDN节点无缓存场景

      HTTP请求流程说明:

      1、用户在浏览器输入要访问的网站域名,向本地DNS发起域名解析请求。

      2、域名解析的请求被发往网站授权DNS服务器。

      3、网站DNS服务器解析发现域名已经CNAME到了www.example.com.c.cdnhwc1.com。

      4、请求被指向CDN服务。

      5、CDN对域名进行智能解析,将响应速度最快的CDN节点IP地址返回给本地DNS。

      6、用户获取响应速度最快的CDN节点IP地址。

      7、浏览器在得到速度最快节点的IP地址以后,向CDN节点发出访问请求。

      8、CDN节点回源站拉取用户所需资源。(无缓存时,第一次请求要回源地址获取资源,因此第一次请求会感觉仍然慢

      9、将回源拉取的资源缓存至节点。

      10、将用户所需资源返回给用户。

      4.2 CDN节点有缓存场景
      CDN节点有缓存场景
      CDN节点有缓存场景

      HTTP请求流程说明:

      1、用户在浏览器输入要访问的网站域名,向本地DNS发起域名解析请求。

      2、域名解析的请求被发往网站授权DNS服务器。

      3、网站DNS服务器解析发现域名已经CNAME到了www.example.com.c.cdnhwc1.com。

      4、请求被指向CDN服务。

      5、CDN对域名进行智能解析,将响应速度最快的CDN节点IP地址返回给本地DNS。

      6、用户获取响应速度最快的CDN节点IP地址。

      7、浏览器在得到速度最快节点的IP地址以后,向CDN节点发出访问请求。

      8、CDN节点将用户所需资源返回给用户。

      5、如何使用华为云CDN?

      华为云CDN配置

      举例说明:我要开通加速的地址是h.sonoscape.com,是公司下的一个子域名,则开通cdn的步骤如下:

      1. 在华为云花钱开通CDN服务;

      2. 添加加速域名,就是把你要加速的域名到华为控制台配置以下,配置好后,它会根据你的域名生成一个CNAME,类似h.sonoscape.com.7bf9428.c.cdnhwc1.com这样;

      3. 配置CNAME解析,要看你的域名是哪里来的,这个h.sonoscape.com是从公司申请的,因此要找公司IT部门去配置一个CNAME,把h.sonoscape.com下配置一个cname,h.sonoscape.com.7bf9428.c.cdnhwc1.com,配置好一般两小时生效;

      4. 验证cname是否生效

        nslookup -qt=cname h.sonoscape.com

        如果回显CNAME,则表示CNAME配置已经生效,如下图:

        点击放大
        点击放大

      6、问题

      1. 为何第一次请求感觉CDN没用?

        正常,由于首次访问时,CDN未对源站的相关资源进行缓存,需要回源拉取资源。您可以在首次访问前,进行缓存预热,将访问频率高的资源预热到CDN。
      ',53)]))}const d=a(s,[["render",p],["__file","CDN.html.vue"]]),h=JSON.parse('{"path":"/other/essay/CDN.html","title":"CDN静态资源加速","lang":"zh-CN","frontmatter":{"title":"CDN静态资源加速","date":"2019-02-20T00:00:00.000Z","author":"John","sticky":8,"tag":["运维"],"description":"1、什么是CDN? 2、工作原理 cdn原理 当用户访问使用CDN服务的网站时,本地DNS服务器通过CNAME方式将最终域名请求重定向到CDN服务。CDN通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的CDN节点IP地址提供给用户,使用户可以以最快的速度获得网站内容。使用CDN后的HTTP请求处理流程如下。 i...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/essay/CDN.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"CDN静态资源加速"}],["meta",{"property":"og:description","content":"1、什么是CDN? 2、工作原理 cdn原理 当用户访问使用CDN服务的网站时,本地DNS服务器通过CNAME方式将最终域名请求重定向到CDN服务。CDN通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的CDN节点IP地址提供给用户,使用户可以以最快的速度获得网站内容。使用CDN后的HTTP请求处理流程如下。 i..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/zh-cn_image_0000001129063959.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"John"}],["meta",{"property":"article:tag","content":"运维"}],["meta",{"property":"article:published_time","content":"2019-02-20T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"CDN静态资源加速\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/zh-cn_image_0000001129063959.png\\",\\"https://support.huaweicloud.com/productdesc-cdn/zh-cn_image_0170805353.png\\",\\"https://support.huaweicloud.com/productdesc-cdn/zh-cn_image_0170805965.png\\",\\"https://support.huaweicloud.com/productdesc-cdn/zh-cn_image_0170806639.png\\",\\"https://res-static.hc-cdn.cn/SEO/CDN%E8%8A%82%E7%82%B9%E6%97%A0%E7%BC%93%E5%AD%98%E5%9C%BA%E6%99%AF.jpg\\",\\"https://res-static.hc-cdn.cn/SEO/CDN%E8%8A%82%E7%82%B9%E6%9C%89%E7%BC%93%E5%AD%98%E5%9C%BA%E6%99%AF.jpg\\",\\"https://support.huaweicloud.com/qs-cdn/zh-cn_image_0298980912.png\\"],\\"datePublished\\":\\"2019-02-20T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"John\\"}]}"]]},"headers":[],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":4},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":7.17,"words":2152},"filePathRelative":"other/essay/CDN.md","localizedDate":"2019年2月20日","excerpt":"

      1、什么是CDN?

      \\n
      CDN(Content Delivery Network,内容分发网络)是构建在现有互联网基础之上的一层智能虚拟网络,通过在网络各处部署节点服务器,实现将源站内容分发至所有CDN节点,使用户可以就近获得所需的内容。CDN服务缩短了用户查看内容的访问延迟,提高了用户访问网站的响应速度与网站的可用性,解决了网络带宽小、用户访问量大、网点分布不均等问题。
      \\n
      ","autoDesc":true}');export{d as comp,h as data}; +import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a as t,o as n}from"./app-BIdVr4mq.js";const s={};function p(c,e){return n(),i("div",null,e[0]||(e[0]=[t('

      1、什么是CDN?

      CDN(Content Delivery Network,内容分发网络)是构建在现有互联网基础之上的一层智能虚拟网络,通过在网络各处部署节点服务器,实现将源站内容分发至所有CDN节点,使用户可以就近获得所需的内容。CDN服务缩短了用户查看内容的访问延迟,提高了用户访问网站的响应速度与网站的可用性,解决了网络带宽小、用户访问量大、网点分布不均等问题。

      2、工作原理

      cdn原理

      当用户访问使用CDN服务的网站时,本地DNS服务器通过CNAME方式将最终域名请求重定向到CDN服务。CDN通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的CDN节点IP地址提供给用户,使用户可以以最快的速度获得网站内容。使用CDN后的HTTP请求处理流程如下。

      img
      img

      HTTP请求流程说明:

      1. 用户在浏览器输入要访问的网站域名www.example.com,向本地DNS发起域名解析请求。
      2. 本地DNS检查缓存中是否有www.example.com的IP地址记录。如果有,则直接返回给终端用户;如果没有,则向网站授权DNS查询。
      3. 网站DNS服务器解析发现域名已经解析到了CNAME:www.example.com.c.cdnhwc1.com。
      4. 请求被指向CDN服务。
      5. CDN对域名进行智能解析,将响应速度最快的CDN节点IP地址返回给本地DNS。
      6. 用户获取响应速度最快的CDN节点IP地址。
      7. 浏览器在得到最佳节点的IP地址以后,向CDN节点发出访问请求。
        • 如果该IP地址对应的节点已缓存该资源,节点将数据直接返回给用户,如图中步骤7和8,请求结束。
        • 如果该IP地址对应的节点未缓存该资源,节点回源请求资源。获取资源后,结合用户自定义配置的缓存策略,将资源缓存至节点,如图中的北京节点,并返回给用户,请求结束。

      3、CDN应用场景

      华为云关于CDN应用场景介绍

      3.1 网站加速

      适用于有加速需求的网站,包括门户网站、电商平台、资讯APP、UGC应用(User Generated Content,用户原创内容)等。CDN网络能够对加速域名下的静态内容提供良好的加速服务。支持自定义缓存规则,用户可以根据数据需求设置缓存过期时间,缓存格式包括但不限于zip、exe、wmv、gif、png、bmp、wma、rar、jpeg、jpg等。

      img
      img
      3.2 文件下载加速

      适用于使用http/https文件下载业务的网站、下载工具、游戏客户端、APP商店等。现在越来越多的新业务需要通过网络对客户端软件进行实时更新,包括APP更新,手游更新等,传统的下载类业务也需要支持更多的文件数量和更大的文件,如果所有的请求都通过源站服务器来处理,服务器和网络会成为很大的瓶颈,导致下载体验变差。使用CDN下载加速可以将下载量大的内容分发到各地的CDN节点,有效减轻源站的压力,同时保证了客户端高速下载的需求。

      img
      img
      3.3 点播加速

      适用于提供音视频点播服务的客户。例如:在线教育类网站、在线视频分享网站、互联网电视点播平台、音乐视频点播APP等。传统的点播服务会加大服务器的负载,并消耗巨大的带宽资源,同时又无法保证终端用户访问时需要的高速体验,CDN点播加速可以提供快速、稳定和安全的点播加速服务,通过分布在各个区域的CDN节点,将音视频内容扩展到距离用户较近的地方,随时随地为用户提供高品质的访问体验。

      点击放大
      点击放大
      3.4 全站加速

      适用于各行业动静态内容混合,含较多动态资源请求(如asp、jsp、php等格式的文件)的网站。全站加速融合了动态和静态加速,用户请求资源时,静态内容从边缘节点就近获取,动态内容通过动态加速技术智能选择较优路由回源获取。CDN全站加速有效提升动态页面的加载速度,避开网络拥堵路由,提高访问成功率,实现网站整体加速与实时优化。

      4、CDN加速原理

      当用户访问使用CDN服务的网站时,本地DNS服务器通过CNAME方式将最终域名请求重定向到CDN服务。CDN通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的CDN节点IP地址提供给用户,使用户可以以最快的速度获得网站内容。
      4.1 CDN节点无缓存场景
      CDN节点无缓存场景
      CDN节点无缓存场景

      HTTP请求流程说明:

      1、用户在浏览器输入要访问的网站域名,向本地DNS发起域名解析请求。

      2、域名解析的请求被发往网站授权DNS服务器。

      3、网站DNS服务器解析发现域名已经CNAME到了www.example.com.c.cdnhwc1.com。

      4、请求被指向CDN服务。

      5、CDN对域名进行智能解析,将响应速度最快的CDN节点IP地址返回给本地DNS。

      6、用户获取响应速度最快的CDN节点IP地址。

      7、浏览器在得到速度最快节点的IP地址以后,向CDN节点发出访问请求。

      8、CDN节点回源站拉取用户所需资源。(无缓存时,第一次请求要回源地址获取资源,因此第一次请求会感觉仍然慢

      9、将回源拉取的资源缓存至节点。

      10、将用户所需资源返回给用户。

      4.2 CDN节点有缓存场景
      CDN节点有缓存场景
      CDN节点有缓存场景

      HTTP请求流程说明:

      1、用户在浏览器输入要访问的网站域名,向本地DNS发起域名解析请求。

      2、域名解析的请求被发往网站授权DNS服务器。

      3、网站DNS服务器解析发现域名已经CNAME到了www.example.com.c.cdnhwc1.com。

      4、请求被指向CDN服务。

      5、CDN对域名进行智能解析,将响应速度最快的CDN节点IP地址返回给本地DNS。

      6、用户获取响应速度最快的CDN节点IP地址。

      7、浏览器在得到速度最快节点的IP地址以后,向CDN节点发出访问请求。

      8、CDN节点将用户所需资源返回给用户。

      5、如何使用华为云CDN?

      华为云CDN配置

      举例说明:我要开通加速的地址是h.sonoscape.com,是公司下的一个子域名,则开通cdn的步骤如下:

      1. 在华为云花钱开通CDN服务;

      2. 添加加速域名,就是把你要加速的域名到华为控制台配置以下,配置好后,它会根据你的域名生成一个CNAME,类似h.sonoscape.com.7bf9428.c.cdnhwc1.com这样;

      3. 配置CNAME解析,要看你的域名是哪里来的,这个h.sonoscape.com是从公司申请的,因此要找公司IT部门去配置一个CNAME,把h.sonoscape.com下配置一个cname,h.sonoscape.com.7bf9428.c.cdnhwc1.com,配置好一般两小时生效;

      4. 验证cname是否生效

        nslookup -qt=cname h.sonoscape.com

        如果回显CNAME,则表示CNAME配置已经生效,如下图:

        点击放大
        点击放大

      6、问题

      1. 为何第一次请求感觉CDN没用?

        正常,由于首次访问时,CDN未对源站的相关资源进行缓存,需要回源拉取资源。您可以在首次访问前,进行缓存预热,将访问频率高的资源预热到CDN。
      ',53)]))}const d=a(s,[["render",p],["__file","CDN.html.vue"]]),h=JSON.parse('{"path":"/other/essay/CDN.html","title":"CDN静态资源加速","lang":"zh-CN","frontmatter":{"title":"CDN静态资源加速","date":"2019-02-20T00:00:00.000Z","author":"John","sticky":8,"tag":["运维"],"description":"1、什么是CDN? 2、工作原理 cdn原理 当用户访问使用CDN服务的网站时,本地DNS服务器通过CNAME方式将最终域名请求重定向到CDN服务。CDN通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的CDN节点IP地址提供给用户,使用户可以以最快的速度获得网站内容。使用CDN后的HTTP请求处理流程如下。 i...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/essay/CDN.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"CDN静态资源加速"}],["meta",{"property":"og:description","content":"1、什么是CDN? 2、工作原理 cdn原理 当用户访问使用CDN服务的网站时,本地DNS服务器通过CNAME方式将最终域名请求重定向到CDN服务。CDN通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的CDN节点IP地址提供给用户,使用户可以以最快的速度获得网站内容。使用CDN后的HTTP请求处理流程如下。 i..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/zh-cn_image_0000001129063959.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"John"}],["meta",{"property":"article:tag","content":"运维"}],["meta",{"property":"article:published_time","content":"2019-02-20T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"CDN静态资源加速\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/zh-cn_image_0000001129063959.png\\",\\"https://support.huaweicloud.com/productdesc-cdn/zh-cn_image_0170805353.png\\",\\"https://support.huaweicloud.com/productdesc-cdn/zh-cn_image_0170805965.png\\",\\"https://support.huaweicloud.com/productdesc-cdn/zh-cn_image_0170806639.png\\",\\"https://res-static.hc-cdn.cn/SEO/CDN%E8%8A%82%E7%82%B9%E6%97%A0%E7%BC%93%E5%AD%98%E5%9C%BA%E6%99%AF.jpg\\",\\"https://res-static.hc-cdn.cn/SEO/CDN%E8%8A%82%E7%82%B9%E6%9C%89%E7%BC%93%E5%AD%98%E5%9C%BA%E6%99%AF.jpg\\",\\"https://support.huaweicloud.com/qs-cdn/zh-cn_image_0298980912.png\\"],\\"datePublished\\":\\"2019-02-20T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"John\\"}]}"]]},"headers":[],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":4},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":7.17,"words":2152},"filePathRelative":"other/essay/CDN.md","localizedDate":"2019年2月20日","excerpt":"

      1、什么是CDN?

      \\n
      CDN(Content Delivery Network,内容分发网络)是构建在现有互联网基础之上的一层智能虚拟网络,通过在网络各处部署节点服务器,实现将源站内容分发至所有CDN节点,使用户可以就近获得所需的内容。CDN服务缩短了用户查看内容的访问延迟,提高了用户访问网站的响应速度与网站的可用性,解决了网络带宽小、用户访问量大、网点分布不均等问题。
      \\n
      ","autoDesc":true}');export{d as comp,h as data}; diff --git a/assets/CPU.html-CrPccORk.js b/assets/CPU.html-BZbWMqTq.js similarity index 98% rename from assets/CPU.html-CrPccORk.js rename to assets/CPU.html-BZbWMqTq.js index 0384a3ee0..c238509bc 100644 --- a/assets/CPU.html-CrPccORk.js +++ b/assets/CPU.html-BZbWMqTq.js @@ -1 +1 @@ -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as l,b as t,d,o as r}from"./app-LRWZkDAq.js";const i={};function c(o,a){return r(),n("div",null,a[0]||(a[0]=[l('

      intel cpu型号

      官方说明

      cpu型号命名

      20230809094135
      20230809094135
      20230809094205
      20230809094205
      20230809094220
      20230809094220

      cpu 后缀

      ',7),t("table",{class:"table image-rendition-feature",disprows:"20"},[t("thead",null,[t("tr",null,[t("th",null,[t("b",null,"外形/功能类型/细分市场")]),t("th",null,[t("b",null,"后缀")]),t("th",null,[t("b",null,"优化/设计")])])]),t("tbody",null,[t("tr",{class:"data","data-category-id":""},[t("td",{rowspan:"5"},"台式机"),t("td",null,"K"),t("td",null,"高性能,未锁频")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"Φ"),t("td",null,"需要独立显卡")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"S"),t("td",null,"特别版")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"T"),t("td",null,"功耗优化生活方式")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"X/XE"),t("td",null,"最高性能,未锁频")]),t("tr",{class:"data","data-category-id":""},[t("td",{rowspan:"7"},"移动设备(笔记本电脑 2、2 合 1 电脑)"),t("td",null,"HX"),t("td",null,"最高性能,所有 SKU 未锁频")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"HK"),t("td",null,"高性能,未锁频")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"H"),t("td",null,"高性能")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"P"),t("td",null,"提供轻薄型设备所需的性能")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"U"),t("td",null,"能效更高")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"Y"),t("td",null,"功耗极低")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"G1-G7"),t("td",null,"显卡级别(采用较新集成显卡技术的处理器)")]),t("tr",{class:"data","data-category-id":""},[t("td",{rowspan:"5"},"嵌入式"),t("td",null,"E"),t("td",null,"嵌入式")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"UE"),t("td",null,"能效更高")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"HE"),t("td",null,"高性能")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"UL"),t("td",null,"高能效,采用 LGA 封装")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"HL"),t("td",null,"高性能,采用 LGA 封装")])])],-1),d(" git ")]))}const u=e(i,[["render",c],["__file","CPU.html.vue"]]),g=JSON.parse('{"path":"/other/hardware/CPU.html","title":"cpu介绍","lang":"zh-CN","frontmatter":{"title":"cpu介绍","date":"2022-08-09T00:00:00.000Z","isOriginal":true,"description":"intel cpu型号 官方说明 cpu型号命名 2023080909413520230809094135 2023080909420520230809094205 2023080909422020230809094220 cpu 后缀 git","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/hardware/CPU.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"cpu介绍"}],["meta",{"property":"og:description","content":"intel cpu型号 官方说明 cpu型号命名 2023080909413520230809094135 2023080909420520230809094205 2023080909422020230809094220 cpu 后缀 git"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20230809094135.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-08-09T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"cpu介绍\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20230809094135.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230809094205.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230809094220.png\\"],\\"datePublished\\":\\"2022-08-09T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"intel cpu型号","slug":"intel-cpu型号","link":"#intel-cpu型号","children":[{"level":3,"title":"cpu型号命名","slug":"cpu型号命名","link":"#cpu型号命名","children":[]},{"level":3,"title":"cpu 后缀","slug":"cpu-后缀","link":"#cpu-后缀","children":[]}]}],"git":{"createdTime":1694571353000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":5}]},"readingTime":{"minutes":1.46,"words":438},"filePathRelative":"other/hardware/CPU.md","localizedDate":"2022年8月9日","excerpt":"

      intel cpu型号

      \\n

      官方说明

      \\n

      cpu型号命名

      \\n
      \\"20230809094135\\"
      20230809094135
      ","autoDesc":true}');export{u as comp,g as data}; +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as l,b as t,d,o as r}from"./app-BIdVr4mq.js";const i={};function c(o,a){return r(),n("div",null,a[0]||(a[0]=[l('

      intel cpu型号

      官方说明

      cpu型号命名

      20230809094135
      20230809094135
      20230809094205
      20230809094205
      20230809094220
      20230809094220

      cpu 后缀

      ',7),t("table",{class:"table image-rendition-feature",disprows:"20"},[t("thead",null,[t("tr",null,[t("th",null,[t("b",null,"外形/功能类型/细分市场")]),t("th",null,[t("b",null,"后缀")]),t("th",null,[t("b",null,"优化/设计")])])]),t("tbody",null,[t("tr",{class:"data","data-category-id":""},[t("td",{rowspan:"5"},"台式机"),t("td",null,"K"),t("td",null,"高性能,未锁频")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"Φ"),t("td",null,"需要独立显卡")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"S"),t("td",null,"特别版")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"T"),t("td",null,"功耗优化生活方式")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"X/XE"),t("td",null,"最高性能,未锁频")]),t("tr",{class:"data","data-category-id":""},[t("td",{rowspan:"7"},"移动设备(笔记本电脑 2、2 合 1 电脑)"),t("td",null,"HX"),t("td",null,"最高性能,所有 SKU 未锁频")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"HK"),t("td",null,"高性能,未锁频")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"H"),t("td",null,"高性能")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"P"),t("td",null,"提供轻薄型设备所需的性能")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"U"),t("td",null,"能效更高")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"Y"),t("td",null,"功耗极低")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"G1-G7"),t("td",null,"显卡级别(采用较新集成显卡技术的处理器)")]),t("tr",{class:"data","data-category-id":""},[t("td",{rowspan:"5"},"嵌入式"),t("td",null,"E"),t("td",null,"嵌入式")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"UE"),t("td",null,"能效更高")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"HE"),t("td",null,"高性能")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"UL"),t("td",null,"高能效,采用 LGA 封装")]),t("tr",{class:"data","data-category-id":""},[t("td",null,"HL"),t("td",null,"高性能,采用 LGA 封装")])])],-1),d(" git ")]))}const u=e(i,[["render",c],["__file","CPU.html.vue"]]),g=JSON.parse('{"path":"/other/hardware/CPU.html","title":"cpu介绍","lang":"zh-CN","frontmatter":{"title":"cpu介绍","date":"2022-08-09T00:00:00.000Z","isOriginal":true,"description":"intel cpu型号 官方说明 cpu型号命名 2023080909413520230809094135 2023080909420520230809094205 2023080909422020230809094220 cpu 后缀 git","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/hardware/CPU.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"cpu介绍"}],["meta",{"property":"og:description","content":"intel cpu型号 官方说明 cpu型号命名 2023080909413520230809094135 2023080909420520230809094205 2023080909422020230809094220 cpu 后缀 git"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20230809094135.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-08-09T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"cpu介绍\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20230809094135.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230809094205.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230809094220.png\\"],\\"datePublished\\":\\"2022-08-09T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"intel cpu型号","slug":"intel-cpu型号","link":"#intel-cpu型号","children":[{"level":3,"title":"cpu型号命名","slug":"cpu型号命名","link":"#cpu型号命名","children":[]},{"level":3,"title":"cpu 后缀","slug":"cpu-后缀","link":"#cpu-后缀","children":[]}]}],"git":{"createdTime":1694571353000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":5}]},"readingTime":{"minutes":1.46,"words":438},"filePathRelative":"other/hardware/CPU.md","localizedDate":"2022年8月9日","excerpt":"

      intel cpu型号

      \\n

      官方说明

      \\n

      cpu型号命名

      \\n
      \\"20230809094135\\"
      20230809094135
      ","autoDesc":true}');export{u as comp,g as data}; diff --git a/assets/CPUOverLoad.html-BlwjFiRj.js b/assets/CPUOverLoad.html-BuVMUTre.js similarity index 99% rename from assets/CPUOverLoad.html-BlwjFiRj.js rename to assets/CPUOverLoad.html-BuVMUTre.js index bbe9817ff..272cfc42c 100644 --- a/assets/CPUOverLoad.html-BlwjFiRj.js +++ b/assets/CPUOverLoad.html-BuVMUTre.js @@ -1,4 +1,4 @@ -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as h,o as l}from"./app-LRWZkDAq.js";const k={};function n(t,s){return l(),a("div",null,s[0]||(s[0]=[h(`

      问题

      某天突然收到预警邮件,服务器CPU超过阈值,并且一直持续居高不下

      分析原因

      1. 使用htop查看资源消耗,按照CPU使用率降序排列,发现都是mysqld进程占用CPU很高
      2. 进入mysql命令行使用show processlist;查看当前正在执行的命令,经过多次执行show processlist发现有几条固定的sql一直在执行,并且每次传递的参数害不一样
      mysql> show processlist;
      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as h,o as l}from"./app-BIdVr4mq.js";const k={};function n(t,s){return l(),a("div",null,s[0]||(s[0]=[h(`

      问题

      某天突然收到预警邮件,服务器CPU超过阈值,并且一直持续居高不下

      分析原因

      1. 使用htop查看资源消耗,按照CPU使用率降序排列,发现都是mysqld进程占用CPU很高
      2. 进入mysql命令行使用show processlist;查看当前正在执行的命令,经过多次执行show processlist发现有几条固定的sql一直在执行,并且每次传递的参数害不一样
      mysql> show processlist;
       +--------+------+-------------------+--------------------------+---------+------+--------------+-----------------------------------------------------------------------------------------------+
       | Id     | User | Host              | db                       | Command | Time | State        | Info                                                                                          |
       +--------+------+-------------------+--------------------------+---------+------+--------------+-----------------------------------------------------------------------------------------------+
      diff --git a/assets/CentOS.html-BdaPB0Ti.js b/assets/CentOS.html-CvNBcUZo.js
      similarity index 98%
      rename from assets/CentOS.html-BdaPB0Ti.js
      rename to assets/CentOS.html-CvNBcUZo.js
      index 33bb8b568..91fc1c1ad 100644
      --- a/assets/CentOS.html-BdaPB0Ti.js
      +++ b/assets/CentOS.html-CvNBcUZo.js
      @@ -1 +1 @@
      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a,o as s}from"./app-LRWZkDAq.js";const n={};function h(l,e){return s(),t("div",null,e[0]||(e[0]=[a('

      1、安装JDK11

      sudo yum -y install java-11-openjdk java-11-openjdk-devel

      2、多版本JDK切换

      参考博客

      sudo alternatives --config javac
      ',5)]))}const o=i(n,[["render",h],["__file","CentOS.html.vue"]]),p=JSON.parse('{"path":"/other/linux/CentOS.html","title":"RedHat系","lang":"zh-CN","frontmatter":{"title":"RedHat系","date":"2022-08-10T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"description":"1、安装JDK11 2、多版本JDK切换 参考博客","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/linux/CentOS.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"RedHat系"}],["meta",{"property":"og:description","content":"1、安装JDK11 2、多版本JDK切换 参考博客"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-10-08T07:30:29.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2022-08-10T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-10-08T07:30:29.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"RedHat系\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-08-10T00:00:00.000Z\\",\\"dateModified\\":\\"2024-10-08T07:30:29.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、安装JDK11","slug":"_1、安装jdk11","link":"#_1、安装jdk11","children":[]},{"level":2,"title":"2、多版本JDK切换","slug":"_2、多版本jdk切换","link":"#_2、多版本jdk切换","children":[]}],"git":{"createdTime":1660113379000,"updatedTime":1728372629000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":0.18,"words":55},"filePathRelative":"other/linux/CentOS.md","localizedDate":"2022年8月10日","excerpt":"

      1、安装JDK11

      \\n
      sudo yum -y install java-11-openjdk java-11-openjdk-devel
      \\n
      ","autoDesc":true}');export{o as comp,p as data}; +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a,o as s}from"./app-BIdVr4mq.js";const n={};function h(l,e){return s(),t("div",null,e[0]||(e[0]=[a('

      1、安装JDK11

      sudo yum -y install java-11-openjdk java-11-openjdk-devel

      2、多版本JDK切换

      参考博客

      sudo alternatives --config javac
      ',5)]))}const o=i(n,[["render",h],["__file","CentOS.html.vue"]]),p=JSON.parse('{"path":"/other/linux/CentOS.html","title":"RedHat系","lang":"zh-CN","frontmatter":{"title":"RedHat系","date":"2022-08-10T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"description":"1、安装JDK11 2、多版本JDK切换 参考博客","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/linux/CentOS.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"RedHat系"}],["meta",{"property":"og:description","content":"1、安装JDK11 2、多版本JDK切换 参考博客"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-10-08T07:30:29.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2022-08-10T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-10-08T07:30:29.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"RedHat系\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-08-10T00:00:00.000Z\\",\\"dateModified\\":\\"2024-10-08T07:30:29.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、安装JDK11","slug":"_1、安装jdk11","link":"#_1、安装jdk11","children":[]},{"level":2,"title":"2、多版本JDK切换","slug":"_2、多版本jdk切换","link":"#_2、多版本jdk切换","children":[]}],"git":{"createdTime":1660113379000,"updatedTime":1728372629000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":0.18,"words":55},"filePathRelative":"other/linux/CentOS.md","localizedDate":"2022年8月10日","excerpt":"

      1、安装JDK11

      \\n
      sudo yum -y install java-11-openjdk java-11-openjdk-devel
      \\n
      ","autoDesc":true}');export{o as comp,p as data}; diff --git a/assets/ChromeDevTools.html-DIFEaEZF.js b/assets/ChromeDevTools.html-CzA-EmEC.js similarity index 97% rename from assets/ChromeDevTools.html-DIFEaEZF.js rename to assets/ChromeDevTools.html-CzA-EmEC.js index 01c4fc198..ed0016a94 100644 --- a/assets/ChromeDevTools.html-DIFEaEZF.js +++ b/assets/ChromeDevTools.html-CzA-EmEC.js @@ -1 +1 @@ -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as r,b as o,o as a}from"./app-LRWZkDAq.js";const m={};function n(l,e){return a(),r("div",null,e[0]||(e[0]=[o("p",null,"学会ChromeDevtool,高效率定位问题",-1),o("p",null,"https://developer.chrome.com/docs/devtools/evaluate-performance/",-1)]))}const s=t(m,[["render",n],["__file","ChromeDevTools.html.vue"]]),i=JSON.parse('{"path":"/other/essay/ChromeDevTools.html","title":"ChromeDevTools学习","lang":"zh-CN","frontmatter":{"title":"ChromeDevTools学习","date":"2021-06-20T00:00:00.000Z","author":"陈老师","tag":["工具使用"],"description":"学会ChromeDevtool,高效率定位问题 https://developer.chrome.com/docs/devtools/evaluate-performance/","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/essay/ChromeDevTools.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"ChromeDevTools学习"}],["meta",{"property":"og:description","content":"学会ChromeDevtool,高效率定位问题 https://developer.chrome.com/docs/devtools/evaluate-performance/"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"陈老师"}],["meta",{"property":"article:tag","content":"工具使用"}],["meta",{"property":"article:published_time","content":"2021-06-20T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"ChromeDevTools学习\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-06-20T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"陈老师\\"}]}"]]},"headers":[],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.1,"words":31},"filePathRelative":"other/essay/ChromeDevTools.md","localizedDate":"2021年6月20日","excerpt":"

      学会ChromeDevtool,高效率定位问题

      \\n\\n

      https://developer.chrome.com/docs/devtools/evaluate-performance/

      \\n","autoDesc":true}');export{s as comp,i as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as r,b as o,o as a}from"./app-BIdVr4mq.js";const m={};function n(l,e){return a(),r("div",null,e[0]||(e[0]=[o("p",null,"学会ChromeDevtool,高效率定位问题",-1),o("p",null,"https://developer.chrome.com/docs/devtools/evaluate-performance/",-1)]))}const s=t(m,[["render",n],["__file","ChromeDevTools.html.vue"]]),i=JSON.parse('{"path":"/other/essay/ChromeDevTools.html","title":"ChromeDevTools学习","lang":"zh-CN","frontmatter":{"title":"ChromeDevTools学习","date":"2021-06-20T00:00:00.000Z","author":"陈老师","tag":["工具使用"],"description":"学会ChromeDevtool,高效率定位问题 https://developer.chrome.com/docs/devtools/evaluate-performance/","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/essay/ChromeDevTools.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"ChromeDevTools学习"}],["meta",{"property":"og:description","content":"学会ChromeDevtool,高效率定位问题 https://developer.chrome.com/docs/devtools/evaluate-performance/"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"陈老师"}],["meta",{"property":"article:tag","content":"工具使用"}],["meta",{"property":"article:published_time","content":"2021-06-20T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"ChromeDevTools学习\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-06-20T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"陈老师\\"}]}"]]},"headers":[],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.1,"words":31},"filePathRelative":"other/essay/ChromeDevTools.md","localizedDate":"2021年6月20日","excerpt":"

      学会ChromeDevtool,高效率定位问题

      \\n\\n

      https://developer.chrome.com/docs/devtools/evaluate-performance/

      \\n","autoDesc":true}');export{s as comp,i as data}; diff --git a/assets/CircularDependency.html-CeopMWPo.js b/assets/CircularDependency.html-B8HCeRou.js similarity index 99% rename from assets/CircularDependency.html-CeopMWPo.js rename to assets/CircularDependency.html-B8HCeRou.js index f96a3630a..ab123b286 100644 --- a/assets/CircularDependency.html-CeopMWPo.js +++ b/assets/CircularDependency.html-B8HCeRou.js @@ -1,4 +1,4 @@ -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const e={};function t(h,s){return l(),a("div",null,s[0]||(s[0]=[n(`

      1、循环依赖的产生

      A依赖B,B也依赖于A

      public class A{
      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const e={};function t(h,s){return l(),a("div",null,s[0]||(s[0]=[n(`

      1、循环依赖的产生

      A依赖B,B也依赖于A

      public class A{
           private A a;
       
           public void setA(A a){
      diff --git a/assets/ClassLoader.html-CMN4swpH.js b/assets/ClassLoader.html-C7KNzZRW.js
      similarity index 99%
      rename from assets/ClassLoader.html-CMN4swpH.js
      rename to assets/ClassLoader.html-C7KNzZRW.js
      index f7a2f814c..433beb2b1 100644
      --- a/assets/ClassLoader.html-CMN4swpH.js
      +++ b/assets/ClassLoader.html-C7KNzZRW.js
      @@ -1,4 +1,4 @@
      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function k(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

      1、自定义类加载器

      自定义类加载器比较简单,只需要继承ClassLoader重写findClass方法即可,因为原ClassLoader中并未实现findClass方法,在方法体中直接抛出了ClassNotFoundException

      
      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function k(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

      1、自定义类加载器

      自定义类加载器比较简单,只需要继承ClassLoader重写findClass方法即可,因为原ClassLoader中并未实现findClass方法,在方法体中直接抛出了ClassNotFoundException

      
       import java.io.ByteArrayOutputStream;
       import java.io.File;
       import java.io.FileInputStream;
      diff --git a/assets/CloudService.html-CjT4ijCP.js b/assets/CloudService.html-VcDoz7-V.js
      similarity index 99%
      rename from assets/CloudService.html-CjT4ijCP.js
      rename to assets/CloudService.html-VcDoz7-V.js
      index d458d6770..4c35edb13 100644
      --- a/assets/CloudService.html-CjT4ijCP.js
      +++ b/assets/CloudService.html-VcDoz7-V.js
      @@ -1,4 +1,4 @@
      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-LRWZkDAq.js";const l={};function t(k,s){return h(),a("div",null,s[0]||(s[0]=[n(`

      1、我的云服务使用场景

      1. OBS用来存储图片,而CDN用来加速图片的访问速度;

      2. ECS服务器上有个导出pdf服务需要访问obs的图片;

      2、遇到的问题

      1. 一开始没有上cdn,用户反应慢,后来配置了cdn加速obs中的图片,这样第一次打开图片会把图片资源拉到就近的服务器,问题就是第一次打开依然慢;
      2. 导出PDF很慢,经常出现504Timeout

      3、问题分析

      先上cdn原理图,对照原理图来进行分析

      img
      img

      3.1 使用cdn 第一次访问慢原因分析

      image-20220620164944526
      image-20220620164944526

      以上是我的服务访问线路:

      1. 客户端通过加速域名obs.sonoscapecloud.com去请求图片,以下是我 在云服务管理台做的配置,使用obs.sonoscapecloud.com加速桶域名ccs.obs.ap-southeast-1.myhuaweicloud.com

        image-20220620163303675
        image-20220620163303675
      2. cdn通过cname去查找是否缓存过此文件,若没有缓存,则回源到原来的obs存储服务器,先把图片从obs服务器拉到就近的cdn服务器,比如我在武汉访问图片,可能就把图片拉到了华中区的服务器,缓存到华中区后,再把图片返回到客户端。


        第二步因为要回源拉取,所以第一次打开会慢点,第二次就直接就近到华中区去获取,就会快一些。这里有个地方要注意,虽然使用cdn后第一次拉取会稍慢,但是比不使用cdn还是快很多的,原因就是从obs源服务器把文件拉取到就近的cdn服务器应该是走的华为内网,所以速度相对来说还是要快一些,具体还要分析从源到cdn是否走了内网。

      3.2 解决第一次访问慢的问题

      防止第一次访问图片,数据回源,只需要在新增文件时,在新增接口添加缓存预热一下即可。具体的代码参考华为云提供的demo即可。

      华为云CDN缓存预热

      创建刷新预热

      private void preHeatingFile(String fileName) {
      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-BIdVr4mq.js";const l={};function t(k,s){return h(),a("div",null,s[0]||(s[0]=[n(`

      1、我的云服务使用场景

      1. OBS用来存储图片,而CDN用来加速图片的访问速度;

      2. ECS服务器上有个导出pdf服务需要访问obs的图片;

      2、遇到的问题

      1. 一开始没有上cdn,用户反应慢,后来配置了cdn加速obs中的图片,这样第一次打开图片会把图片资源拉到就近的服务器,问题就是第一次打开依然慢;
      2. 导出PDF很慢,经常出现504Timeout

      3、问题分析

      先上cdn原理图,对照原理图来进行分析

      img
      img

      3.1 使用cdn 第一次访问慢原因分析

      image-20220620164944526
      image-20220620164944526

      以上是我的服务访问线路:

      1. 客户端通过加速域名obs.sonoscapecloud.com去请求图片,以下是我 在云服务管理台做的配置,使用obs.sonoscapecloud.com加速桶域名ccs.obs.ap-southeast-1.myhuaweicloud.com

        image-20220620163303675
        image-20220620163303675
      2. cdn通过cname去查找是否缓存过此文件,若没有缓存,则回源到原来的obs存储服务器,先把图片从obs服务器拉到就近的cdn服务器,比如我在武汉访问图片,可能就把图片拉到了华中区的服务器,缓存到华中区后,再把图片返回到客户端。


        第二步因为要回源拉取,所以第一次打开会慢点,第二次就直接就近到华中区去获取,就会快一些。这里有个地方要注意,虽然使用cdn后第一次拉取会稍慢,但是比不使用cdn还是快很多的,原因就是从obs源服务器把文件拉取到就近的cdn服务器应该是走的华为内网,所以速度相对来说还是要快一些,具体还要分析从源到cdn是否走了内网。

      3.2 解决第一次访问慢的问题

      防止第一次访问图片,数据回源,只需要在新增文件时,在新增接口添加缓存预热一下即可。具体的代码参考华为云提供的demo即可。

      华为云CDN缓存预热

      创建刷新预热

      private void preHeatingFile(String fileName) {
       		ICredential auth = new GlobalCredentials()
       				.withAk(ossProperties.getAccessKey())
       				.withSk( ossProperties.getSecretKey());
      diff --git a/assets/CloudServiceTraining.html-BbBqNR5J.js b/assets/CloudServiceTraining.html-DFJMNjIC.js
      similarity index 99%
      rename from assets/CloudServiceTraining.html-BbBqNR5J.js
      rename to assets/CloudServiceTraining.html-DFJMNjIC.js
      index 5ec228482..bc2ab48ed 100644
      --- a/assets/CloudServiceTraining.html-BbBqNR5J.js
      +++ b/assets/CloudServiceTraining.html-DFJMNjIC.js
      @@ -1,4 +1,4 @@
      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a,o as e}from"./app-LRWZkDAq.js";const l={};function t(p,s){return e(),n("div",null,s[0]||(s[0]=[a(`

      [TOC]

      问题:

      1. 一个域名能解析出多少个IP?
      2. 请求一个地址,加www和不加是否有区别?

      一、DNS

      本章节参考DNS 原理入门——阮一峰

      1.1 什么是DNS

      DNS (Domain Name System 的缩写)的作用非常简单,就是根据域名查出IP地址。你可以把它想象成一本巨大的电话本。 举例来说,如果你要访问域名math.stackexchange.com,首先要通过DNS查出它的IP地址是151.101.129.69。

      1.1 A(Address)记录

      A (Address) 记录是用来指定主机名(或域名)对应的IP地址记录。用户可以将该域名下的网站服务器指向到自己的web server上。 同时也可以设置您域名的二级域名。一般情况下我们收的域名都指的是A记录,比如sonoscape.com,dccm.sonoscape.com,meet.sonoscape.com 都是A记录

      1.2 CNAME(Canonical Name)

      1.2.1 定义

      CName记录是Canonical Name的简称,通常称别名指向,CNAME记录可用于将一个域名别名为另一个规范名称的域名系统(DNS)资源记录。

      以下摘自国内某dns服务商文档

      如果需要将域名指向另一个域名,再由另一个域名提供 IP 地址,就需要添加 CNAME 记录,最常用到 CNAME 的场景包括做 CDN、做企业邮箱。

      1.2.2 参考博客

      Cname存在的意义cname解析过程

      注意
      CNAME只是一个相对的叫法,CNAME相对A记录叫CNAME,但是它本身也是一条A记录

      1.2.2 场景

      • CDN加速
      • 为特定网络服务(例如电子邮件或 FTP)提供单独的主机名,并将该主机名指向根域
      • 许多托管服务在服务提供商的域(例如 company.hostname.com)上为每个客户提供一个子域,并使用 CNAME 指向客户的域(www.company.com)。
      • 在多个国家注册同一个域并将国家版本指向主“.com”域
      • 从同一组织拥有的多个网站指向一个主网站 -
      • 用于 SSL 证书申请时的域名验证,例如 _dnsauth.yryz.net CNAME mnwwgx3uijnhsvkyjezf6nlpkn4xotzrkjpto6tfgbbuu22g.dcv.httpsauto.com.

      1.3 NS(Name Server)记录

      NS(Name Server)记录是域名服务器记录,用来指定该域名由哪个DNS服务器来进行解析。比如指定sonoscape.com的子域名具体由哪个服务器进行解析(参考后续dig指令)

      ns记录查询使用指令:

      $ dig ns com
      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a,o as e}from"./app-BIdVr4mq.js";const l={};function t(p,s){return e(),n("div",null,s[0]||(s[0]=[a(`

      [TOC]

      问题:

      1. 一个域名能解析出多少个IP?
      2. 请求一个地址,加www和不加是否有区别?

      一、DNS

      本章节参考DNS 原理入门——阮一峰

      1.1 什么是DNS

      DNS (Domain Name System 的缩写)的作用非常简单,就是根据域名查出IP地址。你可以把它想象成一本巨大的电话本。 举例来说,如果你要访问域名math.stackexchange.com,首先要通过DNS查出它的IP地址是151.101.129.69。

      1.1 A(Address)记录

      A (Address) 记录是用来指定主机名(或域名)对应的IP地址记录。用户可以将该域名下的网站服务器指向到自己的web server上。 同时也可以设置您域名的二级域名。一般情况下我们收的域名都指的是A记录,比如sonoscape.com,dccm.sonoscape.com,meet.sonoscape.com 都是A记录

      1.2 CNAME(Canonical Name)

      1.2.1 定义

      CName记录是Canonical Name的简称,通常称别名指向,CNAME记录可用于将一个域名别名为另一个规范名称的域名系统(DNS)资源记录。

      以下摘自国内某dns服务商文档

      如果需要将域名指向另一个域名,再由另一个域名提供 IP 地址,就需要添加 CNAME 记录,最常用到 CNAME 的场景包括做 CDN、做企业邮箱。

      1.2.2 参考博客

      Cname存在的意义cname解析过程

      注意
      CNAME只是一个相对的叫法,CNAME相对A记录叫CNAME,但是它本身也是一条A记录

      1.2.2 场景

      • CDN加速
      • 为特定网络服务(例如电子邮件或 FTP)提供单独的主机名,并将该主机名指向根域
      • 许多托管服务在服务提供商的域(例如 company.hostname.com)上为每个客户提供一个子域,并使用 CNAME 指向客户的域(www.company.com)。
      • 在多个国家注册同一个域并将国家版本指向主“.com”域
      • 从同一组织拥有的多个网站指向一个主网站 -
      • 用于 SSL 证书申请时的域名验证,例如 _dnsauth.yryz.net CNAME mnwwgx3uijnhsvkyjezf6nlpkn4xotzrkjpto6tfgbbuu22g.dcv.httpsauto.com.

      1.3 NS(Name Server)记录

      NS(Name Server)记录是域名服务器记录,用来指定该域名由哪个DNS服务器来进行解析。比如指定sonoscape.com的子域名具体由哪个服务器进行解析(参考后续dig指令)

      ns记录查询使用指令:

      $ dig ns com
       $ dig ns sonoscape.com

      示例:

      $ dig ns sonoscape.com.
       
       ; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7_9.5 <<>> ns sonoscape.com.
      diff --git a/assets/Collection.html-UjyrsFMx.js b/assets/Collection.html-DcRVdFY1.js
      similarity index 99%
      rename from assets/Collection.html-UjyrsFMx.js
      rename to assets/Collection.html-DcRVdFY1.js
      index c6605a21e..6b25e20d3 100644
      --- a/assets/Collection.html-UjyrsFMx.js
      +++ b/assets/Collection.html-DcRVdFY1.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const t={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

      分析集合数据结构,HashMap、ArrayList、LinkedList扩容原理等

      1、Queue 队列

      1.1 queue类图

      queue是java中的队列,可以实现队列特性,即:先进先出,先进先出这里就说明了要从队列中移除元素,只能从头部移除,因为要保证先进先出

      1.2 api介绍

      每个方法都有一个抛出异常(新增时空间不足、为空时获取元素),一个不抛出异常

      方法描述
      add向插入数据,继承自Collectoin,在容量已满的情况下,会抛出IllegalStateException异常
      offer在容量已满的情况下仅仅返回false不抛出异常
      remove移除元素、抛出异常
      poll移除元素、不抛出异常(返回null)
      elemtent不移除元素、抛出异常
      peek不移除元素、不抛异常(返回null)

      2、Deque (double ended queue)双向队列

      实现了Queue,在其基础上扩充了一些自定义方法以及栈

      2.1 栈方法

      push ,从栈顶(头)加入一个元素,pop从栈顶弹出一个元素。这里要注意理解栈的head和队列的head区别,因为栈是单向,可理解为一个桶,桶只有一个口,要取东西只能从口取。队列(有单向队列和双向队列)可理解为一个两边都是通的管道,单向队列只能从一个口进,另一个口出,双向则两个口都可进出。

      Stack中只有一个push方法以及pop方法,下图左边演示了为什么push和pop都是操作的head

      2.2、Dequeue的实现类LinkedList

      linkedlist中实现栈的push方法,可以看到实际就是在头部加了一个元素

         public void push(E e) {
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const t={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

      分析集合数据结构,HashMap、ArrayList、LinkedList扩容原理等

      1、Queue 队列

      1.1 queue类图

      queue是java中的队列,可以实现队列特性,即:先进先出,先进先出这里就说明了要从队列中移除元素,只能从头部移除,因为要保证先进先出

      1.2 api介绍

      每个方法都有一个抛出异常(新增时空间不足、为空时获取元素),一个不抛出异常

      方法描述
      add向插入数据,继承自Collectoin,在容量已满的情况下,会抛出IllegalStateException异常
      offer在容量已满的情况下仅仅返回false不抛出异常
      remove移除元素、抛出异常
      poll移除元素、不抛出异常(返回null)
      elemtent不移除元素、抛出异常
      peek不移除元素、不抛异常(返回null)

      2、Deque (double ended queue)双向队列

      实现了Queue,在其基础上扩充了一些自定义方法以及栈

      2.1 栈方法

      push ,从栈顶(头)加入一个元素,pop从栈顶弹出一个元素。这里要注意理解栈的head和队列的head区别,因为栈是单向,可理解为一个桶,桶只有一个口,要取东西只能从口取。队列(有单向队列和双向队列)可理解为一个两边都是通的管道,单向队列只能从一个口进,另一个口出,双向则两个口都可进出。

      Stack中只有一个push方法以及pop方法,下图左边演示了为什么push和pop都是操作的head

      2.2、Dequeue的实现类LinkedList

      linkedlist中实现栈的push方法,可以看到实际就是在头部加了一个元素

         public void push(E e) {
               addFirst(e);
           }

      linkedlist中实现栈的pop方法也是,直接移除的是第一个元素

      public E pop() {
           return removeFirst();
      diff --git a/assets/CollectionInject.html-BvWa61Sp.js b/assets/CollectionInject.html-FkTZBxrs.js
      similarity index 99%
      rename from assets/CollectionInject.html-BvWa61Sp.js
      rename to assets/CollectionInject.html-FkTZBxrs.js
      index 042f642d1..66ad21028 100644
      --- a/assets/CollectionInject.html-BvWa61Sp.js
      +++ b/assets/CollectionInject.html-FkTZBxrs.js
      @@ -1,4 +1,4 @@
      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const e={};function t(h,s){return l(),a("div",null,s[0]||(s[0]=[n(`

      1、测试

      加入有以下代码,MyProcessor是一个接口,没有提供任何实现,然后启动容器会发现执行Bean1的构造方法时并不会空指针,容器会自动提供一个Collection的实现类LinkedHashMap$LinkedValues,那么容器如何注入我自己的MyProcessor呢?

      @Component
      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const e={};function t(h,s){return l(),a("div",null,s[0]||(s[0]=[n(`

      1、测试

      加入有以下代码,MyProcessor是一个接口,没有提供任何实现,然后启动容器会发现执行Bean1的构造方法时并不会空指针,容器会自动提供一个Collection的实现类LinkedHashMap$LinkedValues,那么容器如何注入我自己的MyProcessor呢?

      @Component
       public class Bean1 {
       
           public Bean1(Collection<MyProcessor> processors){
      diff --git a/assets/CommonUsedCMD.html-CyjonT_t.js b/assets/CommonUsedCMD.html-tJHiFr9g.js
      similarity index 99%
      rename from assets/CommonUsedCMD.html-CyjonT_t.js
      rename to assets/CommonUsedCMD.html-tJHiFr9g.js
      index 10027b33e..c7921d233 100644
      --- a/assets/CommonUsedCMD.html-CyjonT_t.js
      +++ b/assets/CommonUsedCMD.html-tJHiFr9g.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as l}from"./app-LRWZkDAq.js";const n={};function t(h,i){return l(),a("div",null,i[0]||(i[0]=[e(`

      1、查找多个文件中是否包含字符串

      grep -r targetString targetDirectory
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as l}from"./app-BIdVr4mq.js";const n={};function t(h,i){return l(),a("div",null,i[0]||(i[0]=[e(`

      1、查找多个文件中是否包含字符串

      grep -r targetString targetDirectory
       # -r 表示递归查询
       # targetString  表示目标字符串
       # targetDirectory 表示目录

      更多功能: -r 是递归查找 -n 是显示行号 -R 查找所有文件包含子目录 -i 忽略大小写 xargs配合grep查找

      find -type f -name '*.php'|xargs grep 'message'

      2、高亮关键字

      在生产环境查看日志时高亮ERROR关键字,方便定位问题,这里利用linux管道加上perl在管道中替换

      tail -f xxx.log | perl -pe 's/(ERROR)/\\e[1;31m$1\\e[0m/g'

      参考:https://www.cnblogs.com/Detector/p/7246377.html

      3、查询大文件

      #查看当前路径下各目录占用空间大小
      diff --git a/assets/CompileJdk11.html-B-6Zabjd.js b/assets/CompileJdk11.html-DBmPA9mE.js
      similarity index 99%
      rename from assets/CompileJdk11.html-B-6Zabjd.js
      rename to assets/CompileJdk11.html-DBmPA9mE.js
      index 1a413567e..638b5b2a4 100644
      --- a/assets/CompileJdk11.html-B-6Zabjd.js
      +++ b/assets/CompileJdk11.html-DBmPA9mE.js
      @@ -1,4 +1,4 @@
      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function e(k,s){return l(),a("div",null,s[0]||(s[0]=[n(`

      1 下载源码

      网上根据关键词查找jdk源码,查找出来很多可以下载源码的链接,这里我们使用github去官方仓库,openjdk是托管在github的OpenJDK组织下,该组织下有各个版本的openjdk源码,不要直接使用jdk仓库,这个仓库存放的是当前正在开发的最新版本代码,我们要用的是jdk11,因此我们搜索jdk11仓库,我这里选择的是jdk11u这个库。

      20230113095229
      20230113095229
      git clone https://github.com/openjdk/jdk11u.git

      2 编译

      按照readme的文档进行编译

      20230113095821
      20230113095821

      2.1 configure

      首先执行进入源码目录,执行bash configure此命令会检查编译需要的环境,如果报错,根据错误提示安装必要的编译工具。

      注意事项:

      1. 安装必要的环境,包括gcc、autoconf、boot JDK等
      2. 编译一个jdk是需要依赖一个现有的jdk,另外对版本有要求,比如你编译的版本是N,则需要你电脑上有一个版本至少为N-1的版本,这里我编译jdk11时,我电脑事先安装了jdk11,理论上至少需要一个jdk10+
      3. gcc版本不能太老,也不能太新,这里一定要看你当前版本对应的文档,每个jdk版本对gcc版本要求也不一样,新版本jdk肯定能兼容更新的gcc,我在编译jdk11时,因为我的manjro是滚动更新,gcc版本是12,结果太新了导致编译报错。
      bash configure
      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function e(k,s){return l(),a("div",null,s[0]||(s[0]=[n(`

      1 下载源码

      网上根据关键词查找jdk源码,查找出来很多可以下载源码的链接,这里我们使用github去官方仓库,openjdk是托管在github的OpenJDK组织下,该组织下有各个版本的openjdk源码,不要直接使用jdk仓库,这个仓库存放的是当前正在开发的最新版本代码,我们要用的是jdk11,因此我们搜索jdk11仓库,我这里选择的是jdk11u这个库。

      20230113095229
      20230113095229
      git clone https://github.com/openjdk/jdk11u.git

      2 编译

      按照readme的文档进行编译

      20230113095821
      20230113095821

      2.1 configure

      首先执行进入源码目录,执行bash configure此命令会检查编译需要的环境,如果报错,根据错误提示安装必要的编译工具。

      注意事项:

      1. 安装必要的环境,包括gcc、autoconf、boot JDK等
      2. 编译一个jdk是需要依赖一个现有的jdk,另外对版本有要求,比如你编译的版本是N,则需要你电脑上有一个版本至少为N-1的版本,这里我编译jdk11时,我电脑事先安装了jdk11,理论上至少需要一个jdk10+
      3. gcc版本不能太老,也不能太新,这里一定要看你当前版本对应的文档,每个jdk版本对gcc版本要求也不一样,新版本jdk肯定能兼容更新的gcc,我在编译jdk11时,因为我的manjro是滚动更新,gcc版本是12,结果太新了导致编译报错。
      bash configure
       #……省略前边若干日志
       #……
       config.status: creating /home/chenkun/IdeaProjects/jdk11u/build/linux-x86_64-normal-server-release/buildjdk-spec.gmk
      diff --git a/assets/Concurrent.html-DRLYghtT.js b/assets/Concurrent.html-iguxRgDL.js
      similarity index 99%
      rename from assets/Concurrent.html-DRLYghtT.js
      rename to assets/Concurrent.html-iguxRgDL.js
      index cbabe5c86..92911729f 100644
      --- a/assets/Concurrent.html-DRLYghtT.js
      +++ b/assets/Concurrent.html-iguxRgDL.js
      @@ -1,4 +1,4 @@
      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as t,o as h}from"./app-LRWZkDAq.js";const n={};function l(k,s){return h(),a("div",null,s[0]||(s[0]=[t(`

      1、背景

      因没做过大项目,并发经验匮乏,所以很多时候考虑不到并发问题,今天做接口的压力测试,无意间发现一个并发的问题,就是判断数据库是否存在某个记录时,如果没做并发处理,就会出现并发的问题,比如以下代码,给某个医院添加科室,当医院已经有这个科室则不允许重复添加重名的科室

      public void addHospitalDept(SysHospitalDept sysHospitalDept) {
      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as t,o as h}from"./app-BIdVr4mq.js";const n={};function l(k,s){return h(),a("div",null,s[0]||(s[0]=[t(`

      1、背景

      因没做过大项目,并发经验匮乏,所以很多时候考虑不到并发问题,今天做接口的压力测试,无意间发现一个并发的问题,就是判断数据库是否存在某个记录时,如果没做并发处理,就会出现并发的问题,比如以下代码,给某个医院添加科室,当医院已经有这个科室则不允许重复添加重名的科室

      public void addHospitalDept(SysHospitalDept sysHospitalDept) {
               //根据医院id,科室名,校验科室唯一性
               SysHospitalDept hospitalDept = this.queryByHospitalIdAndDeptName(sysHospitalDept.getHospitalId(), sysHospitalDept.getDeptName());
               if (Objects.nonNull(hospitalDept)) {
      diff --git a/assets/ConstantPool.html-DqztC09o.js b/assets/ConstantPool.html-DQ4ql_mB.js
      similarity index 99%
      rename from assets/ConstantPool.html-DqztC09o.js
      rename to assets/ConstantPool.html-DQ4ql_mB.js
      index a42af7aac..a56cbf59b 100644
      --- a/assets/ConstantPool.html-DqztC09o.js
      +++ b/assets/ConstantPool.html-DQ4ql_mB.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-LRWZkDAq.js";const t={};function l(e,i){return h(),a("div",null,i[0]||(i[0]=[n(`

      1. Integer常量池默认的范围

      范围:[-128,127],Integer内部有个缓存池,最小值-128是固定的,最大的值127是可以调整的,看源码知道, 最大值是和integerCacheHighPropValue有关,这个值是可以通过~java.lang.Integer.IntegerCache.high~ 属性指定,实际测试~~~System.setProperty("java.lang.Integer.IntegerCache.high","300")~~~不生效, 因为他是jvm参数应该在启动时设置vm参数,-Djava.lang.Integer.IntegerCache.high=300 使用-XX:AutoBoxCacheMax=300也可以。

      private static class IntegerCache {
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-BIdVr4mq.js";const t={};function l(e,i){return h(),a("div",null,i[0]||(i[0]=[n(`

      1. Integer常量池默认的范围

      范围:[-128,127],Integer内部有个缓存池,最小值-128是固定的,最大的值127是可以调整的,看源码知道, 最大值是和integerCacheHighPropValue有关,这个值是可以通过~java.lang.Integer.IntegerCache.high~ 属性指定,实际测试~~~System.setProperty("java.lang.Integer.IntegerCache.high","300")~~~不生效, 因为他是jvm参数应该在启动时设置vm参数,-Djava.lang.Integer.IntegerCache.high=300 使用-XX:AutoBoxCacheMax=300也可以。

      private static class IntegerCache {
               static final int low = -128;
               static final int high;
               static final Integer cache[];
      diff --git a/assets/Cookie.html-CX4Rmbtp.js b/assets/Cookie.html-BtSuppuo.js
      similarity index 99%
      rename from assets/Cookie.html-CX4Rmbtp.js
      rename to assets/Cookie.html-BtSuppuo.js
      index 9200b3870..bedc28fda 100644
      --- a/assets/Cookie.html-CX4Rmbtp.js
      +++ b/assets/Cookie.html-BtSuppuo.js
      @@ -1 +1 @@
      -import{_ as o}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a as i,o as c}from"./app-LRWZkDAq.js";const n={};function t(p,a){return c(),e("div",null,a[0]||(a[0]=[i('

      Cookie的作用域domain

      一级域名:aaa.com 二级域名:bbb.aaa.com 三级域名:ccc.bbb.aaa.com

      如上例子: aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.bbb.aaa.com 的父域名; 反过来bbb.aaa.com 和 ccc.bbb.aaa.com 是 aaa.com 的子域名;ccc.bbb.aaa.com 是 bbb.aaa.com 的子域名

      划重点

      在当前域名下,只能设置当前域以及父域的cookie,不能设置子域下的cookie。例如我在浏览器访问后端服务的域名为bbb.aaa.com时,我在后端就只能把cookie的 域设置为当前域(缺省状态就是当前)或者设置为其父域名aaa.com,而不能设置为其子域名ccc.bbb.aaa.com,设置子域名前端SetCookie或有警告

      重点

      cookie挂载在某个域下,只有在此域名下或者此域名的子域下才能获取cookie。也就是说例如我当前在浏览器访问的域名为bbb.aaa.com,我只能看到当前域名下的cookie以及父域名aaa.com下的 cookie,而看不到子域名ccc.bbb.aaa.com下的cookie

      Cookie的path

      path和域差不多,默认情况下的path是/也就是域下所有路径都可以看到cookie,如果设置了path为/somepth则浏览器只有访问/somepath或者/somepath/***等这些地址才能看到

      实例

      域名可以看到当前域以及父域名下的cookie,看不到其子域名下的cookie

      20221027151137
      20221027151137
      20221027151245
      20221027151245

      设置了path,要同时满足url中有指定path才能看到

      20221027151447
      20221027151447
      20221027151508
      20221027151508

      浏览器请求时会自动携带其所有能看到的cookie发送到后端

      20221027153648
      20221027153648
      20221027153801
      20221027153801
      20221027153905
      20221027153905
      20221027153954
      20221027153954

      java中Session和Cookie交互

      以上图中可以看到,每次请求在请求头都会携带一个名字为JSESSIONID的COOKIE这个cookie的值是一个sessionId,也就是当前客户端和服务器交互的一个凭证, 客户端吧sessionid给了服务端,服务端就能找到对应session,有了session后,可以从session中获取到对应信息,比如用户信息。 20221027161215

      ',21)]))}const l=o(n,[["render",t],["__file","Cookie.html.vue"]]),h=JSON.parse('{"path":"/other/web/Cookie.html","title":"Cookie","lang":"zh-CN","frontmatter":{"title":"Cookie","date":"2021-10-25T16:57:01.000Z","author":"qianxun","tag":["必会"],"description":"Cookie的作用域domain 一级域名:aaa.com 二级域名:bbb.aaa.com 三级域名:ccc.bbb.aaa.com 如上例子: aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.bbb.aaa.com 的父域名; 反过来bbb.aaa.com 和 ccc.b...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/web/Cookie.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Cookie"}],["meta",{"property":"og:description","content":"Cookie的作用域domain 一级域名:aaa.com 二级域名:bbb.aaa.com 三级域名:ccc.bbb.aaa.com 如上例子: aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.bbb.aaa.com 的父域名; 反过来bbb.aaa.com 和 ccc.b..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221027151137.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"qianxun"}],["meta",{"property":"article:tag","content":"必会"}],["meta",{"property":"article:published_time","content":"2021-10-25T16:57:01.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Cookie\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027151137.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027151245.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027151447.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027151508.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027153648.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027153801.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027153905.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027153954.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027161215.png\\"],\\"datePublished\\":\\"2021-10-25T16:57:01.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"qianxun\\"}]}"]]},"headers":[{"level":2,"title":"Cookie的作用域domain","slug":"cookie的作用域domain","link":"#cookie的作用域domain","children":[]},{"level":2,"title":"Cookie的path","slug":"cookie的path","link":"#cookie的path","children":[]},{"level":2,"title":"实例","slug":"实例","link":"#实例","children":[{"level":3,"title":"域名可以看到当前域以及父域名下的cookie,看不到其子域名下的cookie","slug":"域名可以看到当前域以及父域名下的cookie-看不到其子域名下的cookie","link":"#域名可以看到当前域以及父域名下的cookie-看不到其子域名下的cookie","children":[]},{"level":3,"title":"设置了path,要同时满足url中有指定path才能看到","slug":"设置了path-要同时满足url中有指定path才能看到","link":"#设置了path-要同时满足url中有指定path才能看到","children":[]}]},{"level":2,"title":"浏览器请求时会自动携带其所有能看到的cookie发送到后端","slug":"浏览器请求时会自动携带其所有能看到的cookie发送到后端","link":"#浏览器请求时会自动携带其所有能看到的cookie发送到后端","children":[]},{"level":2,"title":"java中Session和Cookie交互","slug":"java中session和cookie交互","link":"#java中session和cookie交互","children":[]}],"git":{"createdTime":1666854933000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":3}]},"readingTime":{"minutes":1.81,"words":542},"filePathRelative":"other/web/Cookie.md","localizedDate":"2021年10月25日","excerpt":"

      Cookie的作用域domain

      \\n

      一级域名:aaa.com\\n二级域名:bbb.aaa.com\\n三级域名:ccc.bbb.aaa.com

      \\n

      如上例子:\\naaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.bbb.aaa.com 的父域名;\\n反过来bbb.aaa.com 和 ccc.bbb.aaa.com 是 aaa.com 的子域名;ccc.bbb.aaa.com 是 bbb.aaa.com 的子域名

      \\n
      \\n

      划重点

      \\n

      在当前域名下,只能设置当前域以及父域的cookie,不能设置子域下的cookie。例如我在浏览器访问后端服务的域名为bbb.aaa.com时,我在后端就只能把cookie的\\n域设置为当前域(缺省状态就是当前)或者设置为其父域名aaa.com,而不能设置为其子域名ccc.bbb.aaa.com,设置子域名前端SetCookie或有警告

      \\n
      ","autoDesc":true}');export{l as comp,h as data}; +import{_ as o}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a as i,o as c}from"./app-BIdVr4mq.js";const n={};function t(p,a){return c(),e("div",null,a[0]||(a[0]=[i('

      Cookie的作用域domain

      一级域名:aaa.com 二级域名:bbb.aaa.com 三级域名:ccc.bbb.aaa.com

      如上例子: aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.bbb.aaa.com 的父域名; 反过来bbb.aaa.com 和 ccc.bbb.aaa.com 是 aaa.com 的子域名;ccc.bbb.aaa.com 是 bbb.aaa.com 的子域名

      划重点

      在当前域名下,只能设置当前域以及父域的cookie,不能设置子域下的cookie。例如我在浏览器访问后端服务的域名为bbb.aaa.com时,我在后端就只能把cookie的 域设置为当前域(缺省状态就是当前)或者设置为其父域名aaa.com,而不能设置为其子域名ccc.bbb.aaa.com,设置子域名前端SetCookie或有警告

      重点

      cookie挂载在某个域下,只有在此域名下或者此域名的子域下才能获取cookie。也就是说例如我当前在浏览器访问的域名为bbb.aaa.com,我只能看到当前域名下的cookie以及父域名aaa.com下的 cookie,而看不到子域名ccc.bbb.aaa.com下的cookie

      Cookie的path

      path和域差不多,默认情况下的path是/也就是域下所有路径都可以看到cookie,如果设置了path为/somepth则浏览器只有访问/somepath或者/somepath/***等这些地址才能看到

      实例

      域名可以看到当前域以及父域名下的cookie,看不到其子域名下的cookie

      20221027151137
      20221027151137
      20221027151245
      20221027151245

      设置了path,要同时满足url中有指定path才能看到

      20221027151447
      20221027151447
      20221027151508
      20221027151508

      浏览器请求时会自动携带其所有能看到的cookie发送到后端

      20221027153648
      20221027153648
      20221027153801
      20221027153801
      20221027153905
      20221027153905
      20221027153954
      20221027153954

      java中Session和Cookie交互

      以上图中可以看到,每次请求在请求头都会携带一个名字为JSESSIONID的COOKIE这个cookie的值是一个sessionId,也就是当前客户端和服务器交互的一个凭证, 客户端吧sessionid给了服务端,服务端就能找到对应session,有了session后,可以从session中获取到对应信息,比如用户信息。 20221027161215

      ',21)]))}const l=o(n,[["render",t],["__file","Cookie.html.vue"]]),h=JSON.parse('{"path":"/other/web/Cookie.html","title":"Cookie","lang":"zh-CN","frontmatter":{"title":"Cookie","date":"2021-10-25T16:57:01.000Z","author":"qianxun","tag":["必会"],"description":"Cookie的作用域domain 一级域名:aaa.com 二级域名:bbb.aaa.com 三级域名:ccc.bbb.aaa.com 如上例子: aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.bbb.aaa.com 的父域名; 反过来bbb.aaa.com 和 ccc.b...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/web/Cookie.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Cookie"}],["meta",{"property":"og:description","content":"Cookie的作用域domain 一级域名:aaa.com 二级域名:bbb.aaa.com 三级域名:ccc.bbb.aaa.com 如上例子: aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.bbb.aaa.com 的父域名; 反过来bbb.aaa.com 和 ccc.b..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221027151137.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"qianxun"}],["meta",{"property":"article:tag","content":"必会"}],["meta",{"property":"article:published_time","content":"2021-10-25T16:57:01.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Cookie\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027151137.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027151245.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027151447.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027151508.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027153648.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027153801.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027153905.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027153954.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221027161215.png\\"],\\"datePublished\\":\\"2021-10-25T16:57:01.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"qianxun\\"}]}"]]},"headers":[{"level":2,"title":"Cookie的作用域domain","slug":"cookie的作用域domain","link":"#cookie的作用域domain","children":[]},{"level":2,"title":"Cookie的path","slug":"cookie的path","link":"#cookie的path","children":[]},{"level":2,"title":"实例","slug":"实例","link":"#实例","children":[{"level":3,"title":"域名可以看到当前域以及父域名下的cookie,看不到其子域名下的cookie","slug":"域名可以看到当前域以及父域名下的cookie-看不到其子域名下的cookie","link":"#域名可以看到当前域以及父域名下的cookie-看不到其子域名下的cookie","children":[]},{"level":3,"title":"设置了path,要同时满足url中有指定path才能看到","slug":"设置了path-要同时满足url中有指定path才能看到","link":"#设置了path-要同时满足url中有指定path才能看到","children":[]}]},{"level":2,"title":"浏览器请求时会自动携带其所有能看到的cookie发送到后端","slug":"浏览器请求时会自动携带其所有能看到的cookie发送到后端","link":"#浏览器请求时会自动携带其所有能看到的cookie发送到后端","children":[]},{"level":2,"title":"java中Session和Cookie交互","slug":"java中session和cookie交互","link":"#java中session和cookie交互","children":[]}],"git":{"createdTime":1666854933000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":3}]},"readingTime":{"minutes":1.81,"words":542},"filePathRelative":"other/web/Cookie.md","localizedDate":"2021年10月25日","excerpt":"

      Cookie的作用域domain

      \\n

      一级域名:aaa.com\\n二级域名:bbb.aaa.com\\n三级域名:ccc.bbb.aaa.com

      \\n

      如上例子:\\naaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.bbb.aaa.com 的父域名;\\n反过来bbb.aaa.com 和 ccc.bbb.aaa.com 是 aaa.com 的子域名;ccc.bbb.aaa.com 是 bbb.aaa.com 的子域名

      \\n
      \\n

      划重点

      \\n

      在当前域名下,只能设置当前域以及父域的cookie,不能设置子域下的cookie。例如我在浏览器访问后端服务的域名为bbb.aaa.com时,我在后端就只能把cookie的\\n域设置为当前域(缺省状态就是当前)或者设置为其父域名aaa.com,而不能设置为其子域名ccc.bbb.aaa.com,设置子域名前端SetCookie或有警告

      \\n
      ","autoDesc":true}');export{l as comp,h as data}; diff --git a/assets/Curl.html-Bmo3iaqW.js b/assets/Curl.html-7Jrb-SD3.js similarity index 99% rename from assets/Curl.html-Bmo3iaqW.js rename to assets/Curl.html-7Jrb-SD3.js index 880357dbb..07372a404 100644 --- a/assets/Curl.html-Bmo3iaqW.js +++ b/assets/Curl.html-7Jrb-SD3.js @@ -1,4 +1,4 @@ -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const l={};function t(h,s){return e(),a("div",null,s[0]||(s[0]=[n(`

      1、使用CURL分析接口请求耗时

      在服务器上发送请求一般用四字命令curl,本博客记录一下如何用curl测试接口耗时

      1.1 构造curl命令

      浏览器提供了快速构建各种curl请求的方式,直接复制,有需要再编辑即可 构造curl

      1.2、分析耗时

      使用curl的-w选项,其手册如下:

      -w, --write-out <format>
      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const l={};function t(h,s){return e(),a("div",null,s[0]||(s[0]=[n(`

      1、使用CURL分析接口请求耗时

      在服务器上发送请求一般用四字命令curl,本博客记录一下如何用curl测试接口耗时

      1.1 构造curl命令

      浏览器提供了快速构建各种curl请求的方式,直接复制,有需要再编辑即可 构造curl

      1.2、分析耗时

      使用curl的-w选项,其手册如下:

      -w, --write-out <format>
                     Defines  what  to  display  on  stdout after a completed and successful operation. The format is a string that may contain plain text mixed with any number of variables. The string can be
                     specified as "string", to get read from a particular file you specify it "@filename" and to tell curl to read the format from stdin you write "@-".
       
      diff --git a/assets/CustomAuthenticationProvider.html-C5viGtaI.js b/assets/CustomAuthenticationProvider.html-BPtSrpyO.js
      similarity index 99%
      rename from assets/CustomAuthenticationProvider.html-C5viGtaI.js
      rename to assets/CustomAuthenticationProvider.html-BPtSrpyO.js
      index 754029441..5b5079229 100644
      --- a/assets/CustomAuthenticationProvider.html-C5viGtaI.js
      +++ b/assets/CustomAuthenticationProvider.html-BPtSrpyO.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-LRWZkDAq.js";const l={};function h(e,i){return t(),a("div",null,i[0]||(i[0]=[n(`

      1、需求

      前后分离项目使用不同登录方式进行登录
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-BIdVr4mq.js";const l={};function h(e,i){return t(),a("div",null,i[0]||(i[0]=[n(`

      1、需求

      前后分离项目使用不同登录方式进行登录
           1. 使用帐号/密码登录
           2. 使用手机号/验证码登录

      2、实现方法

      Security是一个扩展性很强的框架,预留了各种端点进行扩展,多种方式登录需要扩展AuthenticationProvider,进行自定义实现。默认情况 Security使用的是DAOAuthenticationProvider,就是从数据库中读取用户名/密码进行校验。

      2.1 自定义AuthenticationProvider

      思考

      自定义了AuthenticationProvider后为什么连AuthenticationToken也要自定义?
       为什么不直接用UsernamePasswordAuthenticationToken?
      diff --git a/assets/CustomLRU.html-BdYIfH0T.js b/assets/CustomLRU.html-DerU6oev.js
      similarity index 99%
      rename from assets/CustomLRU.html-BdYIfH0T.js
      rename to assets/CustomLRU.html-DerU6oev.js
      index 068675cce..4daf59a6b 100644
      --- a/assets/CustomLRU.html-BdYIfH0T.js
      +++ b/assets/CustomLRU.html-DerU6oev.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-LRWZkDAq.js";const l={};function t(k,i){return h(),a("div",null,i[0]||(i[0]=[n(`

      LRU介绍

      lru(latest recently used)最近最少使用,在缓存中可以使用LRU算法移除最近最少使用的

      自定义lru算法

      在java中LinkedHashMap已经实现了LRU算法,在使用时只需要继承此类,然后重写removeEldestEntry方法即可

      public class MyLRU<K, V> extends LinkedHashMap<K, V> {
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-BIdVr4mq.js";const l={};function t(k,i){return h(),a("div",null,i[0]||(i[0]=[n(`

      LRU介绍

      lru(latest recently used)最近最少使用,在缓存中可以使用LRU算法移除最近最少使用的

      自定义lru算法

      在java中LinkedHashMap已经实现了LRU算法,在使用时只需要继承此类,然后重写removeEldestEntry方法即可

      public class MyLRU<K, V> extends LinkedHashMap<K, V> {
       
           private int cacheCount;
       
      diff --git a/assets/CustomLoginPage.html-OUdaxTEw.js b/assets/CustomLoginPage.html-C7rOlHXp.js
      similarity index 99%
      rename from assets/CustomLoginPage.html-OUdaxTEw.js
      rename to assets/CustomLoginPage.html-C7rOlHXp.js
      index 63cceddc9..6597b982c 100644
      --- a/assets/CustomLoginPage.html-OUdaxTEw.js
      +++ b/assets/CustomLoginPage.html-C7rOlHXp.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-LRWZkDAq.js";const l={};function h(k,i){return t(),a("div",null,i[0]||(i[0]=[n(`

      注意

      本篇博客是用来配置前后不分离的项目,正常情况下现在的项目都是前后分离了,因此本篇内容 并没有太多学习价值,但是网上大多数教程都特别喜欢讲这一部分内容,就我目前了解到的内容, 在搭建oauth2授权服务器可能会用到,因为授权服务器需要一个登录页面,这个页面可以单独放到后端,仅仅做个登录没有必要开一个前端项目.

      1、修改自定义的登陆页面以及登陆请求校验

      官方文档

      https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html

      1.1 Secutity配置

      在Security配置文件配置loginPage指定登陆的页面,loginProcessingUrl指定用户名密码认证处理地址,同时一定要放行这两个页面,否则会一直被拦截导致重定向到登陆页面。 另外,还要准备自定义页面,以及自定义处理接口。

      注意

      一般情况,无需自定义登陆处理逻辑,只需要修改登陆页面,在登陆页面把action保留原来的login即可

      protected void configure(HttpSecurity http) {
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-BIdVr4mq.js";const l={};function h(k,i){return t(),a("div",null,i[0]||(i[0]=[n(`

      注意

      本篇博客是用来配置前后不分离的项目,正常情况下现在的项目都是前后分离了,因此本篇内容 并没有太多学习价值,但是网上大多数教程都特别喜欢讲这一部分内容,就我目前了解到的内容, 在搭建oauth2授权服务器可能会用到,因为授权服务器需要一个登录页面,这个页面可以单独放到后端,仅仅做个登录没有必要开一个前端项目.

      1、修改自定义的登陆页面以及登陆请求校验

      官方文档

      https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html

      1.1 Secutity配置

      在Security配置文件配置loginPage指定登陆的页面,loginProcessingUrl指定用户名密码认证处理地址,同时一定要放行这两个页面,否则会一直被拦截导致重定向到登陆页面。 另外,还要准备自定义页面,以及自定义处理接口。

      注意

      一般情况,无需自定义登陆处理逻辑,只需要修改登陆页面,在登陆页面把action保留原来的login即可

      protected void configure(HttpSecurity http) {
           http.formLogin().loginPage("/token/login").loginProcessingUrl("/token/custom")
                   .successHandler(tenantSavedRequestAwareAuthenticationSuccessHandler())
                   .failureHandler(authenticationFailureHandler()).and().logout()
      diff --git a/assets/CustomTokenAuthentication.html-phBOu6dq.js b/assets/CustomTokenAuthentication.html-D2kx6W0O.js
      similarity index 99%
      rename from assets/CustomTokenAuthentication.html-phBOu6dq.js
      rename to assets/CustomTokenAuthentication.html-D2kx6W0O.js
      index 370bc3ae4..a16a3fb5b 100644
      --- a/assets/CustomTokenAuthentication.html-phBOu6dq.js
      +++ b/assets/CustomTokenAuthentication.html-D2kx6W0O.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const t={};function h(e,i){return l(),a("div",null,i[0]||(i[0]=[n(`

      注意

      网上很难精准找到一个前后端分离项目自定义Token认证的教程,找了很久终于找到,特此记录

      1、Security配置注意事项

      1. 我在很多教程中都看到他们有讲解自定义登录页面,但是我想说的是,都21世纪了,早都前后分离了,
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const t={};function h(e,i){return l(),a("div",null,i[0]||(i[0]=[n(`

      注意

      网上很难精准找到一个前后端分离项目自定义Token认证的教程,找了很久终于找到,特此记录

      1、Security配置注意事项

      1. 我在很多教程中都看到他们有讲解自定义登录页面,但是我想说的是,都21世纪了,早都前后分离了,
          所以在Security配置中不要配置formLogin了,前后分离项目会直接在Controller自定义登录逻辑,
          一旦配置这个万一Security会自动生成表单登录那几个过滤器。这里重点强调一下,前后分离项目和前
          后不分离在Security中是大大的不一样!!大家学习的时候关键词检索要记得加上前后分离!!
      diff --git a/assets/DelegatingFilterProxy.html-CjzmnpRV.js b/assets/DelegatingFilterProxy.html-DFpCf46S.js
      similarity index 99%
      rename from assets/DelegatingFilterProxy.html-CjzmnpRV.js
      rename to assets/DelegatingFilterProxy.html-DFpCf46S.js
      index 5f5120c2e..34a1ec5a3 100644
      --- a/assets/DelegatingFilterProxy.html-CjzmnpRV.js
      +++ b/assets/DelegatingFilterProxy.html-DFpCf46S.js
      @@ -1 +1 @@
      -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a as r,o as n}from"./app-LRWZkDAq.js";const l={};function a(o,e){return n(),i("div",null,e[0]||(e[0]=[r('

      作用

      在典型的 Spring 应用中,ContextLoaderListener 通常用于加载 Spring 的应用上下文(ApplicationContext),并管理其中定义的 Spring Bean。然而,Servlet 容器在启动时,会优先初始化过滤器(Filter)实例,而此时 Spring 上下文可能还没有完全加载。

      具体解析:

      1. ContextLoaderListener:
      • ContextLoaderListener 是 Spring 中常见的监听器,负责在应用启动时加载 Spring 的 ApplicationContext,从而初始化应用中的所有 Spring Bean。这通常是在应用启动的较晚阶段完成的。
      • ContextLoaderListener 会监听 Web 应用的启动事件,并在合适的时机加载和管理 Spring 上下文。
      1. Servlet 过滤器的加载顺序:
      • 在典型的 Servlet 容器(如 Tomcat)中,过滤器的实例化和注册会在应用启动的早期阶段进行,即在应用的主业务逻辑启动之前。
      • 这些过滤器的初始化是 Servlet 容器的一部分,不依赖于 Spring 的 ApplicationContext。换句话说,过滤器的生命周期由 Servlet 容器本身管理,而不受 Spring 管理的影响。
      1. 问题所在:
      • 因为过滤器(Filter)的实例需要在 Spring 上下文加载之前注册,而 Spring 的 Bean 管理是由 ContextLoaderListener 在较后阶段完成的,这就导致了过滤器实例在创建时无法直接依赖于 Spring 的 Bean 或上下文。如果直接在 web.xml 中定义过滤器,过滤器实例会在 Spring 上下文加载完成之前被创建。

      解决方案——DelegatingFilterProxy:

      • DelegatingFilterProxy 可以解决这个问题。它允许 Servlet 容器在初始化过滤器时,并不直接创建过滤器的实际实例,而是通过代理的方式,将过滤器的执行逻辑委托给 Spring 上下文中的某个 Bean。这样,过滤器的真正逻辑就可以依赖于 Spring 容器管理的 Bean。
      • 具体来说,DelegatingFilterProxy 本身是一个标准的 Servlet 过滤器,在容器启动时被注册。但是它在处理过滤时,会去查找 Spring 上下文中已经定义好的过滤器 Bean(例如 Spring Security 的 FilterChainProxy),并将请求委托给这个 Spring 管理的过滤器 Bean。

      总结:

      • 问题:在传统的 Servlet 应用中,过滤器的初始化顺序早于 Spring 上下文的加载,这导致无法将 Spring 的依赖注入直接应用于过滤器。
      • 解决:通过使用 DelegatingFilterProxy,可以将过滤器的创建延迟到 Spring 上下文加载完成之后,由 Spring 容器管理过滤器实例的生命周期,使得过滤器可以正常依赖 Spring Bean。 因此,DelegatingFilterProxy 是一个桥梁,它确保即使 Servlet 容器在 Spring 上下文加载之前注册过滤器,过滤器的实际执行逻辑依然能够使用 Spring 管理的 Bean 和资源。
      ',13)]))}const s=t(l,[["render",a],["__file","DelegatingFilterProxy.html.vue"]]),c=JSON.parse('{"path":"/java/framework/security/DelegatingFilterProxy.html","title":"DelegatingFilterProxy介绍","lang":"zh-CN","frontmatter":{"title":"DelegatingFilterProxy介绍","date":"2024-09-20T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"作用 在典型的 Spring 应用中,ContextLoaderListener 通常用于加载 Spring 的应用上下文(ApplicationContext),并管理其中定义的 Spring Bean。然而,Servlet 容器在启动时,会优先初始化过滤器(Filter)实例,而此时 Spring 上下文可能还没有完全加载。 具体解析: Conte...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/security/DelegatingFilterProxy.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"DelegatingFilterProxy介绍"}],["meta",{"property":"og:description","content":"作用 在典型的 Spring 应用中,ContextLoaderListener 通常用于加载 Spring 的应用上下文(ApplicationContext),并管理其中定义的 Spring Bean。然而,Servlet 容器在启动时,会优先初始化过滤器(Filter)实例,而此时 Spring 上下文可能还没有完全加载。 具体解析: Conte..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-09-29T09:49:47.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2024-09-20T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-09-29T09:49:47.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"DelegatingFilterProxy介绍\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2024-09-20T00:00:00.000Z\\",\\"dateModified\\":\\"2024-09-29T09:49:47.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":3,"title":"作用","slug":"作用","link":"#作用","children":[]}],"git":{"createdTime":1727603387000,"updatedTime":1727603387000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":2.4,"words":721},"filePathRelative":"java/framework/security/DelegatingFilterProxy.md","localizedDate":"2024年9月20日","excerpt":"

      作用

      \\n

      在典型的 Spring 应用中,ContextLoaderListener 通常用于加载 Spring 的应用上下文(ApplicationContext),并管理其中定义的 Spring Bean。然而,Servlet 容器在启动时,会优先初始化过滤器(Filter)实例,而此时 Spring 上下文可能还没有完全加载。

      \\n

      具体解析:

      \\n
        \\n
      1. ContextLoaderListener:
      2. \\n
      \\n
        \\n
      • ContextLoaderListener 是 Spring 中常见的监听器,负责在应用启动时加载 Spring 的 ApplicationContext,从而初始化应用中的所有 Spring Bean。这通常是在应用启动的较晚阶段完成的。
      • \\n
      • ContextLoaderListener 会监听 Web 应用的启动事件,并在合适的时机加载和管理 Spring 上下文。
      • \\n
      ","autoDesc":true}');export{s as comp,c as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a as r,o as n}from"./app-BIdVr4mq.js";const l={};function a(o,e){return n(),i("div",null,e[0]||(e[0]=[r('

      作用

      在典型的 Spring 应用中,ContextLoaderListener 通常用于加载 Spring 的应用上下文(ApplicationContext),并管理其中定义的 Spring Bean。然而,Servlet 容器在启动时,会优先初始化过滤器(Filter)实例,而此时 Spring 上下文可能还没有完全加载。

      具体解析:

      1. ContextLoaderListener:
      • ContextLoaderListener 是 Spring 中常见的监听器,负责在应用启动时加载 Spring 的 ApplicationContext,从而初始化应用中的所有 Spring Bean。这通常是在应用启动的较晚阶段完成的。
      • ContextLoaderListener 会监听 Web 应用的启动事件,并在合适的时机加载和管理 Spring 上下文。
      1. Servlet 过滤器的加载顺序:
      • 在典型的 Servlet 容器(如 Tomcat)中,过滤器的实例化和注册会在应用启动的早期阶段进行,即在应用的主业务逻辑启动之前。
      • 这些过滤器的初始化是 Servlet 容器的一部分,不依赖于 Spring 的 ApplicationContext。换句话说,过滤器的生命周期由 Servlet 容器本身管理,而不受 Spring 管理的影响。
      1. 问题所在:
      • 因为过滤器(Filter)的实例需要在 Spring 上下文加载之前注册,而 Spring 的 Bean 管理是由 ContextLoaderListener 在较后阶段完成的,这就导致了过滤器实例在创建时无法直接依赖于 Spring 的 Bean 或上下文。如果直接在 web.xml 中定义过滤器,过滤器实例会在 Spring 上下文加载完成之前被创建。

      解决方案——DelegatingFilterProxy:

      • DelegatingFilterProxy 可以解决这个问题。它允许 Servlet 容器在初始化过滤器时,并不直接创建过滤器的实际实例,而是通过代理的方式,将过滤器的执行逻辑委托给 Spring 上下文中的某个 Bean。这样,过滤器的真正逻辑就可以依赖于 Spring 容器管理的 Bean。
      • 具体来说,DelegatingFilterProxy 本身是一个标准的 Servlet 过滤器,在容器启动时被注册。但是它在处理过滤时,会去查找 Spring 上下文中已经定义好的过滤器 Bean(例如 Spring Security 的 FilterChainProxy),并将请求委托给这个 Spring 管理的过滤器 Bean。

      总结:

      • 问题:在传统的 Servlet 应用中,过滤器的初始化顺序早于 Spring 上下文的加载,这导致无法将 Spring 的依赖注入直接应用于过滤器。
      • 解决:通过使用 DelegatingFilterProxy,可以将过滤器的创建延迟到 Spring 上下文加载完成之后,由 Spring 容器管理过滤器实例的生命周期,使得过滤器可以正常依赖 Spring Bean。 因此,DelegatingFilterProxy 是一个桥梁,它确保即使 Servlet 容器在 Spring 上下文加载之前注册过滤器,过滤器的实际执行逻辑依然能够使用 Spring 管理的 Bean 和资源。
      ',13)]))}const s=t(l,[["render",a],["__file","DelegatingFilterProxy.html.vue"]]),c=JSON.parse('{"path":"/java/framework/security/DelegatingFilterProxy.html","title":"DelegatingFilterProxy介绍","lang":"zh-CN","frontmatter":{"title":"DelegatingFilterProxy介绍","date":"2024-09-20T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"作用 在典型的 Spring 应用中,ContextLoaderListener 通常用于加载 Spring 的应用上下文(ApplicationContext),并管理其中定义的 Spring Bean。然而,Servlet 容器在启动时,会优先初始化过滤器(Filter)实例,而此时 Spring 上下文可能还没有完全加载。 具体解析: Conte...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/security/DelegatingFilterProxy.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"DelegatingFilterProxy介绍"}],["meta",{"property":"og:description","content":"作用 在典型的 Spring 应用中,ContextLoaderListener 通常用于加载 Spring 的应用上下文(ApplicationContext),并管理其中定义的 Spring Bean。然而,Servlet 容器在启动时,会优先初始化过滤器(Filter)实例,而此时 Spring 上下文可能还没有完全加载。 具体解析: Conte..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-09-29T09:49:47.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2024-09-20T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-09-29T09:49:47.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"DelegatingFilterProxy介绍\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2024-09-20T00:00:00.000Z\\",\\"dateModified\\":\\"2024-09-29T09:49:47.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":3,"title":"作用","slug":"作用","link":"#作用","children":[]}],"git":{"createdTime":1727603387000,"updatedTime":1727603387000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":2.4,"words":721},"filePathRelative":"java/framework/security/DelegatingFilterProxy.md","localizedDate":"2024年9月20日","excerpt":"

      作用

      \\n

      在典型的 Spring 应用中,ContextLoaderListener 通常用于加载 Spring 的应用上下文(ApplicationContext),并管理其中定义的 Spring Bean。然而,Servlet 容器在启动时,会优先初始化过滤器(Filter)实例,而此时 Spring 上下文可能还没有完全加载。

      \\n

      具体解析:

      \\n
        \\n
      1. ContextLoaderListener:
      2. \\n
      \\n
        \\n
      • ContextLoaderListener 是 Spring 中常见的监听器,负责在应用启动时加载 Spring 的 ApplicationContext,从而初始化应用中的所有 Spring Bean。这通常是在应用启动的较晚阶段完成的。
      • \\n
      • ContextLoaderListener 会监听 Web 应用的启动事件,并在合适的时机加载和管理 Spring 上下文。
      • \\n
      ","autoDesc":true}');export{s as comp,c as data}; diff --git a/assets/DeployGithubPage.html-C3CJu8PT.js b/assets/DeployGithubPage.html-BMfBhB20.js similarity index 99% rename from assets/DeployGithubPage.html-C3CJu8PT.js rename to assets/DeployGithubPage.html-BMfBhB20.js index 93a5a1b95..49fff1c92 100644 --- a/assets/DeployGithubPage.html-C3CJu8PT.js +++ b/assets/DeployGithubPage.html-BMfBhB20.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const l={};function t(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

      1、 GitHubPage介绍

      1.1 ok

      1.2 搭建个人githubpage

      个人page和项目page的区别就是个人page只有一个,所谓的个人Page说白了也是一个特殊的项目Page,无非就是它的仓库名字比较特殊,必须为<username>.github.io,比如java框架\`spring-cloud.github.io\`、\`facebook.github.io\`,注意个人page的仓库名一定要加上 \`.github.io\`才算个人Page,不加的话就是一个普通项目了。
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const l={};function t(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

      1、 GitHubPage介绍

      1.1 ok

      1.2 搭建个人githubpage

      个人page和项目page的区别就是个人page只有一个,所谓的个人Page说白了也是一个特殊的项目Page,无非就是它的仓库名字比较特殊,必须为<username>.github.io,比如java框架\`spring-cloud.github.io\`、\`facebook.github.io\`,注意个人page的仓库名一定要加上 \`.github.io\`才算个人Page,不加的话就是一个普通项目了。
       个人page有啥特殊之处呢?
       在访问页面时可以直接使用https://<username>.github.io,不用加仓库名,普通的项目page,访问时需要加仓库名,比如https://<username>.github.io/<reponame>

      2、配合github的Action实现自动化部署

      2.1 自动部署脚本

      name: docs
       
      diff --git a/assets/DesignPatternInSpring.html-nq8yHSdu.js b/assets/DesignPatternInSpring.html-CoJ7-0Dg.js
      similarity index 99%
      rename from assets/DesignPatternInSpring.html-nq8yHSdu.js
      rename to assets/DesignPatternInSpring.html-CoJ7-0Dg.js
      index 6663d1723..a178517ab 100644
      --- a/assets/DesignPatternInSpring.html-nq8yHSdu.js
      +++ b/assets/DesignPatternInSpring.html-CoJ7-0Dg.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-LRWZkDAq.js";const e={};function l(h,i){return t(),a("div",null,i[0]||(i[0]=[n(`

      1、工厂模式

      BeanFactory是典型的工厂方法模式,其有多个实现,不同的实现有不同的getBean方法,默认实现是DefaultListalbeBeanFactory,我们也可以定义自己的工厂实现BeanFactory接口,重写里面的getBean方法

      2、单例模式

      在spring中使用singleton修饰的bean都是单例模式,org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#addSingleton,在spring实例化出真正的对象后,会把这个对象加到容器中

      修正,以上说singleton=单例模式,是错误的,singleton指的是容器中该对象的bean只有一个,和单例模式不是一回事,单例模式有:Mybatis的连接工厂、redis的连接工厂等

      
      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-BIdVr4mq.js";const e={};function l(h,i){return t(),a("div",null,i[0]||(i[0]=[n(`

      1、工厂模式

      BeanFactory是典型的工厂方法模式,其有多个实现,不同的实现有不同的getBean方法,默认实现是DefaultListalbeBeanFactory,我们也可以定义自己的工厂实现BeanFactory接口,重写里面的getBean方法

      2、单例模式

      在spring中使用singleton修饰的bean都是单例模式,org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#addSingleton,在spring实例化出真正的对象后,会把这个对象加到容器中

      修正,以上说singleton=单例模式,是错误的,singleton指的是容器中该对象的bean只有一个,和单例模式不是一回事,单例模式有:Mybatis的连接工厂、redis的连接工厂等

      
       /**
       * Add the given singleton object to the singleton cache of this factory.
       * <p>To be called for eager registration of singletons.
      diff --git a/assets/DistributeLock.html-DphO16c2.js b/assets/DistributeLock.html-CblYS_y1.js
      similarity index 99%
      rename from assets/DistributeLock.html-DphO16c2.js
      rename to assets/DistributeLock.html-CblYS_y1.js
      index 5ea4fc22b..4b932c966 100644
      --- a/assets/DistributeLock.html-DphO16c2.js
      +++ b/assets/DistributeLock.html-CblYS_y1.js
      @@ -1,4 +1,4 @@
      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a,o as l}from"./app-LRWZkDAq.js";const t={};function n(h,i){return l(),e("div",null,i[0]||(i[0]=[a(`

      1、写在前面

      以前一直没搞清楚分布式锁和分布式事务,对其概念以及使用场景很模糊,今天查查资料,好好总结一下分布式事务和分布式锁。另外提前说一句,使用redis来解决分布式、高并发问题存在一些困难,Redis 分布式锁只能作为一种缓解并发的手段,如果要完全解决并发问题,仍需要数据库的防并发手段。

      2、锁和分布式锁解决了什么问题?

      单机锁:解决单进程中多线程同时操作共有数据(比如java堆数据)带来的安全问题,强调的是单机服务中线程安全问题,这种场景很常见,比如单机多线程售票的例子。这种锁直接通过java的本地锁实现即可,可以使用java自带的synchroized和Lock

      分布式锁:解决集群服务中,多个相同服务操作同一个资源数据的安全问题。比如淘宝双十一抢购,为了支持高并发,下订单的服务肯定是集群模式而非单机,比如有一个商品促销,数量共有1000个,下订单的集群服务是10个,如果抢购开始,这10个集群服务应该是共同拥有这1000个商品,应当避免在并发情况下销售的总量超过1000的情况,这种就是典型的分布式锁需要处理的问题。

      3、分布式锁特性

      • 互斥性: 同一时刻只能有一个服务(进程)持有锁
      • 可重入性: 同一服务节点上的同一个线程如果获取了锁之后能够再次获取锁
      • 锁超时:和J.U.C中的锁一样支持锁超时,防止死锁
      • 高性能和高可用: 加锁和解锁需要高效,同时也需要保证高可用,防止分布式锁失效
      • 具备阻塞和非阻塞性:能够及时从阻塞状态中被唤醒

      4、分布式锁的实现

      4.1 redis实现

      实现思路:利用redis处理网络请求是单线程(其他模块是多线程),并且其操作具有原子性。

      4.1.1 单机redis实现(采用逐步升级的方式来进行分析)
      1. 采用redis 的SETNX(SET IF NOT EXIST),key和value我们可以随意指定,若key不存在此操作成功返回1,如果key已存在则set失败返回0。正常情况第一个抢占锁的服务设置肯定是返回1的,如果此时其他服务(JVM进程)再次来set相同的key就返回0,代表抢锁失败。

        SETNX lock_source_key lock_value # 加锁
        +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a,o as l}from"./app-BIdVr4mq.js";const t={};function n(h,i){return l(),e("div",null,i[0]||(i[0]=[a(`

        1、写在前面

        以前一直没搞清楚分布式锁和分布式事务,对其概念以及使用场景很模糊,今天查查资料,好好总结一下分布式事务和分布式锁。另外提前说一句,使用redis来解决分布式、高并发问题存在一些困难,Redis 分布式锁只能作为一种缓解并发的手段,如果要完全解决并发问题,仍需要数据库的防并发手段。

        2、锁和分布式锁解决了什么问题?

        单机锁:解决单进程中多线程同时操作共有数据(比如java堆数据)带来的安全问题,强调的是单机服务中线程安全问题,这种场景很常见,比如单机多线程售票的例子。这种锁直接通过java的本地锁实现即可,可以使用java自带的synchroized和Lock

        分布式锁:解决集群服务中,多个相同服务操作同一个资源数据的安全问题。比如淘宝双十一抢购,为了支持高并发,下订单的服务肯定是集群模式而非单机,比如有一个商品促销,数量共有1000个,下订单的集群服务是10个,如果抢购开始,这10个集群服务应该是共同拥有这1000个商品,应当避免在并发情况下销售的总量超过1000的情况,这种就是典型的分布式锁需要处理的问题。

        3、分布式锁特性

        • 互斥性: 同一时刻只能有一个服务(进程)持有锁
        • 可重入性: 同一服务节点上的同一个线程如果获取了锁之后能够再次获取锁
        • 锁超时:和J.U.C中的锁一样支持锁超时,防止死锁
        • 高性能和高可用: 加锁和解锁需要高效,同时也需要保证高可用,防止分布式锁失效
        • 具备阻塞和非阻塞性:能够及时从阻塞状态中被唤醒

        4、分布式锁的实现

        4.1 redis实现

        实现思路:利用redis处理网络请求是单线程(其他模块是多线程),并且其操作具有原子性。

        4.1.1 单机redis实现(采用逐步升级的方式来进行分析)
        1. 采用redis 的SETNX(SET IF NOT EXIST),key和value我们可以随意指定,若key不存在此操作成功返回1,如果key已存在则set失败返回0。正常情况第一个抢占锁的服务设置肯定是返回1的,如果此时其他服务(JVM进程)再次来set相同的key就返回0,代表抢锁失败。

          SETNX lock_source_key lock_value # 加锁
           do something #获取锁以后的业务处理代码
           DEL lock_source_key #业务处理完以后释放锁

        如果仅仅是设置key,是存在问题的,比如第一个服务抢到了锁,但是服务挂掉、或者带宽堵塞、GC等种种原因,导致其迟迟不能释放锁,那么其他服务就一直没有机会抢到锁,这样肯定是不合理的。

        1. 在1的基础上升级一下,给key加一个过期时间,如果出现意外情况,key到期以后可以自动释放

          SETNX lock_source_key lock_value # 加锁
           EXPIRE lock_source_key 10 #假设过期时间是10s
          diff --git a/assets/Docker.html-DfENlTQt.js b/assets/Docker.html-DRMULpQx.js
          similarity index 97%
          rename from assets/Docker.html-DfENlTQt.js
          rename to assets/Docker.html-DRMULpQx.js
          index bd0cd47a2..5fb7332b0 100644
          --- a/assets/Docker.html-DfENlTQt.js
          +++ b/assets/Docker.html-DRMULpQx.js
          @@ -1 +1 @@
          -import{_ as o}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as r,b as e,o as n}from"./app-LRWZkDAq.js";const a={};function c(i,t){return n(),r("div",null,t[0]||(t[0]=[e("h2",{id:"_1-命令速查",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-命令速查"},[e("span",null,"1 命令速查")])],-1)]))}const p=o(a,[["render",c],["__file","Docker.html.vue"]]),d=JSON.parse('{"path":"/other/docker/Docker.html","title":"Docker常用命令","lang":"zh-CN","frontmatter":{"title":"Docker常用命令","date":"2021-10-10T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"category":["docker"],"description":"1 命令速查","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/docker/Docker.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Docker常用命令"}],["meta",{"property":"og:description","content":"1 命令速查"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2021-10-10T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Docker常用命令\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-10-10T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1 命令速查","slug":"_1-命令速查","link":"#_1-命令速查","children":[]}],"git":{"createdTime":1665557335000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.07,"words":22},"filePathRelative":"other/docker/Docker.md","localizedDate":"2021年10月10日","excerpt":"

          1 命令速查

          \\n","autoDesc":true}');export{p as comp,d as data}; +import{_ as o}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as r,b as e,o as n}from"./app-BIdVr4mq.js";const a={};function c(i,t){return n(),r("div",null,t[0]||(t[0]=[e("h2",{id:"_1-命令速查",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-命令速查"},[e("span",null,"1 命令速查")])],-1)]))}const p=o(a,[["render",c],["__file","Docker.html.vue"]]),d=JSON.parse('{"path":"/other/docker/Docker.html","title":"Docker常用命令","lang":"zh-CN","frontmatter":{"title":"Docker常用命令","date":"2021-10-10T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"category":["docker"],"description":"1 命令速查","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/docker/Docker.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Docker常用命令"}],["meta",{"property":"og:description","content":"1 命令速查"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2021-10-10T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Docker常用命令\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-10-10T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1 命令速查","slug":"_1-命令速查","link":"#_1-命令速查","children":[]}],"git":{"createdTime":1665557335000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.07,"words":22},"filePathRelative":"other/docker/Docker.md","localizedDate":"2021年10月10日","excerpt":"

          1 命令速查

          \\n","autoDesc":true}');export{p as comp,d as data}; diff --git a/assets/Future.html-axBzZo7a.js b/assets/Future.html-CSv-67Ma.js similarity index 98% rename from assets/Future.html-axBzZo7a.js rename to assets/Future.html-CSv-67Ma.js index 77ed913a4..3cd059cb8 100644 --- a/assets/Future.html-axBzZo7a.js +++ b/assets/Future.html-CSv-67Ma.js @@ -1 +1 @@ -import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as e,d as t,o as a}from"./app-LRWZkDAq.js";const c={};function u(i,r){return a(),o("div",null,r[0]||(r[0]=[e("h2",{id:"_1、future的作用",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1、future的作用"},[e("span",null,"1、Future的作用")])],-1),e("p",null,[t("Future可以用来获取一个异步执行的结果,可以使用"),e("code",null,"isDone"),t("方法检查异步任务是否完成,或者使用"),e("code",null,"get"),t("阻塞住调用线程,直到计算完成返回结果,你也可以使用"),e("code",null,"cancel"),t("方法停止任务的执行。")],-1)]))}const m=n(c,[["render",u],["__file","Future.html.vue"]]),d=JSON.parse('{"path":"/java/advance/Future.html","title":"多线程中的Future","lang":"zh-CN","frontmatter":{"title":"多线程中的Future","date":"2021-04-06T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"category":["线程池","多线程"],"tag":["多线程","线程池"],"description":"1、Future的作用 Future可以用来获取一个异步执行的结果,可以使用isDone方法检查异步任务是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/advance/Future.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"多线程中的Future"}],["meta",{"property":"og:description","content":"1、Future的作用 Future可以用来获取一个异步执行的结果,可以使用isDone方法检查异步任务是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:tag","content":"多线程"}],["meta",{"property":"article:tag","content":"线程池"}],["meta",{"property":"article:published_time","content":"2021-04-06T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"多线程中的Future\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-04-06T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、Future的作用","slug":"_1、future的作用","link":"#_1、future的作用","children":[]}],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.36,"words":107},"filePathRelative":"java/advance/Future.md","localizedDate":"2021年4月6日","excerpt":"\\n

          1、Future的作用

          \\n

          Future可以用来获取一个异步执行的结果,可以使用isDone方法检查异步任务是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。

          \\n","autoDesc":true}');export{m as comp,d as data}; +import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as e,d as t,o as a}from"./app-BIdVr4mq.js";const c={};function u(i,r){return a(),o("div",null,r[0]||(r[0]=[e("h2",{id:"_1、future的作用",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1、future的作用"},[e("span",null,"1、Future的作用")])],-1),e("p",null,[t("Future可以用来获取一个异步执行的结果,可以使用"),e("code",null,"isDone"),t("方法检查异步任务是否完成,或者使用"),e("code",null,"get"),t("阻塞住调用线程,直到计算完成返回结果,你也可以使用"),e("code",null,"cancel"),t("方法停止任务的执行。")],-1)]))}const m=n(c,[["render",u],["__file","Future.html.vue"]]),d=JSON.parse('{"path":"/java/advance/Future.html","title":"多线程中的Future","lang":"zh-CN","frontmatter":{"title":"多线程中的Future","date":"2021-04-06T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"category":["线程池","多线程"],"tag":["多线程","线程池"],"description":"1、Future的作用 Future可以用来获取一个异步执行的结果,可以使用isDone方法检查异步任务是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/advance/Future.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"多线程中的Future"}],["meta",{"property":"og:description","content":"1、Future的作用 Future可以用来获取一个异步执行的结果,可以使用isDone方法检查异步任务是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:tag","content":"多线程"}],["meta",{"property":"article:tag","content":"线程池"}],["meta",{"property":"article:published_time","content":"2021-04-06T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"多线程中的Future\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-04-06T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、Future的作用","slug":"_1、future的作用","link":"#_1、future的作用","children":[]}],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.36,"words":107},"filePathRelative":"java/advance/Future.md","localizedDate":"2021年4月6日","excerpt":"\\n

          1、Future的作用

          \\n

          Future可以用来获取一个异步执行的结果,可以使用isDone方法检查异步任务是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。

          \\n","autoDesc":true}');export{m as comp,d as data}; diff --git a/assets/GitCommands.html-BbarE5Z9.js b/assets/GitCommands.html-OTAFexBu.js similarity index 99% rename from assets/GitCommands.html-BbarE5Z9.js rename to assets/GitCommands.html-OTAFexBu.js index 19f853f50..9609fb6c0 100644 --- a/assets/GitCommands.html-BbarE5Z9.js +++ b/assets/GitCommands.html-OTAFexBu.js @@ -1,4 +1,4 @@ -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const e={};function t(h,s){return l(),a("div",null,s[0]||(s[0]=[n(`

          一、善用手册

          $ git --help
          +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const e={};function t(h,s){return l(),a("div",null,s[0]||(s[0]=[n(`

          一、善用手册

          $ git --help
           用法:git [--version] [--help] [-C <路径>] [-c <名称>=<取值>]
                      [--exec-path[=<路径>]] [--html-path] [--man-path] [--info-path]
                      [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
          diff --git a/assets/Http.html-Ban9xdQ_.js b/assets/Http.html-UGMp5Sj8.js
          similarity index 99%
          rename from assets/Http.html-Ban9xdQ_.js
          rename to assets/Http.html-UGMp5Sj8.js
          index aaf545a1b..2af4d6b2d 100644
          --- a/assets/Http.html-Ban9xdQ_.js
          +++ b/assets/Http.html-UGMp5Sj8.js
          @@ -1,4 +1,4 @@
          -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as t}from"./app-LRWZkDAq.js";const n={};function h(l,i){return t(),a("div",null,i[0]||(i[0]=[e(`

          必看手册

          https://developer.mozilla.org/zh-CN/docs/Web/HTTP

          一、状态码

          1.1 3xx

          1.1.1 304

          HTTP 304 Not Modified 说明无需再次传输请求的内容,也就是说可以使用缓存的内容。这通常是在一些安全的方法(safe),例如GET 或HEAD 或在请求中附带了头部信息: If-None-Match 或If-Modified-Since。

          如果是 200 OK ,响应会带有头部 Cache-Control, Content-Location, Date, ETag, Expires,和 Vary.

          温馨提示

          很多浏览器的 开发者工具 会发出额外的请求,以达到 304 的目的,这样可以把资源以本地缓存的形式展现给开发者。一般缓存静态文件,如果用户在服务器上修改了静态文件,则请求时服务器会读取其修改时间, 这样就知道此文件是已修改过的,需要重新响应给浏览器修改后的内容。

          比如以下请求,浏览器会自动携带If-Modified-Since请求头,然后拿这个时间和服务器上文件修改时间对比,如果服务器上的时间比这个新,就会返回200,否则返回304。 20221101150553

          另外浏览器有个Disable cache选择项,勾选此项代表不允许浏览器自动携带If-Modified-Sinc请求头,也就无法使用本地缓存了。当使用Ctrl+F5刷新页面时也是同样的道理,不携带If-Modified-Sinc请求头。 20221101150938

          二、http请求

          2.1 302重定向

          请求被重定向后,是无法给客户端响应ResbonseBody,但是可以有Response Header,测试代码如下,请求http://localhost:8888/hello/cookie,在请求处理逻辑里面 添加一个cookie,然后重定向到百度。可以看到如下代码我有设置返回值,但是其实毫无意义,在return之前就被redirect到了百度。同时,在Response Headers中可以看到有 Cookie: test=aaaaaa,并且打开localhost能看到在下面有对应的cookie

          @Controller
          +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as t}from"./app-BIdVr4mq.js";const n={};function h(l,i){return t(),a("div",null,i[0]||(i[0]=[e(`

          必看手册

          https://developer.mozilla.org/zh-CN/docs/Web/HTTP

          一、状态码

          1.1 3xx

          1.1.1 304

          HTTP 304 Not Modified 说明无需再次传输请求的内容,也就是说可以使用缓存的内容。这通常是在一些安全的方法(safe),例如GET 或HEAD 或在请求中附带了头部信息: If-None-Match 或If-Modified-Since。

          如果是 200 OK ,响应会带有头部 Cache-Control, Content-Location, Date, ETag, Expires,和 Vary.

          温馨提示

          很多浏览器的 开发者工具 会发出额外的请求,以达到 304 的目的,这样可以把资源以本地缓存的形式展现给开发者。一般缓存静态文件,如果用户在服务器上修改了静态文件,则请求时服务器会读取其修改时间, 这样就知道此文件是已修改过的,需要重新响应给浏览器修改后的内容。

          比如以下请求,浏览器会自动携带If-Modified-Since请求头,然后拿这个时间和服务器上文件修改时间对比,如果服务器上的时间比这个新,就会返回200,否则返回304。 20221101150553

          另外浏览器有个Disable cache选择项,勾选此项代表不允许浏览器自动携带If-Modified-Sinc请求头,也就无法使用本地缓存了。当使用Ctrl+F5刷新页面时也是同样的道理,不携带If-Modified-Sinc请求头。 20221101150938

          二、http请求

          2.1 302重定向

          请求被重定向后,是无法给客户端响应ResbonseBody,但是可以有Response Header,测试代码如下,请求http://localhost:8888/hello/cookie,在请求处理逻辑里面 添加一个cookie,然后重定向到百度。可以看到如下代码我有设置返回值,但是其实毫无意义,在return之前就被redirect到了百度。同时,在Response Headers中可以看到有 Cookie: test=aaaaaa,并且打开localhost能看到在下面有对应的cookie

          @Controller
           @RequestMapping("hello")
           public class HttptestApplication {
           
          diff --git a/assets/Http2.html-2WMDcdOZ.js b/assets/Http2.html-NGu_jTQ-.js
          similarity index 97%
          rename from assets/Http2.html-2WMDcdOZ.js
          rename to assets/Http2.html-NGu_jTQ-.js
          index c3d2a1328..40d5302ab 100644
          --- a/assets/Http2.html-2WMDcdOZ.js
          +++ b/assets/Http2.html-NGu_jTQ-.js
          @@ -1 +1 @@
          -import{_ as o}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,b as t,o as p}from"./app-LRWZkDAq.js";const r={};function i(a,e){return p(),n("div",null,e[0]||(e[0]=[t("h2",{id:"_1、http2-0优势",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#_1、http2-0优势"},[t("span",null,"1、http2.0优势")])],-1),t("h2",{id:"_2、springboot开启http2-0",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#_2、springboot开启http2-0"},[t("span",null,"2、springboot开启http2.0")])],-1)]))}const l=o(r,[["render",i],["__file","Http2.html.vue"]]),c=JSON.parse('{"path":"/java/framework/springboot/Http2.html","title":"springboot开启http2.0","lang":"zh-CN","frontmatter":{"title":"springboot开启http2.0","date":"2022-01-29T00:00:00.000Z","description":"1、http2.0优势 2、springboot开启http2.0","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/springboot/Http2.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"springboot开启http2.0"}],["meta",{"property":"og:description","content":"1、http2.0优势 2、springboot开启http2.0"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-01-29T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"springboot开启http2.0\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-01-29T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、http2.0优势","slug":"_1、http2-0优势","link":"#_1、http2-0优势","children":[]},{"level":2,"title":"2、springboot开启http2.0","slug":"_2、springboot开启http2-0","link":"#_2、springboot开启http2-0","children":[]}],"git":{"createdTime":1674980614000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":0.06,"words":18},"filePathRelative":"java/framework/springboot/Http2.md","localizedDate":"2022年1月29日","excerpt":"

          1、http2.0优势

          \\n

          2、springboot开启http2.0

          \\n","autoDesc":true}');export{l as comp,c as data}; +import{_ as o}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,b as t,o as p}from"./app-BIdVr4mq.js";const r={};function i(a,e){return p(),n("div",null,e[0]||(e[0]=[t("h2",{id:"_1、http2-0优势",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#_1、http2-0优势"},[t("span",null,"1、http2.0优势")])],-1),t("h2",{id:"_2、springboot开启http2-0",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#_2、springboot开启http2-0"},[t("span",null,"2、springboot开启http2.0")])],-1)]))}const l=o(r,[["render",i],["__file","Http2.html.vue"]]),c=JSON.parse('{"path":"/java/framework/springboot/Http2.html","title":"springboot开启http2.0","lang":"zh-CN","frontmatter":{"title":"springboot开启http2.0","date":"2022-01-29T00:00:00.000Z","description":"1、http2.0优势 2、springboot开启http2.0","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/springboot/Http2.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"springboot开启http2.0"}],["meta",{"property":"og:description","content":"1、http2.0优势 2、springboot开启http2.0"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-01-29T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"springboot开启http2.0\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-01-29T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、http2.0优势","slug":"_1、http2-0优势","link":"#_1、http2-0优势","children":[]},{"level":2,"title":"2、springboot开启http2.0","slug":"_2、springboot开启http2-0","link":"#_2、springboot开启http2-0","children":[]}],"git":{"createdTime":1674980614000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":0.06,"words":18},"filePathRelative":"java/framework/springboot/Http2.md","localizedDate":"2022年1月29日","excerpt":"

          1、http2.0优势

          \\n

          2、springboot开启http2.0

          \\n","autoDesc":true}');export{l as comp,c as data}; diff --git "a/assets/IM\345\215\263\346\227\266\351\200\232\344\277\241\346\212\200\346\234\257\351\200\211\345\236\213.html-BrFdlOJV.js" "b/assets/IM\345\215\263\346\227\266\351\200\232\344\277\241\346\212\200\346\234\257\351\200\211\345\236\213.html-veogyBQt.js" similarity index 99% rename from "assets/IM\345\215\263\346\227\266\351\200\232\344\277\241\346\212\200\346\234\257\351\200\211\345\236\213.html-BrFdlOJV.js" rename to "assets/IM\345\215\263\346\227\266\351\200\232\344\277\241\346\212\200\346\234\257\351\200\211\345\236\213.html-veogyBQt.js" index 8f580d60a..ca9055771 100644 --- "a/assets/IM\345\215\263\346\227\266\351\200\232\344\277\241\346\212\200\346\234\257\351\200\211\345\236\213.html-BrFdlOJV.js" +++ "b/assets/IM\345\215\263\346\227\266\351\200\232\344\277\241\346\212\200\346\234\257\351\200\211\345\236\213.html-veogyBQt.js" @@ -1,2 +1,2 @@ -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as l,o as a}from"./app-LRWZkDAq.js";const i={};function d(r,t){return a(),n("div",null,t[0]||(t[0]=[l(`

          1. 需求

          服务端开发语言:Java

          终端:Android、iOS、Web、小程序

          核心需求:发送文字、图片、文件、语音、视频、消息缓存、消息存储、消息未读、已读、撤回,离线消息、历史消息、单聊、群聊,多端同步,以及其他一些需求。

          用户管理:添加好友、查看好友列表、删除好友、查看好友信息、创建群聊、加入群聊、查看群成员信息、退出群聊、修改群昵称、拉人进群、踢人出群、解散群聊、填写群公告、修改群备注以及其他用户相关的需求等。

          权限管理: 针对群聊不同用户有不同的权限,比如群主、管理员、普通成员,普通成员可以发送消息、撤回消息、删除消息、修改消息、查看消息等,管理员可以管理群成员,修改群昵称、拉人进群、踢人出群、解散群聊、填写群公告、修改群备注等。

          2. 原有方案评估

          2.1 webrtc即时通信

          优点:

          • 实时性强:WebRTC 的设计初衷就是为实时通信提供低延迟的解决方案,适合需要快速响应的应用场景。
          • P2P通信:WebRTC 支持点对点(P2P)通信,这可以减少延迟和服务器压力,适用于小规模的直接通信场景。
          • 跨平台支持:WebRTC 在现代浏览器中原生支持,适用于多平台的应用开发。 缺点
          • 复杂性:WebRTC 需要处理信令(比如通过 WebSocket 或其他方式),而且连接的建立和维护(如 NAT 穿透、ICE 候选等)较为复杂。
          • 消息持久化和历史记录:IM 通常需要消息持久化和历史记录功能,而 WebRTC 本身不具备这些功能,需要额外的服务器支持。
          • 扩展性问题:WebRTC 的 P2P 特性使其更适合于小规模的通信,如果用户量大或者需要支持大规模群聊,P2P 方式的扩展性会受到限制。
          • 服务器压力:虽然 P2P 减少了服务器的带宽负担,但信令服务器和消息的中转(在一些情况下)仍然需要服务器支持,尤其是当点对点连接不可用时。

          总结
          WebRTC 可以用于即时通信,但它的优缺点使其更适合特定的场景。例如,如果你需要实现一个注重实时性的小规模视频通话或音频通话的应用, WebRTC 是一个很好的选择。但如果要构建一个大规模的、需要强大消息持久化和历史记录支持的 IM 应用,那么传统的基于 WebSocket更加适合。

          远程会诊的通信技术使用的是WebRTC技术SFU架构,聊天功能模块是基于WebSocket的通信,WebRTC的优势是音视频通信,其即时聊天的多方建立连接需要依赖于信令服务器。 原有方案业务代码和会诊业务耦合度高,im通信不是其核心功能,很多功能需要从头实现,改造成本较大。

          3. IM通信的难点

          20240829105122
          20240829105122

          3.1 消息传递可靠性问题

          离线消息需要被可靠地存储在服务器端,可能涉及大规模的数据存储需求。如何在不影响系统性能的情况下高效存储和检索离线消息? 用户从离线到在线的状态切换时,如何处理未同步的离线消息与新接收的实时消息,并确保两者的有序合并?

          3.2 消息重复问题

          20240829115908 由于网络的不可靠性,如果在如上图的环节5(ack)丢包,则会出现服务端认为消息发送成功,但是客户端认为消息发送失败的问题。当客户端重试后,如果不进行处理, 则会在数据库出现2条相同的消息。

          3.3 消息时序问题

          IM类系统中,都需要考虑消息时序问题,如果后发送的消息先显示,可能严重扰乱聊天消息所要表达的意义。

          消息时序是分布式系统架构设计中非常难的问题,一个分布式的IM系统必须要解决这个问题。

          IM系统中主要有两类消息 (1)单聊消息,两个人之间的聊天。需要确保发送方和接收方消息时序展示一致。 (2)群聊消息,一群人在一起聊天。需要确保所有接收方消息顺序一致。

          一、为什么会出现时序问题 1、时间不一致。 IM系统存在大量的客户端、IM服务器集群、长连接接入层集群、短连接接入层集群、数据库集群,这些应用分布在不同的机器上,时间很可能 不一致,时区也可能不一致。

          2、网络传输 网络传输延迟不同。同一用户后发送的消息可能早与先发送的消息到达服务器;不同用户的发送的消息到达服务器的延时差异可能更大。如下 图,msg1先发送,msg2后发送。由于网络原因,可能msg2先到达消息服务器

          3、服务集群时差 由于IM服务器分布式部署,不同的消息可能路由到不同的逻辑层处理。路由到不同logic的时延不同(尤其是跨机房),且不同logic之间存 在微量时差。

          4、消息处理速度不一致 服务器收到消息后,不同logic,不同线程对消息的处理速度可能不同,导致投递消息的时序出现错乱。

          3.4 音视频QoS问题

          网络抖动、视频延迟、丢包问题... 当同时有多路音视频通话时,如何节流,如何保证通话质量,一般云服务器的出站(上行)带宽价格是非常高昂的。

          跨网络如何保证质量? M应用需要在不同的网络环境下(如Wi-Fi、4G/5G、宽带等)保持一致的服务质量,这需要解决不同网络类型的QoS兼容问题。

          3.5 消息推送问题

          可靠的推送机制:在移动设备上,确保应用能够在后台接收消息,并及时推送通知给用户,即使应用未启动或网络不佳。 推送服务选择:根据不同平台(如Android、iOS)选择合适的推送服务,如Firebase Cloud Messaging(FCM)、APNs等,并确保推送消息的延迟和成功率。 推送策略优化:在推送大量消息时,设计合理的推送策略,避免过度消耗设备电量或造成用户干扰,同时提供灵活的推送设置供用户选择。

          3.6 技术栈问题

          音视频通话都是基于C语言或者C++开发的,超出了java技术范围。 20240830103446

          4. 技术选型目标

          20240812175557
          20240812175557

          1)业务目标:满足需求分析篇章中的各类需求场景;
          2)技术目标:支持扩容,前期最大要能支持万级别用户同时在线聊天;
          3)架构目标:高性能、高可用、可监控、可预警、可伸缩,支持扩展。
          4)价格目标:免费开源或一次买断源码,再做二次开发

          4. 候选方案

          4.1 uni-im

          4.1.1 简介

          地址:https://doc.dcloud.net.cn/uniCloud/uni-im.html

          uni-im是云端一体的、全平台的、免费的、开源即时通讯系统。

          • 基于uni-app,App、小程序、web全端兼容
          • 基于uniCloud,前后端都使用js开发
          • 基于uni-push2,专业稳定的全端推送系统
          • 基于uni-id,完善的账户体系
          • 支持服务端为非uniCloud(比如:应用服务端的开发语言是php、java、go、.net、python、c#等)或 不基于uni-id-pages 开发的项目接入

          4.1.2 优势

          • 比较详细的文档支持
          • 前端工作量大大减少
          • 全端可用
          • App端支持nvue,更好的长列表性能。list组件性能优势详情参考
          • 中心化响应式数据管理,切换会话无需重新加载数据,更流畅的体验
          • App端聚合多个手机厂商推送通道,app不在线也可以收到消息

          4.1.3 劣势

          • 服务收费,且价格不便宜
          • 无法私有化部署

          CDN流量、出网流量、存储费用比较贵

          4.1.4 费用

          按量计费:

          • 调用10000次云函数0.0133元
          • 调用10000次数据库查询仅0.015元

          私聊一次: 1次云函数请求、2次数据库读操作、2次数据库写操作、1次uni-push2推送操作,即 (1 * 0.0133 + 2 * 0.015 + 2 * 0.05 + 1 * 0.0283)/10000 ≈ 0.000017元

          500人的群聊:1次云函数请求、4次数据库读操作、2次数据库写操作、1次uni-push2推送操作,即 (1 * 0.0133 + 4 * 0.015 + 2 * 0.05 + 1 * 0.0283)/10000 ≈ 0.000020元

          资源分类 资源细项 售价(元)
          云函数 资源使用量(GBs) 0.000110592
          调用次数(万次) 0.0133
          出网流量(GB) 0.8
          云数据库 容量(GB/天) 0.07
          读操作使用量(万RU) 0.015
          写操作使用量(万RU) 0.05
          云存储 容量(GB/天) 0.0043
          下载操作次数(万次) 0.01
          上传操作次数(万次) 0.01
          CDN 流量(GB) 0.18
          前端网站托管 容量(GB/天) 0.0043
          流量(GB) 0.18

          套餐计费:

          资源分类 资源细项 免费版 基础版 标准版 专业版 企业版 旗舰版
          云函数 资源使用量(GBs/月) 1000 1万 20万 40万 150万 400万
          调用次数(万次/月) 1.5 15 300 600 2400 6000
          出网流量(GB/月) 1 1 20 40 160 500
          云数据库 容量(GB) 2 2 3 5 10 10
          读操作使用量(万RU/天) 0.05 5 25 50 150 500
          写操作使用量(万WU/天) 0.03 3 15 30 100 300
          集合数量 100 100 100 100 100 100
          索引数量 400 400 400 400 400 400
          云存储 容量(GB) 5 8 10 50 100 500
          下载操作次数(万次/月) 0.2 10 200 750 1500 3750
          上传操作次数(万次/月) 0.1 5 100 300 600 1500
          CDN流量(GB/月) 1 2 10 50 150 500
          前端网页托管 容量(GB) 5 8 10 50 100 500
          CDN流量(GB/月) 1 2 10 50 150 500
          售价(元/月) 免费 5 24 82 316 688

          名词解释:

          资源分类 资源细项 说明 数据更新延迟时间
          云函数 资源使用量(GBs) 资源使用量GBs = 函数配置内存GB × 运行计费时长s。 例如,配置为256MB的函数,单次运行了1760ms,计费时长为1760ms,则单次运行的资源使用量为(256 / 1024) × (1760 / 1000) = 0.44GBs 20分钟
          调用次数 - 20分钟
          出网流量(GB) 在云函数中访问外网时产生的出网流量,包含请求三方服务器发送的数据和返回给客户端的数据。 20分钟
          云数据库 容量(GB) - 1小时
          读操作使用量(RU) 读操作使用量(Read Unit)= ceil(查询数据量KB / 4),即从数据表中读取一条4 KB数据(向上取整)计作1RU,例如读取7.6 KB的数据计作2RU。 20分钟
          写操作使用量(WU) 写操作使用量(Write Unit)= ceil(写入数据量KB / 1),即向数据表中写入一条1 KB数据(向上取整)计作1WU,例如写入1.8 KB的数据计作2WU。 20分钟
          云存储 容量(GB) - 6小时
          下载操作次数 通过CDN加速访问的次数,回源次数暂不收费。 6小时
          上传操作次数 - 6小时
          CDN 流量(GB) 通过CDN加速产生的流量,回源流量暂不收费。 6小时
          前端网站托管 容量(GB) - 6小时
          CDN 流量(GB) 通过CDN加速产生的流量,回源流量暂不收费。 6小时

          4.2 J-IM

          GIT地址:https://gitee.com/xchao/j-im

          4.2.1 简介

          J-IM 是用JAVA语言开发的轻量、高性能、单机支持几十万至百万在线用户IM,主要目标降低即时通讯门槛,快速打造低成本接入在线IM系统,通过极简洁的消息格式就可以实现多端不同协议间的消息发送如内置(Http、Websocket、Tcp自定义IM协议)等,并提供通过http协议的api接口进行消息发送无需关心接收端属于什么协议,一个消息格式搞定一切!

          4.2.2 特性

          1、高性能(单机可支持几十万至百万人同时在线) 2、轻量、可扩展性极强 3、支持集群多机部署 4、支持SSL/TLS加密传输 5、消息格式极其简洁(JSON) 6、一端口支持可插拔多种协议(Socket自定义IM协议、Websocket、Http),各协议可分别独立部署。 7、内置消息持久化(离线、历史、漫游),保证消息可靠性,高性能存储 8、各种丰富的API接口。 9、零成本部署,一键启动。

          4.2.3 劣势

          1. 项目不活跃长期不更新
          2. 没有完善的文档支持,官网打不开

          4.3 cim

          GIT地址: https://gitee.com/farsunset/cim

          4.3.1 简介

          CIM是一套完善的消息推送框架,可应用于信令推送,即时聊天,移动设备指令推送等领域。开发者可沉浸于业务开发,不用关心消息通道长连接、消息编解码协议等繁杂处理。

          CIM采用业内主流开源技术构建,易于扩展和使用,并完美支持集群部署支持海量链接,目前支持websocket,android,ios,桌面应用,系统应用等多端接入持,可应用于移动应用,物联网,智能家居,嵌入式开发,桌面应用,WEB应用即时消服务。

          4.3.2 优势

          1. 维护时间比较长,10年了 ,目前还在活跃提交代码
          2. 提供多个客户端SDK(android/.net/fluttter/ios/swift/uniapp/web)
          3. 有web/vue/android的demo,有后台管理
          4. 文档详细

          4.3.3 劣势

          1. 按照平台SDK收费,uni-app(h5,android,ios)
          20240813112230
          20240813112230

          4.4 V-IM

          GIT地址:https://gitee.com/alyouge/V-IM

          4.4.1 简介

          开源与企业版功能点对比 20240813112349

          企业版优势

          多终端支持:PC(windows、linux、mac、web) 手机(安卓、IOS、H5、小程序); 上传支持两种方案(直接存服务器和minio); 私有云代码仓库永久更新,无加密部分,不依赖第三方。 一对一技术支持。 bug修复优先级最高。 支持付费定制化需求。 功能更新频率高。 聊天记录存储在mongoDB; 支持国产化部署,服务端已对接到snowy开源项目(分支版本)。

          4.4.2 企业版收费情况

          2980元,交付源码
          +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as l,o as a}from"./app-BIdVr4mq.js";const i={};function d(r,t){return a(),n("div",null,t[0]||(t[0]=[l(`

          1. 需求

          服务端开发语言:Java

          终端:Android、iOS、Web、小程序

          核心需求:发送文字、图片、文件、语音、视频、消息缓存、消息存储、消息未读、已读、撤回,离线消息、历史消息、单聊、群聊,多端同步,以及其他一些需求。

          用户管理:添加好友、查看好友列表、删除好友、查看好友信息、创建群聊、加入群聊、查看群成员信息、退出群聊、修改群昵称、拉人进群、踢人出群、解散群聊、填写群公告、修改群备注以及其他用户相关的需求等。

          权限管理: 针对群聊不同用户有不同的权限,比如群主、管理员、普通成员,普通成员可以发送消息、撤回消息、删除消息、修改消息、查看消息等,管理员可以管理群成员,修改群昵称、拉人进群、踢人出群、解散群聊、填写群公告、修改群备注等。

          2. 原有方案评估

          2.1 webrtc即时通信

          优点:

          • 实时性强:WebRTC 的设计初衷就是为实时通信提供低延迟的解决方案,适合需要快速响应的应用场景。
          • P2P通信:WebRTC 支持点对点(P2P)通信,这可以减少延迟和服务器压力,适用于小规模的直接通信场景。
          • 跨平台支持:WebRTC 在现代浏览器中原生支持,适用于多平台的应用开发。 缺点
          • 复杂性:WebRTC 需要处理信令(比如通过 WebSocket 或其他方式),而且连接的建立和维护(如 NAT 穿透、ICE 候选等)较为复杂。
          • 消息持久化和历史记录:IM 通常需要消息持久化和历史记录功能,而 WebRTC 本身不具备这些功能,需要额外的服务器支持。
          • 扩展性问题:WebRTC 的 P2P 特性使其更适合于小规模的通信,如果用户量大或者需要支持大规模群聊,P2P 方式的扩展性会受到限制。
          • 服务器压力:虽然 P2P 减少了服务器的带宽负担,但信令服务器和消息的中转(在一些情况下)仍然需要服务器支持,尤其是当点对点连接不可用时。

          总结
          WebRTC 可以用于即时通信,但它的优缺点使其更适合特定的场景。例如,如果你需要实现一个注重实时性的小规模视频通话或音频通话的应用, WebRTC 是一个很好的选择。但如果要构建一个大规模的、需要强大消息持久化和历史记录支持的 IM 应用,那么传统的基于 WebSocket更加适合。

          远程会诊的通信技术使用的是WebRTC技术SFU架构,聊天功能模块是基于WebSocket的通信,WebRTC的优势是音视频通信,其即时聊天的多方建立连接需要依赖于信令服务器。 原有方案业务代码和会诊业务耦合度高,im通信不是其核心功能,很多功能需要从头实现,改造成本较大。

          3. IM通信的难点

          20240829105122
          20240829105122

          3.1 消息传递可靠性问题

          离线消息需要被可靠地存储在服务器端,可能涉及大规模的数据存储需求。如何在不影响系统性能的情况下高效存储和检索离线消息? 用户从离线到在线的状态切换时,如何处理未同步的离线消息与新接收的实时消息,并确保两者的有序合并?

          3.2 消息重复问题

          20240829115908 由于网络的不可靠性,如果在如上图的环节5(ack)丢包,则会出现服务端认为消息发送成功,但是客户端认为消息发送失败的问题。当客户端重试后,如果不进行处理, 则会在数据库出现2条相同的消息。

          3.3 消息时序问题

          IM类系统中,都需要考虑消息时序问题,如果后发送的消息先显示,可能严重扰乱聊天消息所要表达的意义。

          消息时序是分布式系统架构设计中非常难的问题,一个分布式的IM系统必须要解决这个问题。

          IM系统中主要有两类消息 (1)单聊消息,两个人之间的聊天。需要确保发送方和接收方消息时序展示一致。 (2)群聊消息,一群人在一起聊天。需要确保所有接收方消息顺序一致。

          一、为什么会出现时序问题 1、时间不一致。 IM系统存在大量的客户端、IM服务器集群、长连接接入层集群、短连接接入层集群、数据库集群,这些应用分布在不同的机器上,时间很可能 不一致,时区也可能不一致。

          2、网络传输 网络传输延迟不同。同一用户后发送的消息可能早与先发送的消息到达服务器;不同用户的发送的消息到达服务器的延时差异可能更大。如下 图,msg1先发送,msg2后发送。由于网络原因,可能msg2先到达消息服务器

          3、服务集群时差 由于IM服务器分布式部署,不同的消息可能路由到不同的逻辑层处理。路由到不同logic的时延不同(尤其是跨机房),且不同logic之间存 在微量时差。

          4、消息处理速度不一致 服务器收到消息后,不同logic,不同线程对消息的处理速度可能不同,导致投递消息的时序出现错乱。

          3.4 音视频QoS问题

          网络抖动、视频延迟、丢包问题... 当同时有多路音视频通话时,如何节流,如何保证通话质量,一般云服务器的出站(上行)带宽价格是非常高昂的。

          跨网络如何保证质量? M应用需要在不同的网络环境下(如Wi-Fi、4G/5G、宽带等)保持一致的服务质量,这需要解决不同网络类型的QoS兼容问题。

          3.5 消息推送问题

          可靠的推送机制:在移动设备上,确保应用能够在后台接收消息,并及时推送通知给用户,即使应用未启动或网络不佳。 推送服务选择:根据不同平台(如Android、iOS)选择合适的推送服务,如Firebase Cloud Messaging(FCM)、APNs等,并确保推送消息的延迟和成功率。 推送策略优化:在推送大量消息时,设计合理的推送策略,避免过度消耗设备电量或造成用户干扰,同时提供灵活的推送设置供用户选择。

          3.6 技术栈问题

          音视频通话都是基于C语言或者C++开发的,超出了java技术范围。 20240830103446

          4. 技术选型目标

          20240812175557
          20240812175557

          1)业务目标:满足需求分析篇章中的各类需求场景;
          2)技术目标:支持扩容,前期最大要能支持万级别用户同时在线聊天;
          3)架构目标:高性能、高可用、可监控、可预警、可伸缩,支持扩展。
          4)价格目标:免费开源或一次买断源码,再做二次开发

          4. 候选方案

          4.1 uni-im

          4.1.1 简介

          地址:https://doc.dcloud.net.cn/uniCloud/uni-im.html

          uni-im是云端一体的、全平台的、免费的、开源即时通讯系统。

          • 基于uni-app,App、小程序、web全端兼容
          • 基于uniCloud,前后端都使用js开发
          • 基于uni-push2,专业稳定的全端推送系统
          • 基于uni-id,完善的账户体系
          • 支持服务端为非uniCloud(比如:应用服务端的开发语言是php、java、go、.net、python、c#等)或 不基于uni-id-pages 开发的项目接入

          4.1.2 优势

          • 比较详细的文档支持
          • 前端工作量大大减少
          • 全端可用
          • App端支持nvue,更好的长列表性能。list组件性能优势详情参考
          • 中心化响应式数据管理,切换会话无需重新加载数据,更流畅的体验
          • App端聚合多个手机厂商推送通道,app不在线也可以收到消息

          4.1.3 劣势

          • 服务收费,且价格不便宜
          • 无法私有化部署

          CDN流量、出网流量、存储费用比较贵

          4.1.4 费用

          按量计费:

          • 调用10000次云函数0.0133元
          • 调用10000次数据库查询仅0.015元

          私聊一次: 1次云函数请求、2次数据库读操作、2次数据库写操作、1次uni-push2推送操作,即 (1 * 0.0133 + 2 * 0.015 + 2 * 0.05 + 1 * 0.0283)/10000 ≈ 0.000017元

          500人的群聊:1次云函数请求、4次数据库读操作、2次数据库写操作、1次uni-push2推送操作,即 (1 * 0.0133 + 4 * 0.015 + 2 * 0.05 + 1 * 0.0283)/10000 ≈ 0.000020元

          资源分类 资源细项 售价(元)
          云函数 资源使用量(GBs) 0.000110592
          调用次数(万次) 0.0133
          出网流量(GB) 0.8
          云数据库 容量(GB/天) 0.07
          读操作使用量(万RU) 0.015
          写操作使用量(万RU) 0.05
          云存储 容量(GB/天) 0.0043
          下载操作次数(万次) 0.01
          上传操作次数(万次) 0.01
          CDN 流量(GB) 0.18
          前端网站托管 容量(GB/天) 0.0043
          流量(GB) 0.18

          套餐计费:

          资源分类 资源细项 免费版 基础版 标准版 专业版 企业版 旗舰版
          云函数 资源使用量(GBs/月) 1000 1万 20万 40万 150万 400万
          调用次数(万次/月) 1.5 15 300 600 2400 6000
          出网流量(GB/月) 1 1 20 40 160 500
          云数据库 容量(GB) 2 2 3 5 10 10
          读操作使用量(万RU/天) 0.05 5 25 50 150 500
          写操作使用量(万WU/天) 0.03 3 15 30 100 300
          集合数量 100 100 100 100 100 100
          索引数量 400 400 400 400 400 400
          云存储 容量(GB) 5 8 10 50 100 500
          下载操作次数(万次/月) 0.2 10 200 750 1500 3750
          上传操作次数(万次/月) 0.1 5 100 300 600 1500
          CDN流量(GB/月) 1 2 10 50 150 500
          前端网页托管 容量(GB) 5 8 10 50 100 500
          CDN流量(GB/月) 1 2 10 50 150 500
          售价(元/月) 免费 5 24 82 316 688

          名词解释:

          资源分类 资源细项 说明 数据更新延迟时间
          云函数 资源使用量(GBs) 资源使用量GBs = 函数配置内存GB × 运行计费时长s。 例如,配置为256MB的函数,单次运行了1760ms,计费时长为1760ms,则单次运行的资源使用量为(256 / 1024) × (1760 / 1000) = 0.44GBs 20分钟
          调用次数 - 20分钟
          出网流量(GB) 在云函数中访问外网时产生的出网流量,包含请求三方服务器发送的数据和返回给客户端的数据。 20分钟
          云数据库 容量(GB) - 1小时
          读操作使用量(RU) 读操作使用量(Read Unit)= ceil(查询数据量KB / 4),即从数据表中读取一条4 KB数据(向上取整)计作1RU,例如读取7.6 KB的数据计作2RU。 20分钟
          写操作使用量(WU) 写操作使用量(Write Unit)= ceil(写入数据量KB / 1),即向数据表中写入一条1 KB数据(向上取整)计作1WU,例如写入1.8 KB的数据计作2WU。 20分钟
          云存储 容量(GB) - 6小时
          下载操作次数 通过CDN加速访问的次数,回源次数暂不收费。 6小时
          上传操作次数 - 6小时
          CDN 流量(GB) 通过CDN加速产生的流量,回源流量暂不收费。 6小时
          前端网站托管 容量(GB) - 6小时
          CDN 流量(GB) 通过CDN加速产生的流量,回源流量暂不收费。 6小时

          4.2 J-IM

          GIT地址:https://gitee.com/xchao/j-im

          4.2.1 简介

          J-IM 是用JAVA语言开发的轻量、高性能、单机支持几十万至百万在线用户IM,主要目标降低即时通讯门槛,快速打造低成本接入在线IM系统,通过极简洁的消息格式就可以实现多端不同协议间的消息发送如内置(Http、Websocket、Tcp自定义IM协议)等,并提供通过http协议的api接口进行消息发送无需关心接收端属于什么协议,一个消息格式搞定一切!

          4.2.2 特性

          1、高性能(单机可支持几十万至百万人同时在线) 2、轻量、可扩展性极强 3、支持集群多机部署 4、支持SSL/TLS加密传输 5、消息格式极其简洁(JSON) 6、一端口支持可插拔多种协议(Socket自定义IM协议、Websocket、Http),各协议可分别独立部署。 7、内置消息持久化(离线、历史、漫游),保证消息可靠性,高性能存储 8、各种丰富的API接口。 9、零成本部署,一键启动。

          4.2.3 劣势

          1. 项目不活跃长期不更新
          2. 没有完善的文档支持,官网打不开

          4.3 cim

          GIT地址: https://gitee.com/farsunset/cim

          4.3.1 简介

          CIM是一套完善的消息推送框架,可应用于信令推送,即时聊天,移动设备指令推送等领域。开发者可沉浸于业务开发,不用关心消息通道长连接、消息编解码协议等繁杂处理。

          CIM采用业内主流开源技术构建,易于扩展和使用,并完美支持集群部署支持海量链接,目前支持websocket,android,ios,桌面应用,系统应用等多端接入持,可应用于移动应用,物联网,智能家居,嵌入式开发,桌面应用,WEB应用即时消服务。

          4.3.2 优势

          1. 维护时间比较长,10年了 ,目前还在活跃提交代码
          2. 提供多个客户端SDK(android/.net/fluttter/ios/swift/uniapp/web)
          3. 有web/vue/android的demo,有后台管理
          4. 文档详细

          4.3.3 劣势

          1. 按照平台SDK收费,uni-app(h5,android,ios)
          20240813112230
          20240813112230

          4.4 V-IM

          GIT地址:https://gitee.com/alyouge/V-IM

          4.4.1 简介

          开源与企业版功能点对比 20240813112349

          企业版优势

          多终端支持:PC(windows、linux、mac、web) 手机(安卓、IOS、H5、小程序); 上传支持两种方案(直接存服务器和minio); 私有云代码仓库永久更新,无加密部分,不依赖第三方。 一对一技术支持。 bug修复优先级最高。 支持付费定制化需求。 功能更新频率高。 聊天记录存储在mongoDB; 支持国产化部署,服务端已对接到snowy开源项目(分支版本)。

          4.4.2 企业版收费情况

          2980元,交付源码
           

          4.4.3 劣势

          • 文档不够详细
          • git不活跃

          4.5 MobileIMSDK

          GIT地址:https://gitee.com/jackjiang/MobileIMSDK

          2024083010352720240830103538

          4.5.1 简介

          历经10年、久经考验; 超轻量级、高度提炼,lib包50KB以内; 精心封装,一套API同时支持UDP、TCP、WebSocket三种协议(可能是全网唯一开源的); 客户端支持iOS、Android、标准Java、H5(暂未开源)、微信小程序(暂未开源)、Uniap(暂未开源); 服务端基于Netty,性能卓越、易于扩展 new; 可与姊妹工程 MobileIMSDK-Web 无缝互通实现网页端聊天或推送等; 可应用于跨设备、跨网络的聊天APP、企业OA、消息推送等各种场景。

          20240813114025
          20240813114025

          4.5.2 优势

          1. 轻量级,可支持多端多协议,可扩展性极强
          2. 有自己的论坛,社区活跃,有开发者提供支持,作者响应及时
          3. 文档详细,收费版提供详细的代码注释
          4. 覆盖所有平台,所有平台客户端都提供源码,若需要定制可参考他的源码

          2024083009021120240830090249

          4.5.3 劣势

          1. 仅支持单机部署(单机支持10万人同时在线)
          2. 按照终端收费,安卓/ios/web端分开收费

          4.5.4 费用

          20240821173051
          20240821173051

          4.6 野火IM/im-server

          GIT地址:https://gitee.com/wfchat/im-server

          4.6.1 简介

          野火IM是一套通用的即时通讯和实时音视频组件,能够更加容易地赋予客户IM和RTC能力,使客户可以快速的在自有产品上添加聊天和通话功能或者直接使用野火提供的应用。使用野火可以替代云通讯产品或减少自研即时通讯和实时音视频的工作量,降低客户研发成本和难度。

          4.6.2 提供产品

          1. 即时通讯服务(IM Server)
          2. 应用服务
          3. 推送服务
          4. Android 客户端
          5. iOS 客户端
          6. PC 客户端
          7. Web 客户端
          8. 小程序 Demo
          9. uni-app Demo
          10. 机器人服务
          11. 开发平台
          12. 频道(公众号)管理系统
          13. IM 管理后台系统

          4.6.3 优势

          1. 文档齐全
          2. 支持多端

          4.6.4 劣势

          1. 价格偏贵且后续升级需要付费(原价10% )
          2. 购买SDK要绑定IP/域名,无法部署到不同机器
          3. 只通过github提供技术支持,响应会很慢
          4. 即使付费购买,产品的功能库也是闭源的,不提供源码,只提供demo源码

          4.6.4 体验

          20240830090803
          20240830090803

          4.6.5 收费

          20240830091017
          20240830091017
          20240830091029
          20240830091029
          20240830091040
          20240830091040

          5. 总结

          大厂的im比如腾讯im,都是卖服务卖套餐收费高昂,包括uniapp也是,所以不考虑。 其他都是提供SDK,基于SDK进行业务开发,SDK提供的功能差不多, 其中MobileIMSDK的资料最详细, 支持所有平台的部署,可单机10万人同时在线,10万人对我们业务来说绰绰有余了。另外MobileIMSDK有自己的社区,他的的文档是 最详细的,从社区数据来看作者比较活跃,能及时响应。 综合从费用、开发成本、文档支持、社区支持几个维度来看,MobileIMSDK比较适合。

          `,116)]))}const g=e(i,[["render",d],["__file","IM即时通信技术选型.html.vue"]]),p=JSON.parse('{"path":"/other/essay/IM%E5%8D%B3%E6%97%B6%E9%80%9A%E4%BF%A1%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B.html","title":"IM即时通信选型","lang":"zh-CN","frontmatter":{"title":"IM即时通信选型","date":"2024-08-12T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"1. 需求 服务端开发语言:Java 终端:Android、iOS、Web、小程序 核心需求:发送文字、图片、文件、语音、视频、消息缓存、消息存储、消息未读、已读、撤回,离线消息、历史消息、单聊、群聊,多端同步,以及其他一些需求。 用户管理:添加好友、查看好友列表、删除好友、查看好友信息、创建群聊、加入群聊、查看群成员信息、退出群聊、修改群昵称、拉人进...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/essay/IM%E5%8D%B3%E6%97%B6%E9%80%9A%E4%BF%A1%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"IM即时通信选型"}],["meta",{"property":"og:description","content":"1. 需求 服务端开发语言:Java 终端:Android、iOS、Web、小程序 核心需求:发送文字、图片、文件、语音、视频、消息缓存、消息存储、消息未读、已读、撤回,离线消息、历史消息、单聊、群聊,多端同步,以及其他一些需求。 用户管理:添加好友、查看好友列表、删除好友、查看好友信息、创建群聊、加入群聊、查看群成员信息、退出群聊、修改群昵称、拉人进..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20240829105122.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-01T09:56:25.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2024-08-12T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-01T09:56:25.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"IM即时通信选型\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20240829105122.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240829115908.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240830103446.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240812175557.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240813112230.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240813112349.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240830103527.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240830103538.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240813114025.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240830090211.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240830090249.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240821173051.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240830090803.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240830091017.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240830091029.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20240830091040.png\\"],\\"datePublished\\":\\"2024-08-12T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-01T09:56:25.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":2,"title":"1. 需求","slug":"_1-需求","link":"#_1-需求","children":[]},{"level":2,"title":"2. 原有方案评估","slug":"_2-原有方案评估","link":"#_2-原有方案评估","children":[{"level":3,"title":"2.1 webrtc即时通信","slug":"_2-1-webrtc即时通信","link":"#_2-1-webrtc即时通信","children":[]}]},{"level":2,"title":"3. IM通信的难点","slug":"_3-im通信的难点","link":"#_3-im通信的难点","children":[{"level":3,"title":"3.1 消息传递可靠性问题","slug":"_3-1-消息传递可靠性问题","link":"#_3-1-消息传递可靠性问题","children":[]},{"level":3,"title":"3.2 消息重复问题","slug":"_3-2-消息重复问题","link":"#_3-2-消息重复问题","children":[]},{"level":3,"title":"3.3 消息时序问题","slug":"_3-3-消息时序问题","link":"#_3-3-消息时序问题","children":[]},{"level":3,"title":"3.4 音视频QoS问题","slug":"_3-4-音视频qos问题","link":"#_3-4-音视频qos问题","children":[]},{"level":3,"title":"3.5 消息推送问题","slug":"_3-5-消息推送问题","link":"#_3-5-消息推送问题","children":[]},{"level":3,"title":"3.6 技术栈问题","slug":"_3-6-技术栈问题","link":"#_3-6-技术栈问题","children":[]}]},{"level":2,"title":"4. 技术选型目标","slug":"_4-技术选型目标","link":"#_4-技术选型目标","children":[]},{"level":2,"title":"4. 候选方案","slug":"_4-候选方案","link":"#_4-候选方案","children":[{"level":3,"title":"4.1 uni-im","slug":"_4-1-uni-im","link":"#_4-1-uni-im","children":[]},{"level":3,"title":"4.1.1 简介","slug":"_4-1-1-简介","link":"#_4-1-1-简介","children":[]},{"level":3,"title":"4.1.2 优势","slug":"_4-1-2-优势","link":"#_4-1-2-优势","children":[]},{"level":3,"title":"4.1.3 劣势","slug":"_4-1-3-劣势","link":"#_4-1-3-劣势","children":[]},{"level":3,"title":"4.1.4 费用","slug":"_4-1-4-费用","link":"#_4-1-4-费用","children":[]},{"level":3,"title":"4.2 J-IM","slug":"_4-2-j-im","link":"#_4-2-j-im","children":[]},{"level":3,"title":"4.3 cim","slug":"_4-3-cim","link":"#_4-3-cim","children":[]},{"level":3,"title":"4.4 V-IM","slug":"_4-4-v-im","link":"#_4-4-v-im","children":[]},{"level":3,"title":"4.5 MobileIMSDK","slug":"_4-5-mobileimsdk","link":"#_4-5-mobileimsdk","children":[]},{"level":3,"title":"4.6 野火IM/im-server","slug":"_4-6-野火im-im-server","link":"#_4-6-野火im-im-server","children":[]}]},{"level":2,"title":"5. 总结","slug":"_5-总结","link":"#_5-总结","children":[]}],"git":{"createdTime":1723616084000,"updatedTime":1730454985000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":5}]},"readingTime":{"minutes":19.44,"words":5832},"filePathRelative":"other/essay/IM即时通信技术选型.md","localizedDate":"2024年8月12日","excerpt":"

          1. 需求

          \\n

          服务端开发语言:Java

          \\n

          终端:Android、iOS、Web、小程序

          \\n

          核心需求:发送文字、图片、文件、语音、视频、消息缓存、消息存储、消息未读、已读、撤回,离线消息、历史消息、单聊、群聊,多端同步,以及其他一些需求。

          \\n

          用户管理:添加好友、查看好友列表、删除好友、查看好友信息、创建群聊、加入群聊、查看群成员信息、退出群聊、修改群昵称、拉人进群、踢人出群、解散群聊、填写群公告、修改群备注以及其他用户相关的需求等。

          \\n

          权限管理: 针对群聊不同用户有不同的权限,比如群主、管理员、普通成员,普通成员可以发送消息、撤回消息、删除消息、修改消息、查看消息等,管理员可以管理群成员,修改群昵称、拉人进群、踢人出群、解散群聊、填写群公告、修改群备注等。

          ","autoDesc":true}');export{g as comp,p as data}; diff --git a/assets/IO-Model.html-BT2SPf7N.js b/assets/IO-Model.html-AwEDHwi6.js similarity index 99% rename from assets/IO-Model.html-BT2SPf7N.js rename to assets/IO-Model.html-AwEDHwi6.js index bd9cb65c8..bf7df6fa7 100644 --- a/assets/IO-Model.html-BT2SPf7N.js +++ b/assets/IO-Model.html-AwEDHwi6.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const e={};function t(p,i){return l(),a("div",null,i[0]||(i[0]=[n(`

          本文是转载于知乎100%弄明白5种IO模型,原作者勤劳的小手,在原博客基础上整理了其他 与IO模型相关的优质内容。

          1、从TCP发送数据的流程说起

          所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
          +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const e={};function t(p,i){return l(),a("div",null,i[0]||(i[0]=[n(`

          本文是转载于知乎100%弄明白5种IO模型,原作者勤劳的小手,在原博客基础上整理了其他 与IO模型相关的优质内容。

          1、从TCP发送数据的流程说起

          所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
           
           需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。

          要深入的理解各种IO模型,那么必须先了解下产生各种IO的原因是什么,要知道这其中的本质问题那么我们就必须要知道一条消息是如何从一个人发送到另外一个人的;

          以两个应用程序通讯为例,我们来了解一下当“A”向"B" 发送一条消息,简单来说会经过如下流程:

          第一步:应用A把消息发送到 TCP发送缓冲区。

          第二步: TCP发送缓冲区再把消息发送出去,经过网络传递后,消息会发送到B服务器的TCP接收缓冲区。

          第三步:B再从TCP接收缓冲区去读取属于自己的数据。

          20221227100621
          20221227100621

          根据上图我们基本上了解消息发送要经过 应用A、应用A对应服务器的TCP发送缓冲区、经过网络传输后消息发送到了应用B对应服务器TCP接收缓冲区、然后最终B应用读取到消息。

          如果理解了上面的消息发送流程,那么我们下面开始进入文章的主题;

          2、阻塞IO |非阻塞IO

          们把视角切换到上面图中的第三步, 也就是应用B从TCP缓冲区中读取数据。

          20221227100706
          20221227100706

          思考一个问题:

          因为应用之间发送消息是间断性的,也就是说在上图中TCP缓冲区还没有接收到属于应用B该读取的消息时,那么此时应用B向TCP缓冲区发起读取申请,TCP接收缓冲区是应该马上告诉应用B 现在没有你的数据,还是说让应用B在这里等着,直到有数据再把数据交给应用B。

          把这个问题应用到第一个步骤也是一样,应用A在向TCP发送缓冲区发送数据时,如果TCP发送缓冲区已经满了,那么是告诉应用A现在没空间了,还是让应用A等待着,等TCP发送缓冲区有空间了再把应用A的数据访拷贝到发送缓冲区。

          2.1 什么是阻塞IO

          如果上面的问题你已经思考过了,那么其实你已经明白了什么是阻塞IO了,所谓阻塞IO就是当应用B发起读取数据申请时,在内核数据没有准备好之前,应用B会一直处于等待数据状态,直到内核把数据准备好了交给应用B才结束。

          术语描述:在应用调用recvfrom读取数据时,其系统调用直到数据包到达且被复制到应用缓冲区中或者发送错误时才返回,在此期间一直会等待,进程从调用到返回这段时间内都是被阻塞的称为阻塞IO;

          流程:

          1、应用进程向内核发起recfrom读取数据。

          2、准备数据报(应用进程阻塞)。

          3、将数据从内核负责到应用空间。

          4、复制完成后,返回成功提示。

          20221227100729
          20221227100729

          2.1.1 阻塞IO存在的问题

          如果IO阻塞了,用户线程会变成阻塞态,此时会让出CPU的使用权限,即使让出CPU使用权,也需要在寄存器中记录用户线程当前的状态(也就是上下文), 因为要保证,当用户线程恢复执行时能够正常运行(这里就是常说的线程上下文切换),那么问题就产生了。

          线程是很”贵”的资源,主要表现在:

          1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数
          2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半
          3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态
          4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大

          2.1.1 阻塞IO模型demo

          
           import java.io.IOException;
          diff --git a/assets/IO-model1.html-B5naQOu0.js b/assets/IO-model1.html-CW-n6NiB.js
          similarity index 99%
          rename from assets/IO-model1.html-B5naQOu0.js
          rename to assets/IO-model1.html-CW-n6NiB.js
          index b41192986..6f1edcef8 100644
          --- a/assets/IO-model1.html-B5naQOu0.js
          +++ b/assets/IO-model1.html-CW-n6NiB.js
          @@ -1,4 +1,4 @@
          -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function t(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

          1 BIO

          当给BIO的accept和read方法加上超时机制后,可以在代码层面解决阻塞问题,但这不是真正的非阻塞,通常我们说的非阻塞是指的操作系统层面的非阻塞,就是当accept通过jni调用native方法后,最终系统不会一直被阻塞。真正的非阻塞是操作系统增加非阻塞功能后,java同步添加java.nio出现以后才有的,因此通过java实现非阻塞,需要调用nio中的类。

          2 NIO

          3 IO多路复用

          4 异步IO

          5 事件驱动的io

          6 reactor线程模型

          reactor线程模型可参考Scalable IO in java,该书作者也是java.nio的作者

          注意

          注意reactor线程模型并不是5种io模型之一,它是一种经典的事件驱动的线程模型,它是基于IO多路复用模型衍生出来的:

          Reactor线程模式 = Reactor(I/O多路复用)+ 线程池

          Reactor负责监听和分配事件,线程池负责处理事件

          根据Reactor的数量和线程池的数量,又可以将Reactor分为三种模型

          • 单Reactor单线程模型 (固定大小为1的线程池)
          • 单Reactor多线程模型
          • 多Reactor多线程模型 (一般是主从2个Reactor)

          reactor模型中有三种角色,分别是:

          Acceptor:处理客户端新连接,并分派请求到处理器链中
          Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler
          Handler: 事件处理,如编码、解码等

          6.1 单reactor单线程

          应用:redis4.0

          20230209155247
          20230209155247

          源码示例:

          //------------------------reactor------------------
          +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function t(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

          1 BIO

          当给BIO的accept和read方法加上超时机制后,可以在代码层面解决阻塞问题,但这不是真正的非阻塞,通常我们说的非阻塞是指的操作系统层面的非阻塞,就是当accept通过jni调用native方法后,最终系统不会一直被阻塞。真正的非阻塞是操作系统增加非阻塞功能后,java同步添加java.nio出现以后才有的,因此通过java实现非阻塞,需要调用nio中的类。

          2 NIO

          3 IO多路复用

          4 异步IO

          5 事件驱动的io

          6 reactor线程模型

          reactor线程模型可参考Scalable IO in java,该书作者也是java.nio的作者

          注意

          注意reactor线程模型并不是5种io模型之一,它是一种经典的事件驱动的线程模型,它是基于IO多路复用模型衍生出来的:

          Reactor线程模式 = Reactor(I/O多路复用)+ 线程池

          Reactor负责监听和分配事件,线程池负责处理事件

          根据Reactor的数量和线程池的数量,又可以将Reactor分为三种模型

          • 单Reactor单线程模型 (固定大小为1的线程池)
          • 单Reactor多线程模型
          • 多Reactor多线程模型 (一般是主从2个Reactor)

          reactor模型中有三种角色,分别是:

          Acceptor:处理客户端新连接,并分派请求到处理器链中
          Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler
          Handler: 事件处理,如编码、解码等

          6.1 单reactor单线程

          应用:redis4.0

          20230209155247
          20230209155247

          源码示例:

          //------------------------reactor------------------
           import java.io.IOException;
           import java.net.InetSocketAddress;
           import java.nio.channels.SelectionKey;
          diff --git a/assets/Idea.html-BAc8p-a0.js b/assets/Idea.html-Bdp8uu0g.js
          similarity index 99%
          rename from assets/Idea.html-BAc8p-a0.js
          rename to assets/Idea.html-Bdp8uu0g.js
          index debbba26f..b2033e0a4 100644
          --- a/assets/Idea.html-BAc8p-a0.js
          +++ b/assets/Idea.html-Bdp8uu0g.js
          @@ -1,2 +1,2 @@
          -import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a as i,o as n}from"./app-LRWZkDAq.js";const d={};function l(s,e){return n(),t("div",null,e[0]||(e[0]=[i(`

          1、多线程debug遇到的问题

          多线程调试需要把Thread选上,至于Thread和All的区别请查看官方文档

          • All: all threads are suspended when any of the threads hits the breakpoint.

          • Thread: only the thread which hits the breakpoint is suspended.

          1
          1

          今天用idea调试mybatis-plus多数据源切换时,遇到一个有趣的问题,我有两个线程,分别进入了两个方法,因为多数据源切换使用的ThreadLocal,我想调试为何数据源切换会失败的问题。在调试过程中,我在两个方法分别获取ThreadLocalMap,调试过程如下:

          1. 我先执行了线程1,然后断点停留,查看第一个方法中threadLocalMap的结果,符合预期
          2. 然后执行线程2,查看第二个方法中threadLocalMap的结果,也符合预期
          3. 此时我又用Ctrl+鼠标左键去查看线程1中的threadLocalMap,发现变成和线程2中一样了 2

          经过第三步测试,我蒙蔽了,ThreadLocal不是线程之间不会互相干扰吗?怎么线程2修改了线程1的结果?
          这里说一下,debug中,Ctrl+鼠标左键点击字段确实能快捷查看字段值,但是其实它的本质还是查看当前线程的xxx字段,只要你没切换线程,直接用鼠标点到另一个线程中的字段,它其实只是用了你用快捷键点的那个字段名,实际还是差的当前线程的这个字段 3

          如果想看另一个线程的同名字段,需要先切换线程
          4

          另外如果你把两个线程中的字段名改成不一样,你会发现在线程2中用快捷键点线程1的字段,是取不到值的

          2、Idea指定jdk启动

          Idea升级后无法启动,查看日志报错明显的是jdk版本不兼容,需要用最新的jdk17,解决方法有多个。

          方法1:设置全局jdk为17,这个方法可以启动idea,但是不太好,毕竟其他软件可能不能使用最新的jdk

          方法2:单独给idea配置jdk,我记得以前使用eclipse有个虚拟机参数-vm可以指定,但是在idea中并不好使, 所以就去看了一下idea的启动脚本idea.sh,看一下启动脚本,就知idea启动时到底使用的是那个jdk,根据idea读取 的配置文件去配置就好了,可以设置JRE环境变量、可以指定JDK_HOME环境变量,我这里使用的是配置文件的方式,配置 idea.jdk

          20221202112129
          20221202112129
           cat ~/.config/JetBrains/IntelliJIdea2022.3/idea.jdk                                      
          +import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a as i,o as n}from"./app-BIdVr4mq.js";const d={};function l(s,e){return n(),t("div",null,e[0]||(e[0]=[i(`

          1、多线程debug遇到的问题

          多线程调试需要把Thread选上,至于Thread和All的区别请查看官方文档

          • All: all threads are suspended when any of the threads hits the breakpoint.

          • Thread: only the thread which hits the breakpoint is suspended.

          1
          1

          今天用idea调试mybatis-plus多数据源切换时,遇到一个有趣的问题,我有两个线程,分别进入了两个方法,因为多数据源切换使用的ThreadLocal,我想调试为何数据源切换会失败的问题。在调试过程中,我在两个方法分别获取ThreadLocalMap,调试过程如下:

          1. 我先执行了线程1,然后断点停留,查看第一个方法中threadLocalMap的结果,符合预期
          2. 然后执行线程2,查看第二个方法中threadLocalMap的结果,也符合预期
          3. 此时我又用Ctrl+鼠标左键去查看线程1中的threadLocalMap,发现变成和线程2中一样了 2

          经过第三步测试,我蒙蔽了,ThreadLocal不是线程之间不会互相干扰吗?怎么线程2修改了线程1的结果?
          这里说一下,debug中,Ctrl+鼠标左键点击字段确实能快捷查看字段值,但是其实它的本质还是查看当前线程的xxx字段,只要你没切换线程,直接用鼠标点到另一个线程中的字段,它其实只是用了你用快捷键点的那个字段名,实际还是差的当前线程的这个字段 3

          如果想看另一个线程的同名字段,需要先切换线程
          4

          另外如果你把两个线程中的字段名改成不一样,你会发现在线程2中用快捷键点线程1的字段,是取不到值的

          2、Idea指定jdk启动

          Idea升级后无法启动,查看日志报错明显的是jdk版本不兼容,需要用最新的jdk17,解决方法有多个。

          方法1:设置全局jdk为17,这个方法可以启动idea,但是不太好,毕竟其他软件可能不能使用最新的jdk

          方法2:单独给idea配置jdk,我记得以前使用eclipse有个虚拟机参数-vm可以指定,但是在idea中并不好使, 所以就去看了一下idea的启动脚本idea.sh,看一下启动脚本,就知idea启动时到底使用的是那个jdk,根据idea读取 的配置文件去配置就好了,可以设置JRE环境变量、可以指定JDK_HOME环境变量,我这里使用的是配置文件的方式,配置 idea.jdk

          20221202112129
          20221202112129
           cat ~/.config/JetBrains/IntelliJIdea2022.3/idea.jdk                                      
           /usr/lib/jvm/java-17-openjdk

          3、idea中git操作

          3.1 对比任意非连续的两次提交文件差异

          选中两次提交,右键Compare Versions,

          image-20221229215718576
          image-20221229215718576

          会出现一个change log的视图,这就是两次提价的差异文件

          image-20221229215820791
          image-20221229215820791

          4、debug无法进入jdk源码

          idea2022版本默认,调试时跳过底层源码,具体参考下图,把需要加入debug的源码去掉勾选 20230201173830

          `,23)]))}const o=a(d,[["render",l],["__file","Idea.html.vue"]]),r=JSON.parse('{"path":"/other/tools/Idea.html","title":"Idea","lang":"zh-CN","frontmatter":{"title":"Idea","date":"2022-07-29T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"description":"1、多线程debug遇到的问题 多线程调试需要把Thread选上,至于Thread和All的区别请查看官方文档 All: all threads are suspended when any of the threads hits the breakpoint. Thread: only the thread which hits the breakp...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/tools/Idea.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Idea"}],["meta",{"property":"og:description","content":"1、多线程debug遇到的问题 多线程调试需要把Thread选上,至于Thread和All的区别请查看官方文档 All: all threads are suspended when any of the threads hits the breakpoint. Thread: only the thread which hits the breakp..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20220729150247.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-01T09:56:25.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2022-07-29T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-01T09:56:25.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Idea\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20220729150247.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20220729145847.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20220729151140.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20220729151320.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221202112129.png\\",\\"http://afatpig.oss-cn-chengdu.aliyuncs.com/blog/image-20221229215718576.png\\",\\"http://afatpig.oss-cn-chengdu.aliyuncs.com/blog/image-20221229215820791.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230201173830.png\\"],\\"datePublished\\":\\"2022-07-29T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-01T09:56:25.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、多线程debug遇到的问题","slug":"_1、多线程debug遇到的问题","link":"#_1、多线程debug遇到的问题","children":[]},{"level":2,"title":"2、Idea指定jdk启动","slug":"_2、idea指定jdk启动","link":"#_2、idea指定jdk启动","children":[]},{"level":2,"title":"3、idea中git操作","slug":"_3、idea中git操作","link":"#_3、idea中git操作","children":[{"level":3,"title":"3.1 对比任意非连续的两次提交文件差异","slug":"_3-1-对比任意非连续的两次提交文件差异","link":"#_3-1-对比任意非连续的两次提交文件差异","children":[]}]},{"level":2,"title":"4、debug无法进入jdk源码","slug":"_4、debug无法进入jdk源码","link":"#_4、debug无法进入jdk源码","children":[]}],"git":{"createdTime":1659362219000,"updatedTime":1730454985000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":4},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":2.72,"words":817},"filePathRelative":"other/tools/Idea.md","localizedDate":"2022年7月29日","excerpt":"

          1、多线程debug遇到的问题

          \\n

          多线程调试需要把Thread选上,至于Thread和All的区别请查看官方文档

          \\n
            \\n
          • \\n

            All: all threads are suspended when any of the threads hits the breakpoint.

            \\n
          • \\n
          • \\n

            Thread: only the thread which hits the breakpoint is suspended.

            \\n
          • \\n
          ","autoDesc":true}');export{o as comp,r as data}; diff --git a/assets/ImplementSameInterface.html-kaw5w828.js b/assets/ImplementSameInterface.html-_LYgzeMO.js similarity index 99% rename from assets/ImplementSameInterface.html-kaw5w828.js rename to assets/ImplementSameInterface.html-_LYgzeMO.js index d0ff48e66..a39799558 100644 --- a/assets/ImplementSameInterface.html-kaw5w828.js +++ b/assets/ImplementSameInterface.html-_LYgzeMO.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const e={};function t(h,i){return l(),a("div",null,i[0]||(i[0]=[n(`

          1、背景

          今天看Securfity的源码,其中org.springframework.security.config.annotation.web.builders.HttpSecurity类的UML看着很奇怪,如下图所示,命名其父类和父接口都实现过SecurityBuilder,为什么自己要再次实现呢?

          20230105160622
          20230105160622

          2、探索

          我一开始注意力被泛型吸引了,想着是不是因为用了不同的泛型类的原因,为此我还专门去复习了一下泛型的东西。后来确定和泛型没关系,然后百度了一下,找到了以下网友的博客,为此我还专门写demo验证了他的博客内容,发现确实如此。 20230105161007

          2.1 demo验证

          //接口
          +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const e={};function t(h,i){return l(),a("div",null,i[0]||(i[0]=[n(`

          1、背景

          今天看Securfity的源码,其中org.springframework.security.config.annotation.web.builders.HttpSecurity类的UML看着很奇怪,如下图所示,命名其父类和父接口都实现过SecurityBuilder,为什么自己要再次实现呢?

          20230105160622
          20230105160622

          2、探索

          我一开始注意力被泛型吸引了,想着是不是因为用了不同的泛型类的原因,为此我还专门去复习了一下泛型的东西。后来确定和泛型没关系,然后百度了一下,找到了以下网友的博客,为此我还专门写demo验证了他的博客内容,发现确实如此。 20230105161007

          2.1 demo验证

          //接口
           public interface Animal {
               public void sayHi();
           }
          diff --git a/assets/InstallMysqlWithDocker.html-BJNnL9bp.js b/assets/InstallMysqlWithDocker.html-Bj1rp9yg.js
          similarity index 99%
          rename from assets/InstallMysqlWithDocker.html-BJNnL9bp.js
          rename to assets/InstallMysqlWithDocker.html-Bj1rp9yg.js
          index 4745acd42..9be2a62d6 100644
          --- a/assets/InstallMysqlWithDocker.html-BJNnL9bp.js
          +++ b/assets/InstallMysqlWithDocker.html-Bj1rp9yg.js
          @@ -1,4 +1,4 @@
          -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as l,o as e}from"./app-LRWZkDAq.js";const n={};function t(h,s){return e(),a("div",null,s[0]||(s[0]=[l(`

          1. 不要用太新的版本

          安装一定要选对版本,刚开始我使用的8.0.30镜像,一直无法启动,日志报错大概意思是我映射的目录有问题,应该是新版本mysql的安装文件有变动, 我也懒得去深究,直接换了一个版本就好了了。

          2 安装

          # 1. 下载镜像
          +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as l,o as e}from"./app-BIdVr4mq.js";const n={};function t(h,s){return e(),a("div",null,s[0]||(s[0]=[l(`

          1. 不要用太新的版本

          安装一定要选对版本,刚开始我使用的8.0.30镜像,一直无法启动,日志报错大概意思是我映射的目录有问题,应该是新版本mysql的安装文件有变动, 我也懒得去深究,直接换了一个版本就好了了。

          2 安装

          # 1. 下载镜像
           docker pull mysql:8.0.23
           # 2. 启动
           docker run -p 3306:3306 --name mysql \\
          diff --git a/assets/IntegerConstantPool.html-BpYi02Iy.js b/assets/IntegerConstantPool.html-qcjVgBE7.js
          similarity index 99%
          rename from assets/IntegerConstantPool.html-BpYi02Iy.js
          rename to assets/IntegerConstantPool.html-qcjVgBE7.js
          index 2635c5c7c..5158eeee9 100644
          --- a/assets/IntegerConstantPool.html-BpYi02Iy.js
          +++ b/assets/IntegerConstantPool.html-qcjVgBE7.js
          @@ -1,4 +1,4 @@
          -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-LRWZkDAq.js";const l={};function t(k,i){return h(),a("div",null,i[0]||(i[0]=[n(`

          1. Integer常量池默认的范围

          范围:[-128,127],Integer内部有个缓存池,最小值-128是固定的,最大的值127是可以调整的,看源码知道,最大值是和integerCacheHighPropValue有关,这个值是可以通过~java.lang.Integer.IntegerCache.high~属性指定,实际测试~~~System.setProperty("java.lang.Integer.IntegerCache.high","300")~~~不生效,使用-XX:AutoBoxCacheMax=300可以。

          private static class IntegerCache {
          +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-BIdVr4mq.js";const l={};function t(k,i){return h(),a("div",null,i[0]||(i[0]=[n(`

          1. Integer常量池默认的范围

          范围:[-128,127],Integer内部有个缓存池,最小值-128是固定的,最大的值127是可以调整的,看源码知道,最大值是和integerCacheHighPropValue有关,这个值是可以通过~java.lang.Integer.IntegerCache.high~属性指定,实际测试~~~System.setProperty("java.lang.Integer.IntegerCache.high","300")~~~不生效,使用-XX:AutoBoxCacheMax=300可以。

          private static class IntegerCache {
                   static final int low = -128;
                   static final int high;
                   static final Integer cache[];
          diff --git a/assets/JdkVersion.html-C-a3tlfZ.js b/assets/JdkVersion.html-tQ6JXAtC.js
          similarity index 99%
          rename from assets/JdkVersion.html-C-a3tlfZ.js
          rename to assets/JdkVersion.html-tQ6JXAtC.js
          index 221db4c81..6548aa948 100644
          --- a/assets/JdkVersion.html-C-a3tlfZ.js
          +++ b/assets/JdkVersion.html-tQ6JXAtC.js
          @@ -1 +1 @@
          -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a as n,o as i}from"./app-LRWZkDAq.js";const l={};function o(r,a){return i(),t("div",null,a[0]||(a[0]=[n('

          开发者注意

          Java版本现在6个月发布一个大版本,更新很快,但是对于web开发人员来说,代码层面其实新增的功能并不是那么多,开发人员只需要掌握几个主要的更新内容即可,重点关注LTS版本。

          1、Jdk11对比jdk1.8

          1.1 Java9

          1. 模块系统 模块是一个包的容器,Java 9 最大的变化之一是引入模块系统。(Jigsaw 项目) 作用:解决大型项目模块化开发的需求,更适合客户端软件,在打包时可以自定义依赖模块,根据所依赖的模块打包出一个包含jdk环境的软件,
            这样的好处是打包比较小,用户下载快,并且无需自己安装jdk环境。个人认为这项新特性对于web应用并不是那么重要,web应用无需考虑包的大小。

          2. 支持 HTTP/2 标准 HTTP/2 标准是 HTTP 协议的最新版本,新的 HTTPClient API 支持 Websocket 和 HTTP2 流以及服务器推送特性。

          3. 提供创建不可变集合的静态工厂方法 List、Set、Map 接口中,提供新的静态工厂方法直接创建不可变的集合实例。 作用:创建不可变集合更方便,一行代码就搞定,节省了开销。

          4. 私有接口方法 在接口中也允许编写 private 修饰的私有方法了。 作用:增强了接口的功能,提高了可扩展性。

          5. 轻量级的 JSON API 内置了一个轻量级的 JSON API。

          6. 引入响应式流 API Java 9 引入了新的响应式流 API。

          1.2 Java10

          1. 增加var类型推断

          1.3 Java11(LTS)

          1. HttpClient在java9引入,但是在java11进行优化升级

          ',8)]))}const s=e(l,[["render",o],["__file","JdkVersion.html.vue"]]),c=JSON.parse('{"path":"/java/other/JdkVersion.html","title":"Jdk版本","lang":"zh-CN","frontmatter":{"title":"Jdk版本","date":"2022-08-17T00:00:00.000Z","author":"chenkun","description":"开发者注意 Java版本现在6个月发布一个大版本,更新很快,但是对于web开发人员来说,代码层面其实新增的功能并不是那么多,开发人员只需要掌握几个主要的更新内容即可,重点关注LTS版本。 1、Jdk11对比jdk1.8 1.1 Java9 模块系统 模块是一个包的容器,Java 9 最大的变化之一是引入模块系统。(Jigsaw 项目) 作用:解决大型项...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/other/JdkVersion.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Jdk版本"}],["meta",{"property":"og:description","content":"开发者注意 Java版本现在6个月发布一个大版本,更新很快,但是对于web开发人员来说,代码层面其实新增的功能并不是那么多,开发人员只需要掌握几个主要的更新内容即可,重点关注LTS版本。 1、Jdk11对比jdk1.8 1.1 Java9 模块系统 模块是一个包的容器,Java 9 最大的变化之一是引入模块系统。(Jigsaw 项目) 作用:解决大型项..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-22T03:45:12.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2022-08-17T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-22T03:45:12.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Jdk版本\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-08-17T00:00:00.000Z\\",\\"dateModified\\":\\"2024-03-22T03:45:12.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、Jdk11对比jdk1.8","slug":"_1、jdk11对比jdk1-8","link":"#_1、jdk11对比jdk1-8","children":[{"level":3,"title":"1.1 Java9","slug":"_1-1-java9","link":"#_1-1-java9","children":[]},{"level":3,"title":"1.2 Java10","slug":"_1-2-java10","link":"#_1-2-java10","children":[]},{"level":3,"title":"1.3 Java11(LTS)","slug":"_1-3-java11-lts","link":"#_1-3-java11-lts","children":[]}]}],"git":{"createdTime":1660706572000,"updatedTime":1711079112000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2}]},"readingTime":{"minutes":1.54,"words":463},"filePathRelative":"java/other/JdkVersion.md","localizedDate":"2022年8月17日","excerpt":"
          \\n

          开发者注意

          \\n

          Java版本现在6个月发布一个大版本,更新很快,但是对于web开发人员来说,代码层面其实新增的功能并不是那么多,开发人员只需要掌握几个主要的更新内容即可,重点关注LTS版本。

          \\n
          \\n

          1、Jdk11对比jdk1.8

          \\n

          1.1 Java9

          \\n
            \\n
          1. \\n

            模块系统\\n模块是一个包的容器,Java 9 最大的变化之一是引入模块系统。(Jigsaw 项目)\\n作用:解决大型项目模块化开发的需求,更适合客户端软件,在打包时可以自定义依赖模块,根据所依赖的模块打包出一个包含jdk环境的软件,
            \\n这样的好处是打包比较小,用户下载快,并且无需自己安装jdk环境。个人认为这项新特性对于web应用并不是那么重要,web应用无需考虑包的大小。

            \\n
          2. \\n
          3. \\n

            支持 HTTP/2 标准\\nHTTP/2 标准是 HTTP 协议的最新版本,新的 HTTPClient API 支持 Websocket 和 HTTP2 流以及服务器推送特性。

            \\n
          4. \\n
          5. \\n

            提供创建不可变集合的静态工厂方法\\nList、Set、Map 接口中,提供新的静态工厂方法直接创建不可变的集合实例。\\n作用:创建不可变集合更方便,一行代码就搞定,节省了开销。

            \\n
          6. \\n
          7. \\n

            私有接口方法\\n在接口中也允许编写 private 修饰的私有方法了。\\n作用:增强了接口的功能,提高了可扩展性。

            \\n
          8. \\n
          9. \\n

            轻量级的 JSON API\\n内置了一个轻量级的 JSON API。

            \\n
          10. \\n
          11. \\n

            引入响应式流 API\\nJava 9 引入了新的响应式流 API。

            \\n
          12. \\n
          ","autoDesc":true}');export{s as comp,c as data}; +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a as n,o as i}from"./app-BIdVr4mq.js";const l={};function o(r,a){return i(),t("div",null,a[0]||(a[0]=[n('

          开发者注意

          Java版本现在6个月发布一个大版本,更新很快,但是对于web开发人员来说,代码层面其实新增的功能并不是那么多,开发人员只需要掌握几个主要的更新内容即可,重点关注LTS版本。

          1、Jdk11对比jdk1.8

          1.1 Java9

          1. 模块系统 模块是一个包的容器,Java 9 最大的变化之一是引入模块系统。(Jigsaw 项目) 作用:解决大型项目模块化开发的需求,更适合客户端软件,在打包时可以自定义依赖模块,根据所依赖的模块打包出一个包含jdk环境的软件,
            这样的好处是打包比较小,用户下载快,并且无需自己安装jdk环境。个人认为这项新特性对于web应用并不是那么重要,web应用无需考虑包的大小。

          2. 支持 HTTP/2 标准 HTTP/2 标准是 HTTP 协议的最新版本,新的 HTTPClient API 支持 Websocket 和 HTTP2 流以及服务器推送特性。

          3. 提供创建不可变集合的静态工厂方法 List、Set、Map 接口中,提供新的静态工厂方法直接创建不可变的集合实例。 作用:创建不可变集合更方便,一行代码就搞定,节省了开销。

          4. 私有接口方法 在接口中也允许编写 private 修饰的私有方法了。 作用:增强了接口的功能,提高了可扩展性。

          5. 轻量级的 JSON API 内置了一个轻量级的 JSON API。

          6. 引入响应式流 API Java 9 引入了新的响应式流 API。

          1.2 Java10

          1. 增加var类型推断

          1.3 Java11(LTS)

          1. HttpClient在java9引入,但是在java11进行优化升级

          ',8)]))}const s=e(l,[["render",o],["__file","JdkVersion.html.vue"]]),c=JSON.parse('{"path":"/java/other/JdkVersion.html","title":"Jdk版本","lang":"zh-CN","frontmatter":{"title":"Jdk版本","date":"2022-08-17T00:00:00.000Z","author":"chenkun","description":"开发者注意 Java版本现在6个月发布一个大版本,更新很快,但是对于web开发人员来说,代码层面其实新增的功能并不是那么多,开发人员只需要掌握几个主要的更新内容即可,重点关注LTS版本。 1、Jdk11对比jdk1.8 1.1 Java9 模块系统 模块是一个包的容器,Java 9 最大的变化之一是引入模块系统。(Jigsaw 项目) 作用:解决大型项...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/other/JdkVersion.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Jdk版本"}],["meta",{"property":"og:description","content":"开发者注意 Java版本现在6个月发布一个大版本,更新很快,但是对于web开发人员来说,代码层面其实新增的功能并不是那么多,开发人员只需要掌握几个主要的更新内容即可,重点关注LTS版本。 1、Jdk11对比jdk1.8 1.1 Java9 模块系统 模块是一个包的容器,Java 9 最大的变化之一是引入模块系统。(Jigsaw 项目) 作用:解决大型项..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-22T03:45:12.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2022-08-17T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-22T03:45:12.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Jdk版本\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-08-17T00:00:00.000Z\\",\\"dateModified\\":\\"2024-03-22T03:45:12.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、Jdk11对比jdk1.8","slug":"_1、jdk11对比jdk1-8","link":"#_1、jdk11对比jdk1-8","children":[{"level":3,"title":"1.1 Java9","slug":"_1-1-java9","link":"#_1-1-java9","children":[]},{"level":3,"title":"1.2 Java10","slug":"_1-2-java10","link":"#_1-2-java10","children":[]},{"level":3,"title":"1.3 Java11(LTS)","slug":"_1-3-java11-lts","link":"#_1-3-java11-lts","children":[]}]}],"git":{"createdTime":1660706572000,"updatedTime":1711079112000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2}]},"readingTime":{"minutes":1.54,"words":463},"filePathRelative":"java/other/JdkVersion.md","localizedDate":"2022年8月17日","excerpt":"
          \\n

          开发者注意

          \\n

          Java版本现在6个月发布一个大版本,更新很快,但是对于web开发人员来说,代码层面其实新增的功能并不是那么多,开发人员只需要掌握几个主要的更新内容即可,重点关注LTS版本。

          \\n
          \\n

          1、Jdk11对比jdk1.8

          \\n

          1.1 Java9

          \\n
            \\n
          1. \\n

            模块系统\\n模块是一个包的容器,Java 9 最大的变化之一是引入模块系统。(Jigsaw 项目)\\n作用:解决大型项目模块化开发的需求,更适合客户端软件,在打包时可以自定义依赖模块,根据所依赖的模块打包出一个包含jdk环境的软件,
            \\n这样的好处是打包比较小,用户下载快,并且无需自己安装jdk环境。个人认为这项新特性对于web应用并不是那么重要,web应用无需考虑包的大小。

            \\n
          2. \\n
          3. \\n

            支持 HTTP/2 标准\\nHTTP/2 标准是 HTTP 协议的最新版本,新的 HTTPClient API 支持 Websocket 和 HTTP2 流以及服务器推送特性。

            \\n
          4. \\n
          5. \\n

            提供创建不可变集合的静态工厂方法\\nList、Set、Map 接口中,提供新的静态工厂方法直接创建不可变的集合实例。\\n作用:创建不可变集合更方便,一行代码就搞定,节省了开销。

            \\n
          6. \\n
          7. \\n

            私有接口方法\\n在接口中也允许编写 private 修饰的私有方法了。\\n作用:增强了接口的功能,提高了可扩展性。

            \\n
          8. \\n
          9. \\n

            轻量级的 JSON API\\n内置了一个轻量级的 JSON API。

            \\n
          10. \\n
          11. \\n

            引入响应式流 API\\nJava 9 引入了新的响应式流 API。

            \\n
          12. \\n
          ","autoDesc":true}');export{s as comp,c as data}; diff --git "a/assets/Jellyfin\346\220\255\345\273\272.html-CMT6qNWD.js" "b/assets/Jellyfin\346\220\255\345\273\272.html-BxsZZcjM.js" similarity index 97% rename from "assets/Jellyfin\346\220\255\345\273\272.html-CMT6qNWD.js" rename to "assets/Jellyfin\346\220\255\345\273\272.html-BxsZZcjM.js" index 28444db7b..1285d1335 100644 --- "a/assets/Jellyfin\346\220\255\345\273\272.html-CMT6qNWD.js" +++ "b/assets/Jellyfin\346\220\255\345\273\272.html-BxsZZcjM.js" @@ -1 +1 @@ -import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as e,o as r}from"./app-LRWZkDAq.js";const i={};function a(l,t){return r(),o("div",null,t[0]||(t[0]=[e("h2",{id:"家庭影院",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#家庭影院"},[e("span",null,"家庭影院")])],-1)]))}const m=n(i,[["render",a],["__file","Jellyfin搭建.html.vue"]]),s=JSON.parse('{"path":"/myserver/Jellyfin%E6%90%AD%E5%BB%BA.html","title":"jellyfin搭建","lang":"zh-CN","frontmatter":{"title":"jellyfin搭建","date":"2024-03-22T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"家庭影院","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/myserver/Jellyfin%E6%90%AD%E5%BB%BA.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"jellyfin搭建"}],["meta",{"property":"og:description","content":"家庭影院"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-22T06:08:50.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2024-03-22T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-22T06:08:50.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"jellyfin搭建\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2024-03-22T00:00:00.000Z\\",\\"dateModified\\":\\"2024-03-22T06:08:50.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":2,"title":"家庭影院","slug":"家庭影院","link":"#家庭影院","children":[]}],"git":{"createdTime":1711079050000,"updatedTime":1711087730000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.06,"words":18},"filePathRelative":"myserver/Jellyfin搭建.md","localizedDate":"2024年3月22日","excerpt":"

          家庭影院

          \\n","autoDesc":true}');export{m as comp,s as data}; +import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as e,o as r}from"./app-BIdVr4mq.js";const i={};function a(l,t){return r(),o("div",null,t[0]||(t[0]=[e("h2",{id:"家庭影院",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#家庭影院"},[e("span",null,"家庭影院")])],-1)]))}const m=n(i,[["render",a],["__file","Jellyfin搭建.html.vue"]]),s=JSON.parse('{"path":"/myserver/Jellyfin%E6%90%AD%E5%BB%BA.html","title":"jellyfin搭建","lang":"zh-CN","frontmatter":{"title":"jellyfin搭建","date":"2024-03-22T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"家庭影院","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/myserver/Jellyfin%E6%90%AD%E5%BB%BA.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"jellyfin搭建"}],["meta",{"property":"og:description","content":"家庭影院"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-22T06:08:50.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2024-03-22T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-22T06:08:50.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"jellyfin搭建\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2024-03-22T00:00:00.000Z\\",\\"dateModified\\":\\"2024-03-22T06:08:50.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":2,"title":"家庭影院","slug":"家庭影院","link":"#家庭影院","children":[]}],"git":{"createdTime":1711079050000,"updatedTime":1711087730000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.06,"words":18},"filePathRelative":"myserver/Jellyfin搭建.md","localizedDate":"2024年3月22日","excerpt":"

          家庭影院

          \\n","autoDesc":true}');export{m as comp,s as data}; diff --git a/assets/Jenkins.html-BlEG3ZLM.js b/assets/Jenkins.html-BlvN7BiZ.js similarity index 99% rename from assets/Jenkins.html-BlEG3ZLM.js rename to assets/Jenkins.html-BlvN7BiZ.js index f505494a2..7b3507135 100644 --- a/assets/Jenkins.html-BlEG3ZLM.js +++ b/assets/Jenkins.html-BlvN7BiZ.js @@ -1,4 +1,4 @@ -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function k(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

          1、jenkins插件更新报错

          1.1 报错如下,ssl证书问题

          image-20220415144539319
          image-20220415144539319

          Jenkins(2020年及以后版本,2.260以上)安装后,插件下载时失败,网上找了各种解决方法,修改jenkins插件的下载源地址:

          找到菜单Manage Jenkins → Manage Plugins → Advanced → Update Site,

          把URL改为 https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

          或把默认地址 https://updates.jenkins.io/update-center.json 的https改为http再重启。

          我使用的是最新版本(2022-04-15),使用以上方式无效。

          1.2 解决办法

          1. 新建一个java源文件InstallCert.java,内容如下。
          2. 编译javac InstallCert.java
          3. 执行java InstallCert <hostname>出现提示后按1,回车。会生成jssecacerts 文件。hostname是mirrors.tuna.tsinghua.edu.cn,记得在jenkins中把插件仓库地址设置为https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
          4. 将第3步生成的jssecacerts 拷贝到$JAVA_HOME/jre/lib/security
          5. 重启jenkins
          6. 重新更新插件
          /*
          +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function k(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

          1、jenkins插件更新报错

          1.1 报错如下,ssl证书问题

          image-20220415144539319
          image-20220415144539319

          Jenkins(2020年及以后版本,2.260以上)安装后,插件下载时失败,网上找了各种解决方法,修改jenkins插件的下载源地址:

          找到菜单Manage Jenkins → Manage Plugins → Advanced → Update Site,

          把URL改为 https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

          或把默认地址 https://updates.jenkins.io/update-center.json 的https改为http再重启。

          我使用的是最新版本(2022-04-15),使用以上方式无效。

          1.2 解决办法

          1. 新建一个java源文件InstallCert.java,内容如下。
          2. 编译javac InstallCert.java
          3. 执行java InstallCert <hostname>出现提示后按1,回车。会生成jssecacerts 文件。hostname是mirrors.tuna.tsinghua.edu.cn,记得在jenkins中把插件仓库地址设置为https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
          4. 将第3步生成的jssecacerts 拷贝到$JAVA_HOME/jre/lib/security
          5. 重启jenkins
          6. 重新更新插件
          /*
            * Copyright 2006 Sun Microsystems, Inc.  All Rights Reserved.
            *
            * Redistribution and use in source and binary forms, with or without
          diff --git a/assets/Jwt.html-7z10_PeO.js b/assets/Jwt.html-CIXzxbJn.js
          similarity index 99%
          rename from assets/Jwt.html-7z10_PeO.js
          rename to assets/Jwt.html-CIXzxbJn.js
          index 80fac5e0b..646131781 100644
          --- a/assets/Jwt.html-7z10_PeO.js
          +++ b/assets/Jwt.html-CIXzxbJn.js
          @@ -1,4 +1,4 @@
          -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-LRWZkDAq.js";const e={};function l(h,i){return t(),a("div",null,i[0]||(i[0]=[n(`

          1、jwt在服务端如何校验的?

          之前一直用jwt但是仅仅了解他的基本原理没有去思考一个问题——服务端是如何校验jwt的?

          了解过jwt原理的同学都知道jwt是可以自校验的,token里面有header,payload,我有个想法就是如果用户随便生成一个token,那后端是如何知道这个token不能用?或者我把开发环境的token拿到生产环境使用 是否可行?

          jwt用户认证流程如下,因jwt的token是无状态的,所以每次请求都要经过过滤器进行校验,第一次登陆后把生成的token缓存到redis,当校验时如果找到对应token则继续,解析head中userid信息,根据userid查用户角色权限等信息,然后再设置到security的context中,具体代码如下

          Jwt认证
          Jwt认证
          @Slf4j
          +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-BIdVr4mq.js";const e={};function l(h,i){return t(),a("div",null,i[0]||(i[0]=[n(`

          1、jwt在服务端如何校验的?

          之前一直用jwt但是仅仅了解他的基本原理没有去思考一个问题——服务端是如何校验jwt的?

          了解过jwt原理的同学都知道jwt是可以自校验的,token里面有header,payload,我有个想法就是如果用户随便生成一个token,那后端是如何知道这个token不能用?或者我把开发环境的token拿到生产环境使用 是否可行?

          jwt用户认证流程如下,因jwt的token是无状态的,所以每次请求都要经过过滤器进行校验,第一次登陆后把生成的token缓存到redis,当校验时如果找到对应token则继续,解析head中userid信息,根据userid查用户角色权限等信息,然后再设置到security的context中,具体代码如下

          Jwt认证
          Jwt认证
          @Slf4j
           @Component
           public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
           
          diff --git a/assets/Manjaro.html-Bb8Od0IV.js b/assets/Manjaro.html-e_kr8ypm.js
          similarity index 99%
          rename from assets/Manjaro.html-Bb8Od0IV.js
          rename to assets/Manjaro.html-e_kr8ypm.js
          index dc94cd954..03280e835 100644
          --- a/assets/Manjaro.html-Bb8Od0IV.js
          +++ b/assets/Manjaro.html-e_kr8ypm.js
          @@ -1,4 +1,4 @@
          -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const l={};function t(h,s){return e(),a("div",null,s[0]||(s[0]=[n(`

          1、降级软件包

          安装downgrade程序 sudo pacman -S downgrade 降级 sudo DOWNGRADE_FROM_ALA=1 downgrade xxx包 注意DOWNGRADE_FROM_ALA=1一定要按照我上边这样写,不能单独export DOWNGRADE_FROM_ALA=1 设置忽略升级的包 第二步会让你选择更新的时候是否要忽略更新,选择y的话,它会在/etc/pacman.conf添加一个忽略,如果不想湖绿,把下面的IgnorePkg注释即可

          image-20220322171440300
          image-20220322171440300

          2、开机报错failed to start rotate log files

          2.1 分析问题

          1. logrotate是什么 按照老套路分析,先百度了一下logrotate是什么,参考,说白了就是个日志切割,和java里面的差不多。就是防止单文件日志过大,按照一定的规则切割成多个日志,或者删除,比如设置超过一个月直接删除,或者超过10M直接删除等等。
          2. 查看logrotate是什么时候启动,以及启动后的状态。首先我们知道它是一个systemctl启动的service服务。那就到/lib/systemd/system下看一下ll |grep rotate
           ll|grep rota
          +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const l={};function t(h,s){return e(),a("div",null,s[0]||(s[0]=[n(`

          1、降级软件包

          安装downgrade程序 sudo pacman -S downgrade 降级 sudo DOWNGRADE_FROM_ALA=1 downgrade xxx包 注意DOWNGRADE_FROM_ALA=1一定要按照我上边这样写,不能单独export DOWNGRADE_FROM_ALA=1 设置忽略升级的包 第二步会让你选择更新的时候是否要忽略更新,选择y的话,它会在/etc/pacman.conf添加一个忽略,如果不想湖绿,把下面的IgnorePkg注释即可

          image-20220322171440300
          image-20220322171440300

          2、开机报错failed to start rotate log files

          2.1 分析问题

          1. logrotate是什么 按照老套路分析,先百度了一下logrotate是什么,参考,说白了就是个日志切割,和java里面的差不多。就是防止单文件日志过大,按照一定的规则切割成多个日志,或者删除,比如设置超过一个月直接删除,或者超过10M直接删除等等。
          2. 查看logrotate是什么时候启动,以及启动后的状态。首先我们知道它是一个systemctl启动的service服务。那就到/lib/systemd/system下看一下ll |grep rotate
           ll|grep rota
           -rw-r--r-- 1 root root  870  1月  8  2021 logrotate.service
           -rw-r--r-- 1 root root  191  1月  8  2021 logrotate.timer

          可以看到和这个问题一模一样的套路。 3. 到 logrotate.service查看它实际上执行的是什么命令

          $ cat logrotate.service     
           [Unit]
          diff --git a/assets/MemoryModel.html-DglTwkK-.js b/assets/MemoryModel.html-BbniDibC.js
          similarity index 99%
          rename from assets/MemoryModel.html-DglTwkK-.js
          rename to assets/MemoryModel.html-BbniDibC.js
          index dd674bef9..4ddfc4ddf 100644
          --- a/assets/MemoryModel.html-DglTwkK-.js
          +++ b/assets/MemoryModel.html-BbniDibC.js
          @@ -1,4 +1,4 @@
          -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function k(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

          环境jdk8

          1、元空间

          jdk8中用元空间取代了原来的方法区,元空间是没有上限,只要系统有可用内存那么jvm就能一直申请,那么元空间里放的是啥?比如类元信息,就是类加载器加载的类,就放在元空间。

          以下进行测试,测试之前,需要了解一个知识点,两个class相同的前提是同一个类加载器加载的同一个类,这样得到的才是同一个Class.

          import java.io.IOException;
          +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function k(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

          环境jdk8

          1、元空间

          jdk8中用元空间取代了原来的方法区,元空间是没有上限,只要系统有可用内存那么jvm就能一直申请,那么元空间里放的是啥?比如类元信息,就是类加载器加载的类,就放在元空间。

          以下进行测试,测试之前,需要了解一个知识点,两个class相同的前提是同一个类加载器加载的同一个类,这样得到的才是同一个Class.

          import java.io.IOException;
           import java.util.ArrayList;
           import java.util.List;
           
          diff --git a/assets/MountDisk.html-BzHOCk5h.js b/assets/MountDisk.html-BzdT4jwB.js
          similarity index 99%
          rename from assets/MountDisk.html-BzHOCk5h.js
          rename to assets/MountDisk.html-BzdT4jwB.js
          index 84006ef63..3503352db 100644
          --- a/assets/MountDisk.html-BzHOCk5h.js
          +++ b/assets/MountDisk.html-BzdT4jwB.js
          @@ -1,4 +1,4 @@
          -import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a as i,o as a}from"./app-LRWZkDAq.js";const l={};function p(t,s){return a(),e("div",null,s[0]||(s[0]=[i(`

          参考

          初始化Linux数据盘(fdisk)

          挂载

          划分分区并挂载磁盘

          本操作以该场景为例,当云服务器挂载了一块新的数据盘时,使用fdisk分区工具将该数据盘设为主分区,分区形式默认设置为MBR,文件系统设为ext4格式,挂载在“/mnt/sdc”下,并设置开机启动自动挂载。

          1. fdisk -l 回显类似如下信息:
          [root@ecs-test-0001 ~]# fdisk -l
          +import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a as i,o as a}from"./app-BIdVr4mq.js";const l={};function p(t,s){return a(),e("div",null,s[0]||(s[0]=[i(`

          参考

          初始化Linux数据盘(fdisk)

          挂载

          划分分区并挂载磁盘

          本操作以该场景为例,当云服务器挂载了一块新的数据盘时,使用fdisk分区工具将该数据盘设为主分区,分区形式默认设置为MBR,文件系统设为ext4格式,挂载在“/mnt/sdc”下,并设置开机启动自动挂载。

          1. fdisk -l 回显类似如下信息:
          [root@ecs-test-0001 ~]# fdisk -l
           
           Disk /dev/vda: 42.9 GB, 42949672960 bytes, 83886080 sectors
           Units = sectors of 1 * 512 = 512 bytes
          diff --git a/assets/MultiNetworkCard.html-CK4m44eU.js b/assets/MultiNetworkCard.html-Ckes4Awq.js
          similarity index 99%
          rename from assets/MultiNetworkCard.html-CK4m44eU.js
          rename to assets/MultiNetworkCard.html-Ckes4Awq.js
          index ea137ce36..62bff7bfb 100644
          --- a/assets/MultiNetworkCard.html-CK4m44eU.js
          +++ b/assets/MultiNetworkCard.html-Ckes4Awq.js
          @@ -1,4 +1,4 @@
          -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-LRWZkDAq.js";const l={};function k(e,s){return h(),a("div",null,s[0]||(s[0]=[n(`

          1.1 kde桌面双网卡内外网设置

          环境如下:

          $ screenfetch   
          +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as h}from"./app-BIdVr4mq.js";const l={};function k(e,s){return h(),a("div",null,s[0]||(s[0]=[n(`

          1.1 kde桌面双网卡内外网设置

          环境如下:

          $ screenfetch   
           
            ██████████████████  ████████     chenkun@chenkun-pc
            ██████████████████  ████████     OS: Manjaro 21.3.7 Ruah
          diff --git a/assets/MybatisPlusDataSource.html-Bql0giQE.js b/assets/MybatisPlusDataSource.html-CJvWq9xZ.js
          similarity index 99%
          rename from assets/MybatisPlusDataSource.html-Bql0giQE.js
          rename to assets/MybatisPlusDataSource.html-CJvWq9xZ.js
          index a0d5b53df..165c3c802 100644
          --- a/assets/MybatisPlusDataSource.html-Bql0giQE.js
          +++ b/assets/MybatisPlusDataSource.html-CJvWq9xZ.js
          @@ -1,4 +1,4 @@
          -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as l,o as n}from"./app-LRWZkDAq.js";const t={};function h(k,i){return n(),a("div",null,i[0]||(i[0]=[l(`

          1、问题的背景

          有两个库,ccsx_data、ccsx_weibao,默认库是ccsx_data,我在代码中使用了>mybatis-pulus的@DS()注解,想切换到ccsx_weibao这个库,但是切换一直失败,代码如下:

          	@DS("ccsx_weibao")
          +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as l,o as n}from"./app-BIdVr4mq.js";const t={};function h(k,i){return n(),a("div",null,i[0]||(i[0]=[l(`

          1、问题的背景

          有两个库,ccsx_data、ccsx_weibao,默认库是ccsx_data,我在代码中使用了>mybatis-pulus的@DS()注解,想切换到ccsx_weibao这个库,但是切换一直失败,代码如下:

          	@DS("ccsx_weibao")
           	public IPage<InstallRecordVO> queryByPage(Page page, InstallRecordSearchVO installRecordSearchVO, DataScope dataScope) {
           		Wrapper<InstallRecordSearchVO> wrapper = QueryWrapperUtil.getWrapper(installRecordSearchVO);
           		IPage<InstallRecordVO> installRecordVOIPage = installRecordMapper.queryPageByDataScope(page, wrapper, dataScope);
          diff --git a/assets/MysqlCollate.html-Ddr0cLWB.js b/assets/MysqlCollate.html-FwkcBh96.js
          similarity index 99%
          rename from assets/MysqlCollate.html-Ddr0cLWB.js
          rename to assets/MysqlCollate.html-FwkcBh96.js
          index dca8b4001..538761f6a 100644
          --- a/assets/MysqlCollate.html-Ddr0cLWB.js
          +++ b/assets/MysqlCollate.html-FwkcBh96.js
          @@ -1,4 +1,4 @@
          -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const e={};function t(h,i){return l(),a("div",null,i[0]||(i[0]=[n(`

          本文转载自此处

          在mysql中执行show create table &lt tablename>指令,可以看到一张表的建表语句,example如下:

          CREATE TABLE \`table1\` (
          +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const e={};function t(h,i){return l(),a("div",null,i[0]||(i[0]=[n(`

          本文转载自此处

          在mysql中执行show create table &lt tablename>指令,可以看到一张表的建表语句,example如下:

          CREATE TABLE \`table1\` (
               \`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
               \`field1\` text COLLATE utf8_unicode_ci NOT NULL COMMENT '字段1',
               \`field2\` varchar(128) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '字段2',
          diff --git a/assets/MysqlMasterSlave.html-5i2FdJ-q.js b/assets/MysqlMasterSlave.html-IEFzX7F8.js
          similarity index 99%
          rename from assets/MysqlMasterSlave.html-5i2FdJ-q.js
          rename to assets/MysqlMasterSlave.html-IEFzX7F8.js
          index 36b0f60d8..586ac259f 100644
          --- a/assets/MysqlMasterSlave.html-5i2FdJ-q.js
          +++ b/assets/MysqlMasterSlave.html-IEFzX7F8.js
          @@ -1,4 +1,4 @@
          -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as l,o as n}from"./app-LRWZkDAq.js";const e={};function h(t,s){return n(),a("div",null,s[0]||(s[0]=[l(`

          1、mysql主从复制

          1.1 搭建主从复制目的?

          为了实现读写分离,解决数据库性能问题,读写分离中,“读”的数据是从哪里来呢?其实他是从“写”库copy过来的

          1.2 使用docker搭建基于mysql8的主从复制

          1. 创建容器

            docker run --name mysql_master -p 3001:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:latest

            这一步创建容器的目的是查看那以及获取mysql配置文件,然后把它的配置文件copy到宿主机,这样方便直接在宿主机修改mysql配置,如果不事先把mysql配置文件获取出来,直接用docker的-v去挂载的话会有问题,无法达到把docker容器配置映射到宿主机的目的(可能是我的方式不对)

          2. 把容器内mysql配置copy到宿主机,配置文件在/etc/mysql,直接把整个目录copy到宿主机

            docker cp 容器ID@:/etc/mysql /home/user/master #master 库的配置,路径可以自由在指定
            +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as l,o as n}from"./app-BIdVr4mq.js";const e={};function h(t,s){return n(),a("div",null,s[0]||(s[0]=[l(`

            1、mysql主从复制

            1.1 搭建主从复制目的?

            为了实现读写分离,解决数据库性能问题,读写分离中,“读”的数据是从哪里来呢?其实他是从“写”库copy过来的

            1.2 使用docker搭建基于mysql8的主从复制

            1. 创建容器

              docker run --name mysql_master -p 3001:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:latest

              这一步创建容器的目的是查看那以及获取mysql配置文件,然后把它的配置文件copy到宿主机,这样方便直接在宿主机修改mysql配置,如果不事先把mysql配置文件获取出来,直接用docker的-v去挂载的话会有问题,无法达到把docker容器配置映射到宿主机的目的(可能是我的方式不对)

            2. 把容器内mysql配置copy到宿主机,配置文件在/etc/mysql,直接把整个目录copy到宿主机

              docker cp 容器ID@:/etc/mysql /home/user/master #master 库的配置,路径可以自由在指定
               docker cp 容器ID@:/etc/mysql /home/user/slave  #slave库配置
            3. 分别修改master和slave的配置文件my.cnf,在mysqld下增加以下内容

              master配置

              [mysqld]
               ## 设置server_id,一般设置为IP最后一位,直接写ip会报错,同一局域网内注意要唯一
               server_id=100  
              diff --git a/assets/MysqlNote.html-B-D9CeHp.js b/assets/MysqlNote.html-BbvwFbUm.js
              similarity index 99%
              rename from assets/MysqlNote.html-B-D9CeHp.js
              rename to assets/MysqlNote.html-BbvwFbUm.js
              index fdc09bd15..5bf7c42a3 100644
              --- a/assets/MysqlNote.html-B-D9CeHp.js
              +++ b/assets/MysqlNote.html-BbvwFbUm.js
              @@ -1,4 +1,4 @@
              -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-LRWZkDAq.js";const e={};function l(h,s){return t(),a("div",null,s[0]||(s[0]=[n(`

              1、批量插入速度慢

              项目使用的MyBatis-plus批量插入效率很低,遂百度一下原因

              在jdbc的链接上加上rewriteBatchedStatements=true参数,可以解决此问题。

              默认情况下rewriteBatchedStatements=false,jdbc批量插入会判断rewriteBatchedStatements,当为true才会执行批量语句,以下从源码(以下jdbc驱动源码版本为8.0.20)角度分析:

              1. com.mysql.cj.jdbc.StatementImpl#executeBatch
                @Override
              +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-BIdVr4mq.js";const e={};function l(h,s){return t(),a("div",null,s[0]||(s[0]=[n(`

              1、批量插入速度慢

              项目使用的MyBatis-plus批量插入效率很低,遂百度一下原因

              在jdbc的链接上加上rewriteBatchedStatements=true参数,可以解决此问题。

              默认情况下rewriteBatchedStatements=false,jdbc批量插入会判断rewriteBatchedStatements,当为true才会执行批量语句,以下从源码(以下jdbc驱动源码版本为8.0.20)角度分析:

              1. com.mysql.cj.jdbc.StatementImpl#executeBatch
                @Override
                   public int[] executeBatch() throws SQLException {
                       //注意此处打开executeBatchInternal()源码要进入ClientPreparedStatement的executeBatchInternal方法,别进入StatementImpl的executeBatchInternal方法
                       return Util.truncateAndConvertToInt(executeBatchInternal()源码要进入());
              diff --git a/assets/MysqlRemoteConnect.html-xJMa8jPk.js b/assets/MysqlRemoteConnect.html-BdtTYKvx.js
              similarity index 98%
              rename from assets/MysqlRemoteConnect.html-xJMa8jPk.js
              rename to assets/MysqlRemoteConnect.html-BdtTYKvx.js
              index c55f9d421..665b1a113 100644
              --- a/assets/MysqlRemoteConnect.html-xJMa8jPk.js
              +++ b/assets/MysqlRemoteConnect.html-BdtTYKvx.js
              @@ -1,2 +1,2 @@
              -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a as s,o as n}from"./app-LRWZkDAq.js";const a={};function l(o,e){return n(),i("div",null,e[0]||(e[0]=[s(`

              默认情况下,mysql只允许本地登录,如果要开启远程连接,则需要修改/etc/mysql/my.conf文件。Mariadb可以去/etc/my.cnf看看引用的目录配置

              1、修改/etc/mysql/my.conf

              找到bind-address = 127.0.0.1这一行 改为bind-address = 0.0.0.0即可

              二、为需要远程登录的用户赋予权限

              1、新建用户远程连接mysql数据库 grant all on . to admin@'%' identified by '123456' with grant option; flush privileges; 允许任何ip地址(%表示允许任何ip地址)的电脑用admin帐户和密码(123456)来访问这个mysql server。 注意admin账户不一定要存在。

              2、支持root用户允许远程连接mysql数据库 grant all privileges on . to 'root'@'%' identified by '123456' with grant option; flush privileges;

              三、查看系统用户

              use mysql;
              +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a as s,o as n}from"./app-BIdVr4mq.js";const a={};function l(o,e){return n(),i("div",null,e[0]||(e[0]=[s(`

              默认情况下,mysql只允许本地登录,如果要开启远程连接,则需要修改/etc/mysql/my.conf文件。Mariadb可以去/etc/my.cnf看看引用的目录配置

              1、修改/etc/mysql/my.conf

              找到bind-address = 127.0.0.1这一行 改为bind-address = 0.0.0.0即可

              二、为需要远程登录的用户赋予权限

              1、新建用户远程连接mysql数据库 grant all on . to admin@'%' identified by '123456' with grant option; flush privileges; 允许任何ip地址(%表示允许任何ip地址)的电脑用admin帐户和密码(123456)来访问这个mysql server。 注意admin账户不一定要存在。

              2、支持root用户允许远程连接mysql数据库 grant all privileges on . to 'root'@'%' identified by '123456' with grant option; flush privileges;

              三、查看系统用户

              use mysql;
               select user,host from user;

              四、放开服务器对应端口

              `,9)]))}const c=t(a,[["render",l],["__file","MysqlRemoteConnect.html.vue"]]),h=JSON.parse(`{"path":"/other/database/MysqlRemoteConnect.html","title":"Mysql开启远程连接权限","lang":"zh-CN","frontmatter":{"title":"Mysql开启远程连接权限","date":"2021-05-10T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"description":"默认情况下,mysql只允许本地登录,如果要开启远程连接,则需要修改/etc/mysql/my.conf文件。Mariadb可以去/etc/my.cnf看看引用的目录配置 1、修改/etc/mysql/my.conf 找到bind-address = 127.0.0.1这一行 改为bind-address = 0.0.0.0即可 二、为需要远程登录的用...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/database/MysqlRemoteConnect.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Mysql开启远程连接权限"}],["meta",{"property":"og:description","content":"默认情况下,mysql只允许本地登录,如果要开启远程连接,则需要修改/etc/mysql/my.conf文件。Mariadb可以去/etc/my.cnf看看引用的目录配置 1、修改/etc/mysql/my.conf 找到bind-address = 127.0.0.1这一行 改为bind-address = 0.0.0.0即可 二、为需要远程登录的用..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2021-05-10T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Mysql开启远程连接权限\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-05-10T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.76,"words":227},"filePathRelative":"other/database/MysqlRemoteConnect.md","localizedDate":"2021年5月10日","excerpt":"\\n
              \\n

              默认情况下,mysql只允许本地登录,如果要开启远程连接,则需要修改/etc/mysql/my.conf文件。Mariadb可以去/etc/my.cnf看看引用的目录配置

              \\n
              \\n

              1、修改/etc/mysql/my.conf

              \\n

              找到bind-address = 127.0.0.1这一行\\n改为bind-address = 0.0.0.0即可

              \\n

              二、为需要远程登录的用户赋予权限

              \\n

              1、新建用户远程连接mysql数据库\\ngrant all on . to admin@'%' identified by '123456' with grant option;\\nflush privileges;\\n允许任何ip地址(%表示允许任何ip地址)的电脑用admin帐户和密码(123456)来访问这个mysql server。\\n注意admin账户不一定要存在。

              ","autoDesc":true}`);export{c as comp,h as data}; diff --git a/assets/Nacos.html-CHmBYmVv.js b/assets/Nacos.html-KVubxHn8.js similarity index 98% rename from assets/Nacos.html-CHmBYmVv.js rename to assets/Nacos.html-KVubxHn8.js index 47aff5aa4..96f22fb76 100644 --- a/assets/Nacos.html-CHmBYmVv.js +++ b/assets/Nacos.html-KVubxHn8.js @@ -1 +1 @@ -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,a,o as n}from"./app-LRWZkDAq.js";const p={};function i(r,t){return n(),o("div",null,t[0]||(t[0]=[a('

              1、配置管理

              命名空间——>Group——>Data Id——>配置项

              命名空间:

              默认使用的命名空间是public,一个命名空间下可以有多个Group,多个服务。命名空间使用场景是租户粒度的隔离,可以给不同租户不同的命名空间,还可以用于隔离不同环境比如dev、test、prod。

              如下图,我给dev环境设置了两个Group,CCS代表中控系统,BOM代表bom系统。

              image-20220602103505994
              image-20220602103505994

              Group

              group是应用程序(服务)粒度的隔离,我在dev环境有两个微服务,中控微服务使用CCS分组下的配置,bom微服务使用bom分组下的配置

              DataId

              配置集,就是一个配置文件,类似springboot中的application.yml

              ',9)]))}const l=e(p,[["render",i],["__file","Nacos.html.vue"]]),d=JSON.parse('{"path":"/other/distributeservice/Nacos.html","title":"Nacos学习","lang":"zh-CN","frontmatter":{"title":"Nacos学习","date":"2021-03-17T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"description":"1、配置管理 命名空间——>Group——>Data Id——>配置项 命名空间: 默认使用的命名空间是public,一个命名空间下可以有多个Group,多个服务。命名空间使用场景是租户粒度的隔离,可以给不同租户不同的命名空间,还可以用于隔离不同环境比如dev、test、prod。 如下图,我给dev环境设置了两个Group,CCS代表中控系统,BOM...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/distributeservice/Nacos.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Nacos学习"}],["meta",{"property":"og:description","content":"1、配置管理 命名空间——>Group——>Data Id——>配置项 命名空间: 默认使用的命名空间是public,一个命名空间下可以有多个Group,多个服务。命名空间使用场景是租户粒度的隔离,可以给不同租户不同的命名空间,还可以用于隔离不同环境比如dev、test、prod。 如下图,我给dev环境设置了两个Group,CCS代表中控系统,BOM..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/image-20220602103505994.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2021-03-17T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Nacos学习\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/image-20220602103505994.png\\"],\\"datePublished\\":\\"2021-03-17T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、配置管理","slug":"_1、配置管理","link":"#_1、配置管理","children":[]}],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":4},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.7,"words":209},"filePathRelative":"other/distributeservice/Nacos.md","localizedDate":"2021年3月17日","excerpt":"

              1、配置管理

              \\n

              命名空间——>Group——>Data Id——>配置项

              \\n

              命名空间:

              \\n
              \\n

              默认使用的命名空间是public,一个命名空间下可以有多个Group,多个服务。命名空间使用场景是租户粒度的隔离,可以给不同租户不同的命名空间,还可以用于隔离不同环境比如dev、test、prod。

              \\n

              如下图,我给dev环境设置了两个Group,CCS代表中控系统,BOM代表bom系统。

              \\n
              \\n
              \\"image-20220602103505994\\"
              image-20220602103505994
              ","autoDesc":true}');export{l as comp,d as data}; +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,a,o as n}from"./app-BIdVr4mq.js";const p={};function i(r,t){return n(),o("div",null,t[0]||(t[0]=[a('

              1、配置管理

              命名空间——>Group——>Data Id——>配置项

              命名空间:

              默认使用的命名空间是public,一个命名空间下可以有多个Group,多个服务。命名空间使用场景是租户粒度的隔离,可以给不同租户不同的命名空间,还可以用于隔离不同环境比如dev、test、prod。

              如下图,我给dev环境设置了两个Group,CCS代表中控系统,BOM代表bom系统。

              image-20220602103505994
              image-20220602103505994

              Group

              group是应用程序(服务)粒度的隔离,我在dev环境有两个微服务,中控微服务使用CCS分组下的配置,bom微服务使用bom分组下的配置

              DataId

              配置集,就是一个配置文件,类似springboot中的application.yml

              ',9)]))}const l=e(p,[["render",i],["__file","Nacos.html.vue"]]),d=JSON.parse('{"path":"/other/distributeservice/Nacos.html","title":"Nacos学习","lang":"zh-CN","frontmatter":{"title":"Nacos学习","date":"2021-03-17T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"description":"1、配置管理 命名空间——>Group——>Data Id——>配置项 命名空间: 默认使用的命名空间是public,一个命名空间下可以有多个Group,多个服务。命名空间使用场景是租户粒度的隔离,可以给不同租户不同的命名空间,还可以用于隔离不同环境比如dev、test、prod。 如下图,我给dev环境设置了两个Group,CCS代表中控系统,BOM...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/distributeservice/Nacos.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Nacos学习"}],["meta",{"property":"og:description","content":"1、配置管理 命名空间——>Group——>Data Id——>配置项 命名空间: 默认使用的命名空间是public,一个命名空间下可以有多个Group,多个服务。命名空间使用场景是租户粒度的隔离,可以给不同租户不同的命名空间,还可以用于隔离不同环境比如dev、test、prod。 如下图,我给dev环境设置了两个Group,CCS代表中控系统,BOM..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/image-20220602103505994.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2021-03-17T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Nacos学习\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/image-20220602103505994.png\\"],\\"datePublished\\":\\"2021-03-17T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、配置管理","slug":"_1、配置管理","link":"#_1、配置管理","children":[]}],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":4},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.7,"words":209},"filePathRelative":"other/distributeservice/Nacos.md","localizedDate":"2021年3月17日","excerpt":"

              1、配置管理

              \\n

              命名空间——>Group——>Data Id——>配置项

              \\n

              命名空间:

              \\n
              \\n

              默认使用的命名空间是public,一个命名空间下可以有多个Group,多个服务。命名空间使用场景是租户粒度的隔离,可以给不同租户不同的命名空间,还可以用于隔离不同环境比如dev、test、prod。

              \\n

              如下图,我给dev环境设置了两个Group,CCS代表中控系统,BOM代表bom系统。

              \\n
              \\n
              \\"image-20220602103505994\\"
              image-20220602103505994
              ","autoDesc":true}');export{l as comp,d as data}; diff --git a/assets/NativeMethod.html-B7wItcKZ.js b/assets/NativeMethod.html-ClQ8_R4Z.js similarity index 99% rename from assets/NativeMethod.html-B7wItcKZ.js rename to assets/NativeMethod.html-ClQ8_R4Z.js index f84963424..78aef83d0 100644 --- a/assets/NativeMethod.html-B7wItcKZ.js +++ b/assets/NativeMethod.html-ClQ8_R4Z.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const l={};function t(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

              使用c++实现一个native方法供java调用

              环境

              实验环境linux、jdk11、gcc
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const l={};function t(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

              使用c++实现一个native方法供java调用

              环境

              实验环境linux、jdk11、gcc
               

              1、native方法

              2、自定义实现native方法

              用c++实现一个动态链接库,使用java调用链接库中的方法

              2.1 定义java源文件

              public class TestMain
               {
                   static
              diff --git a/assets/NewObject.html-DOxLULz4.js b/assets/NewObject.html-Cvalksc5.js
              similarity index 99%
              rename from assets/NewObject.html-DOxLULz4.js
              rename to assets/NewObject.html-Cvalksc5.js
              index 652e3bf63..e9579d1c9 100644
              --- a/assets/NewObject.html-DOxLULz4.js
              +++ b/assets/NewObject.html-Cvalksc5.js
              @@ -1,4 +1,4 @@
              -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function t(e,s){return l(),a("div",null,s[0]||(s[0]=[n(`

              1、在类中本地变量引用自身类,会引发的问题

              public class BaseFormBean {
              +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function t(e,s){return l(),a("div",null,s[0]||(s[0]=[n(`

              1、在类中本地变量引用自身类,会引发的问题

              public class BaseFormBean {
                   private BaseFormBean baseBean = new BaseFormBean();
                   {
                        String bar = "非静态代码块中字段";
              diff --git a/assets/OAUTH_LOGIN.html-BU7CthLn.js b/assets/OAUTH_LOGIN.html-CrucBoKd.js
              similarity index 99%
              rename from assets/OAUTH_LOGIN.html-BU7CthLn.js
              rename to assets/OAUTH_LOGIN.html-CrucBoKd.js
              index 708a0a7d7..68616852e 100644
              --- a/assets/OAUTH_LOGIN.html-BU7CthLn.js
              +++ b/assets/OAUTH_LOGIN.html-CrucBoKd.js
              @@ -1,4 +1,4 @@
              -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function t(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

              本博客介绍前后端分离项目的完整接入oauth的流程,本博客使用github来示范,因为github注册oauth应用无须审核,微信审核特别麻烦,并且个人用户无法注册

              1 oauth2

              1.1 典型应用场景

                  1. 社交帐号登录应用,比如使用微信、微博登录其它应用
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function t(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

              本博客介绍前后端分离项目的完整接入oauth的流程,本博客使用github来示范,因为github注册oauth应用无须审核,微信审核特别麻烦,并且个人用户无法注册

              1 oauth2

              1.1 典型应用场景

                  1. 社交帐号登录应用,比如使用微信、微博登录其它应用
                   2. 从第三方获取用户资料比如:手机号、邮箱、头像等
                   3. 从第三方获取业务数据,比如:通过自己的系统想从京东获取订单

              1.2 完整时序图

              假设有一个软件叫开立权限管理系统,该系统接入了github登录,则使用github登录的完整的oauth流程如下:

              sequenceDiagram
                   actor E as 资源所有者(github用户)
              diff --git a/assets/OAuth2Authentication.html-CAGOYH3n.js b/assets/OAuth2Authentication.html-azcJcZ3I.js
              similarity index 99%
              rename from assets/OAuth2Authentication.html-CAGOYH3n.js
              rename to assets/OAuth2Authentication.html-azcJcZ3I.js
              index 4a02f0e36..68f0f6c91 100644
              --- a/assets/OAuth2Authentication.html-CAGOYH3n.js
              +++ b/assets/OAuth2Authentication.html-azcJcZ3I.js
              @@ -1 +1 @@
              -import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as e,o as i}from"./app-LRWZkDAq.js";const o={};function p(s,t){return i(),n("div",null,t[0]||(t[0]=[e('

              1、时序图

              时序图
              时序图

              2、流程解析

              本流程是以使用Ruoyi对接Pig授权中心为例,进行讲解,其他网站的的oauth的原理都和这个一样,所以只要把这个流程搞懂了即可,接下来就按照真实的流程进行逐步解析。

              2.1 第1步

              用户还未登录,访问ruoyi前端,ruoyi会自动跳转到自己的登录首页
              20230104105625

              2.2 第2步

              点击SSO登录会访问如下这样子的一个url,申请授权,(127.0.0.1:3000就是授权服务器) https://127.0.0.1:3000/oauth/authorize?client_id=ruyi&response_type=code&scope=server&redirect_uri=http://127.0.0.1:1024/sso&TENANT-ID=1,当授权服务收到这个请求时会发现用户还未登录授权服务器,会重定向到授权服务器的登录页面http://127.0.0.1:3000/token/login,大概就是下面这样

              20230104105039
              20230104105039

              2.3 第3步

              用户输入帐号密码提交后pig授权中心会校验ruoyi客户端是否合法,就是验证你这个客户端是否在pigx中已经注册,验证通过再验证你输入的用户名密码是否正确,验证通过后再生成一个授权页面,就是和平时我们微信授权给第三方小程序弹出一个让你确认授权的那个界面差不多,参考下一步

              2.4 第4步

              用户点击授权, 20230104105954

              2.5 第5步

              pig授权中心生成授权码

              2.6 第6步

              pig产生授权码后,会带着这个授权码重定向到注册客户端时填的那个地址,这里就是http://127.0.0.1:1024/sso?code=U1wLD7 这个地址是在数据库注册好的,它是ruoyi前端的一个页面,并不是后端接口

              20230104110557
              20230104110557
              20230104110449
              20230104110449

              2.7 第7步

              ruoyi前端构造一个请求,并且携带code请求ruoyi后端接口, 2023010411090920230104110921

              2.8 第8步

              ruoyi后端拿到code,发送post请求到http://127.0.0.1:3000/oauth/token获取token,注意这是个oauth的默认端点,不是用户写的,在TokenEndPoint类中 20230104111117

              2.9 第9步

              pig授权中心生成token,这里生成token的逻辑可以自定义实现,具体的请参考pig的源码TokenService类 20230104111420

              2.10 第10步

              pig授权中心生成的token返回到ruoyi后端,ruoyi后端拿到token 20230104111504

              2.11 第11步

              有了token后面的就不再赘述,可以拿token访问资源了,这里其实仅仅用了oauth2的特性来做了一个sso单点登录,并没有用到oauth2的核心来进行资源服务器的访问,oauth2的核心其实是用来无需帐号密码即可访问资源服务器的资源。

              ',29)]))}const c=a(o,[["render",p],["__file","OAuth2Authentication.html.vue"]]),r=JSON.parse('{"path":"/java/framework/security/OAuth2Authentication.html","title":"Ruoyi使用oauth对接pig","lang":"zh-CN","frontmatter":{"title":"Ruoyi使用oauth对接pig","date":"2023-01-03T00:00:00.000Z","author":"chenkun","keys":null,"category":["Security","OAuth"],"description":"1、时序图 时序图时序图 2、流程解析 本流程是以使用Ruoyi对接Pig授权中心为例,进行讲解,其他网站的的oauth的原理都和这个一样,所以只要把这个流程搞懂了即可,接下来就按照真实的流程进行逐步解析。 2.1 第1步 用户还未登录,访问ruoyi前端,ruoyi会自动跳转到自己的登录首页 20230104105625 2.2 第2步 点击SSO登...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/security/OAuth2Authentication.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Ruoyi使用oauth对接pig"}],["meta",{"property":"og:description","content":"1、时序图 时序图时序图 2、流程解析 本流程是以使用Ruoyi对接Pig授权中心为例,进行讲解,其他网站的的oauth的原理都和这个一样,所以只要把这个流程搞懂了即可,接下来就按照真实的流程进行逐步解析。 2.1 第1步 用户还未登录,访问ruoyi前端,ruoyi会自动跳转到自己的登录首页 20230104105625 2.2 第2步 点击SSO登..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/oauth.drawio.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-01T09:56:25.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2023-01-03T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-01T09:56:25.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Ruoyi使用oauth对接pig\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/oauth.drawio.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104105625.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104105039.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104105954.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104110557.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104110449.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104110909.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104110921.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104111117.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104111420.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104111504.png\\"],\\"datePublished\\":\\"2023-01-03T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-01T09:56:25.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、时序图","slug":"_1、时序图","link":"#_1、时序图","children":[]},{"level":2,"title":"2、流程解析","slug":"_2、流程解析","link":"#_2、流程解析","children":[{"level":3,"title":"2.1 第1步","slug":"_2-1-第1步","link":"#_2-1-第1步","children":[]},{"level":3,"title":"2.2 第2步","slug":"_2-2-第2步","link":"#_2-2-第2步","children":[]},{"level":3,"title":"2.3 第3步","slug":"_2-3-第3步","link":"#_2-3-第3步","children":[]},{"level":3,"title":"2.4 第4步","slug":"_2-4-第4步","link":"#_2-4-第4步","children":[]},{"level":3,"title":"2.5 第5步","slug":"_2-5-第5步","link":"#_2-5-第5步","children":[]},{"level":3,"title":"2.6 第6步","slug":"_2-6-第6步","link":"#_2-6-第6步","children":[]},{"level":3,"title":"2.7 第7步","slug":"_2-7-第7步","link":"#_2-7-第7步","children":[]},{"level":3,"title":"2.8 第8步","slug":"_2-8-第8步","link":"#_2-8-第8步","children":[]},{"level":3,"title":"2.9 第9步","slug":"_2-9-第9步","link":"#_2-9-第9步","children":[]},{"level":3,"title":"2.10 第10步","slug":"_2-10-第10步","link":"#_2-10-第10步","children":[]},{"level":3,"title":"2.11 第11步","slug":"_2-11-第11步","link":"#_2-11-第11步","children":[]}]}],"git":{"createdTime":1672740702000,"updatedTime":1730454985000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":3}]},"readingTime":{"minutes":2.4,"words":720},"filePathRelative":"java/framework/security/OAuth2Authentication.md","localizedDate":"2023年1月3日","excerpt":"

              1、时序图

              \\n
              \\"时序图\\"
              时序图
              \\n

              2、流程解析

              \\n
              \\n

              本流程是以使用Ruoyi对接Pig授权中心为例,进行讲解,其他网站的的oauth的原理都和这个一样,所以只要把这个流程搞懂了即可,接下来就按照真实的流程进行逐步解析。

              \\n
              ","autoDesc":true}');export{c as comp,r as data}; +import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as e,o as i}from"./app-BIdVr4mq.js";const o={};function p(s,t){return i(),n("div",null,t[0]||(t[0]=[e('

              1、时序图

              时序图
              时序图

              2、流程解析

              本流程是以使用Ruoyi对接Pig授权中心为例,进行讲解,其他网站的的oauth的原理都和这个一样,所以只要把这个流程搞懂了即可,接下来就按照真实的流程进行逐步解析。

              2.1 第1步

              用户还未登录,访问ruoyi前端,ruoyi会自动跳转到自己的登录首页
              20230104105625

              2.2 第2步

              点击SSO登录会访问如下这样子的一个url,申请授权,(127.0.0.1:3000就是授权服务器) https://127.0.0.1:3000/oauth/authorize?client_id=ruyi&response_type=code&scope=server&redirect_uri=http://127.0.0.1:1024/sso&TENANT-ID=1,当授权服务收到这个请求时会发现用户还未登录授权服务器,会重定向到授权服务器的登录页面http://127.0.0.1:3000/token/login,大概就是下面这样

              20230104105039
              20230104105039

              2.3 第3步

              用户输入帐号密码提交后pig授权中心会校验ruoyi客户端是否合法,就是验证你这个客户端是否在pigx中已经注册,验证通过再验证你输入的用户名密码是否正确,验证通过后再生成一个授权页面,就是和平时我们微信授权给第三方小程序弹出一个让你确认授权的那个界面差不多,参考下一步

              2.4 第4步

              用户点击授权, 20230104105954

              2.5 第5步

              pig授权中心生成授权码

              2.6 第6步

              pig产生授权码后,会带着这个授权码重定向到注册客户端时填的那个地址,这里就是http://127.0.0.1:1024/sso?code=U1wLD7 这个地址是在数据库注册好的,它是ruoyi前端的一个页面,并不是后端接口

              20230104110557
              20230104110557
              20230104110449
              20230104110449

              2.7 第7步

              ruoyi前端构造一个请求,并且携带code请求ruoyi后端接口, 2023010411090920230104110921

              2.8 第8步

              ruoyi后端拿到code,发送post请求到http://127.0.0.1:3000/oauth/token获取token,注意这是个oauth的默认端点,不是用户写的,在TokenEndPoint类中 20230104111117

              2.9 第9步

              pig授权中心生成token,这里生成token的逻辑可以自定义实现,具体的请参考pig的源码TokenService类 20230104111420

              2.10 第10步

              pig授权中心生成的token返回到ruoyi后端,ruoyi后端拿到token 20230104111504

              2.11 第11步

              有了token后面的就不再赘述,可以拿token访问资源了,这里其实仅仅用了oauth2的特性来做了一个sso单点登录,并没有用到oauth2的核心来进行资源服务器的访问,oauth2的核心其实是用来无需帐号密码即可访问资源服务器的资源。

              ',29)]))}const c=a(o,[["render",p],["__file","OAuth2Authentication.html.vue"]]),r=JSON.parse('{"path":"/java/framework/security/OAuth2Authentication.html","title":"Ruoyi使用oauth对接pig","lang":"zh-CN","frontmatter":{"title":"Ruoyi使用oauth对接pig","date":"2023-01-03T00:00:00.000Z","author":"chenkun","keys":null,"category":["Security","OAuth"],"description":"1、时序图 时序图时序图 2、流程解析 本流程是以使用Ruoyi对接Pig授权中心为例,进行讲解,其他网站的的oauth的原理都和这个一样,所以只要把这个流程搞懂了即可,接下来就按照真实的流程进行逐步解析。 2.1 第1步 用户还未登录,访问ruoyi前端,ruoyi会自动跳转到自己的登录首页 20230104105625 2.2 第2步 点击SSO登...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/security/OAuth2Authentication.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Ruoyi使用oauth对接pig"}],["meta",{"property":"og:description","content":"1、时序图 时序图时序图 2、流程解析 本流程是以使用Ruoyi对接Pig授权中心为例,进行讲解,其他网站的的oauth的原理都和这个一样,所以只要把这个流程搞懂了即可,接下来就按照真实的流程进行逐步解析。 2.1 第1步 用户还未登录,访问ruoyi前端,ruoyi会自动跳转到自己的登录首页 20230104105625 2.2 第2步 点击SSO登..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/oauth.drawio.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-01T09:56:25.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2023-01-03T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-01T09:56:25.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Ruoyi使用oauth对接pig\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/oauth.drawio.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104105625.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104105039.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104105954.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104110557.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104110449.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104110909.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104110921.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104111117.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104111420.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104111504.png\\"],\\"datePublished\\":\\"2023-01-03T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-01T09:56:25.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、时序图","slug":"_1、时序图","link":"#_1、时序图","children":[]},{"level":2,"title":"2、流程解析","slug":"_2、流程解析","link":"#_2、流程解析","children":[{"level":3,"title":"2.1 第1步","slug":"_2-1-第1步","link":"#_2-1-第1步","children":[]},{"level":3,"title":"2.2 第2步","slug":"_2-2-第2步","link":"#_2-2-第2步","children":[]},{"level":3,"title":"2.3 第3步","slug":"_2-3-第3步","link":"#_2-3-第3步","children":[]},{"level":3,"title":"2.4 第4步","slug":"_2-4-第4步","link":"#_2-4-第4步","children":[]},{"level":3,"title":"2.5 第5步","slug":"_2-5-第5步","link":"#_2-5-第5步","children":[]},{"level":3,"title":"2.6 第6步","slug":"_2-6-第6步","link":"#_2-6-第6步","children":[]},{"level":3,"title":"2.7 第7步","slug":"_2-7-第7步","link":"#_2-7-第7步","children":[]},{"level":3,"title":"2.8 第8步","slug":"_2-8-第8步","link":"#_2-8-第8步","children":[]},{"level":3,"title":"2.9 第9步","slug":"_2-9-第9步","link":"#_2-9-第9步","children":[]},{"level":3,"title":"2.10 第10步","slug":"_2-10-第10步","link":"#_2-10-第10步","children":[]},{"level":3,"title":"2.11 第11步","slug":"_2-11-第11步","link":"#_2-11-第11步","children":[]}]}],"git":{"createdTime":1672740702000,"updatedTime":1730454985000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":3}]},"readingTime":{"minutes":2.4,"words":720},"filePathRelative":"java/framework/security/OAuth2Authentication.md","localizedDate":"2023年1月3日","excerpt":"

              1、时序图

              \\n
              \\"时序图\\"
              时序图
              \\n

              2、流程解析

              \\n
              \\n

              本流程是以使用Ruoyi对接Pig授权中心为例,进行讲解,其他网站的的oauth的原理都和这个一样,所以只要把这个流程搞懂了即可,接下来就按照真实的流程进行逐步解析。

              \\n
              ","autoDesc":true}');export{c as comp,r as data}; diff --git a/assets/ObjectReference.html-CrcN467V.js b/assets/ObjectReference.html-D9v0-LGB.js similarity index 99% rename from assets/ObjectReference.html-CrcN467V.js rename to assets/ObjectReference.html-D9v0-LGB.js index 4534876bd..96d7b2b71 100644 --- a/assets/ObjectReference.html-CrcN467V.js +++ b/assets/ObjectReference.html-D9v0-LGB.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const t={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

              1、强引用

              StrongReference,java中默认的引用类型都是强引用,比如\`Objectg obj = new Object()\`,这个obj就是强引用。强引用的特性是只要引用存在,被引用的对象就不会被垃圾回收器回收,这个比较简单,不做测试。
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const t={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

              1、强引用

              StrongReference,java中默认的引用类型都是强引用,比如\`Objectg obj = new Object()\`,这个obj就是强引用。强引用的特性是只要引用存在,被引用的对象就不会被垃圾回收器回收,这个比较简单,不做测试。
               

              2、软引用

              SoftReference,软引用代码比较简单,直接调用其构造函数即可构造一个软引用对象。软引用的特性是只要虚拟机内存够用,则这个对象会 一直存在,不会被垃圾回收器回收,当内存不够时会触发垃圾回收,当回收后内存够用则不会回收软引用的对象,当执行了一次回收后内存依然不够才会考虑回收软引用的对象。

                  /**
                    * 堆内存分配30m,默认情况老年代和新生代2:1,eden和survivor是8:1,因此老年代是
                    * 20m,新生代10m,其中eden是8m,suvivor是1m,因此新生代总共8+1=9m,还有1m是浪* 费的
              diff --git a/assets/OncePerRequestFilter.html-D66nF_K3.js b/assets/OncePerRequestFilter.html-BCQqcD7O.js
              similarity index 98%
              rename from assets/OncePerRequestFilter.html-D66nF_K3.js
              rename to assets/OncePerRequestFilter.html-BCQqcD7O.js
              index 6a47ff8c4..afda094df 100644
              --- a/assets/OncePerRequestFilter.html-D66nF_K3.js
              +++ b/assets/OncePerRequestFilter.html-BCQqcD7O.js
              @@ -1 +1 @@
              -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as r,a as n,o as i}from"./app-LRWZkDAq.js";const a={};function o(l,e){return i(),r("div",null,e[0]||(e[0]=[n('

              1、OncePerRequestFilter

              org.springframework.web.filter.OncePerRequestFilter是springweb中的一个过滤器,是为了确保一个请求只被过滤器执行一次。什么?难道一个请求还能被同一个过滤器执行多次? 其实是有这中可能的,在不同版本下的servlet下,过滤器的行为是不一样的。比如在servlet-2.3中,Filter会过滤一切请求,包括服务器内部使用forward转发请求和<%@ include file="/index.jsp"%>的情况。到了servlet-2.4中Filter默认下只拦截外部提交的请求,forward和include这些内部转发都不会被过滤。因为Springweb无法保证每个用户使用的Servlet容器是一样的,因此,在有些场景如果我们要确保同一个请求只能被过滤器处理一次,那就需要spring自己来实现了,因此有了这个类。其注释下写的很清楚,就是为了要陈any servlet container都确保只执行一次。

              20221103172449
              20221103172449

              最开始被误导是因为看了这篇20221103173249 看完这篇,我就做实验想复现他说的这个,因为我用的springboot,内嵌的tomcat是9.x版本,对应的servlet是4.0,这个版本servlet不存在它说的问题,所以一直无法复现,导致我理解出现了偏差。后来看了别人博客,才知道怎么回事。

              2、参考

              Spring的OncePerRequestFilter过滤器
              Spring的OncePerRequestFilter的作用

              ',6)]))}const p=t(a,[["render",o],["__file","OncePerRequestFilter.html.vue"]]),g=JSON.parse('{"path":"/java/framework/spring/OncePerRequestFilter.html","title":"OncePerRequestFilter","lang":"zh-CN","frontmatter":{"title":"OncePerRequestFilter","date":"2017-05-29T00:00:00.000Z","author":"chensino","description":"1、OncePerRequestFilter org.springframework.web.filter.OncePerRequestFilter是springweb中的一个过滤器,是为了确保一个请求只被过滤器执行一次。什么?难道一个请求还能被同一个过滤器执行多次? 其实是有这中可能的,在不同版本下的servlet下,过滤器的行为是不一样的。比如在s...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/spring/OncePerRequestFilter.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"OncePerRequestFilter"}],["meta",{"property":"og:description","content":"1、OncePerRequestFilter org.springframework.web.filter.OncePerRequestFilter是springweb中的一个过滤器,是为了确保一个请求只被过滤器执行一次。什么?难道一个请求还能被同一个过滤器执行多次? 其实是有这中可能的,在不同版本下的servlet下,过滤器的行为是不一样的。比如在s..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221103172449.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2017-05-29T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"OncePerRequestFilter\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221103172449.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221103173249.png\\"],\\"datePublished\\":\\"2017-05-29T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":2,"title":"1、OncePerRequestFilter","slug":"_1、onceperrequestfilter","link":"#_1、onceperrequestfilter","children":[]},{"level":2,"title":"2、参考","slug":"_2、参考","link":"#_2、参考","children":[]}],"git":{"createdTime":1667467771000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":3}]},"readingTime":{"minutes":1.26,"words":378},"filePathRelative":"java/framework/spring/OncePerRequestFilter.md","localizedDate":"2017年5月29日","excerpt":"

              1、OncePerRequestFilter

              \\n

              org.springframework.web.filter.OncePerRequestFilter是springweb中的一个过滤器,是为了确保一个请求只被过滤器执行一次。什么?难道一个请求还能被同一个过滤器执行多次?\\n其实是有这中可能的,在不同版本下的servlet下,过滤器的行为是不一样的。比如在servlet-2.3中,Filter会过滤一切请求,包括服务器内部使用forward转发请求和<%@ include file=\\"/index.jsp\\"%>的情况。到了servlet-2.4中Filter默认下只拦截外部提交的请求,forward和include这些内部转发都不会被过滤。因为Springweb无法保证每个用户使用的Servlet容器是一样的,因此,在有些场景如果我们要确保同一个请求只能被过滤器处理一次,那就需要spring自己来实现了,因此有了这个类。其注释下写的很清楚,就是为了要陈any servlet container都确保只执行一次。

              ","autoDesc":true}');export{p as comp,g as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as r,a as n,o as i}from"./app-BIdVr4mq.js";const a={};function o(l,e){return i(),r("div",null,e[0]||(e[0]=[n('

              1、OncePerRequestFilter

              org.springframework.web.filter.OncePerRequestFilter是springweb中的一个过滤器,是为了确保一个请求只被过滤器执行一次。什么?难道一个请求还能被同一个过滤器执行多次? 其实是有这中可能的,在不同版本下的servlet下,过滤器的行为是不一样的。比如在servlet-2.3中,Filter会过滤一切请求,包括服务器内部使用forward转发请求和<%@ include file="/index.jsp"%>的情况。到了servlet-2.4中Filter默认下只拦截外部提交的请求,forward和include这些内部转发都不会被过滤。因为Springweb无法保证每个用户使用的Servlet容器是一样的,因此,在有些场景如果我们要确保同一个请求只能被过滤器处理一次,那就需要spring自己来实现了,因此有了这个类。其注释下写的很清楚,就是为了要陈any servlet container都确保只执行一次。

              20221103172449
              20221103172449

              最开始被误导是因为看了这篇20221103173249 看完这篇,我就做实验想复现他说的这个,因为我用的springboot,内嵌的tomcat是9.x版本,对应的servlet是4.0,这个版本servlet不存在它说的问题,所以一直无法复现,导致我理解出现了偏差。后来看了别人博客,才知道怎么回事。

              2、参考

              Spring的OncePerRequestFilter过滤器
              Spring的OncePerRequestFilter的作用

              ',6)]))}const p=t(a,[["render",o],["__file","OncePerRequestFilter.html.vue"]]),g=JSON.parse('{"path":"/java/framework/spring/OncePerRequestFilter.html","title":"OncePerRequestFilter","lang":"zh-CN","frontmatter":{"title":"OncePerRequestFilter","date":"2017-05-29T00:00:00.000Z","author":"chensino","description":"1、OncePerRequestFilter org.springframework.web.filter.OncePerRequestFilter是springweb中的一个过滤器,是为了确保一个请求只被过滤器执行一次。什么?难道一个请求还能被同一个过滤器执行多次? 其实是有这中可能的,在不同版本下的servlet下,过滤器的行为是不一样的。比如在s...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/spring/OncePerRequestFilter.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"OncePerRequestFilter"}],["meta",{"property":"og:description","content":"1、OncePerRequestFilter org.springframework.web.filter.OncePerRequestFilter是springweb中的一个过滤器,是为了确保一个请求只被过滤器执行一次。什么?难道一个请求还能被同一个过滤器执行多次? 其实是有这中可能的,在不同版本下的servlet下,过滤器的行为是不一样的。比如在s..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221103172449.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2017-05-29T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"OncePerRequestFilter\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221103172449.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221103173249.png\\"],\\"datePublished\\":\\"2017-05-29T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":2,"title":"1、OncePerRequestFilter","slug":"_1、onceperrequestfilter","link":"#_1、onceperrequestfilter","children":[]},{"level":2,"title":"2、参考","slug":"_2、参考","link":"#_2、参考","children":[]}],"git":{"createdTime":1667467771000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":3}]},"readingTime":{"minutes":1.26,"words":378},"filePathRelative":"java/framework/spring/OncePerRequestFilter.md","localizedDate":"2017年5月29日","excerpt":"

              1、OncePerRequestFilter

              \\n

              org.springframework.web.filter.OncePerRequestFilter是springweb中的一个过滤器,是为了确保一个请求只被过滤器执行一次。什么?难道一个请求还能被同一个过滤器执行多次?\\n其实是有这中可能的,在不同版本下的servlet下,过滤器的行为是不一样的。比如在servlet-2.3中,Filter会过滤一切请求,包括服务器内部使用forward转发请求和<%@ include file=\\"/index.jsp\\"%>的情况。到了servlet-2.4中Filter默认下只拦截外部提交的请求,forward和include这些内部转发都不会被过滤。因为Springweb无法保证每个用户使用的Servlet容器是一样的,因此,在有些场景如果我们要确保同一个请求只能被过滤器处理一次,那就需要spring自己来实现了,因此有了这个类。其注释下写的很清楚,就是为了要陈any servlet container都确保只执行一次。

              ","autoDesc":true}');export{p as comp,g as data}; diff --git a/assets/OncePerRequestFilter.html-B3TQmcGq.js b/assets/OncePerRequestFilter.html-B_WUT3Wi.js similarity index 99% rename from assets/OncePerRequestFilter.html-B3TQmcGq.js rename to assets/OncePerRequestFilter.html-B_WUT3Wi.js index 25dd733a8..2de53b1e0 100644 --- a/assets/OncePerRequestFilter.html-B3TQmcGq.js +++ b/assets/OncePerRequestFilter.html-B_WUT3Wi.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as t,o as e}from"./app-LRWZkDAq.js";const n={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[t(`

              1、OnecePerRequestFilter初识

              第一次接触这个类,在SpringSecurity中,大概百度了一下,知道此类是限制一次请求只
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as t,o as e}from"./app-BIdVr4mq.js";const n={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[t(`

              1、OnecePerRequestFilter初识

              第一次接触这个类,在SpringSecurity中,大概百度了一下,知道此类是限制一次请求只
               走一次过滤器,但是我不明白为啥要做这个限制,或者说难道还有一次请求会走两次过滤器?

              1.1 源码doc

              学习一个框架最好的文档肯定是看官方doc,以下是官方doc对此类的注释,简单来说就是确保 一个请求在一个过滤器只执行一次doFilter,因在在不同版本的Servlet容器中是存在多次执 行doFilter的可能的,比如一个request forward到另一个request,在servlet2.0和3.0 表现可能都不一样,在Tomcat和weblogic容器中可能表现也不一样,为了统一此行为,所以 Spring官方提供了此类。

              Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal method with HttpServletRequest and HttpServletResponse arguments.
               As of Servlet 3.0, a filter may be invoked as part of a REQUEST or ASYNC dispatches that occur in separate threads. A filter can be configured in web.xml whether it should be involved in async dispatches. However, in some cases servlet containers assume different default configuration. Therefore, subclasses can override the method shouldNotFilterAsyncDispatch() to declare statically if they should indeed be invoked, once, during both types of dispatches in order to provide thread initialization, logging, security, and so on. This mechanism complements and does not replace the need to configure a filter in web.xml with dispatcher types.
               Subclasses may use isAsyncDispatch(HttpServletRequest) to determine when a filter is invoked as part of an async dispatch, and use isAsyncStarted(HttpServletRequest) to determine when the request has been placed in async mode and therefore the current dispatch won't be the last one for the given request.
              diff --git a/assets/OpenWRT.html-DzrmVEsx.js b/assets/OpenWRT.html-DJ_JkW49.js
              similarity index 98%
              rename from assets/OpenWRT.html-DzrmVEsx.js
              rename to assets/OpenWRT.html-DJ_JkW49.js
              index 03f91d200..a8e5e4e4b 100644
              --- a/assets/OpenWRT.html-DzrmVEsx.js
              +++ b/assets/OpenWRT.html-DJ_JkW49.js
              @@ -1 +1 @@
              -import{_ as r}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as e,d as t,o as p}from"./app-LRWZkDAq.js";const a={};function i(s,n){return p(),o("div",null,n[0]||(n[0]=[e("h3",{id:"问题1-新安装的openwrt没有网络",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#问题1-新安装的openwrt没有网络"},[e("span",null,"问题1:新安装的openwrt没有网络")])],-1),e("blockquote",null,[e("p",null,[t("表现为"),e("code",null,"/etc/resolv.conf"),t("中的dns解析配置nameserver是127.0.0.1,这时ping baidu.com是不通的,但是ping外网ip是可以的,如果手动修改nameserver为 192.168.1.1则发现openwrt网络恢复,但是过一会这个nameserver会被还原为127.0.0.1。说明问题就出现在dns解析上")])],-1),e("p",null,[e("strong",null,"解决方法:")],-1),e("p",null,"在openwrt设置页面,网络——DHCP/DNS设置页面,把dns转发填写为192.168.1.1,或者其他114.114.114.114之类的dns服务即可。",-1),e("p",null,"原理就是当nameserver配置127.0.0.1使用的其实是openwrt自带的dnsmasq提供dns服务,但是没给这个服务设置上游dns所以无法解析。",-1)]))}const m=r(a,[["render",i],["__file","OpenWRT.html.vue"]]),d=JSON.parse('{"path":"/myserver/OpenWRT.html","title":"OpenWRT","lang":"zh-CN","frontmatter":{"title":"OpenWRT","date":"2024-12-10T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"问题1:新安装的openwrt没有网络 表现为/etc/resolv.conf中的dns解析配置nameserver是127.0.0.1,这时ping baidu.com是不通的,但是ping外网ip是可以的,如果手动修改nameserver为 192.168.1.1则发现openwrt网络恢复,但是过一会这个nameserver会被还原为127.0....","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/myserver/OpenWRT.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"OpenWRT"}],["meta",{"property":"og:description","content":"问题1:新安装的openwrt没有网络 表现为/etc/resolv.conf中的dns解析配置nameserver是127.0.0.1,这时ping baidu.com是不通的,但是ping外网ip是可以的,如果手动修改nameserver为 192.168.1.1则发现openwrt网络恢复,但是过一会这个nameserver会被还原为127.0...."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-12-10T00:47:22.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2024-12-10T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-12-10T00:47:22.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"OpenWRT\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2024-12-10T00:00:00.000Z\\",\\"dateModified\\":\\"2024-12-10T00:47:22.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":3,"title":"问题1:新安装的openwrt没有网络","slug":"问题1-新安装的openwrt没有网络","link":"#问题1-新安装的openwrt没有网络","children":[]}],"git":{"createdTime":1733791642000,"updatedTime":1733791642000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.6,"words":181},"filePathRelative":"myserver/OpenWRT.md","localizedDate":"2024年12月10日","excerpt":"

              问题1:新安装的openwrt没有网络

              \\n
              \\n

              表现为/etc/resolv.conf中的dns解析配置nameserver是127.0.0.1,这时ping baidu.com是不通的,但是ping外网ip是可以的,如果手动修改nameserver为\\n192.168.1.1则发现openwrt网络恢复,但是过一会这个nameserver会被还原为127.0.0.1。说明问题就出现在dns解析上

              \\n
              \\n

              解决方法:

              \\n

              在openwrt设置页面,网络——DHCP/DNS设置页面,把dns转发填写为192.168.1.1,或者其他114.114.114.114之类的dns服务即可。

              ","autoDesc":true}');export{m as comp,d as data}; +import{_ as r}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as e,d as t,o as p}from"./app-BIdVr4mq.js";const a={};function i(s,n){return p(),o("div",null,n[0]||(n[0]=[e("h3",{id:"问题1-新安装的openwrt没有网络",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#问题1-新安装的openwrt没有网络"},[e("span",null,"问题1:新安装的openwrt没有网络")])],-1),e("blockquote",null,[e("p",null,[t("表现为"),e("code",null,"/etc/resolv.conf"),t("中的dns解析配置nameserver是127.0.0.1,这时ping baidu.com是不通的,但是ping外网ip是可以的,如果手动修改nameserver为 192.168.1.1则发现openwrt网络恢复,但是过一会这个nameserver会被还原为127.0.0.1。说明问题就出现在dns解析上")])],-1),e("p",null,[e("strong",null,"解决方法:")],-1),e("p",null,"在openwrt设置页面,网络——DHCP/DNS设置页面,把dns转发填写为192.168.1.1,或者其他114.114.114.114之类的dns服务即可。",-1),e("p",null,"原理就是当nameserver配置127.0.0.1使用的其实是openwrt自带的dnsmasq提供dns服务,但是没给这个服务设置上游dns所以无法解析。",-1)]))}const m=r(a,[["render",i],["__file","OpenWRT.html.vue"]]),d=JSON.parse('{"path":"/myserver/OpenWRT.html","title":"OpenWRT","lang":"zh-CN","frontmatter":{"title":"OpenWRT","date":"2024-12-10T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"问题1:新安装的openwrt没有网络 表现为/etc/resolv.conf中的dns解析配置nameserver是127.0.0.1,这时ping baidu.com是不通的,但是ping外网ip是可以的,如果手动修改nameserver为 192.168.1.1则发现openwrt网络恢复,但是过一会这个nameserver会被还原为127.0....","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/myserver/OpenWRT.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"OpenWRT"}],["meta",{"property":"og:description","content":"问题1:新安装的openwrt没有网络 表现为/etc/resolv.conf中的dns解析配置nameserver是127.0.0.1,这时ping baidu.com是不通的,但是ping外网ip是可以的,如果手动修改nameserver为 192.168.1.1则发现openwrt网络恢复,但是过一会这个nameserver会被还原为127.0...."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-12-10T00:47:22.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2024-12-10T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-12-10T00:47:22.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"OpenWRT\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2024-12-10T00:00:00.000Z\\",\\"dateModified\\":\\"2024-12-10T00:47:22.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":3,"title":"问题1:新安装的openwrt没有网络","slug":"问题1-新安装的openwrt没有网络","link":"#问题1-新安装的openwrt没有网络","children":[]}],"git":{"createdTime":1733791642000,"updatedTime":1733791642000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.6,"words":181},"filePathRelative":"myserver/OpenWRT.md","localizedDate":"2024年12月10日","excerpt":"

              问题1:新安装的openwrt没有网络

              \\n
              \\n

              表现为/etc/resolv.conf中的dns解析配置nameserver是127.0.0.1,这时ping baidu.com是不通的,但是ping外网ip是可以的,如果手动修改nameserver为\\n192.168.1.1则发现openwrt网络恢复,但是过一会这个nameserver会被还原为127.0.0.1。说明问题就出现在dns解析上

              \\n
              \\n

              解决方法:

              \\n

              在openwrt设置页面,网络——DHCP/DNS设置页面,把dns转发填写为192.168.1.1,或者其他114.114.114.114之类的dns服务即可。

              ","autoDesc":true}');export{m as comp,d as data}; diff --git "a/assets/PVE\350\231\232\346\213\237\346\234\272.html-t1tsCGLO.js" "b/assets/PVE\350\231\232\346\213\237\346\234\272.html-D-5k4U5W.js" similarity index 98% rename from "assets/PVE\350\231\232\346\213\237\346\234\272.html-t1tsCGLO.js" rename to "assets/PVE\350\231\232\346\213\237\346\234\272.html-D-5k4U5W.js" index 7f8537b71..67fe45c9c 100644 --- "a/assets/PVE\350\231\232\346\213\237\346\234\272.html-t1tsCGLO.js" +++ "b/assets/PVE\350\231\232\346\213\237\346\234\272.html-D-5k4U5W.js" @@ -1 +1 @@ -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as r}from"./app-LRWZkDAq.js";const i={};function h(l,e){return r(),a("div",null,e[0]||(e[0]=[n('

              PVE安装

              虚拟机系统安装

              UBUNTU

              OPENWRT旁路由

              zerotier组网

              飞牛nas

              直通网卡

              直通硬盘

              直通显卡

              防火墙

              SSL证书

              挂载硬盘

              ',12)]))}const c=t(i,[["render",h],["__file","PVE虚拟机.html.vue"]]),d=JSON.parse('{"path":"/myserver/PVE%E8%99%9A%E6%8B%9F%E6%9C%BA.html","title":"PVE虚拟机","lang":"zh-CN","frontmatter":{"title":"PVE虚拟机","date":"2023-11-29T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"PVE安装 虚拟机系统安装 UBUNTU OPENWRT旁路由 zerotier组网 飞牛nas 直通网卡 直通硬盘 直通显卡 防火墙 SSL证书 挂载硬盘","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/myserver/PVE%E8%99%9A%E6%8B%9F%E6%9C%BA.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"PVE虚拟机"}],["meta",{"property":"og:description","content":"PVE安装 虚拟机系统安装 UBUNTU OPENWRT旁路由 zerotier组网 飞牛nas 直通网卡 直通硬盘 直通显卡 防火墙 SSL证书 挂载硬盘"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T09:14:36.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2023-11-29T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T09:14:36.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"PVE虚拟机\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2023-11-29T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T09:14:36.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":2,"title":"PVE安装","slug":"pve安装","link":"#pve安装","children":[]},{"level":2,"title":"虚拟机系统安装","slug":"虚拟机系统安装","link":"#虚拟机系统安装","children":[{"level":3,"title":"UBUNTU","slug":"ubuntu","link":"#ubuntu","children":[]},{"level":3,"title":"OPENWRT旁路由","slug":"openwrt旁路由","link":"#openwrt旁路由","children":[]},{"level":3,"title":"zerotier组网","slug":"zerotier组网","link":"#zerotier组网","children":[]},{"level":3,"title":"飞牛nas","slug":"飞牛nas","link":"#飞牛nas","children":[]}]},{"level":2,"title":"直通网卡","slug":"直通网卡","link":"#直通网卡","children":[]},{"level":2,"title":"直通硬盘","slug":"直通硬盘","link":"#直通硬盘","children":[]},{"level":2,"title":"直通显卡","slug":"直通显卡","link":"#直通显卡","children":[]},{"level":2,"title":"防火墙","slug":"防火墙","link":"#防火墙","children":[]},{"level":2,"title":"SSL证书","slug":"ssl证书","link":"#ssl证书","children":[]},{"level":2,"title":"挂载硬盘","slug":"挂载硬盘","link":"#挂载硬盘","children":[]}],"git":{"createdTime":1732871676000,"updatedTime":1732871676000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.19,"words":58},"filePathRelative":"myserver/PVE虚拟机.md","localizedDate":"2023年11月29日","excerpt":"

              PVE安装

              \\n

              虚拟机系统安装

              \\n

              UBUNTU

              \\n

              OPENWRT旁路由

              \\n

              zerotier组网

              \\n

              飞牛nas

              \\n

              直通网卡

              \\n

              直通硬盘

              \\n

              直通显卡

              \\n

              防火墙

              \\n

              SSL证书

              \\n

              挂载硬盘

              \\n","autoDesc":true}');export{c as comp,d as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as r}from"./app-BIdVr4mq.js";const i={};function h(l,e){return r(),a("div",null,e[0]||(e[0]=[n('

              PVE安装

              虚拟机系统安装

              UBUNTU

              OPENWRT旁路由

              zerotier组网

              飞牛nas

              直通网卡

              直通硬盘

              直通显卡

              防火墙

              SSL证书

              挂载硬盘

              ',12)]))}const c=t(i,[["render",h],["__file","PVE虚拟机.html.vue"]]),d=JSON.parse('{"path":"/myserver/PVE%E8%99%9A%E6%8B%9F%E6%9C%BA.html","title":"PVE虚拟机","lang":"zh-CN","frontmatter":{"title":"PVE虚拟机","date":"2023-11-29T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"PVE安装 虚拟机系统安装 UBUNTU OPENWRT旁路由 zerotier组网 飞牛nas 直通网卡 直通硬盘 直通显卡 防火墙 SSL证书 挂载硬盘","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/myserver/PVE%E8%99%9A%E6%8B%9F%E6%9C%BA.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"PVE虚拟机"}],["meta",{"property":"og:description","content":"PVE安装 虚拟机系统安装 UBUNTU OPENWRT旁路由 zerotier组网 飞牛nas 直通网卡 直通硬盘 直通显卡 防火墙 SSL证书 挂载硬盘"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T09:14:36.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2023-11-29T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T09:14:36.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"PVE虚拟机\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2023-11-29T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T09:14:36.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":2,"title":"PVE安装","slug":"pve安装","link":"#pve安装","children":[]},{"level":2,"title":"虚拟机系统安装","slug":"虚拟机系统安装","link":"#虚拟机系统安装","children":[{"level":3,"title":"UBUNTU","slug":"ubuntu","link":"#ubuntu","children":[]},{"level":3,"title":"OPENWRT旁路由","slug":"openwrt旁路由","link":"#openwrt旁路由","children":[]},{"level":3,"title":"zerotier组网","slug":"zerotier组网","link":"#zerotier组网","children":[]},{"level":3,"title":"飞牛nas","slug":"飞牛nas","link":"#飞牛nas","children":[]}]},{"level":2,"title":"直通网卡","slug":"直通网卡","link":"#直通网卡","children":[]},{"level":2,"title":"直通硬盘","slug":"直通硬盘","link":"#直通硬盘","children":[]},{"level":2,"title":"直通显卡","slug":"直通显卡","link":"#直通显卡","children":[]},{"level":2,"title":"防火墙","slug":"防火墙","link":"#防火墙","children":[]},{"level":2,"title":"SSL证书","slug":"ssl证书","link":"#ssl证书","children":[]},{"level":2,"title":"挂载硬盘","slug":"挂载硬盘","link":"#挂载硬盘","children":[]}],"git":{"createdTime":1732871676000,"updatedTime":1732871676000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.19,"words":58},"filePathRelative":"myserver/PVE虚拟机.md","localizedDate":"2023年11月29日","excerpt":"

              PVE安装

              \\n

              虚拟机系统安装

              \\n

              UBUNTU

              \\n

              OPENWRT旁路由

              \\n

              zerotier组网

              \\n

              飞牛nas

              \\n

              直通网卡

              \\n

              直通硬盘

              \\n

              直通显卡

              \\n

              防火墙

              \\n

              SSL证书

              \\n

              挂载硬盘

              \\n","autoDesc":true}');export{c as comp,d as data}; diff --git a/assets/ParentDelegationClassLoader.html-DD8LdvjV.js b/assets/ParentDelegationClassLoader.html-BTelhs6W.js similarity index 99% rename from assets/ParentDelegationClassLoader.html-DD8LdvjV.js rename to assets/ParentDelegationClassLoader.html-BTelhs6W.js index caf1aa972..a2d5ee9a7 100644 --- a/assets/ParentDelegationClassLoader.html-DD8LdvjV.js +++ b/assets/ParentDelegationClassLoader.html-BTelhs6W.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const l={};function t(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

              为什么说spi打破了双亲委派机制?

              1、什么是双亲委派?

              image-20220330170731913
              image-20220330170731913

              注:此处直接摘抄周志明老师的《深入理解java虚拟机》

              站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现[1] ,是虚拟机自身的一部分;另外一种就是其他所有 的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。 ​站在Java开发人员的角度来看,类加载器就应当划分得更细致一些。自JDK 1.2以来,Java一直保 持着三层类加载器、双亲委派的类加载架构,尽管这套架构在Java模块化系统出现后有了一些调整变 动,但依然未改变其主体结构,我们将在7.5节中专门讨论模块化系统下的类加载器。 本节内容将针对JDK 8及之前版本的Java来介绍什么是三层类加载器,以及什么是双亲委派模型。 对于这个时期的Java应用,绝大多数Java程序都会使用到以下3个系统提供的类加载器来进行加载。 ·启动类加载器(Bootstrap Class Loader):前面已经介绍过,这个类加载器负责加载存放在 JAVA_HOM/lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够 识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类 库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时, 如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可,代码清单7-9展示的就是 java.lang.ClassLoader.getClassLoader()方法的代码片段,其中的注释和代码实现都明确地说明了以null值 来代表引导类加载器的约定规则。

              ·扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载<JAVA_HOME>\\lib\\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。根据“扩展类加载器”这个名称,就可以推断出这是一种Java系统类库的扩 展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK 9之后,这种扩展机制被模块化带来的天然的扩展能力所取代。由于扩展类加载器是由Java代码实现 的,开发者可以直接在程序中使用扩展类加载器来加载Class文件。 ·应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem- ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

              ​JDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的,如果用户认为有必要,还可 以加入自定义的类加载器来进行拓展,典型的如增加除了磁盘位置之外的Class文件来源,或者通过类 加载器实现类的隔离、重载等功能。这些类加载器之间的协作关系“通常”会如图7-2所示。 图7-2中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation M odel)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载 器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用 组合(Composition)关系来复用父加载器的代码。 读者可能注意到前面描述这种类加载器协作关系时,笔者专门用双引号强调这是“通常”的协作关 系。类加载器的双亲委派模型在JDK 1.2时期被引入,并被广泛应用于此后几乎所有的Java程序中,但 它并不是一个具有强制性约束力的模型,而是Java设计者们推荐给开发者的一种类加载器实现的最佳 实践。 ​双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。 使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类 加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一 个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类 在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个 类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的 ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应 用程序将会变得一片混乱。如果读者有兴趣的话,可以尝试去写一个与rt.jar类库中已有类重名的Java 类,将会发现它可以正常编译,但永远无法被加载运行[2]。 ​双亲委派模型对于保证Java程序的稳定运作极为重要,但它的实现却异常简单,用以实现双亲委 派的代码只有短短十余行,全部集中在java.lang.ClassLoader的loadClass()方法之中。

              2、什么是SPI

              2.1 定义

              SPI(Service provide interface),直译过来是服务提供接口,在这里指的是厂商负责定义一个接口但不负责提供实现类,定义完接口后厂商直接使用这个接口的方法,但是如果不给此接口提供实现肯定运行要报错的,所以谁要想用厂商这个接口,谁负责实现。最典型的是jdbc,java可以连接各种数据库,比如mysql、oracle、h2……若是让各个数据库厂商都去实现自己的数据库连接方式,那么非常不利于统一管理,所以sun公司为了避免这种各自为战的乱象,他们就规定了一个规范,这就是jdbc了,在java.sql包下,sun指定一个接口叫做Driver,各大厂商负责实现这个Driver就可以了,只要你实现按要求这个接口的方法,那么你就可以直接连接到你的数据库。此处不得不说一句“一流的公司卖标准,二流公司卖实物,三流公司卖服务”

              2.2 使用场景

              1. jdbc4(jdbc4是随着jdk1.6发布的,此版本才开始支持SPI)
              2. springboot的自动话配置也是同样的原理
              3. 阿里的dubbo
              4. 其他

              2.3 自己写一个SPI模拟jdbc的spi

              2.3.1 定义规范(sun公司定义的jdbc规范在java.sql包)

              <?xml version="1.0" encoding="UTF-8"?>
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const l={};function t(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

              为什么说spi打破了双亲委派机制?

              1、什么是双亲委派?

              image-20220330170731913
              image-20220330170731913

              注:此处直接摘抄周志明老师的《深入理解java虚拟机》

              站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现[1] ,是虚拟机自身的一部分;另外一种就是其他所有 的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。 ​站在Java开发人员的角度来看,类加载器就应当划分得更细致一些。自JDK 1.2以来,Java一直保 持着三层类加载器、双亲委派的类加载架构,尽管这套架构在Java模块化系统出现后有了一些调整变 动,但依然未改变其主体结构,我们将在7.5节中专门讨论模块化系统下的类加载器。 本节内容将针对JDK 8及之前版本的Java来介绍什么是三层类加载器,以及什么是双亲委派模型。 对于这个时期的Java应用,绝大多数Java程序都会使用到以下3个系统提供的类加载器来进行加载。 ·启动类加载器(Bootstrap Class Loader):前面已经介绍过,这个类加载器负责加载存放在 JAVA_HOM/lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够 识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类 库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时, 如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可,代码清单7-9展示的就是 java.lang.ClassLoader.getClassLoader()方法的代码片段,其中的注释和代码实现都明确地说明了以null值 来代表引导类加载器的约定规则。

              ·扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载<JAVA_HOME>\\lib\\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。根据“扩展类加载器”这个名称,就可以推断出这是一种Java系统类库的扩 展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK 9之后,这种扩展机制被模块化带来的天然的扩展能力所取代。由于扩展类加载器是由Java代码实现 的,开发者可以直接在程序中使用扩展类加载器来加载Class文件。 ·应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem- ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

              ​JDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的,如果用户认为有必要,还可 以加入自定义的类加载器来进行拓展,典型的如增加除了磁盘位置之外的Class文件来源,或者通过类 加载器实现类的隔离、重载等功能。这些类加载器之间的协作关系“通常”会如图7-2所示。 图7-2中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation M odel)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载 器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用 组合(Composition)关系来复用父加载器的代码。 读者可能注意到前面描述这种类加载器协作关系时,笔者专门用双引号强调这是“通常”的协作关 系。类加载器的双亲委派模型在JDK 1.2时期被引入,并被广泛应用于此后几乎所有的Java程序中,但 它并不是一个具有强制性约束力的模型,而是Java设计者们推荐给开发者的一种类加载器实现的最佳 实践。 ​双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。 使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类 加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一 个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类 在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个 类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的 ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应 用程序将会变得一片混乱。如果读者有兴趣的话,可以尝试去写一个与rt.jar类库中已有类重名的Java 类,将会发现它可以正常编译,但永远无法被加载运行[2]。 ​双亲委派模型对于保证Java程序的稳定运作极为重要,但它的实现却异常简单,用以实现双亲委 派的代码只有短短十余行,全部集中在java.lang.ClassLoader的loadClass()方法之中。

              2、什么是SPI

              2.1 定义

              SPI(Service provide interface),直译过来是服务提供接口,在这里指的是厂商负责定义一个接口但不负责提供实现类,定义完接口后厂商直接使用这个接口的方法,但是如果不给此接口提供实现肯定运行要报错的,所以谁要想用厂商这个接口,谁负责实现。最典型的是jdbc,java可以连接各种数据库,比如mysql、oracle、h2……若是让各个数据库厂商都去实现自己的数据库连接方式,那么非常不利于统一管理,所以sun公司为了避免这种各自为战的乱象,他们就规定了一个规范,这就是jdbc了,在java.sql包下,sun指定一个接口叫做Driver,各大厂商负责实现这个Driver就可以了,只要你实现按要求这个接口的方法,那么你就可以直接连接到你的数据库。此处不得不说一句“一流的公司卖标准,二流公司卖实物,三流公司卖服务”

              2.2 使用场景

              1. jdbc4(jdbc4是随着jdk1.6发布的,此版本才开始支持SPI)
              2. springboot的自动话配置也是同样的原理
              3. 阿里的dubbo
              4. 其他

              2.3 自己写一个SPI模拟jdbc的spi

              2.3.1 定义规范(sun公司定义的jdbc规范在java.sql包)

              <?xml version="1.0" encoding="UTF-8"?>
               <project xmlns="http://maven.apache.org/POM/4.0.0"
                        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
              diff --git a/assets/PreAuthorize.html-DzIfTkjN.js b/assets/PreAuthorize.html-QEyDJ3Om.js
              similarity index 97%
              rename from assets/PreAuthorize.html-DzIfTkjN.js
              rename to assets/PreAuthorize.html-QEyDJ3Om.js
              index 8e8efed9c..b6ec938f6 100644
              --- a/assets/PreAuthorize.html-DzIfTkjN.js
              +++ b/assets/PreAuthorize.html-QEyDJ3Om.js
              @@ -1 +1 @@
              -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as r,a as i,o}from"./app-LRWZkDAq.js";const a={};function n(h,e){return o(),r("div",null,e[0]||(e[0]=[i('

              1、使用方式

              比较简单,省略.....

              2、@PreAuthorize注解是何时生效的?

              2.1

              ',4)]))}const c=t(a,[["render",n],["__file","PreAuthorize.html.vue"]]),s=JSON.parse('{"path":"/java/framework/security/PreAuthorize.html","title":"PreAuthorize注解","lang":"zh-CN","frontmatter":{"title":"PreAuthorize注解","date":"2022-05-22T00:00:00.000Z","isOriginal":true,"description":"1、使用方式 比较简单,省略..... 2、@PreAuthorize注解是何时生效的? 2.1","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/security/PreAuthorize.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"PreAuthorize注解"}],["meta",{"property":"og:description","content":"1、使用方式 比较简单,省略..... 2、@PreAuthorize注解是何时生效的? 2.1"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-05-22T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"PreAuthorize注解\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-05-22T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、使用方式","slug":"_1、使用方式","link":"#_1、使用方式","children":[]},{"level":2,"title":"2、@PreAuthorize注解是何时生效的?","slug":"_2、-preauthorize注解是何时生效的","link":"#_2、-preauthorize注解是何时生效的","children":[{"level":3,"title":"2.1","slug":"_2-1","link":"#_2-1","children":[]}]}],"git":{"createdTime":1685430003000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3}]},"readingTime":{"minutes":0.11,"words":33},"filePathRelative":"java/framework/security/PreAuthorize.md","localizedDate":"2022年5月22日","excerpt":"

              1、使用方式

              \\n

              比较简单,省略.....

              \\n

              2、@PreAuthorize注解是何时生效的?

              \\n

              2.1

              \\n","autoDesc":true}');export{c as comp,s as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as r,a as i,o}from"./app-BIdVr4mq.js";const a={};function n(h,e){return o(),r("div",null,e[0]||(e[0]=[i('

              1、使用方式

              比较简单,省略.....

              2、@PreAuthorize注解是何时生效的?

              2.1

              ',4)]))}const c=t(a,[["render",n],["__file","PreAuthorize.html.vue"]]),s=JSON.parse('{"path":"/java/framework/security/PreAuthorize.html","title":"PreAuthorize注解","lang":"zh-CN","frontmatter":{"title":"PreAuthorize注解","date":"2022-05-22T00:00:00.000Z","isOriginal":true,"description":"1、使用方式 比较简单,省略..... 2、@PreAuthorize注解是何时生效的? 2.1","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/security/PreAuthorize.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"PreAuthorize注解"}],["meta",{"property":"og:description","content":"1、使用方式 比较简单,省略..... 2、@PreAuthorize注解是何时生效的? 2.1"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-05-22T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"PreAuthorize注解\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-05-22T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、使用方式","slug":"_1、使用方式","link":"#_1、使用方式","children":[]},{"level":2,"title":"2、@PreAuthorize注解是何时生效的?","slug":"_2、-preauthorize注解是何时生效的","link":"#_2、-preauthorize注解是何时生效的","children":[{"level":3,"title":"2.1","slug":"_2-1","link":"#_2-1","children":[]}]}],"git":{"createdTime":1685430003000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3}]},"readingTime":{"minutes":0.11,"words":33},"filePathRelative":"java/framework/security/PreAuthorize.md","localizedDate":"2022年5月22日","excerpt":"

              1、使用方式

              \\n

              比较简单,省略.....

              \\n

              2、@PreAuthorize注解是何时生效的?

              \\n

              2.1

              \\n","autoDesc":true}');export{c as comp,s as data}; diff --git a/assets/ProxyInJava.html-Bw3KCeSC.js b/assets/ProxyInJava.html-D4L5WYNV.js similarity index 99% rename from assets/ProxyInJava.html-Bw3KCeSC.js rename to assets/ProxyInJava.html-D4L5WYNV.js index 9df2e3e29..f5fb37839 100644 --- a/assets/ProxyInJava.html-Bw3KCeSC.js +++ b/assets/ProxyInJava.html-D4L5WYNV.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function t(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

              是兄弟就来看我的博客

              1、代理的分类

              graph LR;
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function t(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

              是兄弟就来看我的博客

              1、代理的分类

              graph LR;
               A[JAVA中的代理]-->B[动态代理]-->C[jdk动态代理]
               B[动态代理]-->D[cglib动态代理]
               A[JAVA中的代理]--> E[静态代理]-->F[AspectJ]

              2、各种代理的区别

              2.1 动态代理

              动态代理指的是程序运行期间动态的生成代理类,比如jdk的动态代理,技术上是通过反射来生成代理的类。cglib也是在运行时动态生成代理类,和jdk动态代理区别在于cglib是利用asm开源框架直接操作class字节码文件来生成代理类。

              2.2 静态代理

              静态代理值得是在编译期间就已经生成了一个新的代理类,比如:AspectJ,Lombok

              3、Spring中提到的AOP和AspectJ属于什么代理

              3.1 SpringAOP

              SpringAOP并不是新东西,本质就是代理,底层使用的是Jdk动态代理或者Cglib代理,默认使用的是Jdk动态代理(当代理的类是一个接口时直接用jdk动态代理,当被代理的目标类不是接口,则调用Cglib进行代理,因为jdk动态代理只支持接口形式代理)。 另外我们在Spring中使用的AspectJ,主要是为了使用它的注解类,也就是说引入了Aspectj,我们在spring才能用注解的方式开发AOP,比如注解@Around、@Aspect、@Before、@JoinPoint等都是Aspectj包中的。Spring其实并没有用到Aspectj的 代理功能,仅仅是用了它这个包中的注解而已。

              3.2 CGlib和JDK动态代理切换

              AOP底层实现 2中代理方式
              diff --git a/assets/Recurse.html-BYNa9G5A.js b/assets/Recurse.html-Bux7wAbT.js
              similarity index 99%
              rename from assets/Recurse.html-BYNa9G5A.js
              rename to assets/Recurse.html-Bux7wAbT.js
              index 6bd22d010..782bb56b5 100644
              --- a/assets/Recurse.html-BYNa9G5A.js
              +++ b/assets/Recurse.html-Bux7wAbT.js
              @@ -1,4 +1,4 @@
              -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const t={};function e(h,i){return l(),a("div",null,i[0]||(i[0]=[n(`

              注意!!!

              只有在mysql8.0之后才有递归,5.7及之前是不支持的

              1、递归下钻

              MySQL中的递归查询通常用于处理树形结构数据,如组织架构、文件目录、级联地址、多级菜单等。

              2、示例

              WITH RECURSIVE cte (id, parent_id, level) AS (
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const t={};function e(h,i){return l(),a("div",null,i[0]||(i[0]=[n(`

              注意!!!

              只有在mysql8.0之后才有递归,5.7及之前是不支持的

              1、递归下钻

              MySQL中的递归查询通常用于处理树形结构数据,如组织架构、文件目录、级联地址、多级菜单等。

              2、示例

              WITH RECURSIVE cte (id, parent_id, level) AS (
                 -- 初始查询
                 SELECT id, parent_id, 0 FROM your_table WHERE id = <your starting id>
               
              diff --git a/assets/RefreshToken.html-D2M8Zi5t.js b/assets/RefreshToken.html-DrzHWxB_.js
              similarity index 99%
              rename from assets/RefreshToken.html-D2M8Zi5t.js
              rename to assets/RefreshToken.html-DrzHWxB_.js
              index 0758d7b22..6ed02b1a4 100644
              --- a/assets/RefreshToken.html-D2M8Zi5t.js
              +++ b/assets/RefreshToken.html-DrzHWxB_.js
              @@ -1 +1 @@
              -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as o,o as r}from"./app-LRWZkDAq.js";const s={};function a(h,e){return r(),n("div",null,e[0]||(e[0]=[o('

              1、 refresh_token介绍

              refresh_token是OAuth2协议中的一个概念,用于在access_token过期时获取新的access_token,说道token续签大多博客上来就refresh_token巴拉巴拉。在OAuth2协议中,通过客户端以授权服务器的名义请求access_token,客户端会收到access_token和refresh_token,并在access_token过期时使用refresh_token申请新的access_token。refresh_token的安全性非常重要,授权服务器会根据设定的有效期对refresh_token进行管理,以确保对用户的信息和资源进行保护。同时,在使用refresh_token时,也需要进行国家或行业标准的数据保护和安全性认证,以避免出现不必要的风险和隐私泄漏。

              2、token过期处理

              2.1 方式一,重新登录

              让用户重新登录,获得新的Token,但是这种方式体验很差,通常Token过期时间都比较短,每次都要重新登录操作。

              2.2 方式二,续签(刷新)token

              续签token,避免用户在操作的过程中被强制下线,续签也有多种方式

              1. 在每个请求响应后进行拦截,如果发现请求失败(Token过期导致的)时,刷新Token再刷新请求接口。这种方式的优点是无需Token过期时间字段且无需判断时间,缺点在于多消耗一次请求。
              2. 在每个请求发起前进行拦截,根据expires_in判断token是否过期,如果过期则会刷新后再继续请求接口。这个方法的优点是请求前拦截处理,能节省请求次数,缺点是后端需要提供Token过期时间字段,并且需要结合计算机本地时间判断,如果计算机时间被篡改,拦截就会失败。
              3. 添加一个过滤器,每个请求进来都重置token的过期时间,这种方式对服务器资源消耗增加了
              4. 在OAuth2.0协议中,可以使用refresh_token实现自动续期token。这个方案需要你的应用程序使用OAuth2.0的认证机制,并且获取到了refresh_token。
              5. 使用JSON Web Token:对于使用JSON Web Token (JWT)的应用程序,可以在JWT中设置token的过期时间,预先考虑过期时间,合理设置有效期,通常情况下,token的有效期建议短一些,这样更安全。在即将到期时,在后端进行更新或延长有效期。
              ',8)]))}const i=t(s,[["render",a],["__file","RefreshToken.html.vue"]]),_=JSON.parse('{"path":"/other/web/RefreshToken.html","title":"refresh_token","lang":"zh-CN","frontmatter":{"title":"refresh_token","date":"2022-05-08T00:00:00.000Z","isOriginal":true,"description":"1、 refresh_token介绍 refresh_token是OAuth2协议中的一个概念,用于在access_token过期时获取新的access_token,说道token续签大多博客上来就refresh_token巴拉巴拉。在OAuth2协议中,通过客户端以授权服务器的名义请求access_token,客户端会收到access_token和r...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/web/RefreshToken.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"refresh_token"}],["meta",{"property":"og:description","content":"1、 refresh_token介绍 refresh_token是OAuth2协议中的一个概念,用于在access_token过期时获取新的access_token,说道token续签大多博客上来就refresh_token巴拉巴拉。在OAuth2协议中,通过客户端以授权服务器的名义请求access_token,客户端会收到access_token和r..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-05-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"refresh_token\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-05-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、 refresh_token介绍","slug":"_1、-refresh-token介绍","link":"#_1、-refresh-token介绍","children":[]},{"level":2,"title":"2、token过期处理","slug":"_2、token过期处理","link":"#_2、token过期处理","children":[{"level":3,"title":"2.1 方式一,重新登录","slug":"_2-1-方式一-重新登录","link":"#_2-1-方式一-重新登录","children":[]},{"level":3,"title":"2.2 方式二,续签(刷新)token","slug":"_2-2-方式二-续签-刷新-token","link":"#_2-2-方式二-续签-刷新-token","children":[]}]}],"git":{"createdTime":1683634730000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2},{"name":"chenxk","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":2.1,"words":631},"filePathRelative":"other/web/RefreshToken.md","localizedDate":"2022年5月8日","excerpt":"

              1、 refresh_token介绍

              \\n

              refresh_token是OAuth2协议中的一个概念,用于在access_token过期时获取新的access_token,说道token续签大多博客上来就refresh_token巴拉巴拉。在OAuth2协议中,通过客户端以授权服务器的名义请求access_token,客户端会收到access_token和refresh_token,并在access_token过期时使用refresh_token申请新的access_token。refresh_token的安全性非常重要,授权服务器会根据设定的有效期对refresh_token进行管理,以确保对用户的信息和资源进行保护。同时,在使用refresh_token时,也需要进行国家或行业标准的数据保护和安全性认证,以避免出现不必要的风险和隐私泄漏。

              ","autoDesc":true}');export{i as comp,_ as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as o,o as r}from"./app-BIdVr4mq.js";const s={};function a(h,e){return r(),n("div",null,e[0]||(e[0]=[o('

              1、 refresh_token介绍

              refresh_token是OAuth2协议中的一个概念,用于在access_token过期时获取新的access_token,说道token续签大多博客上来就refresh_token巴拉巴拉。在OAuth2协议中,通过客户端以授权服务器的名义请求access_token,客户端会收到access_token和refresh_token,并在access_token过期时使用refresh_token申请新的access_token。refresh_token的安全性非常重要,授权服务器会根据设定的有效期对refresh_token进行管理,以确保对用户的信息和资源进行保护。同时,在使用refresh_token时,也需要进行国家或行业标准的数据保护和安全性认证,以避免出现不必要的风险和隐私泄漏。

              2、token过期处理

              2.1 方式一,重新登录

              让用户重新登录,获得新的Token,但是这种方式体验很差,通常Token过期时间都比较短,每次都要重新登录操作。

              2.2 方式二,续签(刷新)token

              续签token,避免用户在操作的过程中被强制下线,续签也有多种方式

              1. 在每个请求响应后进行拦截,如果发现请求失败(Token过期导致的)时,刷新Token再刷新请求接口。这种方式的优点是无需Token过期时间字段且无需判断时间,缺点在于多消耗一次请求。
              2. 在每个请求发起前进行拦截,根据expires_in判断token是否过期,如果过期则会刷新后再继续请求接口。这个方法的优点是请求前拦截处理,能节省请求次数,缺点是后端需要提供Token过期时间字段,并且需要结合计算机本地时间判断,如果计算机时间被篡改,拦截就会失败。
              3. 添加一个过滤器,每个请求进来都重置token的过期时间,这种方式对服务器资源消耗增加了
              4. 在OAuth2.0协议中,可以使用refresh_token实现自动续期token。这个方案需要你的应用程序使用OAuth2.0的认证机制,并且获取到了refresh_token。
              5. 使用JSON Web Token:对于使用JSON Web Token (JWT)的应用程序,可以在JWT中设置token的过期时间,预先考虑过期时间,合理设置有效期,通常情况下,token的有效期建议短一些,这样更安全。在即将到期时,在后端进行更新或延长有效期。
              ',8)]))}const i=t(s,[["render",a],["__file","RefreshToken.html.vue"]]),_=JSON.parse('{"path":"/other/web/RefreshToken.html","title":"refresh_token","lang":"zh-CN","frontmatter":{"title":"refresh_token","date":"2022-05-08T00:00:00.000Z","isOriginal":true,"description":"1、 refresh_token介绍 refresh_token是OAuth2协议中的一个概念,用于在access_token过期时获取新的access_token,说道token续签大多博客上来就refresh_token巴拉巴拉。在OAuth2协议中,通过客户端以授权服务器的名义请求access_token,客户端会收到access_token和r...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/web/RefreshToken.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"refresh_token"}],["meta",{"property":"og:description","content":"1、 refresh_token介绍 refresh_token是OAuth2协议中的一个概念,用于在access_token过期时获取新的access_token,说道token续签大多博客上来就refresh_token巴拉巴拉。在OAuth2协议中,通过客户端以授权服务器的名义请求access_token,客户端会收到access_token和r..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-05-08T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"refresh_token\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-05-08T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、 refresh_token介绍","slug":"_1、-refresh-token介绍","link":"#_1、-refresh-token介绍","children":[]},{"level":2,"title":"2、token过期处理","slug":"_2、token过期处理","link":"#_2、token过期处理","children":[{"level":3,"title":"2.1 方式一,重新登录","slug":"_2-1-方式一-重新登录","link":"#_2-1-方式一-重新登录","children":[]},{"level":3,"title":"2.2 方式二,续签(刷新)token","slug":"_2-2-方式二-续签-刷新-token","link":"#_2-2-方式二-续签-刷新-token","children":[]}]}],"git":{"createdTime":1683634730000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2},{"name":"chenxk","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":2.1,"words":631},"filePathRelative":"other/web/RefreshToken.md","localizedDate":"2022年5月8日","excerpt":"

              1、 refresh_token介绍

              \\n

              refresh_token是OAuth2协议中的一个概念,用于在access_token过期时获取新的access_token,说道token续签大多博客上来就refresh_token巴拉巴拉。在OAuth2协议中,通过客户端以授权服务器的名义请求access_token,客户端会收到access_token和refresh_token,并在access_token过期时使用refresh_token申请新的access_token。refresh_token的安全性非常重要,授权服务器会根据设定的有效期对refresh_token进行管理,以确保对用户的信息和资源进行保护。同时,在使用refresh_token时,也需要进行国家或行业标准的数据保护和安全性认证,以避免出现不必要的风险和隐私泄漏。

              ","autoDesc":true}');export{i as comp,_ as data}; diff --git a/assets/Restful.html-DGA73Lg6.js b/assets/Restful.html-DLGwYhd3.js similarity index 97% rename from assets/Restful.html-DGA73Lg6.js rename to assets/Restful.html-DLGwYhd3.js index 5c9c719de..bad754014 100644 --- a/assets/Restful.html-DGA73Lg6.js +++ b/assets/Restful.html-DLGwYhd3.js @@ -1 +1 @@ -import{_ as r}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,b as e,o}from"./app-LRWZkDAq.js";const l={};function a(i,t){return o(),n("div",null,t[0]||(t[0]=[e("h2",{id:"一、参考",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#一、参考"},[e("span",null,"一、参考")])],-1),e("hr",null,null,-1),e("p",null,[e("a",{href:"https://www.ruanyifeng.com/blog/2011/09/restful.html",target:"_blank",rel:"noopener noreferrer"},"RESTful架构")],-1),e("p",null,[e("a",{href:"https://www.ruanyifeng.com/blog/2014/05/restful_api.html",target:"_blank",rel:"noopener noreferrer"},"RESTful API 设计")],-1)]))}const u=r(l,[["render",a],["__file","Restful.html.vue"]]),m=JSON.parse('{"path":"/other/web/Restful.html","title":"HTTP Restful","lang":"zh-CN","frontmatter":{"title":"HTTP Restful","date":"2021-07-11T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"category":["web"],"tag":["web"],"description":"一、参考 RESTful架构 RESTful API 设计","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/web/Restful.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"HTTP Restful"}],["meta",{"property":"og:description","content":"一、参考 RESTful架构 RESTful API 设计"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:tag","content":"web"}],["meta",{"property":"article:published_time","content":"2021-07-11T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"HTTP Restful\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-07-11T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"一、参考","slug":"一、参考","link":"#一、参考","children":[]}],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.1,"words":30},"filePathRelative":"other/web/Restful.md","localizedDate":"2021年7月11日","excerpt":"

              一、参考

              \\n
              \\n

              RESTful架构

              \\n

              RESTful API 设计

              \\n","autoDesc":true}');export{u as comp,m as data}; +import{_ as r}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,b as e,o}from"./app-BIdVr4mq.js";const l={};function a(i,t){return o(),n("div",null,t[0]||(t[0]=[e("h2",{id:"一、参考",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#一、参考"},[e("span",null,"一、参考")])],-1),e("hr",null,null,-1),e("p",null,[e("a",{href:"https://www.ruanyifeng.com/blog/2011/09/restful.html",target:"_blank",rel:"noopener noreferrer"},"RESTful架构")],-1),e("p",null,[e("a",{href:"https://www.ruanyifeng.com/blog/2014/05/restful_api.html",target:"_blank",rel:"noopener noreferrer"},"RESTful API 设计")],-1)]))}const u=r(l,[["render",a],["__file","Restful.html.vue"]]),m=JSON.parse('{"path":"/other/web/Restful.html","title":"HTTP Restful","lang":"zh-CN","frontmatter":{"title":"HTTP Restful","date":"2021-07-11T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"category":["web"],"tag":["web"],"description":"一、参考 RESTful架构 RESTful API 设计","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/web/Restful.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"HTTP Restful"}],["meta",{"property":"og:description","content":"一、参考 RESTful架构 RESTful API 设计"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:tag","content":"web"}],["meta",{"property":"article:published_time","content":"2021-07-11T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"HTTP Restful\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-07-11T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"一、参考","slug":"一、参考","link":"#一、参考","children":[]}],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.1,"words":30},"filePathRelative":"other/web/Restful.md","localizedDate":"2021年7月11日","excerpt":"

              一、参考

              \\n
              \\n

              RESTful架构

              \\n

              RESTful API 设计

              \\n","autoDesc":true}');export{u as comp,m as data}; diff --git a/assets/SQLOptimization.html-Dm2zmzdd.js b/assets/SQLOptimization.html-CC5fHt2c.js similarity index 99% rename from assets/SQLOptimization.html-Dm2zmzdd.js rename to assets/SQLOptimization.html-CC5fHt2c.js index 92f5af380..489855303 100644 --- a/assets/SQLOptimization.html-Dm2zmzdd.js +++ b/assets/SQLOptimization.html-CC5fHt2c.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a,o as t}from"./app-LRWZkDAq.js";const n={};function l(r,i){return t(),e("div",null,i[0]||(i[0]=[a(`

              背景:

              最近在改一个老项目,其中使用到框架是mybatis,有一个业务表install_record,代表装机记录,一个accessory代表备件表,一个sys_file代表文件表,业务关系是一个install_record对应多个accessory、以及多个sys_file,在一开始使用的是mybatis的嵌套查询的方式,但此方式有N+1的问题,比如一个装机表对应10个accessory、20个sys_file,则就要查询1+10+20 = 31次数据库,效率是很低的,因此想改成嵌套查询的方式。

              1、改造后的语句如下

              SELECT xxx   FROM
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a,o as t}from"./app-BIdVr4mq.js";const n={};function l(r,i){return t(),e("div",null,i[0]||(i[0]=[a(`

              背景:

              最近在改一个老项目,其中使用到框架是mybatis,有一个业务表install_record,代表装机记录,一个accessory代表备件表,一个sys_file代表文件表,业务关系是一个install_record对应多个accessory、以及多个sys_file,在一开始使用的是mybatis的嵌套查询的方式,但此方式有N+1的问题,比如一个装机表对应10个accessory、20个sys_file,则就要查询1+10+20 = 31次数据库,效率是很低的,因此想改成嵌套查询的方式。

              1、改造后的语句如下

              SELECT xxx   FROM
               	ccsx_weibao.install_record ir
               	LEFT JOIN ccsx_weibao.install_record_accessory ira ON ira.host_id = ir.id
               	LEFT JOIN ccsx.sys_file f ON f.business_type = 1 
              diff --git a/assets/SSO.html-o59ROGA3.js b/assets/SSO.html-CT7CTtUX.js
              similarity index 99%
              rename from assets/SSO.html-o59ROGA3.js
              rename to assets/SSO.html-CT7CTtUX.js
              index 9a2b85be2..7afaa29d4 100644
              --- a/assets/SSO.html-o59ROGA3.js
              +++ b/assets/SSO.html-CT7CTtUX.js
              @@ -1 +1 @@
              -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as s,o as i}from"./app-LRWZkDAq.js";const a={};function o(r,e){return i(),n("div",null,e[0]||(e[0]=[s('

              1、SSO

              1.1 SSO介绍

              单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

              1.2 SSO使用场景(解决了什么问题)

              很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影豆瓣日记......真的会让人崩溃的。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。怎么做?

              一次注册不难,想一下是不是只要Server之间同步用户信息就行了?可以,但这样描述不太完整,后续讲用户注册的时候详细说。实际上用户信息的管理才是SSO真正的难点,只是作为初学者,我们的难点在于实现SSO的技术!我们先讨论实现手段。

              一次登录与一次退出。 回头看看普通商场的故事,什么东西才是保持登录状态关键的东西?记录器(session)?那种叫做cookie的纸张?写在纸张上的ID? 是session里面记录的信息跟那个ID,cookie不只是记录ID的工具而已。客户端持有ID,服务端持有session,两者一起用来保持登录状态。客户端需要用ID来作为凭证,而服务端需要用session来验证ID的有效性(ID可能过期、可能根本就是伪造的找不到对应的信息、ID下对应的客户端还没有进行登录验证等)。但是session这东西一开始是每个server自己独有的,豆瓣FM有自己的session、豆瓣读书有自己的session,而记录ID的cookie又是不能跨域的。所以,我们要实现一次登录一次退出,只需要想办法让各个server的共用一个session的信息,让客户端在各个域名下都能持有这个ID就好了。再进一步讲,只要各个server拿到同一个ID,都能有办法检验出ID的有效性、并且能得到ID对应的用户信息就行了,也就是能检验ID 。

              SSO就是解决以上问题,当使用浏览器访问公司任何一个服务时,只要其中一个服务登陆了,那么再打开一个新页面访问另一个服务,是无需再次输入用户名和密码。比如:登陆了OA后,再访问考勤系统是无需登陆。

              1.3 使用SSO的好处

              • 方便用户,用户使用应用系统时,能够一次登录,多次使用。用户不再需要每次输入用户名称和用户密码,也不需要牢记多套用户名称和用户密码。单点登录平台能够改善用户使用应用系统的体验。
              • 方便管理员,系统管理员只需要维护一套统一的用户账号,方便、简单。相比之下,系统管理员以前需要管理很多套的用户账号。每一个应用系统就有一套用户账号,不仅给管理上带来不方便,而且,也容易出现管理漏洞。
              • 简化应用系统开发,开发新的应用系统时,可以直接使用单点登录平台的用户认证服务,简化开发流程。单点登录平台通过提供统一的认证平台,实现单点登录。因此,应用系统并不需要开发用户认证程序。

              2、SSO常见实现方案

              注意

              SSO只是一个规范,具体的实现有多种途径,类似Jdbc定义了java数据库连接的规范,具体的实现方案由各数据库厂商自行实现。

              单点登录的实现方案一般包含:Cookies、Session 同步、分布式 Session 方式、统一认证授权方式等。目前的大型网站通常采用分布式 Session 及第三方认证授权的方式。

              基于 Cookie 的单点登录是最简单的单点登录实现方式,它使用 Cookie 作为媒介,存放用户凭证。 用户登录父应用之后,应用返回一个加密的 Cookie,用户访问子应用的时候,携带上这个 Cookie,授权应用解密 Cookie 并进行校验,校验通过则登录当前用户。 这种方式虽然实现简单,但 Cookie 不够安全,容易泄漏,且不能跨域实现免登。

              2.2 分布式 Session 实现单点登录

              分布式 Session 实现单点登录原理是将用户认证信息保存于 Session 中,即以 Session 内存储的值为用户凭证,一般采用 Cache 中间件实现(如 Redis)。用户再次登录时,应用服务端获取分布式 Session 来校验用户信息。如图所示: 20221011175808 一般情况下都是基于 Redis 实现 Session 共享,将 Session 存储于 Redis 上,然后将整个系统的全局 Cookie Domain 设置于顶级域名上,这样 SessionID 就能在各个子系统间共享。 这种方式也有一个问题,共享 Session 无法处理跨顶级域名。

              2.3 统一认证授权方式实现单点登录

              20221011175916
              20221011175916

              由图可知,通过统一认证授权方式实现单点登录,需要有一个独立的认证系统。

              用户第一次访问应用系统时,由于还未登录,被引导到认证系统中进行登录,认证系统接受用户名密码等安全信息,生成访问令牌(ticket)。用户通过 ticket 访问应用系统,应用系统接受到请求之后会访问认证系统检查 ticket 的合法性,如果检查通过,用户就可以在不用再次登录的情况下访问应用系统资源。

              以上介绍了单点登录常见的 3 种实现方案,在实际应用中还需要根据具体场景来实现。

              接下来通过几个实际生产场景,来阐述单点登录的详细实现方案。

              3、实际场景中SSO实现

              3.1 JWT(完全跨域方案)

              JWT (JSON Web Token)是一个开放标准(RFC7519),它是一个含签名并携带用户相关信息的加密串。页面请求校验登录接口时,请求头中携带 JWT 串到后端服务,后端通过签名加密串匹配校验,保证信息未被篡改,校验通过则认为是可靠的请求,将正常返回数据。

              DataSimba(奇点云数据中台产品)结合 JWT 与分布式 session,实现多域多空间单点登录。通过 JWT 生成和校验令牌,将刷新令牌存储在 redis 中,网关统一校验令牌,校验通过后将用户信息设置在请求头中,应用在拦截器中获取到用户信息后即可验证通过。

              不同域中的 DataSimba 共用一套密钥并且实时同步用户信息,通过 JWT 生成和校验令牌,用户登录其中一个域后,前端获取 JWT 加密串并存储在 Local Storage 中,当用户切换到其他域时前端传入加密串,后端网关校验,由此实现免登录访问其他域资源,如下图所示: 20221011180112

              3.2 使用 OAuth2.0 实现单点登录

              OAuth2.0是可以实现SSO的功能,但是OAuth 诞生之初就是为了解决 Web 浏览器场景下的授权问题

              4、参考资料

              OAuth2.0授权码模式中为什么一定要有code授权码的补充授权码模式中code的意义授权码模式是让token在服务器和服务器之间传递,不会经过资源拥有者,资源拥有者无需看到token

              ',32)]))}const p=t(a,[["render",o],["__file","SSO.html.vue"]]),c=JSON.parse('{"path":"/other/training/SSO.html","title":"OAuth2分享","lang":"zh-CN","frontmatter":{"title":"OAuth2分享","date":"2021-10-09T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"category":["小组分享"],"description":"1、SSO 1.1 SSO介绍 单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 1.2 SSO使用场景(解决了什么问题) 很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/training/SSO.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"OAuth2分享"}],["meta",{"property":"og:description","content":"1、SSO 1.1 SSO介绍 单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 1.2 SSO使用场景(解决了什么问题) 很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221011175808.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2021-10-09T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"OAuth2分享\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221011175808.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221011175916.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221011180112.png\\"],\\"datePublished\\":\\"2021-10-09T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、SSO","slug":"_1、sso","link":"#_1、sso","children":[{"level":3,"title":"1.1 SSO介绍","slug":"_1-1-sso介绍","link":"#_1-1-sso介绍","children":[]},{"level":3,"title":"1.2 SSO使用场景(解决了什么问题)","slug":"_1-2-sso使用场景-解决了什么问题","link":"#_1-2-sso使用场景-解决了什么问题","children":[]},{"level":3,"title":"1.3 使用SSO的好处","slug":"_1-3-使用sso的好处","link":"#_1-3-使用sso的好处","children":[]}]},{"level":2,"title":"2、SSO常见实现方案","slug":"_2、sso常见实现方案","link":"#_2、sso常见实现方案","children":[{"level":3,"title":"2.1 基于 Cookie 的单点登录","slug":"_2-1-基于-cookie-的单点登录","link":"#_2-1-基于-cookie-的单点登录","children":[]},{"level":3,"title":"2.2 分布式 Session 实现单点登录","slug":"_2-2-分布式-session-实现单点登录","link":"#_2-2-分布式-session-实现单点登录","children":[]},{"level":3,"title":"2.3 统一认证授权方式实现单点登录","slug":"_2-3-统一认证授权方式实现单点登录","link":"#_2-3-统一认证授权方式实现单点登录","children":[]}]},{"level":2,"title":"3、实际场景中SSO实现","slug":"_3、实际场景中sso实现","link":"#_3、实际场景中sso实现","children":[{"level":3,"title":"3.1 JWT(完全跨域方案)","slug":"_3-1-jwt-完全跨域方案","link":"#_3-1-jwt-完全跨域方案","children":[]},{"level":3,"title":"3.2 使用 OAuth2.0 实现单点登录","slug":"_3-2-使用-oauth2-0-实现单点登录","link":"#_3-2-使用-oauth2-0-实现单点登录","children":[]}]},{"level":2,"title":"4、参考资料","slug":"_4、参考资料","link":"#_4、参考资料","children":[]}],"git":{"createdTime":1665557335000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2}]},"readingTime":{"minutes":7.15,"words":2144},"filePathRelative":"other/training/SSO.md","localizedDate":"2021年10月9日","excerpt":"

              1、SSO

              \\n

              1.1 SSO介绍

              \\n

              单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

              \\n

              1.2 SSO使用场景(解决了什么问题)

              \\n

              很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影豆瓣日记......真的会让人崩溃的。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。怎么做?

              ","autoDesc":true}');export{p as comp,c as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as s,o as i}from"./app-BIdVr4mq.js";const a={};function o(r,e){return i(),n("div",null,e[0]||(e[0]=[s('

              1、SSO

              1.1 SSO介绍

              单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

              1.2 SSO使用场景(解决了什么问题)

              很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影豆瓣日记......真的会让人崩溃的。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。怎么做?

              一次注册不难,想一下是不是只要Server之间同步用户信息就行了?可以,但这样描述不太完整,后续讲用户注册的时候详细说。实际上用户信息的管理才是SSO真正的难点,只是作为初学者,我们的难点在于实现SSO的技术!我们先讨论实现手段。

              一次登录与一次退出。 回头看看普通商场的故事,什么东西才是保持登录状态关键的东西?记录器(session)?那种叫做cookie的纸张?写在纸张上的ID? 是session里面记录的信息跟那个ID,cookie不只是记录ID的工具而已。客户端持有ID,服务端持有session,两者一起用来保持登录状态。客户端需要用ID来作为凭证,而服务端需要用session来验证ID的有效性(ID可能过期、可能根本就是伪造的找不到对应的信息、ID下对应的客户端还没有进行登录验证等)。但是session这东西一开始是每个server自己独有的,豆瓣FM有自己的session、豆瓣读书有自己的session,而记录ID的cookie又是不能跨域的。所以,我们要实现一次登录一次退出,只需要想办法让各个server的共用一个session的信息,让客户端在各个域名下都能持有这个ID就好了。再进一步讲,只要各个server拿到同一个ID,都能有办法检验出ID的有效性、并且能得到ID对应的用户信息就行了,也就是能检验ID 。

              SSO就是解决以上问题,当使用浏览器访问公司任何一个服务时,只要其中一个服务登陆了,那么再打开一个新页面访问另一个服务,是无需再次输入用户名和密码。比如:登陆了OA后,再访问考勤系统是无需登陆。

              1.3 使用SSO的好处

              • 方便用户,用户使用应用系统时,能够一次登录,多次使用。用户不再需要每次输入用户名称和用户密码,也不需要牢记多套用户名称和用户密码。单点登录平台能够改善用户使用应用系统的体验。
              • 方便管理员,系统管理员只需要维护一套统一的用户账号,方便、简单。相比之下,系统管理员以前需要管理很多套的用户账号。每一个应用系统就有一套用户账号,不仅给管理上带来不方便,而且,也容易出现管理漏洞。
              • 简化应用系统开发,开发新的应用系统时,可以直接使用单点登录平台的用户认证服务,简化开发流程。单点登录平台通过提供统一的认证平台,实现单点登录。因此,应用系统并不需要开发用户认证程序。

              2、SSO常见实现方案

              注意

              SSO只是一个规范,具体的实现有多种途径,类似Jdbc定义了java数据库连接的规范,具体的实现方案由各数据库厂商自行实现。

              单点登录的实现方案一般包含:Cookies、Session 同步、分布式 Session 方式、统一认证授权方式等。目前的大型网站通常采用分布式 Session 及第三方认证授权的方式。

              基于 Cookie 的单点登录是最简单的单点登录实现方式,它使用 Cookie 作为媒介,存放用户凭证。 用户登录父应用之后,应用返回一个加密的 Cookie,用户访问子应用的时候,携带上这个 Cookie,授权应用解密 Cookie 并进行校验,校验通过则登录当前用户。 这种方式虽然实现简单,但 Cookie 不够安全,容易泄漏,且不能跨域实现免登。

              2.2 分布式 Session 实现单点登录

              分布式 Session 实现单点登录原理是将用户认证信息保存于 Session 中,即以 Session 内存储的值为用户凭证,一般采用 Cache 中间件实现(如 Redis)。用户再次登录时,应用服务端获取分布式 Session 来校验用户信息。如图所示: 20221011175808 一般情况下都是基于 Redis 实现 Session 共享,将 Session 存储于 Redis 上,然后将整个系统的全局 Cookie Domain 设置于顶级域名上,这样 SessionID 就能在各个子系统间共享。 这种方式也有一个问题,共享 Session 无法处理跨顶级域名。

              2.3 统一认证授权方式实现单点登录

              20221011175916
              20221011175916

              由图可知,通过统一认证授权方式实现单点登录,需要有一个独立的认证系统。

              用户第一次访问应用系统时,由于还未登录,被引导到认证系统中进行登录,认证系统接受用户名密码等安全信息,生成访问令牌(ticket)。用户通过 ticket 访问应用系统,应用系统接受到请求之后会访问认证系统检查 ticket 的合法性,如果检查通过,用户就可以在不用再次登录的情况下访问应用系统资源。

              以上介绍了单点登录常见的 3 种实现方案,在实际应用中还需要根据具体场景来实现。

              接下来通过几个实际生产场景,来阐述单点登录的详细实现方案。

              3、实际场景中SSO实现

              3.1 JWT(完全跨域方案)

              JWT (JSON Web Token)是一个开放标准(RFC7519),它是一个含签名并携带用户相关信息的加密串。页面请求校验登录接口时,请求头中携带 JWT 串到后端服务,后端通过签名加密串匹配校验,保证信息未被篡改,校验通过则认为是可靠的请求,将正常返回数据。

              DataSimba(奇点云数据中台产品)结合 JWT 与分布式 session,实现多域多空间单点登录。通过 JWT 生成和校验令牌,将刷新令牌存储在 redis 中,网关统一校验令牌,校验通过后将用户信息设置在请求头中,应用在拦截器中获取到用户信息后即可验证通过。

              不同域中的 DataSimba 共用一套密钥并且实时同步用户信息,通过 JWT 生成和校验令牌,用户登录其中一个域后,前端获取 JWT 加密串并存储在 Local Storage 中,当用户切换到其他域时前端传入加密串,后端网关校验,由此实现免登录访问其他域资源,如下图所示: 20221011180112

              3.2 使用 OAuth2.0 实现单点登录

              OAuth2.0是可以实现SSO的功能,但是OAuth 诞生之初就是为了解决 Web 浏览器场景下的授权问题

              4、参考资料

              OAuth2.0授权码模式中为什么一定要有code授权码的补充授权码模式中code的意义授权码模式是让token在服务器和服务器之间传递,不会经过资源拥有者,资源拥有者无需看到token

              ',32)]))}const p=t(a,[["render",o],["__file","SSO.html.vue"]]),c=JSON.parse('{"path":"/other/training/SSO.html","title":"OAuth2分享","lang":"zh-CN","frontmatter":{"title":"OAuth2分享","date":"2021-10-09T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"category":["小组分享"],"description":"1、SSO 1.1 SSO介绍 单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 1.2 SSO使用场景(解决了什么问题) 很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/training/SSO.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"OAuth2分享"}],["meta",{"property":"og:description","content":"1、SSO 1.1 SSO介绍 单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 1.2 SSO使用场景(解决了什么问题) 很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20221011175808.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2021-10-09T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"OAuth2分享\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20221011175808.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221011175916.png\\",\\"https://ddns.chensina.cn:29000/afatpig/blog/20221011180112.png\\"],\\"datePublished\\":\\"2021-10-09T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":2,"title":"1、SSO","slug":"_1、sso","link":"#_1、sso","children":[{"level":3,"title":"1.1 SSO介绍","slug":"_1-1-sso介绍","link":"#_1-1-sso介绍","children":[]},{"level":3,"title":"1.2 SSO使用场景(解决了什么问题)","slug":"_1-2-sso使用场景-解决了什么问题","link":"#_1-2-sso使用场景-解决了什么问题","children":[]},{"level":3,"title":"1.3 使用SSO的好处","slug":"_1-3-使用sso的好处","link":"#_1-3-使用sso的好处","children":[]}]},{"level":2,"title":"2、SSO常见实现方案","slug":"_2、sso常见实现方案","link":"#_2、sso常见实现方案","children":[{"level":3,"title":"2.1 基于 Cookie 的单点登录","slug":"_2-1-基于-cookie-的单点登录","link":"#_2-1-基于-cookie-的单点登录","children":[]},{"level":3,"title":"2.2 分布式 Session 实现单点登录","slug":"_2-2-分布式-session-实现单点登录","link":"#_2-2-分布式-session-实现单点登录","children":[]},{"level":3,"title":"2.3 统一认证授权方式实现单点登录","slug":"_2-3-统一认证授权方式实现单点登录","link":"#_2-3-统一认证授权方式实现单点登录","children":[]}]},{"level":2,"title":"3、实际场景中SSO实现","slug":"_3、实际场景中sso实现","link":"#_3、实际场景中sso实现","children":[{"level":3,"title":"3.1 JWT(完全跨域方案)","slug":"_3-1-jwt-完全跨域方案","link":"#_3-1-jwt-完全跨域方案","children":[]},{"level":3,"title":"3.2 使用 OAuth2.0 实现单点登录","slug":"_3-2-使用-oauth2-0-实现单点登录","link":"#_3-2-使用-oauth2-0-实现单点登录","children":[]}]},{"level":2,"title":"4、参考资料","slug":"_4、参考资料","link":"#_4、参考资料","children":[]}],"git":{"createdTime":1665557335000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2}]},"readingTime":{"minutes":7.15,"words":2144},"filePathRelative":"other/training/SSO.md","localizedDate":"2021年10月9日","excerpt":"

              1、SSO

              \\n

              1.1 SSO介绍

              \\n

              单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

              \\n

              1.2 SSO使用场景(解决了什么问题)

              \\n

              很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影豆瓣日记......真的会让人崩溃的。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。怎么做?

              ","autoDesc":true}');export{p as comp,c as data}; diff --git a/assets/SSO.html-BQ6lKX33.js b/assets/SSO.html-DntfVSpP.js similarity index 99% rename from assets/SSO.html-BQ6lKX33.js rename to assets/SSO.html-DntfVSpP.js index 9151f1335..0a61ff61c 100644 --- a/assets/SSO.html-BQ6lKX33.js +++ b/assets/SSO.html-DntfVSpP.js @@ -1 +1 @@ -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a,o}from"./app-LRWZkDAq.js";const r={};function i(s,t){return o(),n("div",null,t[0]||(t[0]=[a('

              1、写在前面

              一个系统一般都会有用户角色权限相关的概念,用户需要登录系统并且拥有相应的权限后才能访问对应的资源。

              • 认证(Authentication),即确认用户的身份,也就是上面说的要输入帐号密码登录
              • 授权(Authorization),即用户要访问某资源,必须要拥有对应权限

              也就是说,用户登录(认证)成功后,会获得此用户拥有的权限,用户在去访问资源时,系统会校验其拥有的权限是否可以访问这个资源。

              2、SSO和OAUTH2介绍

              SSO(SingleSignOn)的出现是为了解决多系统认证的问题的,这里特别强调一下,它是解决认证问题的。就是通过用户的一次性鉴别登录,当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。

              oauth是一个协议,它们定义了一套流程规范,你要实现我这个协议,就得按照我的规范来,和java中jdbc一样,sun公司负责规定一套规范,提供一套接口,下游的用户自行负责实现,你具体怎么实现我不关心,你要做的就是实现我规定的一套规范。

              OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。OAuth强调的是授权,当然要想获取授权前你必须认证,就像第一节说的,用户只有登录后才能获得对应用户的权限,这一步是无法绕开的。

              3、SSO实现

              单点登录实现中,系统之间的协议对接是非常重要的一环,一般涉及的标准协议类型有 CAS、OAuth、OpenID Connect、SAML。

              另外国产的SA-TOKEN项目也有sso和oauth实现

              3.1 CAS

              Central Authentication Service简称CAS,是一种常见的B/S架构的SSO协议。和其他任何SSO协议一样,用户仅需登陆一次,访问其他应用则无需再次登陆。顾名思义,CAS是一种仅用于Authentication的服务,它和OAuth/OIDC协议不一样,并不能作为一种Authorization的协议。当前CAS协议包括CAS 1.0、CAS2.0、CAS3.0版本,这三个版本的认证流程基本类似。CAS的认证流程通过包括几部分参与者:

              Client: 通常为使用浏览器的用户 CAS Client: 实现CAS协议的Web应用 CAS Server: 作为统一认证的CAS服务器

              4、各种协议对比

              单纯的单点登录,其实无需授权中心有一套自己的用户和权限,单点登录是只管登录,登录后从业务系统自行获取自己的权限,所以从这个角度来说sso也有它自己的优势,它不像oauth那样还要维护一套用户权限系统,如果仅仅作单点登录不用权限的话用cas显然更有优势。

              以下图片是各种实现的对比图来自知乎,我并不赞同他的观点,仅作参考 20230104171826

              以下对比来自sa-token官网

              功能点SSO单点登录OAuth2.0
              统一认证支持度高支持度高
              统一注销支持度高支持度低
              多个系统会话一致性强一致弱一致
              第三方应用授权管理不支持支持度高
              自有系统授权管理支持度高支持度低
              Client级的权限校验不支持支持度高
              集成简易度比较简单难度中等
              ',19)]))}const p=e(r,[["render",i],["__file","SSO.html.vue"]]),l=JSON.parse('{"path":"/java/framework/security/SSO.html","title":"SSO协议","lang":"zh-CN","frontmatter":{"title":"SSO协议","date":"2022-01-03T00:00:00.000Z","tag":["oauth","sso"],"description":"1、写在前面 一个系统一般都会有用户角色权限相关的概念,用户需要登录系统并且拥有相应的权限后才能访问对应的资源。 认证(Authentication),即确认用户的身份,也就是上面说的要输入帐号密码登录 授权(Authorization),即用户要访问某资源,必须要拥有对应权限 也就是说,用户登录(认证)成功后,会获得此用户拥有的权限,用户在去访问资源...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/security/SSO.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"SSO协议"}],["meta",{"property":"og:description","content":"1、写在前面 一个系统一般都会有用户角色权限相关的概念,用户需要登录系统并且拥有相应的权限后才能访问对应的资源。 认证(Authentication),即确认用户的身份,也就是上面说的要输入帐号密码登录 授权(Authorization),即用户要访问某资源,必须要拥有对应权限 也就是说,用户登录(认证)成功后,会获得此用户拥有的权限,用户在去访问资源..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20230104171826.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:tag","content":"oauth"}],["meta",{"property":"article:tag","content":"sso"}],["meta",{"property":"article:published_time","content":"2022-01-03T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"SSO协议\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104171826.png\\"],\\"datePublished\\":\\"2022-01-03T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、写在前面","slug":"_1、写在前面","link":"#_1、写在前面","children":[]},{"level":2,"title":"2、SSO和OAUTH2介绍","slug":"_2、sso和oauth2介绍","link":"#_2、sso和oauth2介绍","children":[]},{"level":2,"title":"3、SSO实现","slug":"_3、sso实现","link":"#_3、sso实现","children":[{"level":3,"title":"3.1 CAS","slug":"_3-1-cas","link":"#_3-1-cas","children":[]}]},{"level":2,"title":"4、各种协议对比","slug":"_4、各种协议对比","link":"#_4、各种协议对比","children":[]}],"git":{"createdTime":1672824556000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2}]},"readingTime":{"minutes":3.65,"words":1094},"filePathRelative":"java/framework/security/SSO.md","localizedDate":"2022年1月3日","excerpt":"

              1、写在前面

              \\n

              一个系统一般都会有用户角色权限相关的概念,用户需要登录系统并且拥有相应的权限后才能访问对应的资源。

              \\n
                \\n
              • 认证(Authentication),即确认用户的身份,也就是上面说的要输入帐号密码登录
              • \\n
              • 授权(Authorization),即用户要访问某资源,必须要拥有对应权限
              • \\n
              \\n

              也就是说,用户登录(认证)成功后,会获得此用户拥有的权限,用户在去访问资源时,系统会校验其拥有的权限是否可以访问这个资源。

              \\n

              2、SSO和OAUTH2介绍

              \\n

              SSO(SingleSignOn)的出现是为了解决多系统认证的问题的,这里特别强调一下,它是解决认证问题的。就是通过用户的一次性鉴别登录,当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。

              ","autoDesc":true}');export{p as comp,l as data}; +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a,o}from"./app-BIdVr4mq.js";const r={};function i(s,t){return o(),n("div",null,t[0]||(t[0]=[a('

              1、写在前面

              一个系统一般都会有用户角色权限相关的概念,用户需要登录系统并且拥有相应的权限后才能访问对应的资源。

              • 认证(Authentication),即确认用户的身份,也就是上面说的要输入帐号密码登录
              • 授权(Authorization),即用户要访问某资源,必须要拥有对应权限

              也就是说,用户登录(认证)成功后,会获得此用户拥有的权限,用户在去访问资源时,系统会校验其拥有的权限是否可以访问这个资源。

              2、SSO和OAUTH2介绍

              SSO(SingleSignOn)的出现是为了解决多系统认证的问题的,这里特别强调一下,它是解决认证问题的。就是通过用户的一次性鉴别登录,当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。

              oauth是一个协议,它们定义了一套流程规范,你要实现我这个协议,就得按照我的规范来,和java中jdbc一样,sun公司负责规定一套规范,提供一套接口,下游的用户自行负责实现,你具体怎么实现我不关心,你要做的就是实现我规定的一套规范。

              OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。OAuth强调的是授权,当然要想获取授权前你必须认证,就像第一节说的,用户只有登录后才能获得对应用户的权限,这一步是无法绕开的。

              3、SSO实现

              单点登录实现中,系统之间的协议对接是非常重要的一环,一般涉及的标准协议类型有 CAS、OAuth、OpenID Connect、SAML。

              另外国产的SA-TOKEN项目也有sso和oauth实现

              3.1 CAS

              Central Authentication Service简称CAS,是一种常见的B/S架构的SSO协议。和其他任何SSO协议一样,用户仅需登陆一次,访问其他应用则无需再次登陆。顾名思义,CAS是一种仅用于Authentication的服务,它和OAuth/OIDC协议不一样,并不能作为一种Authorization的协议。当前CAS协议包括CAS 1.0、CAS2.0、CAS3.0版本,这三个版本的认证流程基本类似。CAS的认证流程通过包括几部分参与者:

              Client: 通常为使用浏览器的用户 CAS Client: 实现CAS协议的Web应用 CAS Server: 作为统一认证的CAS服务器

              4、各种协议对比

              单纯的单点登录,其实无需授权中心有一套自己的用户和权限,单点登录是只管登录,登录后从业务系统自行获取自己的权限,所以从这个角度来说sso也有它自己的优势,它不像oauth那样还要维护一套用户权限系统,如果仅仅作单点登录不用权限的话用cas显然更有优势。

              以下图片是各种实现的对比图来自知乎,我并不赞同他的观点,仅作参考 20230104171826

              以下对比来自sa-token官网

              功能点SSO单点登录OAuth2.0
              统一认证支持度高支持度高
              统一注销支持度高支持度低
              多个系统会话一致性强一致弱一致
              第三方应用授权管理不支持支持度高
              自有系统授权管理支持度高支持度低
              Client级的权限校验不支持支持度高
              集成简易度比较简单难度中等
              ',19)]))}const p=e(r,[["render",i],["__file","SSO.html.vue"]]),l=JSON.parse('{"path":"/java/framework/security/SSO.html","title":"SSO协议","lang":"zh-CN","frontmatter":{"title":"SSO协议","date":"2022-01-03T00:00:00.000Z","tag":["oauth","sso"],"description":"1、写在前面 一个系统一般都会有用户角色权限相关的概念,用户需要登录系统并且拥有相应的权限后才能访问对应的资源。 认证(Authentication),即确认用户的身份,也就是上面说的要输入帐号密码登录 授权(Authorization),即用户要访问某资源,必须要拥有对应权限 也就是说,用户登录(认证)成功后,会获得此用户拥有的权限,用户在去访问资源...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/security/SSO.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"SSO协议"}],["meta",{"property":"og:description","content":"1、写在前面 一个系统一般都会有用户角色权限相关的概念,用户需要登录系统并且拥有相应的权限后才能访问对应的资源。 认证(Authentication),即确认用户的身份,也就是上面说的要输入帐号密码登录 授权(Authorization),即用户要访问某资源,必须要拥有对应权限 也就是说,用户登录(认证)成功后,会获得此用户拥有的权限,用户在去访问资源..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://ddns.chensina.cn:29000/afatpig/blog/20230104171826.png"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:tag","content":"oauth"}],["meta",{"property":"article:tag","content":"sso"}],["meta",{"property":"article:published_time","content":"2022-01-03T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"SSO协议\\",\\"image\\":[\\"https://ddns.chensina.cn:29000/afatpig/blog/20230104171826.png\\"],\\"datePublished\\":\\"2022-01-03T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、写在前面","slug":"_1、写在前面","link":"#_1、写在前面","children":[]},{"level":2,"title":"2、SSO和OAUTH2介绍","slug":"_2、sso和oauth2介绍","link":"#_2、sso和oauth2介绍","children":[]},{"level":2,"title":"3、SSO实现","slug":"_3、sso实现","link":"#_3、sso实现","children":[{"level":3,"title":"3.1 CAS","slug":"_3-1-cas","link":"#_3-1-cas","children":[]}]},{"level":2,"title":"4、各种协议对比","slug":"_4、各种协议对比","link":"#_4、各种协议对比","children":[]}],"git":{"createdTime":1672824556000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":2}]},"readingTime":{"minutes":3.65,"words":1094},"filePathRelative":"java/framework/security/SSO.md","localizedDate":"2022年1月3日","excerpt":"

              1、写在前面

              \\n

              一个系统一般都会有用户角色权限相关的概念,用户需要登录系统并且拥有相应的权限后才能访问对应的资源。

              \\n
                \\n
              • 认证(Authentication),即确认用户的身份,也就是上面说的要输入帐号密码登录
              • \\n
              • 授权(Authorization),即用户要访问某资源,必须要拥有对应权限
              • \\n
              \\n

              也就是说,用户登录(认证)成功后,会获得此用户拥有的权限,用户在去访问资源时,系统会校验其拥有的权限是否可以访问这个资源。

              \\n

              2、SSO和OAUTH2介绍

              \\n

              SSO(SingleSignOn)的出现是为了解决多系统认证的问题的,这里特别强调一下,它是解决认证问题的。就是通过用户的一次性鉴别登录,当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。

              ","autoDesc":true}');export{p as comp,l as data}; diff --git a/assets/Samba.html-CBqUaBWb.js b/assets/Samba.html-DzYkwvBv.js similarity index 99% rename from assets/Samba.html-CBqUaBWb.js rename to assets/Samba.html-DzYkwvBv.js index 6a5b02114..5ee55e9a2 100644 --- a/assets/Samba.html-CBqUaBWb.js +++ b/assets/Samba.html-DzYkwvBv.js @@ -1,4 +1,4 @@ -import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as i}from"./app-LRWZkDAq.js";const l={};function p(t,s){return i(),a("div",null,s[0]||(s[0]=[e(`

              1、安装过程省略

              2、配置

              2.1 配置文件

              配置目录在 /etc/samba,修改smb.conf在最后加一组[test],同时修改[global]在里面加上ntlm auth = yes,最终加完如下

              # See smb.conf.example for a more detailed config file or
              +import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as i}from"./app-BIdVr4mq.js";const l={};function p(t,s){return i(),a("div",null,s[0]||(s[0]=[e(`

              1、安装过程省略

              2、配置

              2.1 配置文件

              配置目录在 /etc/samba,修改smb.conf在最后加一组[test],同时修改[global]在里面加上ntlm auth = yes,最终加完如下

              # See smb.conf.example for a more detailed config file or
               # read the smb.conf manpage.
               # Run 'testparm' to verify the config is correct after
               # you modified it.
              diff --git a/assets/SecurityFilterChain.html-Brlhg7Xm.js b/assets/SecurityFilterChain.html-CklH0qq_.js
              similarity index 99%
              rename from assets/SecurityFilterChain.html-Brlhg7Xm.js
              rename to assets/SecurityFilterChain.html-CklH0qq_.js
              index fd15a5e2f..aafbecfb9 100644
              --- a/assets/SecurityFilterChain.html-Brlhg7Xm.js
              +++ b/assets/SecurityFilterChain.html-CklH0qq_.js
              @@ -1,4 +1,4 @@
              -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const t={};function e(h,i){return l(),a("div",null,i[0]||(i[0]=[n(`

              1、配置类

              1. 5.7版本后Security把WebSecurityConfigurerAdapter标记为废弃,鼓励程序员使用SecurityFilterChain进行配置,如果看过官网Security的架构图对SecurityFilterChain一定不会陌生,此类是Security过滤器的核心,所以用它来配置寓意更为明显。
                  @Bean
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const t={};function e(h,i){return l(),a("div",null,i[0]||(i[0]=[n(`

              1、配置类

              1. 5.7版本后Security把WebSecurityConfigurerAdapter标记为废弃,鼓励程序员使用SecurityFilterChain进行配置,如果看过官网Security的架构图对SecurityFilterChain一定不会陌生,此类是Security过滤器的核心,所以用它来配置寓意更为明显。
                  @Bean
                   @Order(3)
                   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
                       return http.authorizeRequests()
              diff --git a/assets/Serialization.html-BSKOYFIh.js b/assets/Serialization.html-OnvjGjMN.js
              similarity index 99%
              rename from assets/Serialization.html-BSKOYFIh.js
              rename to assets/Serialization.html-OnvjGjMN.js
              index 6339e43c8..03ba2691e 100644
              --- a/assets/Serialization.html-BSKOYFIh.js
              +++ b/assets/Serialization.html-OnvjGjMN.js
              @@ -1,4 +1,4 @@
              -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function t(k,s){return l(),a("div",null,s[0]||(s[0]=[n(`

              1、序列化、反序列化是什么?

              • 序列化:把对象转化成字节码
              • 反序列化:把字节码(从IO流获取或者从硬盘文件读取)转化为对象

              2、 举例说明作用?

              只有序列化成字节码文件后,对象才能在网络中通过IO流(传输的是字节码)传输或者存到硬盘上

              2.1 实现分布式对象

              例如在RMI(Remote Method Invoke)中利用对象序列化来运行远程主机上的服务,就像执行本地的对象方法一样;

              2.2 Java对象序列化不仅保留一个对象的数据,而且会递归保留引用的对象

              序列化时,把虚拟机内存中的对象保存到文件,反序列化时再把文件中的对象还原到内存中去

              注意如果序列化Student时,Teacher类没有被序列化则会抛出异常~~~ java.io.NotSerializableException: com.chen.Teacher~~~

              如果想不抛出异常有两种方法:

              1. 老老实实把Teacher类也实现Serializable接口
              2. 在Student类中把Teacher字段用transient修饰,则反序列化后看到的teacher是null这是和第一种方法不一样的地方
              //学生类实现序列化就接口,并且学生有一个老师属性字段
              +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function t(k,s){return l(),a("div",null,s[0]||(s[0]=[n(`

              1、序列化、反序列化是什么?

              • 序列化:把对象转化成字节码
              • 反序列化:把字节码(从IO流获取或者从硬盘文件读取)转化为对象

              2、 举例说明作用?

              只有序列化成字节码文件后,对象才能在网络中通过IO流(传输的是字节码)传输或者存到硬盘上

              2.1 实现分布式对象

              例如在RMI(Remote Method Invoke)中利用对象序列化来运行远程主机上的服务,就像执行本地的对象方法一样;

              2.2 Java对象序列化不仅保留一个对象的数据,而且会递归保留引用的对象

              序列化时,把虚拟机内存中的对象保存到文件,反序列化时再把文件中的对象还原到内存中去

              注意如果序列化Student时,Teacher类没有被序列化则会抛出异常~~~ java.io.NotSerializableException: com.chen.Teacher~~~

              如果想不抛出异常有两种方法:

              1. 老老实实把Teacher类也实现Serializable接口
              2. 在Student类中把Teacher字段用transient修饰,则反序列化后看到的teacher是null这是和第一种方法不一样的地方
              //学生类实现序列化就接口,并且学生有一个老师属性字段
               
               public class Student implements Serializable {
                   private Teacher teacher = new Teacher("James Li", 33);
              diff --git a/assets/ServiceInstall.html-a8eSZceK.js b/assets/ServiceInstall.html-IjUvufHh.js
              similarity index 99%
              rename from assets/ServiceInstall.html-a8eSZceK.js
              rename to assets/ServiceInstall.html-IjUvufHh.js
              index 19692a207..7c79f3b8c 100644
              --- a/assets/ServiceInstall.html-a8eSZceK.js
              +++ b/assets/ServiceInstall.html-IjUvufHh.js
              @@ -1,4 +1,4 @@
              -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a,o as n}from"./app-LRWZkDAq.js";const t={};function l(d,i){return n(),e("div",null,i[0]||(i[0]=[a(`

              1. docker安装Redis

              起初的需求是用docker启动一个redis,并且指定一个配置,死活不成功,主要是少设置了data目录的映射

              注意

              使用docker-compose安装redis时,若指定外置redis.conf配置文件,要切记同时设置data存储目录,默认docker中的数据存在/data下,所以 使用卷映射时需要映射到容器内的/data docker-compose.yml

              目录结构如下,需要事先准备一个redis.conf放到项目suc/redis/conf,另外需要创建目录suc/redis/data

              suc
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a,o as n}from"./app-BIdVr4mq.js";const t={};function l(d,i){return n(),e("div",null,i[0]||(i[0]=[a(`

              1. docker安装Redis

              起初的需求是用docker启动一个redis,并且指定一个配置,死活不成功,主要是少设置了data目录的映射

              注意

              使用docker-compose安装redis时,若指定外置redis.conf配置文件,要切记同时设置data存储目录,默认docker中的数据存在/data下,所以 使用卷映射时需要映射到容器内的/data docker-compose.yml

              目录结构如下,需要事先准备一个redis.conf放到项目suc/redis/conf,另外需要创建目录suc/redis/data

              suc
               ├── redis
               │   ├── conf
               │   │   └── redis.conf
              diff --git a/assets/Session.html-B4ArK8cx.js b/assets/Session.html-C7UWc88l.js
              similarity index 99%
              rename from assets/Session.html-B4ArK8cx.js
              rename to assets/Session.html-C7UWc88l.js
              index ea00d10f4..ad0b23b75 100644
              --- a/assets/Session.html-B4ArK8cx.js
              +++ b/assets/Session.html-C7UWc88l.js
              @@ -1,4 +1,4 @@
              -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as s,a as n,o as t}from"./app-LRWZkDAq.js";const a={};function r(o,e){return t(),s("div",null,e[0]||(e[0]=[n(`

              1、背景(问题复现)

              
              +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as s,a as n,o as t}from"./app-BIdVr4mq.js";const a={};function r(o,e){return t(),s("div",null,e[0]||(e[0]=[n(`

              1、背景(问题复现)

              
               1. 使用idea新建一个springboot项目,引入web和security框架,不做任何配置直接启动web项目
               2. 随便访问一个接口比如:http://localhost:8080/hello/aa,此时由于接口被security默认保护,会重定向到登录页面(如图一),此时查看sessionid(也就是name为JSESSIONID的cookie)是91E9629F748637154F86CCB44FB2B23D
               3. 然后输入用户名:user,密码:控制台随机生成的,登录后会重定向到之前访问的接口,但此时
              diff --git a/assets/SetObjectNull.html-Cv2qsWfW.js b/assets/SetObjectNull.html-CiI27fz6.js
              similarity index 99%
              rename from assets/SetObjectNull.html-Cv2qsWfW.js
              rename to assets/SetObjectNull.html-CiI27fz6.js
              index 7b58f34d2..1e48ea929 100644
              --- a/assets/SetObjectNull.html-Cv2qsWfW.js
              +++ b/assets/SetObjectNull.html-CiI27fz6.js
              @@ -1,4 +1,4 @@
              -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as l}from"./app-LRWZkDAq.js";const n={};function h(t,i){return l(),a("div",null,i[0]||(i[0]=[e(`

              前言:是否需要把不用的对象设置为null?

              1、开始写代码测试(所有测试都要加上以下指令)

              jvm参数-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=2097152

              简单解释一下:

              • -Xms20m -Xmx20m这两个指令限制堆内存固定为20m不允许扩容
              • -Xmn10m代表分配给新生代的总内存为10m
              • -XX:SurvivorRatio=8代表Eden区和Survivor的比例8:1,即新生代被分为3部分,分别8m,1m,1m
              • XX:PretenureSizeThreshold=2097152,这个指令用的比较少,在虚拟机中,普通对象都在新生代分配内存,但是大对象是直接在老年代分配,至于多大算大对象,就是这个参数来设置的,我设置的是2m用来测试(2097152 =2 * 1024 * 1024 ),设置2m是方便我测试,保证我在下面代码设置1m的MB_1对象,内存是在新生代分配,而不是直接进入老年代
              • -XX:+PrintGCDetails打印垃圾回收日志

              1.1 第一次测试,直接创建一个512kb的数组,调用回收

                public static void main(String[] args) throws InterruptedException {
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as e,o as l}from"./app-BIdVr4mq.js";const n={};function h(t,i){return l(),a("div",null,i[0]||(i[0]=[e(`

              前言:是否需要把不用的对象设置为null?

              1、开始写代码测试(所有测试都要加上以下指令)

              jvm参数-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=2097152

              简单解释一下:

              • -Xms20m -Xmx20m这两个指令限制堆内存固定为20m不允许扩容
              • -Xmn10m代表分配给新生代的总内存为10m
              • -XX:SurvivorRatio=8代表Eden区和Survivor的比例8:1,即新生代被分为3部分,分别8m,1m,1m
              • XX:PretenureSizeThreshold=2097152,这个指令用的比较少,在虚拟机中,普通对象都在新生代分配内存,但是大对象是直接在老年代分配,至于多大算大对象,就是这个参数来设置的,我设置的是2m用来测试(2097152 =2 * 1024 * 1024 ),设置2m是方便我测试,保证我在下面代码设置1m的MB_1对象,内存是在新生代分配,而不是直接进入老年代
              • -XX:+PrintGCDetails打印垃圾回收日志

              1.1 第一次测试,直接创建一个512kb的数组,调用回收

                public static void main(String[] args) throws InterruptedException {
                       byte[] KB_512 = new byte[1 * 1024 * 512];
                       System.gc();
                   }

              结果:

              
              diff --git a/assets/ShareBetweenWindowsAndLinux.html-ChX2AFxh.js b/assets/ShareBetweenWindowsAndLinux.html-B04nBB2Z.js
              similarity index 99%
              rename from assets/ShareBetweenWindowsAndLinux.html-ChX2AFxh.js
              rename to assets/ShareBetweenWindowsAndLinux.html-B04nBB2Z.js
              index 0ea0c679b..dcf66b165 100644
              --- a/assets/ShareBetweenWindowsAndLinux.html-ChX2AFxh.js
              +++ b/assets/ShareBetweenWindowsAndLinux.html-B04nBB2Z.js
              @@ -1,4 +1,4 @@
              -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as s,a as n,o as a}from"./app-LRWZkDAq.js";const t={};function l(d,i){return a(),s("div",null,i[0]||(i[0]=[n(`

              1、在windows设置共享目录

              设置过程省略……

              2、在linux下挂载

                  ## 1. 创建空白目录
              +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as s,a as n,o as a}from"./app-BIdVr4mq.js";const t={};function l(d,i){return a(),s("div",null,i[0]||(i[0]=[n(`

              1、在windows设置共享目录

              设置过程省略……

              2、在linux下挂载

                  ## 1. 创建空白目录
                       mkdir /home/data/share
                   ## 2. 修改/etc/fstab开机自动挂载
                   //10.10.102.97/tempfile    /home/data/share  cifs    defaults,user=xxx,password=xxx,uid=1000,gid=1000  0 0

              解释: //10.10.102.97/tempfile是在windows设置的共享目录,设置好了可以用其他windows电脑测试下看是否共享成功 /home/data/share 挂载的位置,一定要是个空目录, cifs 不要修改 user windows帐户, password windows密码 uid 挂载的目录所属用户,在linux终端用id 你的用户名查看 gid 用户组,和uid一个道理,这两个一定要指定,不然就被挂到root了

              参考博客

              参考

              `,7)]))}const o=e(t,[["render",l],["__file","ShareBetweenWindowsAndLinux.html.vue"]]),p=JSON.parse('{"path":"/other/linux/ShareBetweenWindowsAndLinux.html","title":"Linux挂载windows共享目录","lang":"zh-CN","frontmatter":{"title":"Linux挂载windows共享目录","date":"2023-10-26T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"description":"1、在windows设置共享目录 设置过程省略…… 2、在linux下挂载 解释: //10.10.102.97/tempfile是在windows设置的共享目录,设置好了可以用其他windows电脑测试下看是否共享成功 /home/data/share 挂载的位置,一定要是个空目录, cifs 不要修改 user windows帐户, passwor...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/linux/ShareBetweenWindowsAndLinux.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Linux挂载windows共享目录"}],["meta",{"property":"og:description","content":"1、在windows设置共享目录 设置过程省略…… 2、在linux下挂载 解释: //10.10.102.97/tempfile是在windows设置的共享目录,设置好了可以用其他windows电脑测试下看是否共享成功 /home/data/share 挂载的位置,一定要是个空目录, cifs 不要修改 user windows帐户, passwor..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-22T03:45:12.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2023-10-26T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-22T03:45:12.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Linux挂载windows共享目录\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2023-10-26T00:00:00.000Z\\",\\"dateModified\\":\\"2024-03-22T03:45:12.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":3,"title":"1、在windows设置共享目录","slug":"_1、在windows设置共享目录","link":"#_1、在windows设置共享目录","children":[]},{"level":3,"title":"2、在linux下挂载","slug":"_2、在linux下挂载","link":"#_2、在linux下挂载","children":[]},{"level":3,"title":"参考博客","slug":"参考博客","link":"#参考博客","children":[]}],"git":{"createdTime":1698298706000,"updatedTime":1711079112000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2}]},"readingTime":{"minutes":0.64,"words":192},"filePathRelative":"other/linux/ShareBetweenWindowsAndLinux.md","localizedDate":"2023年10月26日","excerpt":"

              1、在windows设置共享目录

              \\n

              设置过程省略……

              \\n

              2、在linux下挂载

              \\n
                  ## 1. 创建空白目录\\n        mkdir /home/data/share\\n    ## 2. 修改/etc/fstab开机自动挂载\\n    //10.10.102.97/tempfile    /home/data/share  cifs    defaults,user=xxx,password=xxx,uid=1000,gid=1000  0 0
              \\n
              ","autoDesc":true}');export{o as comp,p as data}; diff --git a/assets/SoftWare.html-BZYs1Hsf.js b/assets/SoftWare.html-6ZCndbox.js similarity index 98% rename from assets/SoftWare.html-BZYs1Hsf.js rename to assets/SoftWare.html-6ZCndbox.js index 4bd455628..4945103b7 100644 --- a/assets/SoftWare.html-BZYs1Hsf.js +++ b/assets/SoftWare.html-6ZCndbox.js @@ -1,3 +1,3 @@ -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a,o as s}from"./app-LRWZkDAq.js";const n={};function r(o,e){return s(),t("div",null,e[0]||(e[0]=[a(`

              1、Beyond Compare3

              1.1、下载

              资源地址

              1.2 、集成到git 对比

              参考此处配置,主要有两步,

              第一步,设置:

               git config --global merge.tool bc3
              +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,a,o as s}from"./app-BIdVr4mq.js";const n={};function r(o,e){return s(),t("div",null,e[0]||(e[0]=[a(`

              1、Beyond Compare3

              1.1、下载

              资源地址

              1.2 、集成到git 对比

              参考此处配置,主要有两步,

              第一步,设置:

               git config --global merge.tool bc3
               $ git config --global mergetool.bc3.path "c:/program files/beyond compare 3/bcomp.exe"

              第二部,使用:

              #注意对比使用git difftool不要用git diff
               git difftool [xxx]
              `,9)]))}const p=i(n,[["render",r],["__file","SoftWare.html.vue"]]),d=JSON.parse('{"path":"/other/tools/SoftWare.html","title":"软件分享","lang":"zh-CN","frontmatter":{"title":"软件分享","date":"2021-07-22T00:00:00.000Z","author":"chenkun","publish":true,"keys":null,"description":"1、Beyond Compare3 1.1、下载 资源地址 1.2 、集成到git 对比 参考此处配置,主要有两步, 第一步,设置: 第二部,使用:","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/tools/SoftWare.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"软件分享"}],["meta",{"property":"og:description","content":"1、Beyond Compare3 1.1、下载 资源地址 1.2 、集成到git 对比 参考此处配置,主要有两步, 第一步,设置: 第二部,使用:"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:author","content":"chenkun"}],["meta",{"property":"article:published_time","content":"2021-07-22T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"软件分享\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2021-07-22T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chenkun\\"}]}"]]},"headers":[{"level":3,"title":"1、Beyond Compare3","slug":"_1、beyond-compare3","link":"#_1、beyond-compare3","children":[]}],"git":{"createdTime":1659362219000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":5},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.3,"words":89},"filePathRelative":"other/tools/SoftWare.md","localizedDate":"2021年7月22日","excerpt":"

              1、Beyond Compare3

              \\n

              1.1、下载

              \\n

              资源地址

              \\n

              1.2 、集成到git 对比

              \\n

              参考此处配置,主要有两步,

              ","autoDesc":true}');export{p as comp,d as data}; diff --git a/assets/SpringAOP.html-2cvojWKp.js b/assets/SpringAOP.html-DLZP8sCz.js similarity index 99% rename from assets/SpringAOP.html-2cvojWKp.js rename to assets/SpringAOP.html-DLZP8sCz.js index 62f5c3cfb..7f25c7143 100644 --- a/assets/SpringAOP.html-2cvojWKp.js +++ b/assets/SpringAOP.html-DLZP8sCz.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const t={};function e(h,i){return l(),a("div",null,i[0]||(i[0]=[n(`

              1、SpringAOP

              > SpringAOP的本质就是动态代理,底层使用JDK动态代理或者CGlib动态代理,通过代理框架生成代理类,实现对目标类的增强,Spring代理是方法级别的代理,是对方法增强,
              +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const t={};function e(h,i){return l(),a("div",null,i[0]||(i[0]=[n(`

              1、SpringAOP

              > SpringAOP的本质就是动态代理,底层使用JDK动态代理或者CGlib动态代理,通过代理框架生成代理类,实现对目标类的增强,Spring代理是方法级别的代理,是对方法增强,
               > 
               > 代理有四个要素:
               > 1. 目标类 
              diff --git a/assets/SpringBootAutoConfiguration.html-BJXaC7pW.js b/assets/SpringBootAutoConfiguration.html-xbEEkjIP.js
              similarity index 99%
              rename from assets/SpringBootAutoConfiguration.html-BJXaC7pW.js
              rename to assets/SpringBootAutoConfiguration.html-xbEEkjIP.js
              index b5d830158..e79ec3967 100644
              --- a/assets/SpringBootAutoConfiguration.html-BJXaC7pW.js
              +++ b/assets/SpringBootAutoConfiguration.html-xbEEkjIP.js
              @@ -1,4 +1,4 @@
              -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-LRWZkDAq.js";const l={};function h(k,i){return t(),a("div",null,i[0]||(i[0]=[n(`

              1. springboot自动配置的原理初探

              ​以下注解都在springboot的自动化配置包中:spring-boot-autoconfigure

              1. springboot程序的入口是在启动类,该类有个关键注解SpringBootApplication

                @Target(ElementType.TYPE)
                +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-BIdVr4mq.js";const l={};function h(k,i){return t(),a("div",null,i[0]||(i[0]=[n(`

                1. springboot自动配置的原理初探

                ​以下注解都在springboot的自动化配置包中:spring-boot-autoconfigure

                1. springboot程序的入口是在启动类,该类有个关键注解SpringBootApplication

                  @Target(ElementType.TYPE)
                   @Retention(RetentionPolicy.RUNTIME)
                   @Documented
                   @Inherited
                  diff --git a/assets/SpringCache.html-keNWZMJo.js b/assets/SpringCache.html-BsDFP-Bt.js
                  similarity index 99%
                  rename from assets/SpringCache.html-keNWZMJo.js
                  rename to assets/SpringCache.html-BsDFP-Bt.js
                  index 2407ebd87..d62b2a3f2 100644
                  --- a/assets/SpringCache.html-keNWZMJo.js
                  +++ b/assets/SpringCache.html-BsDFP-Bt.js
                  @@ -1,4 +1,4 @@
                  -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const t={};function h(l,i){return e(),a("div",null,i[0]||(i[0]=[n(`

                  Spring缓存大揭秘

                  注意

                  使用springboot测试springcache切记需要在启动类加上@EnableCache

                  1、背景

                  使用springboot项目时看到很多对spring缓存注解的使用,比如@Cacheable、@Evict、@CachePut等,之前一直都是知道个大概,使用方式也就局限于配置好redisTemplate然后直接开始使用,但是一直不明白为什么我配置好redisTemplate就可以用了,甚至不配置redis也能有缓存效果。之前还一直有个疑问,新增数据spring把返回结果缓存后,那么下次如果更新了数据,如何同时更新缓存列表呢?

                  2、spring-cache介绍

                  springcache不是一个单独的jar包,它位于spring-context的org.springframework.cache包下,spring提供了各种注解来使用缓存,并且提供了多种缓存实现,比如常见的redis,EhCache等。

                  image-20211014105423666
                  image-20211014105423666

                  3、springcache的实现

                  如果不引入redis等第三方包,则spring默认采用的是ConcurrentMapCache来管理缓存,它里面有个ConcurrentMap(具体实现是ConcurrentHashMap,在ConcurrentMapCacheManager类中传入的),如果引入第三方比如redis,则会自动使用redis

                  image-20211014105857365
                  image-20211014105857365
                  image-20211014110150052
                  image-20211014110150052

                  在上图中可以看到ConcurrentMapCacheManager创建了一个ConcurrentHashMap对象,用来初始化ConcurrentMapCache,所以由此我们可以知道spring默认缓存实际上是一个Map对象,占用的是JVM内存,一旦创建后就永不消失,因为它不像redis有过期时间,所以使用默认缓存要慎重,注意OOM

                  总结一下spring缓存默认实现:

                  1. SimpleCacheConfiguration是一个springboot的配置类,类中会实例化一个ConcurrentMapCacheManager的Bean,
                  @Configuration(proxyBeanMethods = false)
                  +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const t={};function h(l,i){return e(),a("div",null,i[0]||(i[0]=[n(`

                  Spring缓存大揭秘

                  注意

                  使用springboot测试springcache切记需要在启动类加上@EnableCache

                  1、背景

                  使用springboot项目时看到很多对spring缓存注解的使用,比如@Cacheable、@Evict、@CachePut等,之前一直都是知道个大概,使用方式也就局限于配置好redisTemplate然后直接开始使用,但是一直不明白为什么我配置好redisTemplate就可以用了,甚至不配置redis也能有缓存效果。之前还一直有个疑问,新增数据spring把返回结果缓存后,那么下次如果更新了数据,如何同时更新缓存列表呢?

                  2、spring-cache介绍

                  springcache不是一个单独的jar包,它位于spring-context的org.springframework.cache包下,spring提供了各种注解来使用缓存,并且提供了多种缓存实现,比如常见的redis,EhCache等。

                  image-20211014105423666
                  image-20211014105423666

                  3、springcache的实现

                  如果不引入redis等第三方包,则spring默认采用的是ConcurrentMapCache来管理缓存,它里面有个ConcurrentMap(具体实现是ConcurrentHashMap,在ConcurrentMapCacheManager类中传入的),如果引入第三方比如redis,则会自动使用redis

                  image-20211014105857365
                  image-20211014105857365
                  image-20211014110150052
                  image-20211014110150052

                  在上图中可以看到ConcurrentMapCacheManager创建了一个ConcurrentHashMap对象,用来初始化ConcurrentMapCache,所以由此我们可以知道spring默认缓存实际上是一个Map对象,占用的是JVM内存,一旦创建后就永不消失,因为它不像redis有过期时间,所以使用默认缓存要慎重,注意OOM

                  总结一下spring缓存默认实现:

                  1. SimpleCacheConfiguration是一个springboot的配置类,类中会实例化一个ConcurrentMapCacheManager的Bean,
                  @Configuration(proxyBeanMethods = false)
                   @ConditionalOnMissingBean(CacheManager.class)
                   @Conditional(CacheCondition.class)
                   class SimpleCacheConfiguration {
                  diff --git a/assets/SpringCloudGateway.html-CGUU2vu8.js b/assets/SpringCloudGateway.html-BsaY0McN.js
                  similarity index 99%
                  rename from assets/SpringCloudGateway.html-CGUU2vu8.js
                  rename to assets/SpringCloudGateway.html-BsaY0McN.js
                  index 16826f47e..3919f3843 100644
                  --- a/assets/SpringCloudGateway.html-CGUU2vu8.js
                  +++ b/assets/SpringCloudGateway.html-BsaY0McN.js
                  @@ -1,4 +1,4 @@
                  -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const l={};function t(h,s){return e(),a("div",null,s[0]||(s[0]=[n(`

                  问题描述

                  有一个业务服务,启动了两个做成负载均衡,分别为10.6.6.11:2221,10.6.6.11:5221,为了调试,所以把route修改为只路由到5221,但是网关服务配置好route后,发送请求无法路由到指定的10.6.6.11:5221服务,一直路由到另一个服务10.6.6.11:2221上,并且连自定义的gatewayfilter都失效了

                  请求路径为:http://gateway:port/mcs/test 配置如下

                  spring:
                  +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const l={};function t(h,s){return e(),a("div",null,s[0]||(s[0]=[n(`

                  问题描述

                  有一个业务服务,启动了两个做成负载均衡,分别为10.6.6.11:2221,10.6.6.11:5221,为了调试,所以把route修改为只路由到5221,但是网关服务配置好route后,发送请求无法路由到指定的10.6.6.11:5221服务,一直路由到另一个服务10.6.6.11:2221上,并且连自定义的gatewayfilter都失效了

                  请求路径为:http://gateway:port/mcs/test 配置如下

                  spring:
                     cloud:
                       gateway:
                         discovery:
                  diff --git a/assets/SpringExtensionPoint.html-De7aTg35.js b/assets/SpringExtensionPoint.html-BwGgiHY7.js
                  similarity index 99%
                  rename from assets/SpringExtensionPoint.html-De7aTg35.js
                  rename to assets/SpringExtensionPoint.html-BwGgiHY7.js
                  index 599f2fc52..9ba637283 100644
                  --- a/assets/SpringExtensionPoint.html-De7aTg35.js
                  +++ b/assets/SpringExtensionPoint.html-BwGgiHY7.js
                  @@ -1,4 +1,4 @@
                  -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const t={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

                  [TOC]

                  1. spring生命周期

                  spring容器实例化一个对象往大说主要是分为两步

                  1.1 第一步:根据配置生成BeanDefinition

                  根据配置文件(properties、xml)、注解等生成Bean的定义,BeanDefinition的作用是用来描述如何生成真正的对象,对象的生成是通过反射实现,这里的关键词是“描述”,它的作用是告诉容器如何去生成一个真正的对象,它不是真正的对象!!!!

                  BeanDefinition里包含信息如下:

                  1. class:有了bean的class用反射就可以创建出一个真正的对象;
                  2. scope :可取singleton、prototype,即单例还是多例;
                  3. lazyInit:表明此bean是否为懒加载,如果是true代表容器启动后自动创建出bean真实对象,若为false,则代表需要显示的调用容器的getBean()方法时才会实例化出对象;
                  4. 其它字段不再介绍

                  1.2 第二步:根据BeanDefinition来生成真正的对象

                  image-20210526182642991
                  image-20210526182642991

                  2. 扩展点介绍

                  2.1 扩展点1-BeanFactoryPostProcessor

                  此接口在容器启动后,并且BeanDefinition已经注册到容器中以后,调用其回调函数,作用就是能拿到ConfigurableListableBeanFactory,然后操作里面的容器里面的BeanDefinition

                  前面说了第一步是生成BeanDefinition此时真实的对象还未生成,所以可以用spring预留的扩展接口做一些事情,比如在代码中修改一个bean的BeanDefinition

                  举例说明(此场景可能没啥卵用,只是为了证明可以在实例化出对象之前可以修改它的BeanDefinition):

                  第一次测试:在xml随便配一个bean,bean标签有个lazyInit,默认是false

                    <bean id="address" class="com.chen.bean.Address">
                  +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const t={};function l(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

                  [TOC]

                  1. spring生命周期

                  spring容器实例化一个对象往大说主要是分为两步

                  1.1 第一步:根据配置生成BeanDefinition

                  根据配置文件(properties、xml)、注解等生成Bean的定义,BeanDefinition的作用是用来描述如何生成真正的对象,对象的生成是通过反射实现,这里的关键词是“描述”,它的作用是告诉容器如何去生成一个真正的对象,它不是真正的对象!!!!

                  BeanDefinition里包含信息如下:

                  1. class:有了bean的class用反射就可以创建出一个真正的对象;
                  2. scope :可取singleton、prototype,即单例还是多例;
                  3. lazyInit:表明此bean是否为懒加载,如果是true代表容器启动后自动创建出bean真实对象,若为false,则代表需要显示的调用容器的getBean()方法时才会实例化出对象;
                  4. 其它字段不再介绍

                  1.2 第二步:根据BeanDefinition来生成真正的对象

                  image-20210526182642991
                  image-20210526182642991

                  2. 扩展点介绍

                  2.1 扩展点1-BeanFactoryPostProcessor

                  此接口在容器启动后,并且BeanDefinition已经注册到容器中以后,调用其回调函数,作用就是能拿到ConfigurableListableBeanFactory,然后操作里面的容器里面的BeanDefinition

                  前面说了第一步是生成BeanDefinition此时真实的对象还未生成,所以可以用spring预留的扩展接口做一些事情,比如在代码中修改一个bean的BeanDefinition

                  举例说明(此场景可能没啥卵用,只是为了证明可以在实例化出对象之前可以修改它的BeanDefinition):

                  第一次测试:在xml随便配一个bean,bean标签有个lazyInit,默认是false

                    <bean id="address" class="com.chen.bean.Address">
                           <property name="addressName" value="startName"/>
                       </bean>

                  第二次测试:在实例化之前通过扩展点的功能把lazyInit改为true

                  实现方式:

                  @Component
                   public class MyBeanFactoryPostProccessor implements BeanFactoryPostProcessor {
                  diff --git a/assets/SpringIOC.html-DvmNTL7R.js b/assets/SpringIOC.html-Dv9Nuies.js
                  similarity index 99%
                  rename from assets/SpringIOC.html-DvmNTL7R.js
                  rename to assets/SpringIOC.html-Dv9Nuies.js
                  index 46902baa1..c420e1528 100644
                  --- a/assets/SpringIOC.html-DvmNTL7R.js
                  +++ b/assets/SpringIOC.html-Dv9Nuies.js
                  @@ -1,4 +1,4 @@
                  -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function t(e,i){return l(),a("div",null,i[0]||(i[0]=[n(`

                  Spring 最重要的概念是 IOC 和 AOP,本篇文章其实就是要带领大家来分析下 Spring 的 IOC 容器。既然大家平时都要用到 Spring,怎么可以不好好了解 Spring 呢?阅读本文并不能让你成为 Spring 专家,不过一定有助于大家理解 Spring 的很多概念,帮助大家排查应用中和 Spring 相关的一些问题。

                  本文采用的源码版本是 4.3.11.RELEASE,算是 5.0.x 前比较新的版本了。为了降低难度,本文所说的所有的内容都是基于 xml 的配置的方式,实际使用已经很少人这么做了,至少不是纯 xml 配置,不过从理解源码的角度来看用这种方式来说无疑是最合适的。

                  阅读建议:读者至少需要知道怎么配置 Spring,了解 Spring 中的各种概念,少部分内容我还假设读者使用过 SpringMVC。本文要说的 IOC 总体来说有两处地方最重要,一个是创建 Bean 容器,一个是初始化 Bean,如果读者觉得一次性看完本文压力有点大,那么可以按这个思路分两次消化。读者不一定对 Spring 容器的源码感兴趣,也许附录部分介绍的知识对读者有些许作用。

                  希望通过本文可以让读者不惧怕阅读 Spring 源码,也希望大家能反馈表述错误或不合理的地方。

                  引言

                  先看下最基本的启动 Spring 容器的例子:

                  public static void main(String[] args) {
                  +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function t(e,i){return l(),a("div",null,i[0]||(i[0]=[n(`

                  Spring 最重要的概念是 IOC 和 AOP,本篇文章其实就是要带领大家来分析下 Spring 的 IOC 容器。既然大家平时都要用到 Spring,怎么可以不好好了解 Spring 呢?阅读本文并不能让你成为 Spring 专家,不过一定有助于大家理解 Spring 的很多概念,帮助大家排查应用中和 Spring 相关的一些问题。

                  本文采用的源码版本是 4.3.11.RELEASE,算是 5.0.x 前比较新的版本了。为了降低难度,本文所说的所有的内容都是基于 xml 的配置的方式,实际使用已经很少人这么做了,至少不是纯 xml 配置,不过从理解源码的角度来看用这种方式来说无疑是最合适的。

                  阅读建议:读者至少需要知道怎么配置 Spring,了解 Spring 中的各种概念,少部分内容我还假设读者使用过 SpringMVC。本文要说的 IOC 总体来说有两处地方最重要,一个是创建 Bean 容器,一个是初始化 Bean,如果读者觉得一次性看完本文压力有点大,那么可以按这个思路分两次消化。读者不一定对 Spring 容器的源码感兴趣,也许附录部分介绍的知识对读者有些许作用。

                  希望通过本文可以让读者不惧怕阅读 Spring 源码,也希望大家能反馈表述错误或不合理的地方。

                  引言

                  先看下最基本的启动 Spring 容器的例子:

                  public static void main(String[] args) {
                       ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationfile.xml");
                   }

                  以上代码就可以利用配置文件来启动一个 Spring 容器了,请使用 maven 的小伙伴直接在 dependencies 中加上以下依赖即可,个人比较反对那些不知道要添加什么依赖,然后把 Spring 的所有相关的东西都加进来的方式。

                  <dependency>
                     <groupId>org.springframework</groupId>
                  diff --git a/assets/SpringMVC.html-BSrJLxWA.js b/assets/SpringMVC.html-7ciGTcsU.js
                  similarity index 99%
                  rename from assets/SpringMVC.html-BSrJLxWA.js
                  rename to assets/SpringMVC.html-7ciGTcsU.js
                  index b6b4c906a..57cac034a 100644
                  --- a/assets/SpringMVC.html-BSrJLxWA.js
                  +++ b/assets/SpringMVC.html-7ciGTcsU.js
                  @@ -1,4 +1,4 @@
                  -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const e={};function h(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

                  SpringMVC处理请求的流程

                  20230625180034
                  20230625180034

                  前端控制器源码

                  
                  +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const e={};function h(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

                  SpringMVC处理请求的流程

                  20230625180034
                  20230625180034

                  前端控制器源码

                  
                   protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
                     HttpServletRequest processedRequest = request;
                     HandlerExecutionChain mappedHandler = null;
                  diff --git a/assets/SpringSourceAnalize.html-CA7pc5uW.js b/assets/SpringSourceAnalize.html-BPAH3F93.js
                  similarity index 96%
                  rename from assets/SpringSourceAnalize.html-CA7pc5uW.js
                  rename to assets/SpringSourceAnalize.html-BPAH3F93.js
                  index 4b36f4c86..055635822 100644
                  --- a/assets/SpringSourceAnalize.html-CA7pc5uW.js
                  +++ b/assets/SpringSourceAnalize.html-BPAH3F93.js
                  @@ -1 +1 @@
                  -import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,o as n}from"./app-LRWZkDAq.js";const r={};function o(i,a){return n(),t("div")}const m=e(r,[["render",o],["__file","SpringSourceAnalize.html.vue"]]),l=JSON.parse('{"path":"/java/framework/spring/SpringSourceAnalize.html","title":"SpringIOC源码分析","lang":"zh-CN","frontmatter":{"title":"SpringIOC源码分析","date":"2022-02-14T00:00:00.000Z","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/spring/SpringSourceAnalize.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"SpringIOC源码分析"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-02-14T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"SpringIOC源码分析\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-02-14T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[],"git":{"createdTime":1676623349000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2},{"name":"chenxk","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":0.03,"words":10},"filePathRelative":"java/framework/spring/SpringSourceAnalize.md","localizedDate":"2022年2月14日","excerpt":""}');export{m as comp,l as data};
                  +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as t,o as n}from"./app-BIdVr4mq.js";const r={};function o(i,a){return n(),t("div")}const m=e(r,[["render",o],["__file","SpringSourceAnalize.html.vue"]]),l=JSON.parse('{"path":"/java/framework/spring/SpringSourceAnalize.html","title":"SpringIOC源码分析","lang":"zh-CN","frontmatter":{"title":"SpringIOC源码分析","date":"2022-02-14T00:00:00.000Z","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/framework/spring/SpringSourceAnalize.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"SpringIOC源码分析"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-02-14T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"SpringIOC源码分析\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-02-14T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[],"git":{"createdTime":1676623349000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2},{"name":"chenxk","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":0.03,"words":10},"filePathRelative":"java/framework/spring/SpringSourceAnalize.md","localizedDate":"2022年2月14日","excerpt":""}');export{m as comp,l as data};
                  diff --git a/assets/String.html-BEF_M3Rd.js b/assets/String.html-CuetWaFK.js
                  similarity index 99%
                  rename from assets/String.html-BEF_M3Rd.js
                  rename to assets/String.html-CuetWaFK.js
                  index 46c097722..cb72105d5 100644
                  --- a/assets/String.html-BEF_M3Rd.js
                  +++ b/assets/String.html-CuetWaFK.js
                  @@ -1,4 +1,4 @@
                  -import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as i,o as e}from"./app-LRWZkDAq.js";const l={};function p(t,s){return e(),a("div",null,s[0]||(s[0]=[i(`
                  1、String类
                  1. 为什么String类要被设计为不可变?

                    string为什么不可变

                    Thanks to the immutability of Strings in Java, the JVM can optimize the amount of memory allocated for them by storing only one copy of each literal String in the pool. This process is called interning.
                    +import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as i,o as e}from"./app-BIdVr4mq.js";const l={};function p(t,s){return e(),a("div",null,s[0]||(s[0]=[i(`
                    1、String类
                    1. 为什么String类要被设计为不可变?

                      string为什么不可变

                      Thanks to the immutability of Strings in Java, the JVM can optimize the amount of memory allocated for them by storing only one copy of each literal String in the pool. This process is called interning.
                       
                       When we create a String variable and assign a value to it, the JVM searches the pool for a String of equal value.
                       
                      diff --git a/assets/StringAdd.html-ByCo-I_5.js b/assets/StringAdd.html-DiHKxvsE.js
                      similarity index 99%
                      rename from assets/StringAdd.html-ByCo-I_5.js
                      rename to assets/StringAdd.html-DiHKxvsE.js
                      index a0b76c484..7e0483b17 100644
                      --- a/assets/StringAdd.html-ByCo-I_5.js
                      +++ b/assets/StringAdd.html-DiHKxvsE.js
                      @@ -1,4 +1,4 @@
                      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-LRWZkDAq.js";const l={};function h(k,s){return t(),a("div",null,s[0]||(s[0]=[n(`

                      1、先看问题,以下结果是什么?

                      String s1 = "Hello";
                      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-BIdVr4mq.js";const l={};function h(k,s){return t(),a("div",null,s[0]||(s[0]=[n(`

                      1、先看问题,以下结果是什么?

                      String s1 = "Hello";
                       String s2 = "Hello";
                       String s3 = "Hel" + "lo";
                       String s4 = "Hel" + new String("lo");
                      diff --git a/assets/Swagger.html-BUArKIxO.js b/assets/Swagger.html-CSZJriy0.js
                      similarity index 99%
                      rename from assets/Swagger.html-BUArKIxO.js
                      rename to assets/Swagger.html-CSZJriy0.js
                      index 11f31ecaa..822816d85 100644
                      --- a/assets/Swagger.html-BUArKIxO.js
                      +++ b/assets/Swagger.html-CSZJriy0.js
                      @@ -1,4 +1,4 @@
                      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const t={};function r(g,i){return e(),a("div",null,i[0]||(i[0]=[n(`

                      Swagger和SpringFox关系

                      java项目在引入swagger的时候,一般会引入如下依赖,当时只知道照着博客抄,也有好奇为啥叫springfox,怎么没有swagger,不知你是否有同样的疑问?直到今天问了一下GPT

                          <dependency>
                      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const t={};function r(g,i){return e(),a("div",null,i[0]||(i[0]=[n(`

                      Swagger和SpringFox关系

                      java项目在引入swagger的时候,一般会引入如下依赖,当时只知道照着博客抄,也有好奇为啥叫springfox,怎么没有swagger,不知你是否有同样的疑问?直到今天问了一下GPT

                          <dependency>
                               <groupId>io.springfox</groupId>
                               <artifactId>springfox-boot-starter</artifactId>
                           </dependency>
                      问:springfox过时了吗?
                      diff --git a/assets/Synchronized.html-C99mj8uR.js b/assets/Synchronized.html-B9MlqwzL.js
                      similarity index 96%
                      rename from assets/Synchronized.html-C99mj8uR.js
                      rename to assets/Synchronized.html-B9MlqwzL.js
                      index 5e4770777..7a79ecb5c 100644
                      --- a/assets/Synchronized.html-C99mj8uR.js
                      +++ b/assets/Synchronized.html-B9MlqwzL.js
                      @@ -1 +1 @@
                      -import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as e,o as a}from"./app-LRWZkDAq.js";const i={};function r(c,t){return a(),o("div",null,t[0]||(t[0]=[e("h2",{id:"_1、",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1、"},[e("span",null,"1、")])],-1)]))}const m=n(i,[["render",r],["__file","Synchronized.html.vue"]]),h=JSON.parse('{"path":"/java/advance/Synchronized.html","title":"synchronized实现 原理","lang":"zh-CN","frontmatter":{"title":"synchronized实现 原理","date":"2022-01-31T00:00:00.000Z","category":["对象锁"],"description":"1、","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/advance/Synchronized.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"synchronized实现 原理"}],["meta",{"property":"og:description","content":"1、"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-01-31T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"synchronized实现 原理\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-01-31T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、","slug":"_1、","link":"#_1、","children":[]}],"git":{"createdTime":1675240920000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":0.05,"words":15},"filePathRelative":"java/advance/Synchronized.md","localizedDate":"2022年1月31日","excerpt":"

                      1、

                      \\n","autoDesc":true}');export{m as comp,h as data}; +import{_ as n}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as o,b as e,o as a}from"./app-BIdVr4mq.js";const i={};function r(c,t){return a(),o("div",null,t[0]||(t[0]=[e("h2",{id:"_1、",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1、"},[e("span",null,"1、")])],-1)]))}const m=n(i,[["render",r],["__file","Synchronized.html.vue"]]),h=JSON.parse('{"path":"/java/advance/Synchronized.html","title":"synchronized实现 原理","lang":"zh-CN","frontmatter":{"title":"synchronized实现 原理","date":"2022-01-31T00:00:00.000Z","category":["对象锁"],"description":"1、","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/advance/Synchronized.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"synchronized实现 原理"}],["meta",{"property":"og:description","content":"1、"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-01-31T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"synchronized实现 原理\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-01-31T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、","slug":"_1、","link":"#_1、","children":[]}],"git":{"createdTime":1675240920000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2},{"name":"ChenSino","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":0.05,"words":15},"filePathRelative":"java/advance/Synchronized.md","localizedDate":"2022年1月31日","excerpt":"

                      1、

                      \\n","autoDesc":true}');export{m as comp,h as data}; diff --git a/assets/TCP-IP.html-DBoM_5OE.js b/assets/TCP-IP.html-CbApdGPu.js similarity index 99% rename from assets/TCP-IP.html-DBoM_5OE.js rename to assets/TCP-IP.html-CbApdGPu.js index 04cf31ea7..1565516c5 100644 --- a/assets/TCP-IP.html-DBoM_5OE.js +++ b/assets/TCP-IP.html-CbApdGPu.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function t(e,i){return l(),a("div",null,i[0]||(i[0]=[n(`

                      1、TCP/IP与OSI的关系

                      网上学习网络模型时一会是七层osi模型,一会又五层,一会又是四层模型,着实五花八门让人费解,实际上七层模型是osi的标准,目前大家所讲的tcp/ip也是符合osi的标准的,不过是把其中几层合并到一层了而已,点击查看tcp/ip和osi的关系。TCP/IP实际上是一个协议簇,包含很多协议,比如TCP、IP、UDP、ICMP、RIP、TELNETFTP、SMTP、ARP、TFTP等,这些协议一起称为TCP/IP协议。常见协议简单介绍:

                      TCP(Transport Control Protocol)传输控制协议

                      IP(Internetworking Protocol)网间网协议

                      UDP(User Datagram Protocol)用户数据报协议

                      ICMP(Internet Control Message Protocol)互联网控制信息协议

                      SMTP(Simple Mail Transfer Protocol)简单邮件传输协议

                      SNMP(Simple Network manage Protocol)简单网络管理协议

                      FTP(File Transfer Protocol)文件传输协议

                      ARP(Address Resolation Protocol)地址解析协议

                      20230130104228
                      20230130104228
                      20230130103942
                      20230130103942

                      2、TCP/IP

                      完整的osi七层模型,在tcp/ip中被简化为了4层模型,其中应用层、表示层、会话层被压缩成了一个应用层,数据链路层、物理层被压缩为数据链路层。

                      flowchart LR
                      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function t(e,i){return l(),a("div",null,i[0]||(i[0]=[n(`

                      1、TCP/IP与OSI的关系

                      网上学习网络模型时一会是七层osi模型,一会又五层,一会又是四层模型,着实五花八门让人费解,实际上七层模型是osi的标准,目前大家所讲的tcp/ip也是符合osi的标准的,不过是把其中几层合并到一层了而已,点击查看tcp/ip和osi的关系。TCP/IP实际上是一个协议簇,包含很多协议,比如TCP、IP、UDP、ICMP、RIP、TELNETFTP、SMTP、ARP、TFTP等,这些协议一起称为TCP/IP协议。常见协议简单介绍:

                      TCP(Transport Control Protocol)传输控制协议

                      IP(Internetworking Protocol)网间网协议

                      UDP(User Datagram Protocol)用户数据报协议

                      ICMP(Internet Control Message Protocol)互联网控制信息协议

                      SMTP(Simple Mail Transfer Protocol)简单邮件传输协议

                      SNMP(Simple Network manage Protocol)简单网络管理协议

                      FTP(File Transfer Protocol)文件传输协议

                      ARP(Address Resolation Protocol)地址解析协议

                      20230130104228
                      20230130104228
                      20230130103942
                      20230130103942

                      2、TCP/IP

                      完整的osi七层模型,在tcp/ip中被简化为了4层模型,其中应用层、表示层、会话层被压缩成了一个应用层,数据链路层、物理层被压缩为数据链路层。

                      flowchart LR
                           应用层 --> 传输控制层 --> 网络层 --> 链路层

                      2.1 应用层

                      应用层顾名思义退就是和应用相关,既然和应用相关,那么不同应用肯定就不一样,这一部分的东西需要自定义,就是程序员需要实现的,比如HTTP、FTP、SMTP、NFS等。既然是互联网应用,数据传输肯定得有双方,就是我们说的客户端应用、服务端应用,而应用层这里就是制定客户端和服务端交互的规范,说白了就是保证客户端发的东西服务端能识别,服务端发的东西客户端也能识别。以http为例,http中就制定了请求行、请求体、请求头等,发送的时候必须有这些东西,服务端才能够解析,而相应的服务端响应时也得有对应的东西,这样客户端才能够解析出响应信息。这里只是制定规范,真正的传输还要用到后面的tcp,所以通常说http是基于tcp,就是这个道理。

                      2.2 传输控制层(TCP,不讲UDP)

                      关键词: 可靠性、三次握手四次挥手、拆包

                      tcp负责建立连接,断开连接,控制数据包大小等,建立连接就是常说的三次握手四次挥手。建立连接的过程分为3步:

                      1. 客户端发送连接请求的标识符SYN
                      2. 服务端回复SYN+ACK
                      3. 客户端回复ACK

                      具体的交互可以使用抓包工具wireshark查看,或者使用tcpdump命令行工具。

                      # 先打开监听
                       sudo tcpdump -nn -i <卡> port 80
                       # 访问baidu,端口80
                      diff --git a/assets/TcpDump.html-oAiwcaO7.js b/assets/TcpDump.html-mCRY-l5s.js
                      similarity index 99%
                      rename from assets/TcpDump.html-oAiwcaO7.js
                      rename to assets/TcpDump.html-mCRY-l5s.js
                      index ab84dd1a9..1a72386d7 100644
                      --- a/assets/TcpDump.html-oAiwcaO7.js
                      +++ b/assets/TcpDump.html-mCRY-l5s.js
                      @@ -1,4 +1,4 @@
                      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as t,o as h}from"./app-LRWZkDAq.js";const n={};function e(l,i){return h(),a("div",null,i[0]||(i[0]=[t(`

                      简要介绍Linux抓报

                      作为web开发我抓报主要是针对http请求,主要是看请求行,请求头,请求体,以及响应

                      1、所用抓报命令

                      sudo tcpdump tcp -i eth1 -t -s 0 -c 100 and dst port ! 22 and src net 192.168.1.0/24 -w ./target.cap

                      (1)tcp: ip icmp arp rarp 和 tcp、udp、icmp这些选项等都要放到第一个参数的位置,用来过滤数据报的类型 (2)-i enp3s0f1 : 只抓经过接口eth1的包 (3)-t : 不显示时间戳 (4)-s 0 : 抓取数据包时默认抓取长度为68字节。加上-S 0 后可以抓到完整的数据包 (5)-c 100 : 只抓取100个数据包 (6)dst port ! 22 : 不抓取目标端口是22的数据包 (7)src net 192.168.1.0/24 : 数据包的源网络地址为192.168.1.0/24 (8)-w ./target.cap : 保存成cap文件,方便用ethereal(即wireshark)分析

                      2、抓包后的处理

                      #把所抓的报文解析成可阅读的文本
                      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as t,o as h}from"./app-BIdVr4mq.js";const n={};function e(l,i){return h(),a("div",null,i[0]||(i[0]=[t(`

                      简要介绍Linux抓报

                      作为web开发我抓报主要是针对http请求,主要是看请求行,请求头,请求体,以及响应

                      1、所用抓报命令

                      sudo tcpdump tcp -i eth1 -t -s 0 -c 100 and dst port ! 22 and src net 192.168.1.0/24 -w ./target.cap

                      (1)tcp: ip icmp arp rarp 和 tcp、udp、icmp这些选项等都要放到第一个参数的位置,用来过滤数据报的类型 (2)-i enp3s0f1 : 只抓经过接口eth1的包 (3)-t : 不显示时间戳 (4)-s 0 : 抓取数据包时默认抓取长度为68字节。加上-S 0 后可以抓到完整的数据包 (5)-c 100 : 只抓取100个数据包 (6)dst port ! 22 : 不抓取目标端口是22的数据包 (7)src net 192.168.1.0/24 : 数据包的源网络地址为192.168.1.0/24 (8)-w ./target.cap : 保存成cap文件,方便用ethereal(即wireshark)分析

                      2、抓包后的处理

                      #把所抓的报文解析成可阅读的文本
                       strings target.cap>temp.txt

                      3、其他使用实例

                      # port指定要抓包端口,
                       sudo tcpdump tcp -i enp3s0f1  -t -s 0 -c 100 and port 6061 -w  ./target.cap
                      #抓包dns解析
                        tcpdump -n -i any port 53
                      diff --git a/assets/ThreadLocal.html-u61CIRhJ.js b/assets/ThreadLocal.html-BAQekg_Z.js
                      similarity index 99%
                      rename from assets/ThreadLocal.html-u61CIRhJ.js
                      rename to assets/ThreadLocal.html-BAQekg_Z.js
                      index 114581ce4..b35249311 100644
                      --- a/assets/ThreadLocal.html-u61CIRhJ.js
                      +++ b/assets/ThreadLocal.html-BAQekg_Z.js
                      @@ -1,4 +1,4 @@
                      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function k(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

                      1、介绍

                      2、使用

                      3、 使用反射在当前线程获取所有的ThreadLocal

                      一个线程是可能有多个ThreadLocal的,所以源码中字段使用的复数形式threadLocals

                      public class ThreadLocalUtil {
                      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function k(t,s){return l(),a("div",null,s[0]||(s[0]=[n(`

                      1、介绍

                      2、使用

                      3、 使用反射在当前线程获取所有的ThreadLocal

                      一个线程是可能有多个ThreadLocal的,所以源码中字段使用的复数形式threadLocals

                      public class ThreadLocalUtil {
                       
                           public static Map<ThreadLocal, Object> getThreadLocalMap(){
                               Map<ThreadLocal, Object> threadLocals = new HashMap<>();
                      diff --git a/assets/ThreadPool.html-ChF6p8sJ.js b/assets/ThreadPool.html-xcxHOFbM.js
                      similarity index 99%
                      rename from assets/ThreadPool.html-ChF6p8sJ.js
                      rename to assets/ThreadPool.html-xcxHOFbM.js
                      index 1ede3c531..231416160 100644
                      --- a/assets/ThreadPool.html-ChF6p8sJ.js
                      +++ b/assets/ThreadPool.html-xcxHOFbM.js
                      @@ -1,4 +1,4 @@
                      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function t(e,i){return l(),a("div",null,i[0]||(i[0]=[n(`

                      线程池

                      前言:

                      线程池使用submit提交任务若遇到异常,线程不会直接抛出异常,在开发中要注意处理异常情况

                      1、先上测试代码

                          public static void main(String[] args) throws InterruptedException {
                      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function t(e,i){return l(),a("div",null,i[0]||(i[0]=[n(`

                      线程池

                      前言:

                      线程池使用submit提交任务若遇到异常,线程不会直接抛出异常,在开发中要注意处理异常情况

                      1、先上测试代码

                          public static void main(String[] args) throws InterruptedException {
                               ExecutorService executorService = Executors.newFixedThreadPool(3);
                               executorService.submit(() -> {
                                   System.out.println(Thread.currentThread().getName());
                      diff --git a/assets/TooManyOpenFiles.html-BYzQ7KMi.js b/assets/TooManyOpenFiles.html-DcMwQWIU.js
                      similarity index 99%
                      rename from assets/TooManyOpenFiles.html-BYzQ7KMi.js
                      rename to assets/TooManyOpenFiles.html-DcMwQWIU.js
                      index 205f6c3b4..dd648fc3f 100644
                      --- a/assets/TooManyOpenFiles.html-BYzQ7KMi.js
                      +++ b/assets/TooManyOpenFiles.html-DcMwQWIU.js
                      @@ -1,4 +1,4 @@
                      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const l={};function t(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

                      Linux服务器打开文件过多

                      线上异常

                      java.io.IOException: Too many open files
                      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const l={};function t(h,i){return e(),a("div",null,i[0]||(i[0]=[n(`

                      Linux服务器打开文件过多

                      线上异常

                      java.io.IOException: Too many open files
                               at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method)
                               at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422)
                               at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250)
                      diff --git a/assets/TyporaPicgo.html-BFOgiNZO.js b/assets/TyporaPicgo.html-DiCAdyNh.js
                      similarity index 99%
                      rename from assets/TyporaPicgo.html-BFOgiNZO.js
                      rename to assets/TyporaPicgo.html-DiCAdyNh.js
                      index 82ff18715..e0e8dc050 100644
                      --- a/assets/TyporaPicgo.html-BFOgiNZO.js
                      +++ b/assets/TyporaPicgo.html-DiCAdyNh.js
                      @@ -1,4 +1,4 @@
                      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-LRWZkDAq.js";const e={};function h(l,i){return t(),a("div",null,i[0]||(i[0]=[n(`

                      前言

                      平时用MarkDown写博客少不了需要截图,我用的是Typora,刚开始截图是保存在本地,有时想把博客分享到网上,就发现各种图全挂了,需要手动一个一个再复制一下,着实麻烦,今天无意间发现有个叫PicGo的工具,此工具专门上传图到各大图床,着实方便。

                      picgo配置

                      picgo-core

                      picgo的ui剪贴板上传功能,需要win10+

                      1、搭建Typora + PicGo + gitee

                      我的电脑环境如下,

                      								  OS: Manjaro 21.2.4 Qonos
                      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-BIdVr4mq.js";const e={};function h(l,i){return t(),a("div",null,i[0]||(i[0]=[n(`

                      前言

                      平时用MarkDown写博客少不了需要截图,我用的是Typora,刚开始截图是保存在本地,有时想把博客分享到网上,就发现各种图全挂了,需要手动一个一个再复制一下,着实麻烦,今天无意间发现有个叫PicGo的工具,此工具专门上传图到各大图床,着实方便。

                      picgo配置

                      picgo-core

                      picgo的ui剪贴板上传功能,需要win10+

                      1、搭建Typora + PicGo + gitee

                      我的电脑环境如下,

                      								  OS: Manjaro 21.2.4 Qonos
                        ██████████████████  ████████     Kernel: x86_64 Linux 5.15.25-1-MANJARO
                        ██████████████████  ████████     Uptime: 5h 26m
                        ████████            ████████     Packages: 1556
                      diff --git "a/assets/Ubuntu\346\234\215\345\212\241\346\220\255\345\273\272.html-B5ncaYIz.js" "b/assets/Ubuntu\346\234\215\345\212\241\346\220\255\345\273\272.html-Dn60vq3V.js"
                      similarity index 98%
                      rename from "assets/Ubuntu\346\234\215\345\212\241\346\220\255\345\273\272.html-B5ncaYIz.js"
                      rename to "assets/Ubuntu\346\234\215\345\212\241\346\220\255\345\273\272.html-Dn60vq3V.js"
                      index c1affa9de..e3e88bbfa 100644
                      --- "a/assets/Ubuntu\346\234\215\345\212\241\346\220\255\345\273\272.html-B5ncaYIz.js"
                      +++ "b/assets/Ubuntu\346\234\215\345\212\241\346\220\255\345\273\272.html-Dn60vq3V.js"
                      @@ -1 +1 @@
                      -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as i,o as a}from"./app-LRWZkDAq.js";const r={};function o(l,e){return a(),n("div",null,e[0]||(e[0]=[i('

                      介绍

                      本篇介绍我的ubuntu系统上搭建的服务

                      面板

                      博客

                      minio

                      nginx

                      ELK

                      JAVA后端

                      ',7)]))}const c=t(r,[["render",o],["__file","Ubuntu服务搭建.html.vue"]]),p=JSON.parse('{"path":"/myserver/Ubuntu%E6%9C%8D%E5%8A%A1%E6%90%AD%E5%BB%BA.html","title":"UBUNTU系统服务搭建","lang":"zh-CN","frontmatter":{"title":"UBUNTU系统服务搭建","date":"2024-11-29T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"介绍 本篇介绍我的ubuntu系统上搭建的服务 面板 博客 minio nginx ELK JAVA后端","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/myserver/Ubuntu%E6%9C%8D%E5%8A%A1%E6%90%AD%E5%BB%BA.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"UBUNTU系统服务搭建"}],["meta",{"property":"og:description","content":"介绍 本篇介绍我的ubuntu系统上搭建的服务 面板 博客 minio nginx ELK JAVA后端"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T09:14:36.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2024-11-29T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T09:14:36.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"UBUNTU系统服务搭建\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2024-11-29T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T09:14:36.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":2,"title":"面板","slug":"面板","link":"#面板","children":[]},{"level":2,"title":"博客","slug":"博客","link":"#博客","children":[]},{"level":2,"title":"minio","slug":"minio","link":"#minio","children":[]},{"level":2,"title":"nginx","slug":"nginx","link":"#nginx","children":[]},{"level":2,"title":"ELK","slug":"elk","link":"#elk","children":[]},{"level":2,"title":"JAVA后端","slug":"java后端","link":"#java后端","children":[]}],"git":{"createdTime":1732871676000,"updatedTime":1732871676000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.15,"words":46},"filePathRelative":"myserver/Ubuntu服务搭建.md","localizedDate":"2024年11月29日","excerpt":"
                      \\n

                      介绍

                      \\n

                      本篇介绍我的ubuntu系统上搭建的服务

                      \\n
                      \\n

                      面板

                      \\n

                      博客

                      \\n

                      minio

                      \\n

                      nginx

                      \\n

                      ELK

                      \\n

                      JAVA后端

                      \\n","autoDesc":true}');export{c as comp,p as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as n,a as i,o as a}from"./app-BIdVr4mq.js";const r={};function o(l,e){return a(),n("div",null,e[0]||(e[0]=[i('

                      介绍

                      本篇介绍我的ubuntu系统上搭建的服务

                      面板

                      博客

                      minio

                      nginx

                      ELK

                      JAVA后端

                      ',7)]))}const c=t(r,[["render",o],["__file","Ubuntu服务搭建.html.vue"]]),p=JSON.parse('{"path":"/myserver/Ubuntu%E6%9C%8D%E5%8A%A1%E6%90%AD%E5%BB%BA.html","title":"UBUNTU系统服务搭建","lang":"zh-CN","frontmatter":{"title":"UBUNTU系统服务搭建","date":"2024-11-29T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"介绍 本篇介绍我的ubuntu系统上搭建的服务 面板 博客 minio nginx ELK JAVA后端","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/myserver/Ubuntu%E6%9C%8D%E5%8A%A1%E6%90%AD%E5%BB%BA.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"UBUNTU系统服务搭建"}],["meta",{"property":"og:description","content":"介绍 本篇介绍我的ubuntu系统上搭建的服务 面板 博客 minio nginx ELK JAVA后端"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T09:14:36.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2024-11-29T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T09:14:36.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"UBUNTU系统服务搭建\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2024-11-29T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T09:14:36.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":2,"title":"面板","slug":"面板","link":"#面板","children":[]},{"level":2,"title":"博客","slug":"博客","link":"#博客","children":[]},{"level":2,"title":"minio","slug":"minio","link":"#minio","children":[]},{"level":2,"title":"nginx","slug":"nginx","link":"#nginx","children":[]},{"level":2,"title":"ELK","slug":"elk","link":"#elk","children":[]},{"level":2,"title":"JAVA后端","slug":"java后端","link":"#java后端","children":[]}],"git":{"createdTime":1732871676000,"updatedTime":1732871676000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.15,"words":46},"filePathRelative":"myserver/Ubuntu服务搭建.md","localizedDate":"2024年11月29日","excerpt":"
                      \\n

                      介绍

                      \\n

                      本篇介绍我的ubuntu系统上搭建的服务

                      \\n
                      \\n

                      面板

                      \\n

                      博客

                      \\n

                      minio

                      \\n

                      nginx

                      \\n

                      ELK

                      \\n

                      JAVA后端

                      \\n","autoDesc":true}');export{c as comp,p as data}; diff --git a/assets/Undertow.xxxNotFount.html-D6hJS7YH.js b/assets/Undertow.xxxNotFount.html-DNDQtsiq.js similarity index 99% rename from assets/Undertow.xxxNotFount.html-D6hJS7YH.js rename to assets/Undertow.xxxNotFount.html-DNDQtsiq.js index 9fba2dee1..d71a344fa 100644 --- a/assets/Undertow.xxxNotFount.html-D6hJS7YH.js +++ b/assets/Undertow.xxxNotFount.html-DNDQtsiq.js @@ -1,4 +1,4 @@ -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a,o as n}from"./app-LRWZkDAq.js";const l={};function s(o,e){return n(),i("div",null,e[0]||(e[0]=[a(`

                      背景

                      用户反馈上传图片失败,查看日志报错上传文件提示:java.nio.file.NoSuchFileException: /tmp/undertow.xxxx.xxxxx,解决方案,问题是这个问题之前用户就反馈过,之前是直接重启就好了,之后也没深究。最近用户又反馈不行了,所以花了点时间深究一下从根源解决问题。

                      原因

                      在 Linux 系统中,Spring Boot 应用以 java -jar 命令启动时,会在操作系统的 /tmp 目录下随机生成一个 tomcat(或 undertow )临时目录,上传的文件先要转换成临时文件保存在这个文件夹中。 由于临时 /tmp 目录下的文件,在长时间(默认10天)没有使用的情况下,操作系统会执行 tmp 目录清理服务(systemd-tmpfiles-clean.service),导致 /tmp/undertow.xxxx.xxxxxxx 文件被清理; 导致在上传文件时,java调用 Files.createFile(…) 在目录/tmp/undertow.xxxx.xxxxxxx下创建临时文件时,发现找不到目录,就会抛出以上的错误。

                      解决方法

                      方法一

                      可以根据报错信息,新建 /tmp/undertow.xxxx.xxxxxxxx 目录,不影响用户正常使用。 执行 mkdir -p /tmp/undertow.8760.570269926767628882命令;

                      方法二

                      先创建好文件,配置问yml中

                      spring:
                      +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a,o as n}from"./app-BIdVr4mq.js";const l={};function s(o,e){return n(),i("div",null,e[0]||(e[0]=[a(`

                      背景

                      用户反馈上传图片失败,查看日志报错上传文件提示:java.nio.file.NoSuchFileException: /tmp/undertow.xxxx.xxxxx,解决方案,问题是这个问题之前用户就反馈过,之前是直接重启就好了,之后也没深究。最近用户又反馈不行了,所以花了点时间深究一下从根源解决问题。

                      原因

                      在 Linux 系统中,Spring Boot 应用以 java -jar 命令启动时,会在操作系统的 /tmp 目录下随机生成一个 tomcat(或 undertow )临时目录,上传的文件先要转换成临时文件保存在这个文件夹中。 由于临时 /tmp 目录下的文件,在长时间(默认10天)没有使用的情况下,操作系统会执行 tmp 目录清理服务(systemd-tmpfiles-clean.service),导致 /tmp/undertow.xxxx.xxxxxxx 文件被清理; 导致在上传文件时,java调用 Files.createFile(…) 在目录/tmp/undertow.xxxx.xxxxxxx下创建临时文件时,发现找不到目录,就会抛出以上的错误。

                      解决方法

                      方法一

                      可以根据报错信息,新建 /tmp/undertow.xxxx.xxxxxxxx 目录,不影响用户正常使用。 执行 mkdir -p /tmp/undertow.8760.570269926767628882命令;

                      方法二

                      先创建好文件,配置问yml中

                      spring:
                         servlet:
                           multipart:
                             location: xxxx

                      方法三

                      java -jar -Dspring.servlet.multipart.location=xxxx

                      验证是否生效

                      方式1

                      使用以上几个方法发现创建的目录在程序启动后里面还是空白,是因为还没有上传文件当然就没有东西了,可以使用watch -n 1 'ls -l'来查看,方法如下:

                      1. 先在对应路径下执行watch -n 1 'ls -l',每秒打印一次
                      2. 上传文件
                      3. 运气好会看到watch的命令会打印出一个tmp文件,但是会马上被删除

                      方式2

                      删除你新建的目录,上传文件,再次报之前的错误,说明我们的配置是生效的

                      `,18)]))}const p=t(l,[["render",s],["__file","Undertow.xxxNotFount.html.vue"]]),d=JSON.parse('{"path":"/java/other/locateproblem/Undertow.xxxNotFount.html","title":"undertow.xxx not found","lang":"zh-CN","frontmatter":{"title":"undertow.xxx not found","date":"2022-05-06T00:00:00.000Z","isOriginal":true,"description":"背景 用户反馈上传图片失败,查看日志报错上传文件提示:java.nio.file.NoSuchFileException: /tmp/undertow.xxxx.xxxxx,解决方案,问题是这个问题之前用户就反馈过,之前是直接重启就好了,之后也没深究。最近用户又反馈不行了,所以花了点时间深究一下从根源解决问题。 原因 在 Linux 系统中,Sprin...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/java/other/locateproblem/Undertow.xxxNotFount.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"undertow.xxx not found"}],["meta",{"property":"og:description","content":"背景 用户反馈上传图片失败,查看日志报错上传文件提示:java.nio.file.NoSuchFileException: /tmp/undertow.xxxx.xxxxx,解决方案,问题是这个问题之前用户就反馈过,之前是直接重启就好了,之后也没深究。最近用户又反馈不行了,所以花了点时间深究一下从根源解决问题。 原因 在 Linux 系统中,Sprin..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-11-29T02:08:20.000Z"}],["meta",{"property":"article:published_time","content":"2022-05-06T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-11-29T02:08:20.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"undertow.xxx not found\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-05-06T00:00:00.000Z\\",\\"dateModified\\":\\"2024-11-29T02:08:20.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"背景","slug":"背景","link":"#背景","children":[]},{"level":2,"title":"原因","slug":"原因","link":"#原因","children":[]},{"level":2,"title":"解决方法","slug":"解决方法","link":"#解决方法","children":[{"level":3,"title":"方法一","slug":"方法一","link":"#方法一","children":[]},{"level":3,"title":"方法二","slug":"方法二","link":"#方法二","children":[]},{"level":3,"title":"方法三","slug":"方法三","link":"#方法三","children":[]}]},{"level":2,"title":"验证是否生效","slug":"验证是否生效","link":"#验证是否生效","children":[{"level":3,"title":"方式1","slug":"方式1","link":"#方式1","children":[]},{"level":3,"title":"方式2","slug":"方式2","link":"#方式2","children":[]}]}],"git":{"createdTime":1683342104000,"updatedTime":1732846100000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":3},{"name":"chenxk","email":"chenxk@sonoscape.net","commits":1}]},"readingTime":{"minutes":1.59,"words":478},"filePathRelative":"java/other/locateproblem/Undertow.xxxNotFount.md","localizedDate":"2022年5月6日","excerpt":"

                      背景

                      \\n

                      用户反馈上传图片失败,查看日志报错上传文件提示:java.nio.file.NoSuchFileException: /tmp/undertow.xxxx.xxxxx,解决方案,问题是这个问题之前用户就反馈过,之前是直接重启就好了,之后也没深究。最近用户又反馈不行了,所以花了点时间深究一下从根源解决问题。

                      \\n

                      原因

                      \\n

                      在 Linux 系统中,Spring Boot 应用以 java -jar 命令启动时,会在操作系统的 /tmp 目录下随机生成一个 tomcat(或 undertow )临时目录,上传的文件先要转换成临时文件保存在这个文件夹中。\\n由于临时 /tmp 目录下的文件,在长时间(默认10天)没有使用的情况下,操作系统会执行 tmp 目录清理服务(systemd-tmpfiles-clean.service),导致 /tmp/undertow.xxxx.xxxxxxx 文件被清理;\\n导致在上传文件时,java调用 Files.createFile(…) 在目录/tmp/undertow.xxxx.xxxxxxx下创建临时文件时,发现找不到目录,就会抛出以上的错误。

                      ","autoDesc":true}');export{p as comp,d as data}; diff --git a/assets/Validator.html-Cgd70-bx.js b/assets/Validator.html-CVtkvwdc.js similarity index 99% rename from assets/Validator.html-Cgd70-bx.js rename to assets/Validator.html-CVtkvwdc.js index d6b645f5a..253bba7b8 100644 --- a/assets/Validator.html-Cgd70-bx.js +++ b/assets/Validator.html-CVtkvwdc.js @@ -1,4 +1,4 @@ -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const t={};function h(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

                      自定义校验

                      
                      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const t={};function h(k,i){return l(),a("div",null,i[0]||(i[0]=[n(`

                      自定义校验

                      
                       import com.chensino.core.api.dto.UserLoginDTO;
                       import com.chensino.core.api.validate.group.PhoneLogin;
                       import com.chensino.core.api.validate.group.UserNameLogin;
                      diff --git a/assets/VsCode.html-BydNEOJq.js b/assets/VsCode.html-B0jpbkGz.js
                      similarity index 99%
                      rename from assets/VsCode.html-BydNEOJq.js
                      rename to assets/VsCode.html-B0jpbkGz.js
                      index 2a3ff83e3..4ebe9dfce 100644
                      --- a/assets/VsCode.html-BydNEOJq.js
                      +++ b/assets/VsCode.html-B0jpbkGz.js
                      @@ -1,4 +1,4 @@
                      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-LRWZkDAq.js";const t={};function l(h,s){return e(),a("div",null,s[0]||(s[0]=[n(`

                      版本: 1.69.1 (user setup) 提交: b06ae3b2d2dbfe28bca3134cc6be65935cdfea6a 日期: 2022-07-12T08:21:24.514Z Electron: 18.3.5 Chromium: 100.0.4896.160 Node.js: 16.13.2 V8: 10.0.139.17-electron.0 OS: Windows_NT x64 6.1.7601

                      1. vscode终端无法打开

                      现象,使用vscod打开终端一直卡着。

                      解决方法是禁用GPU加速,猜测是因为公司电脑没有独显的原因

                      1、取消win7的兼容模式 2、启动方式后加 --disable-gpu,似乎是禁用 GPU 硬件加速 找其他各种方法都无效,管理员、兼容、改setting.json都没用。

                      1
                      1

                      2、在vscode中使用git-bash作为默认终端

                      1. 给vscode设置以管理员方式运行,
                      2. 在配置文件加入以下配置,把git的path改成自己的
                        "terminal.integrated.defaultProfile.windows": "GitBash",
                      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as e}from"./app-BIdVr4mq.js";const t={};function l(h,s){return e(),a("div",null,s[0]||(s[0]=[n(`

                      版本: 1.69.1 (user setup) 提交: b06ae3b2d2dbfe28bca3134cc6be65935cdfea6a 日期: 2022-07-12T08:21:24.514Z Electron: 18.3.5 Chromium: 100.0.4896.160 Node.js: 16.13.2 V8: 10.0.139.17-electron.0 OS: Windows_NT x64 6.1.7601

                      1. vscode终端无法打开

                      现象,使用vscod打开终端一直卡着。

                      解决方法是禁用GPU加速,猜测是因为公司电脑没有独显的原因

                      1、取消win7的兼容模式 2、启动方式后加 --disable-gpu,似乎是禁用 GPU 硬件加速 找其他各种方法都无效,管理员、兼容、改setting.json都没用。

                      1
                      1

                      2、在vscode中使用git-bash作为默认终端

                      1. 给vscode设置以管理员方式运行,
                      2. 在配置文件加入以下配置,把git的path改成自己的
                        "terminal.integrated.defaultProfile.windows": "GitBash",
                           "terminal.integrated.profiles.windows": {
                               "PowerShell": {
                                   "source": "PowerShell",
                      diff --git a/assets/WSL.html-DOItsf24.js b/assets/WSL.html-C3w2j3-t.js
                      similarity index 99%
                      rename from assets/WSL.html-DOItsf24.js
                      rename to assets/WSL.html-C3w2j3-t.js
                      index ed7f33f30..0da55d8e1 100644
                      --- a/assets/WSL.html-DOItsf24.js
                      +++ b/assets/WSL.html-C3w2j3-t.js
                      @@ -1,3 +1,3 @@
                      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a,o as t}from"./app-LRWZkDAq.js";const h={};function l(n,i){return t(),e("div",null,i[0]||(i[0]=[a(`

                      1. wsl系统设置桥接网络

                      参考:https://www.cnblogs.com/cheyunhua/p/17577895.html

                      1.1 开启hyper-v

                      桥接功能需要windows的hyper-v组件支持,但是win10/11家庭版是不包含hyper-v的,专业版才包含。网上也有文章提到家庭版安装hyper-v的方法,但是我没有测试,以下内容都是在win11专业版上进行的测试,win10专业版应该也是一样的。 首先,进入控制面板—程序—启用或关闭windows功能,勾选hyper-v,确认后重启电脑。

                      1.2 桥接网络

                      WSL2 默认采用了一个 NAT 网络,这对于大多数情况而言都是没有问题的,但是如果想要把 WSL 中的服务直接暴露出来,就不得不考虑做端口转发等问题。以及如果要使用 IPv6,自带的 NAT 方案也不能满足。

                      因此,这种时候如果能让 WSL2 使用直接接在 NIC 上自然是最好的,可惜 Windows 中没有直接提供这样的配置选项,如果在 Hyper-V 管理器中配置 WSL 网卡为外部网络则会直接报错。

                      万幸的是,可以使用 PowerShell 直接进行配置,本文则记录使用 PowerShell 让 WSL2 用上桥接网络的方法。

                      以下内容需要以管理员身份在PowerShell内执行。

                      一切开始之前首先需要启动 WSL,直接运行 wsl 即可,这样 WSL 的网卡才会被自动创建出来。

                      重启后首先运行wsl2(这样才能出现WSL的虚拟网卡),以管理员方式打开powershell,执行Get-NetAdapter,可以列出系统所有的网卡,记住想要桥接的网卡名称,比如我想桥接到有线网络其名称为“以太网”。

                      桥接网卡输入以下代码:

                      Set-VMSwitch WSL -NetAdapterName <你的网卡名字>

                      将wsl虚拟网络和主机有线网络桥接起来。

                      1.3 修改wsl

                      接下来进入 WSL 配置 IP 地址和网关,假设WSL的有线网络为eth0,网关为 192.168.1.1,IP 设置为 192.168.1.64/24:

                      ip addr del $(ip addr show eth0 | grep 'inet\\b' | awk '{print $2}' | head -n 1) dev eth0
                      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as e,a,o as t}from"./app-BIdVr4mq.js";const h={};function l(n,i){return t(),e("div",null,i[0]||(i[0]=[a(`

                      1. wsl系统设置桥接网络

                      参考:https://www.cnblogs.com/cheyunhua/p/17577895.html

                      1.1 开启hyper-v

                      桥接功能需要windows的hyper-v组件支持,但是win10/11家庭版是不包含hyper-v的,专业版才包含。网上也有文章提到家庭版安装hyper-v的方法,但是我没有测试,以下内容都是在win11专业版上进行的测试,win10专业版应该也是一样的。 首先,进入控制面板—程序—启用或关闭windows功能,勾选hyper-v,确认后重启电脑。

                      1.2 桥接网络

                      WSL2 默认采用了一个 NAT 网络,这对于大多数情况而言都是没有问题的,但是如果想要把 WSL 中的服务直接暴露出来,就不得不考虑做端口转发等问题。以及如果要使用 IPv6,自带的 NAT 方案也不能满足。

                      因此,这种时候如果能让 WSL2 使用直接接在 NIC 上自然是最好的,可惜 Windows 中没有直接提供这样的配置选项,如果在 Hyper-V 管理器中配置 WSL 网卡为外部网络则会直接报错。

                      万幸的是,可以使用 PowerShell 直接进行配置,本文则记录使用 PowerShell 让 WSL2 用上桥接网络的方法。

                      以下内容需要以管理员身份在PowerShell内执行。

                      一切开始之前首先需要启动 WSL,直接运行 wsl 即可,这样 WSL 的网卡才会被自动创建出来。

                      重启后首先运行wsl2(这样才能出现WSL的虚拟网卡),以管理员方式打开powershell,执行Get-NetAdapter,可以列出系统所有的网卡,记住想要桥接的网卡名称,比如我想桥接到有线网络其名称为“以太网”。

                      桥接网卡输入以下代码:

                      Set-VMSwitch WSL -NetAdapterName <你的网卡名字>

                      将wsl虚拟网络和主机有线网络桥接起来。

                      1.3 修改wsl

                      接下来进入 WSL 配置 IP 地址和网关,假设WSL的有线网络为eth0,网关为 192.168.1.1,IP 设置为 192.168.1.64/24:

                      ip addr del $(ip addr show eth0 | grep 'inet\\b' | awk '{print $2}' | head -n 1) dev eth0
                       ip addr add 192.168.1.64/24 broadcast 192.168.1.255 dev eth0
                       ip route add 0.0.0.0/0 via 192.168.1.1 dev eth0

                      接下来更新名称解析服务器地址,执行 nano /etc/resolv.conf,修改其中内容为 nameserver 192.168.1.1,

                      1.4 取消桥接

                      在windows中管理员方式打开powershell,执行以下指令:

                      Set-VMSwitch WSL -SwitchType Internal

                      然后执行 wsl --shutdown 重启wsl,即可恢复原有的虚拟内部网络。

                      `,22)]))}const d=s(h,[["render",l],["__file","WSL.html.vue"]]),k=JSON.parse('{"path":"/other/windows/WSL.html","title":"wsl问题","lang":"zh-CN","frontmatter":{"title":"wsl问题","date":"2024-09-23T00:00:00.000Z","author":"chensino","publish":true,"isOriginal":true,"description":"1. wsl系统设置桥接网络 参考:https://www.cnblogs.com/cheyunhua/p/17577895.html 1.1 开启hyper-v 桥接功能需要windows的hyper-v组件支持,但是win10/11家庭版是不包含hyper-v的,专业版才包含。网上也有文章提到家庭版安装hyper-v的方法,但是我没有测试,以下内容...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/windows/WSL.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"wsl问题"}],["meta",{"property":"og:description","content":"1. wsl系统设置桥接网络 参考:https://www.cnblogs.com/cheyunhua/p/17577895.html 1.1 开启hyper-v 桥接功能需要windows的hyper-v组件支持,但是win10/11家庭版是不包含hyper-v的,专业版才包含。网上也有文章提到家庭版安装hyper-v的方法,但是我没有测试,以下内容..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-10-08T07:30:29.000Z"}],["meta",{"property":"article:author","content":"chensino"}],["meta",{"property":"article:published_time","content":"2024-09-23T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-10-08T07:30:29.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"wsl问题\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2024-09-23T00:00:00.000Z\\",\\"dateModified\\":\\"2024-10-08T07:30:29.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"chensino\\"}]}"]]},"headers":[{"level":2,"title":"1. wsl系统设置桥接网络","slug":"_1-wsl系统设置桥接网络","link":"#_1-wsl系统设置桥接网络","children":[{"level":3,"title":"1.1 开启hyper-v","slug":"_1-1-开启hyper-v","link":"#_1-1-开启hyper-v","children":[]},{"level":3,"title":"1.2 桥接网络","slug":"_1-2-桥接网络","link":"#_1-2-桥接网络","children":[]},{"level":3,"title":"1.3 修改wsl","slug":"_1-3-修改wsl","link":"#_1-3-修改wsl","children":[]},{"level":3,"title":"1.4 取消桥接","slug":"_1-4-取消桥接","link":"#_1-4-取消桥接","children":[]}]}],"git":{"createdTime":1727060320000,"updatedTime":1728372629000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":2}]},"readingTime":{"minutes":2.15,"words":645},"filePathRelative":"other/windows/WSL.md","localizedDate":"2024年9月23日","excerpt":"

                      1. wsl系统设置桥接网络

                      \\n

                      参考:https://www.cnblogs.com/cheyunhua/p/17577895.html

                      \\n

                      1.1 开启hyper-v

                      \\n

                      桥接功能需要windows的hyper-v组件支持,但是win10/11家庭版是不包含hyper-v的,专业版才包含。网上也有文章提到家庭版安装hyper-v的方法,但是我没有测试,以下内容都是在win11专业版上进行的测试,win10专业版应该也是一样的。\\n首先,进入控制面板—程序—启用或关闭windows功能,勾选hyper-v,确认后重启电脑。

                      ","autoDesc":true}');export{d as comp,k as data}; diff --git a/assets/Wifi.html-CN2euKMr.js b/assets/Wifi.html-cQsjtWBO.js similarity index 98% rename from assets/Wifi.html-CN2euKMr.js rename to assets/Wifi.html-cQsjtWBO.js index 611522a1e..92245f08e 100644 --- a/assets/Wifi.html-CN2euKMr.js +++ b/assets/Wifi.html-cQsjtWBO.js @@ -1 +1 @@ -import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a as n,o}from"./app-LRWZkDAq.js";const r={};function a(p,e){return o(),i("div",null,e[0]||(e[0]=[n('

                      1、需求场景

                      场景1:

                      有一个没有wifi模块的电脑,想给他加装一个wifi模块,要求不仅支持windows系统,还需要支持Linux系统,最容易想到的方式是买一个USB无线网卡, 但此方法有个问题是一般USB无线网卡不会对Linux系统进行适配,即使有适配也是对系统条件要求很苛刻,比如只能用ubuntu,系统内核限制为指定版本等;

                      场景2:

                      有一个笔记本电脑,自带的有wifi模块,但是此无线网卡不支持5G、WIFI6等,此时可以根据情况升级无线网卡;

                      2、解决方案——使用板载无线网卡

                      使用板载无线网卡相对usb无线网卡来说,不会存在驱动的问题,驱动由Intel提供了支持,具体可以参考此处

                      使用板载网卡一般使用的是NGFF接口,也就是常见的M2接口,一般2015年以后的主板都有这个扩展口。

                      ',8)]))}const u=t(r,[["render",a],["__file","Wifi.html.vue"]]),s=JSON.parse('{"path":"/other/linux/Wifi.html","title":"Linux下加装wifi模块","lang":"zh-CN","frontmatter":{"title":"Linux下加装wifi模块","date":"2022-08-28T00:00:00.000Z","keys":null,"description":"1、需求场景 场景1: 有一个没有wifi模块的电脑,想给他加装一个wifi模块,要求不仅支持windows系统,还需要支持Linux系统,最容易想到的方式是买一个USB无线网卡, 但此方法有个问题是一般USB无线网卡不会对Linux系统进行适配,即使有适配也是对系统条件要求很苛刻,比如只能用ubuntu,系统内核限制为指定版本等; 场景2: 有一个笔...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/linux/Wifi.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Linux下加装wifi模块"}],["meta",{"property":"og:description","content":"1、需求场景 场景1: 有一个没有wifi模块的电脑,想给他加装一个wifi模块,要求不仅支持windows系统,还需要支持Linux系统,最容易想到的方式是买一个USB无线网卡, 但此方法有个问题是一般USB无线网卡不会对Linux系统进行适配,即使有适配也是对系统条件要求很苛刻,比如只能用ubuntu,系统内核限制为指定版本等; 场景2: 有一个笔..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-22T03:45:12.000Z"}],["meta",{"property":"article:published_time","content":"2022-08-28T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-22T03:45:12.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Linux下加装wifi模块\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-08-28T00:00:00.000Z\\",\\"dateModified\\":\\"2024-03-22T03:45:12.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、需求场景","slug":"_1、需求场景","link":"#_1、需求场景","children":[]},{"level":2,"title":"2、解决方案——使用板载无线网卡","slug":"_2、解决方案——使用板载无线网卡","link":"#_2、解决方案——使用板载无线网卡","children":[]}],"git":{"createdTime":1661677278000,"updatedTime":1711079112000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.95,"words":284},"filePathRelative":"other/linux/Wifi.md","localizedDate":"2022年8月28日","excerpt":"

                      1、需求场景

                      \\n

                      场景1:

                      \\n
                      \\n

                      有一个没有wifi模块的电脑,想给他加装一个wifi模块,要求不仅支持windows系统,还需要支持Linux系统,最容易想到的方式是买一个USB无线网卡,\\n但此方法有个问题是一般USB无线网卡不会对Linux系统进行适配,即使有适配也是对系统条件要求很苛刻,比如只能用ubuntu,系统内核限制为指定版本等;

                      \\n
                      \\n

                      场景2:

                      \\n
                      \\n

                      有一个笔记本电脑,自带的有wifi模块,但是此无线网卡不支持5G、WIFI6等,此时可以根据情况升级无线网卡;

                      \\n
                      ","autoDesc":true}');export{u as comp,s as data}; +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as i,a as n,o}from"./app-BIdVr4mq.js";const r={};function a(p,e){return o(),i("div",null,e[0]||(e[0]=[n('

                      1、需求场景

                      场景1:

                      有一个没有wifi模块的电脑,想给他加装一个wifi模块,要求不仅支持windows系统,还需要支持Linux系统,最容易想到的方式是买一个USB无线网卡, 但此方法有个问题是一般USB无线网卡不会对Linux系统进行适配,即使有适配也是对系统条件要求很苛刻,比如只能用ubuntu,系统内核限制为指定版本等;

                      场景2:

                      有一个笔记本电脑,自带的有wifi模块,但是此无线网卡不支持5G、WIFI6等,此时可以根据情况升级无线网卡;

                      2、解决方案——使用板载无线网卡

                      使用板载无线网卡相对usb无线网卡来说,不会存在驱动的问题,驱动由Intel提供了支持,具体可以参考此处

                      使用板载网卡一般使用的是NGFF接口,也就是常见的M2接口,一般2015年以后的主板都有这个扩展口。

                      ',8)]))}const u=t(r,[["render",a],["__file","Wifi.html.vue"]]),s=JSON.parse('{"path":"/other/linux/Wifi.html","title":"Linux下加装wifi模块","lang":"zh-CN","frontmatter":{"title":"Linux下加装wifi模块","date":"2022-08-28T00:00:00.000Z","keys":null,"description":"1、需求场景 场景1: 有一个没有wifi模块的电脑,想给他加装一个wifi模块,要求不仅支持windows系统,还需要支持Linux系统,最容易想到的方式是买一个USB无线网卡, 但此方法有个问题是一般USB无线网卡不会对Linux系统进行适配,即使有适配也是对系统条件要求很苛刻,比如只能用ubuntu,系统内核限制为指定版本等; 场景2: 有一个笔...","head":[["meta",{"property":"og:url","content":"https://ChenSino.github.io/other/linux/Wifi.html"}],["meta",{"property":"og:site_name","content":"ChenSino"}],["meta",{"property":"og:title","content":"Linux下加装wifi模块"}],["meta",{"property":"og:description","content":"1、需求场景 场景1: 有一个没有wifi模块的电脑,想给他加装一个wifi模块,要求不仅支持windows系统,还需要支持Linux系统,最容易想到的方式是买一个USB无线网卡, 但此方法有个问题是一般USB无线网卡不会对Linux系统进行适配,即使有适配也是对系统条件要求很苛刻,比如只能用ubuntu,系统内核限制为指定版本等; 场景2: 有一个笔..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"zh-CN"}],["meta",{"property":"og:updated_time","content":"2024-03-22T03:45:12.000Z"}],["meta",{"property":"article:published_time","content":"2022-08-28T00:00:00.000Z"}],["meta",{"property":"article:modified_time","content":"2024-03-22T03:45:12.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Linux下加装wifi模块\\",\\"image\\":[\\"\\"],\\"datePublished\\":\\"2022-08-28T00:00:00.000Z\\",\\"dateModified\\":\\"2024-03-22T03:45:12.000Z\\",\\"author\\":[{\\"@type\\":\\"Person\\",\\"name\\":\\"ChenSino\\",\\"url\\":\\"https://ChenSino.github.io\\"}]}"]]},"headers":[{"level":2,"title":"1、需求场景","slug":"_1、需求场景","link":"#_1、需求场景","children":[]},{"level":2,"title":"2、解决方案——使用板载无线网卡","slug":"_2、解决方案——使用板载无线网卡","link":"#_2、解决方案——使用板载无线网卡","children":[]}],"git":{"createdTime":1661677278000,"updatedTime":1711079112000,"contributors":[{"name":"ChenSino","email":"462488588@qq.com","commits":1},{"name":"chenkun","email":"462488588@qq.com","commits":1}]},"readingTime":{"minutes":0.95,"words":284},"filePathRelative":"other/linux/Wifi.md","localizedDate":"2022年8月28日","excerpt":"

                      1、需求场景

                      \\n

                      场景1:

                      \\n
                      \\n

                      有一个没有wifi模块的电脑,想给他加装一个wifi模块,要求不仅支持windows系统,还需要支持Linux系统,最容易想到的方式是买一个USB无线网卡,\\n但此方法有个问题是一般USB无线网卡不会对Linux系统进行适配,即使有适配也是对系统条件要求很苛刻,比如只能用ubuntu,系统内核限制为指定版本等;

                      \\n
                      \\n

                      场景2:

                      \\n
                      \\n

                      有一个笔记本电脑,自带的有wifi模块,但是此无线网卡不支持5G、WIFI6等,此时可以根据情况升级无线网卡;

                      \\n
                      ","autoDesc":true}');export{u as comp,s as data}; diff --git a/assets/aboutAsync.html-D_L12OCl.js b/assets/aboutAsync.html-oq_Ucfhr.js similarity index 99% rename from assets/aboutAsync.html-D_L12OCl.js rename to assets/aboutAsync.html-oq_Ucfhr.js index e5e96af14..40553618a 100644 --- a/assets/aboutAsync.html-D_L12OCl.js +++ b/assets/aboutAsync.html-oq_Ucfhr.js @@ -1,4 +1,4 @@ -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function t(k,s){return l(),a("div",null,s[0]||(s[0]=[n(`

                      一,async函数的定义

                      async函数是使用async关键字声明的函数。 并且其中允许使用await关键字。async和await关键字让我们可以用一种更简洁的方式写出基于Promise异步行为,而无需刻意地链式调用promise。

                      备注:async/await的目的为了简化使用基于 promise 的 API 时所需的语法。async/await的行为就好像搭配使用了生成器和 promise。

                      async函数的书写方式如下:

                      // 函数声明
                      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function t(k,s){return l(),a("div",null,s[0]||(s[0]=[n(`

                      一,async函数的定义

                      async函数是使用async关键字声明的函数。 并且其中允许使用await关键字。async和await关键字让我们可以用一种更简洁的方式写出基于Promise异步行为,而无需刻意地链式调用promise。

                      备注:async/await的目的为了简化使用基于 promise 的 API 时所需的语法。async/await的行为就好像搭配使用了生成器和 promise。

                      async函数的书写方式如下:

                      // 函数声明
                       async function foo() {}
                       
                       // 函数表达式
                      diff --git a/assets/aboutEvent.html-D7Q2TjUz.js b/assets/aboutEvent.html-CfGtwxE3.js
                      similarity index 99%
                      rename from assets/aboutEvent.html-D7Q2TjUz.js
                      rename to assets/aboutEvent.html-CfGtwxE3.js
                      index d95f0a37e..46bcd356c 100644
                      --- a/assets/aboutEvent.html-D7Q2TjUz.js
                      +++ b/assets/aboutEvent.html-CfGtwxE3.js
                      @@ -1,4 +1,4 @@
                      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-LRWZkDAq.js";const h={};function t(k,s){return l(),a("div",null,s[0]||(s[0]=[n(`

                      一,事件注册的三种方式

                      1,通过 HTML 元素指定事件属性来绑定

                      <button onclick ="clickFu()">点我吧</button>
                      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as l}from"./app-BIdVr4mq.js";const h={};function t(k,s){return l(),a("div",null,s[0]||(s[0]=[n(`

                      一,事件注册的三种方式

                      1,通过 HTML 元素指定事件属性来绑定

                      <button onclick ="clickFu()">点我吧</button>
                       <script>
                          function clickFu(){
                              alert(3333)
                      diff --git a/assets/aboutThis.html-BzHERr3b.js b/assets/aboutThis.html-CGTlZqFV.js
                      similarity index 99%
                      rename from assets/aboutThis.html-BzHERr3b.js
                      rename to assets/aboutThis.html-CGTlZqFV.js
                      index db6c247d4..ea4afda13 100644
                      --- a/assets/aboutThis.html-BzHERr3b.js
                      +++ b/assets/aboutThis.html-CGTlZqFV.js
                      @@ -1,4 +1,4 @@
                      -import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as t,o as n}from"./app-LRWZkDAq.js";const h={};function l(e,i){return n(),a("div",null,i[0]||(i[0]=[t(`

                      一,函数内部的this指向

                      函数内this指向,是当我们调用函数的时候才能确定。调用方式的不同决定this的指向不同。
                      +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as t,o as n}from"./app-BIdVr4mq.js";const h={};function l(e,i){return n(),a("div",null,i[0]||(i[0]=[t(`

                      一,函数内部的this指向

                      函数内this指向,是当我们调用函数的时候才能确定。调用方式的不同决定this的指向不同。
                       this的指向,是在调用函数时根据执行上下文所动态确定的。
                       
                      调用方式this指向
                      普通函数调用window
                      构造函数调用实例对象,原型对象里的方法也指向实例对象
                      对象方法调用该方法所属的对象
                      事件绑定方法调用绑定事件对象
                      定时器函数window
                      立即执行函数window

                      二,setTimeout & setInterval

                      对于延时函数内部的回调函数的this指向全局对象window(当然我们可以通过bind方法改变其内部函数的this指向.

                      function Person() {  
                           this.age = 0;  
                      diff --git a/assets/action-usage.html-CMjNTcaP.js b/assets/action-usage.html-rUKU9K_y.js
                      similarity index 99%
                      rename from assets/action-usage.html-CMjNTcaP.js
                      rename to assets/action-usage.html-rUKU9K_y.js
                      index 639cbd834..35d8fdaea 100644
                      --- a/assets/action-usage.html-CMjNTcaP.js
                      +++ b/assets/action-usage.html-rUKU9K_y.js
                      @@ -1,4 +1,4 @@
                      -import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-LRWZkDAq.js";const h={};function l(k,s){return t(),a("div",null,s[0]||(s[0]=[n(`

                      一,利用typeScript实现新增,删除一行数据

                      这里基本涵盖了typeScript在项目中的实战用法

                      1,html部分

                      <!DOCTYPE html>
                      +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{c as a,a as n,o as t}from"./app-BIdVr4mq.js";const h={};function l(k,s){return t(),a("div",null,s[0]||(s[0]=[n(`

                      一,利用typeScript实现新增,删除一行数据

                      这里基本涵盖了typeScript在项目中的实战用法

                      1,html部分

                      <!DOCTYPE html>
                       <html lang="en">
                       <head>
                       	<meta charset="UTF-8">
                      diff --git a/assets/app-LRWZkDAq.js b/assets/app-BIdVr4mq.js
                      similarity index 83%
                      rename from assets/app-LRWZkDAq.js
                      rename to assets/app-BIdVr4mq.js
                      index f333c412c..ab8206515 100644
                      --- a/assets/app-LRWZkDAq.js
                      +++ b/assets/app-BIdVr4mq.js
                      @@ -1,4 +1,4 @@
                      -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index.html-KwmoBcRm.js","assets/plugin-vue_export-helper-DlAUqK2U.js","assets/home.html-aGGYOR7m.js","assets/slide.html-D-H9bRlk.js","assets/index.html-BRFnlh32.js","assets/建造者模式.html-BFH9gyJJ.js","assets/模板方法模式.html-BHkIE83-.js","assets/策略模式.html-6Sf1ytqy.js","assets/装饰模式.html-CiXRBiu9.js","assets/责任链模式.html-BnEIojaU.js","assets/index.html-DGOKX5rc.js","assets/index.html-DcYo10ls.js","assets/disable.html-Cyr3Xy2m.js","assets/encrypt.html-g93oybTM.js","assets/markdown.html-DEfZHKXr.js","assets/page.html-CGr9yG-I.js","assets/index.html-M6ciET1C.js","assets/Jellyfin搭建.html-CMT6qNWD.js","assets/OpenWRT.html-DzrmVEsx.js","assets/PVE虚拟机.html-t1tsCGLO.js","assets/index.html-CvNGtBHP.js","assets/Ubuntu服务搭建.html-B5ncaYIz.js","assets/ddns申请证书.html-CQQa8cy8.js","assets/firewall.html-CDNDwMUM.js","assets/fnos.html-BbY5Cor0.js","assets/nginx反向代理ddns解析问题.html-DmX4Ir6j.js","assets/x86_openwrt.html-Cq0wSqzW.js","assets/网络设置.html-BJTIC__2.js","assets/自建nas.html-2XSY6hdf.js","assets/证书自动续签.html-R5o--Pmc.js","assets/index.html-CXgHAvGT.js","assets/1.html-CERq7o11.js","assets/2.html-CMg5IU4s.js","assets/index.html-DkjjOyEZ.js","assets/index.html-CI5-E8zh.js","assets/aboutAsync.html-D_L12OCl.js","assets/aboutEvent.html-D7Q2TjUz.js","assets/aboutThis.html-BzHERr3b.js","assets/asyncError.html-B7e9KI7M.js","assets/crossDomain.html-D4cFVnCU.js","assets/crossDomain2.html-DUOpY0k2.js","assets/lazyLoad.html-CBB2VYk3.js","assets/throttle.html-BkFDlaU-.js","assets/index.html-C5owgjh1.js","assets/js中整数的最大值.html-iX2inHhn.js","assets/promise.html-B1x3cimS.js","assets/useModule.html-BNNVuyQF.js","assets/useNpm.html-BN-sNads.js","assets/usePnpm.html-DUVqoXBg.js","assets/useYarn.html-CGJq63Qp.js","assets/index.html-Ct1GdAKI.js","assets/安装问题.html-BJMb7W8z.js","assets/index.html-YtRDZV2L.js","assets/action-usage.html-CMjNTcaP.js","assets/axios.html-BLOOWYNb.js","assets/basic-usage.html-B8UvuKMd.js","assets/fanType.html-BMxjGDLX.js","assets/tsAndvue3.html-CfNs7pkY.js","assets/index.html-Cluv8BOI.js","assets/index.html-Dpjcv5aI.js","assets/elementui表单自定义校验.html-BU27BiWU.js","assets/eventBus.html-DDvVENca.js","assets/vue-Direactive.html-DMCWSWu1.js","assets/vue-authority.html-C-Omz_-1.js","assets/vue-in-action.html-pzeMhg7a.js","assets/vue-nextTick.html-a7ywGpHU.js","assets/vue-pic.html-BNNPKouB.js","assets/vue-router1.html-CUUy_5yC.js","assets/vue-router2.html-wszoFzb3.js","assets/vue3Emit.html-dWosLNLo.js","assets/vue3LifeTime.html-CTIvTlow.js","assets/vueExtend.html-DMEGEpFb.js","assets/vue项目本地一直发websocket.html-wKQRhvFR.js","assets/自定义指令的实践.html-NCx1Pibh.js","assets/Arthas.html-CX1s5XJ4.js","assets/Collection.html-UjyrsFMx.js","assets/CompileJdk11.html-B-6Zabjd.js","assets/Concurrent.html-DRLYghtT.js","assets/Future.html-axBzZo7a.js","assets/IO-Model.html-BT2SPf7N.js","assets/IO-model1.html-B5naQOu0.js","assets/ImplementSameInterface.html-kaw5w828.js","assets/MysqlMasterSlave.html-5i2FdJ-q.js","assets/NativeMethod.html-B7wItcKZ.js","assets/ParentDelegationClassLoader.html-DD8LdvjV.js","assets/ProxyInJava.html-Bw3KCeSC.js","assets/index.html-CuUIxB-V.js","assets/Synchronized.html-C99mj8uR.js","assets/ThreadLocal.html-u61CIRhJ.js","assets/ThreadPool.html-ChF6p8sJ.js","assets/fail-fast.html-DkT_nq5X.js","assets/io.html-6grZaOsg.js","assets/ConstantPool.html-DqztC09o.js","assets/CustomLRU.html-BdYIfH0T.js","assets/IntegerConstantPool.html-BpYi02Iy.js","assets/index.html-ckEdUTqL.js","assets/Serialization.html-BSKOYFIh.js","assets/String.html-BEF_M3Rd.js","assets/ClassLoader.html-CMN4swpH.js","assets/MemoryModel.html-DglTwkK-.js","assets/NewObject.html-DOxLULz4.js","assets/ObjectReference.html-CrcN467V.js","assets/index.html-C7CD60O0.js","assets/SetObjectNull.html-Cv2qsWfW.js","assets/StringAdd.html-ByCo-I_5.js","assets/volatile.html-BgiFSb0F.js","assets/名词解释.html-DixGDuzA.js","assets/JdkVersion.html-C-a3tlfZ.js","assets/index.html-DfVsPsyN.js","assets/ebooks.html-CmQ0pdLk.js","assets/TCP-IP.html-DBoM_5OE.js","assets/CPUOverLoad.html-BlwjFiRj.js","assets/MysqlCollate.html-Ddr0cLWB.js","assets/MysqlNote.html-B-D9CeHp.js","assets/MysqlRemoteConnect.html-xJMa8jPk.js","assets/index.html-bDZGzk6b.js","assets/Recurse.html-BYNa9G5A.js","assets/SQLOptimization.html-Dm2zmzdd.js","assets/mysql主从复制.html-COc1k69_.js","assets/mysql索引.html-CBuRUHP0.js","assets/数据库备份.html-Dyo0xHVz.js","assets/DistributeLock.html-DphO16c2.js","assets/Nacos.html-CHmBYmVv.js","assets/index.html-CiezHrmK.js","assets/Docker.html-DfENlTQt.js","assets/ServiceInstall.html-a8eSZceK.js","assets/通过代理拉取镜像.html-DlMILAav.js","assets/2022-04-12.html-VXRZFk6l.js","assets/BTree.html-CLTFqYVA.js","assets/CDN.html-BrUklWPJ.js","assets/ChromeDevTools.html-DIFEaEZF.js","assets/CloudService.html-CjT4ijCP.js","assets/DeployGithubPage.html-C3CJu8PT.js","assets/IM即时通信技术选型.html-BrFdlOJV.js","assets/Jenkins.html-BlEG3ZLM.js","assets/index.html-Ci0fk4NS.js","assets/TyporaPicgo.html-BFOgiNZO.js","assets/elasticSearch操作.html-hVwrnBmy.js","assets/elk部署.html-D33Pj7hN.js","assets/im即时通信的需求.html-jBUJLx5g.js","assets/windows下服务注册.html-Dcz3rIgQ.js","assets/操作系统引导.html-Dkn3DOho.js","assets/BatchDeleteGitHubRepo.html-DsXldxOW.js","assets/GitCommands.html-BbarE5Z9.js","assets/index.html-DaMYstTJ.js","assets/branch01.html-DjNYYC56.js","assets/branch02.html-DC9xWnxN.js","assets/fatal.html-BMpaFBLz.js","assets/gitConflict.html-DVdpn_6G.js","assets/gitRebase.html-BC1KUd03.js","assets/gitwork.html-Ck1n4MrT.js","assets/mergeBranch.html-DLnnhDxz.js","assets/proxy.html-Q237dBqx.js","assets/rebaseAndMerge.html-BU7yi9md.js","assets/reset.html-C034UaNH.js","assets/stash.html-B-CQ6rNg.js","assets/CPU.html-CrPccORk.js","assets/CentOS.html-BdaPB0Ti.js","assets/CommonUsedCMD.html-CyjonT_t.js","assets/Curl.html-Bmo3iaqW.js","assets/InstallMysqlWithDocker.html-BJNnL9bp.js","assets/Manjaro.html-Bb8Od0IV.js","assets/MountDisk.html-BzHOCk5h.js","assets/MultiNetworkCard.html-CK4m44eU.js","assets/index.html-DURvABzp.js","assets/Samba.html-CBqUaBWb.js","assets/ShareBetweenWindowsAndLinux.html-ChX2AFxh.js","assets/TcpDump.html-oAiwcaO7.js","assets/Wifi.html-CN2euKMr.js","assets/firewall.html-v7dOSz8g.js","assets/wsl.html-B09klqwv.js","assets/截图.html-C375TSox.js","assets/index.html-DNYsUsip.js","assets/01.html-BqSw6zV7.js","assets/02.html-hTc330ny.js","assets/03.html-BlykeApW.js","assets/04.html-Bb3h6Bb4.js","assets/05.html-Cxt8rLap.js","assets/06.html-Bcc4cjIC.js","assets/07.html-CN-w1qxn.js","assets/08.html-BdpbItx3.js","assets/09.html-FHq4Vqms.js","assets/10.html-CxJyQyMC.js","assets/11.html-C-Hi5p0c.js","assets/12.html-BNyGNDL2.js","assets/13.html-DtCYSHOP.js","assets/14.html-C7QC-4vS.js","assets/index.html-ByhguBAg.js","assets/index.html-00yXAu2E.js","assets/破解软件.html-ufEFOt5K.js","assets/Idea.html-BAc8p-a0.js","assets/index.html-CwfcZ1tZ.js","assets/SoftWare.html-BZYs1Hsf.js","assets/VsCode.html-BydNEOJq.js","assets/CloudServiceTraining.html-BbBqNR5J.js","assets/index.html-BNQrYStM.js","assets/SSO.html-o59ROGA3.js","assets/draw.html-B6CWzEZ_.js","assets/BuildWebProject.html-C1h9cPZ9.js","assets/Cookie.html-CX4Rmbtp.js","assets/Http.html-Ban9xdQ_.js","assets/Jwt.html-7z10_PeO.js","assets/OAUTH_LOGIN.html-BU7CthLn.js","assets/index.html-CmAgccQa.js","assets/RefreshToken.html-D2M8Zi5t.js","assets/Restful.html-DGA73Lg6.js","assets/token自动刷新.html-tntla8SI.js","assets/前后分离oauth2.0.html-Be8XZSRK.js","assets/WSL.html-DOItsf24.js","assets/系统安装.html-B1sTpAMR.js","assets/MybatisPlusDataSource.html-Bql0giQE.js","assets/index.html-DDYvG21u.js","assets/mybatis.html-C4UCNyV0.js","assets/Authorization.html-CaZDVM9e.js","assets/CustomAuthenticationProvider.html-C5viGtaI.js","assets/CustomLoginPage.html-OUdaxTEw.js","assets/CustomTokenAuthentication.html-phBOu6dq.js","assets/DelegatingFilterProxy.html-CjzmnpRV.js","assets/OAuth2Authentication.html-CAGOYH3n.js","assets/OncePerRequestFilter.html-B3TQmcGq.js","assets/PreAuthorize.html-DzIfTkjN.js","assets/index.html-BqsUMbdU.js","assets/SSO.html-BQ6lKX33.js","assets/SecurityFilterChain.html-Brlhg7Xm.js","assets/Session.html-B4ArK8cx.js","assets/note.html-YoblFkQS.js","assets/spring-security-oauth2-authorization-server.html-BNQ7rVT9.js","assets/Annotation.html-DEhyElMf.js","assets/BeanPostProcessor.html-B4kV_I9i.js","assets/CircularDependency.html-CeopMWPo.js","assets/DesignPatternInSpring.html-nq8yHSdu.js","assets/OncePerRequestFilter.html-D66nF_K3.js","assets/index.html-AKycjFxt.js","assets/SpringAOP.html-2cvojWKp.js","assets/SpringCache.html-keNWZMJo.js","assets/SpringExtensionPoint.html-De7aTg35.js","assets/SpringIOC.html-DvmNTL7R.js","assets/SpringMVC.html-BSrJLxWA.js","assets/SpringSourceAnalize.html-CA7pc5uW.js","assets/Validator.html-Cgd70-bx.js","assets/AOPLog.html-Bm02oOMf.js","assets/CollectionInject.html-BvWa61Sp.js","assets/Http2.html-2WMDcdOZ.js","assets/index.html-CFDA46HI.js","assets/SpringBootAutoConfiguration.html-BJXaC7pW.js","assets/Swagger.html-BUArKIxO.js","assets/读写分离.html-C1zXUnSi.js","assets/部署.html-D4gxZ9sd.js","assets/SpringCloudGateway.html-CGUU2vu8.js","assets/index.html-CL9jj7rw.js","assets/wrapper.html-BLTTVm9X.js","assets/index.html-RViehNRf.js","assets/TooManyOpenFiles.html-BYzQ7KMi.js","assets/Undertow.xxxNotFount.html-D6hJS7YH.js","assets/各种框架版本的坑.html-COGDmhTZ.js","assets/各种问题.html-B7HHDH3m.js","assets/logback.html-P_GvzsK2.js","assets/build标签.html-DY2foH_V.js","assets/import.html-CiYx9gmq.js","assets/multiModule.html-DxNg0iZD.js","assets/problem.html-BgKe_lWA.js","assets/生命周期.html-BF6EG3Sg.js","assets/404.html-BagFMDd7.js","assets/index.html-BRm5Gvor.js","assets/index.html-BPC3zJSU.js","assets/index.html-DzNwwRfO.js","assets/index.html-DOEKJOHw.js","assets/index.html-Dz1hbfvm.js","assets/index.html-DNVny5DL.js","assets/index.html-D8BV5Mgu.js","assets/index.html-DP3vVcUi.js","assets/index.html-Biz09ahT.js","assets/index.html-CHMcR43A.js","assets/index.html-BEpN4Kx1.js","assets/index.html-DAgZUna0.js","assets/index.html-B6W_yULn.js","assets/index.html-DU4GHQR6.js","assets/index.html-CmzcNC6M.js","assets/index.html-CFahR_Uz.js","assets/index.html-bniV5DET.js","assets/index.html-CaaT6W-w.js","assets/index.html-CQ2SoX7l.js","assets/index.html-DAAyVcR0.js","assets/index.html-BCmoJY04.js","assets/index.html-D0V6L-UM.js","assets/index.html-35GNTpgO.js","assets/index.html-CDZvytPE.js","assets/index.html-DyVVIrWa.js","assets/index.html-C3RPpUVY.js","assets/index.html-m-Am85s9.js","assets/index.html-DmaFZnd1.js","assets/index.html-Cyfg2yKH.js","assets/index.html-WwuxfzLA.js","assets/index.html-D1sJtXWQ.js","assets/index.html-Ckn7YqIK.js","assets/index.html-D48OAIhP.js","assets/index.html-hFoavWQa.js","assets/index.html-CU-UF1rP.js","assets/index.html-BGGeTkjF.js","assets/index.html-BArKTdeW.js","assets/index.html-DecTW5Vq.js","assets/index.html-KsfGbA7X.js","assets/index.html-Cimzf8US.js","assets/index.html-C---1nFZ.js","assets/index.html-ncHBdLCM.js","assets/index.html-DerE9RKl.js","assets/index.html-DPMWiYpK.js","assets/index.html-DmIcEJVt.js","assets/index.html-CCYbeGLn.js","assets/index.html-DY5iuE1D.js","assets/index.html-B26PLbnt.js","assets/index.html-CEVfOHM2.js","assets/index.html-DRDdWuur.js","assets/index.html-gC5jCLxE.js","assets/index.html-BCxXA6GU.js","assets/index.html-Bg-N4ojs.js","assets/index.html-E-Cx3csN.js","assets/index.html-D7cnqetB.js","assets/index.html-CJX-cM8l.js","assets/index.html-RbiR8Hxk.js","assets/index.html-CnSAmy10.js","assets/index.html-CX-Lzl1D.js","assets/index.html-BYhgblfy.js","assets/index.html-CnvT0Mlm.js","assets/index.html-DOt2JR9K.js","assets/index.html-4XNFUA_b.js","assets/index.html-zRLBETDX.js","assets/index.html-Cn-vemYg.js","assets/index.html-X1bIOYsb.js","assets/index.html-BqzZCNMV.js","assets/index.html-DE-84drN.js","assets/index.html-CLZJl0yu.js","assets/index.html-JNXR2WUl.js","assets/index.html-B1T6Eb8n.js","assets/index.html-CbMP3z62.js","assets/index.html-BkiR12t2.js","assets/index.html-gvETmnnw.js","assets/index.html-BVhcyzEe.js","assets/index.html-Cv8nLQFM.js","assets/index.html-CIIy3e5B.js","assets/index.html-DmAZd2-G.js","assets/index.html-Cr1BJnp5.js"])))=>i.map(i=>d[i]);
                      +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index.html-BIyQQ4YT.js","assets/plugin-vue_export-helper-DlAUqK2U.js","assets/home.html-NeR1GMte.js","assets/slide.html-Y7bXu_We.js","assets/index.html-pcaAAK7Q.js","assets/建造者模式.html-BewjSrrw.js","assets/模板方法模式.html-B7vHULWV.js","assets/策略模式.html-BMf3v7YI.js","assets/装饰模式.html-B1IPnvpl.js","assets/责任链模式.html-BiFSpWx5.js","assets/index.html-Cp5ukHy1.js","assets/index.html-DTTN-fqR.js","assets/disable.html-BIHRTCCC.js","assets/encrypt.html-DiZeFQ0R.js","assets/markdown.html-qweyEKPp.js","assets/page.html-CiuNj2kU.js","assets/index.html-C-EyMnTc.js","assets/Jellyfin搭建.html-BxsZZcjM.js","assets/OpenWRT.html-DJ_JkW49.js","assets/PVE虚拟机.html-D-5k4U5W.js","assets/index.html-BDFAfq7g.js","assets/Ubuntu服务搭建.html-Dn60vq3V.js","assets/ddns申请证书.html-9E4jX9TA.js","assets/firewall.html-BD988R_L.js","assets/fnos.html-CdbKMm57.js","assets/nginx反向代理ddns解析问题.html-DaFsuZUP.js","assets/x86_openwrt.html-BMoOjmEk.js","assets/网络设置.html-DwuvRbKr.js","assets/自建nas.html-CK_-5RCz.js","assets/证书自动续签.html-DTlcMbEd.js","assets/index.html-DR85PmVq.js","assets/1.html-BKAh0zdH.js","assets/2.html-DYUUXpT6.js","assets/index.html-Cm_vJXFf.js","assets/index.html-4zQyDuTf.js","assets/aboutAsync.html-oq_Ucfhr.js","assets/aboutEvent.html-CfGtwxE3.js","assets/aboutThis.html-CGTlZqFV.js","assets/asyncError.html-DIaCTfUi.js","assets/crossDomain.html-DiNllvHl.js","assets/crossDomain2.html-UM0H1KZK.js","assets/lazyLoad.html-x9qhamqy.js","assets/throttle.html-lVY8dAD-.js","assets/index.html-CKiw-mPF.js","assets/js中整数的最大值.html-BTBkXAEb.js","assets/promise.html-Dqg8o8wE.js","assets/useModule.html-DpfNyX4_.js","assets/useNpm.html-CO34N8n0.js","assets/usePnpm.html-DeDjO0-6.js","assets/useYarn.html-DWtrjvbO.js","assets/index.html-CQVuIZLy.js","assets/安装问题.html-fx1na5Iw.js","assets/index.html-WPNLoBTh.js","assets/action-usage.html-rUKU9K_y.js","assets/axios.html-CMfeDl50.js","assets/basic-usage.html-CvR9Cwjv.js","assets/fanType.html-DX1_TAT6.js","assets/tsAndvue3.html-ulSXqKHP.js","assets/index.html-BTa4HD8h.js","assets/index.html-ClGHx_x3.js","assets/elementui表单自定义校验.html-Bd7ZB8Po.js","assets/eventBus.html-md5G45mS.js","assets/vue-Direactive.html-Ddh2keS8.js","assets/vue-authority.html-CuO4qx38.js","assets/vue-in-action.html-CmgeKTEO.js","assets/vue-nextTick.html-DoRZxd81.js","assets/vue-pic.html-D3e779S-.js","assets/vue-router1.html-BsuAzZy1.js","assets/vue-router2.html-CFBH3XYr.js","assets/vue3Emit.html-Bi1THuTe.js","assets/vue3LifeTime.html-raVaHUQD.js","assets/vueExtend.html-DXi8EFTR.js","assets/vue项目本地一直发websocket.html-BJoQpGdp.js","assets/自定义指令的实践.html-hhrMuLV0.js","assets/Arthas.html-iIzMcD21.js","assets/Collection.html-DcRVdFY1.js","assets/CompileJdk11.html-DBmPA9mE.js","assets/Concurrent.html-iguxRgDL.js","assets/Future.html-CSv-67Ma.js","assets/IO-Model.html-AwEDHwi6.js","assets/IO-model1.html-CW-n6NiB.js","assets/ImplementSameInterface.html-_LYgzeMO.js","assets/MysqlMasterSlave.html-IEFzX7F8.js","assets/NativeMethod.html-ClQ8_R4Z.js","assets/ParentDelegationClassLoader.html-BTelhs6W.js","assets/ProxyInJava.html-D4L5WYNV.js","assets/index.html-CiCX_JhU.js","assets/Synchronized.html-B9MlqwzL.js","assets/ThreadLocal.html-BAQekg_Z.js","assets/ThreadPool.html-xcxHOFbM.js","assets/fail-fast.html-CIJb3icO.js","assets/io.html-DnJ9HVIs.js","assets/ConstantPool.html-DQ4ql_mB.js","assets/CustomLRU.html-DerU6oev.js","assets/IntegerConstantPool.html-qcjVgBE7.js","assets/index.html-D7mz958k.js","assets/Serialization.html-OnvjGjMN.js","assets/String.html-CuetWaFK.js","assets/ClassLoader.html-C7KNzZRW.js","assets/MemoryModel.html-BbniDibC.js","assets/NewObject.html-Cvalksc5.js","assets/ObjectReference.html-D9v0-LGB.js","assets/index.html-CpCokxqx.js","assets/SetObjectNull.html-CiI27fz6.js","assets/StringAdd.html-DiHKxvsE.js","assets/volatile.html-DQhpE7DJ.js","assets/名词解释.html-gUVey7Ns.js","assets/JdkVersion.html-tQ6JXAtC.js","assets/index.html-3T2vvqio.js","assets/ebooks.html-Bvqqu9te.js","assets/TCP-IP.html-CbApdGPu.js","assets/CPUOverLoad.html-BuVMUTre.js","assets/MysqlCollate.html-FwkcBh96.js","assets/MysqlNote.html-BbvwFbUm.js","assets/MysqlRemoteConnect.html-BdtTYKvx.js","assets/index.html-Ck4VzfbX.js","assets/Recurse.html-Bux7wAbT.js","assets/SQLOptimization.html-CC5fHt2c.js","assets/mysql主从复制.html-DtlLv9Nn.js","assets/mysql索引.html-CuLUAXuj.js","assets/数据库备份.html-DzmVZIdZ.js","assets/DistributeLock.html-CblYS_y1.js","assets/Nacos.html-KVubxHn8.js","assets/index.html-E9Wkx9cd.js","assets/Docker.html-DRMULpQx.js","assets/ServiceInstall.html-IjUvufHh.js","assets/通过代理拉取镜像.html-DDfh6M-Q.js","assets/2022-04-12.html-COB6RKZC.js","assets/BTree.html-B6mC9nUD.js","assets/CDN.html-DEIMFq2d.js","assets/ChromeDevTools.html-CzA-EmEC.js","assets/CloudService.html-VcDoz7-V.js","assets/DeployGithubPage.html-BMfBhB20.js","assets/IM即时通信技术选型.html-veogyBQt.js","assets/Jenkins.html-BlvN7BiZ.js","assets/index.html-CkTmkVqY.js","assets/TyporaPicgo.html-DiCAdyNh.js","assets/elasticSearch操作.html-B7JMEaBD.js","assets/elk部署.html-mVatN34N.js","assets/im即时通信的需求.html-DyFOtblG.js","assets/windows下服务注册.html-Hl5CGc6m.js","assets/操作系统引导.html-CEnMYqvl.js","assets/BatchDeleteGitHubRepo.html-i5eo4HNC.js","assets/GitCommands.html-OTAFexBu.js","assets/index.html-CsoAqmHA.js","assets/branch01.html-Cyy-Y0us.js","assets/branch02.html-DzulXNoK.js","assets/fatal.html-1jDaL0H2.js","assets/gitConflict.html-BGZttGsy.js","assets/gitRebase.html-Dz5w3eml.js","assets/gitwork.html-DbshE2ks.js","assets/mergeBranch.html-CJReB61K.js","assets/proxy.html-Ciypu-jF.js","assets/rebaseAndMerge.html-qrvKpauy.js","assets/reset.html-BnywMEUr.js","assets/stash.html-DDps8CE5.js","assets/CPU.html-BZbWMqTq.js","assets/CentOS.html-CvNBcUZo.js","assets/CommonUsedCMD.html-tJHiFr9g.js","assets/Curl.html-7Jrb-SD3.js","assets/InstallMysqlWithDocker.html-Bj1rp9yg.js","assets/Manjaro.html-e_kr8ypm.js","assets/MountDisk.html-BzdT4jwB.js","assets/MultiNetworkCard.html-Ckes4Awq.js","assets/index.html-Ckf605kx.js","assets/Samba.html-DzYkwvBv.js","assets/ShareBetweenWindowsAndLinux.html-B04nBB2Z.js","assets/TcpDump.html-mCRY-l5s.js","assets/Wifi.html-cQsjtWBO.js","assets/firewall.html-DhAgXCrJ.js","assets/wsl.html-q1A1mBMo.js","assets/截图.html-DpqSuPpa.js","assets/index.html-DFaAAH7O.js","assets/01.html-DTbWNLK7.js","assets/02.html-0inoDdd1.js","assets/03.html-0MOLhHcg.js","assets/04.html-CODd576A.js","assets/05.html-BlGcSftZ.js","assets/06.html-FuByxaV8.js","assets/07.html-Cg_BlXZ8.js","assets/08.html-C2-4HV62.js","assets/09.html-CYHAg00J.js","assets/10.html-N4UaRZCw.js","assets/11.html-q32x2bnY.js","assets/12.html-DGRJGE6K.js","assets/13.html-DiPY3dlK.js","assets/14.html-Cm6GW1t2.js","assets/index.html-DoiJAPwv.js","assets/index.html-ilrnDPtM.js","assets/破解软件.html-tKbsG-Zr.js","assets/Idea.html-Bdp8uu0g.js","assets/index.html-BuYJ3g5z.js","assets/SoftWare.html-6ZCndbox.js","assets/VsCode.html-B0jpbkGz.js","assets/CloudServiceTraining.html-DFJMNjIC.js","assets/index.html-oyZf_SP4.js","assets/SSO.html-CT7CTtUX.js","assets/draw.html-Dr2RFJVL.js","assets/BuildWebProject.html-iK6Db4et.js","assets/Cookie.html-BtSuppuo.js","assets/Http.html-UGMp5Sj8.js","assets/Jwt.html-CIXzxbJn.js","assets/OAUTH_LOGIN.html-CrucBoKd.js","assets/index.html-DDQpBdVL.js","assets/RefreshToken.html-DrzHWxB_.js","assets/Restful.html-DLGwYhd3.js","assets/token自动刷新.html-CSV4znys.js","assets/前后分离oauth2.0.html-BjSMXlJY.js","assets/WSL.html-C3w2j3-t.js","assets/系统安装.html-Dtd-JGht.js","assets/MybatisPlusDataSource.html-CJvWq9xZ.js","assets/index.html-BY3G0JcQ.js","assets/mybatis.html-lxSGPQZl.js","assets/Authorization.html-J2At3l0r.js","assets/CustomAuthenticationProvider.html-BPtSrpyO.js","assets/CustomLoginPage.html-C7rOlHXp.js","assets/CustomTokenAuthentication.html-D2kx6W0O.js","assets/DelegatingFilterProxy.html-DFpCf46S.js","assets/OAuth2Authentication.html-azcJcZ3I.js","assets/OncePerRequestFilter.html-B_WUT3Wi.js","assets/PreAuthorize.html-QEyDJ3Om.js","assets/index.html-CgoRxHB7.js","assets/SSO.html-DntfVSpP.js","assets/SecurityFilterChain.html-CklH0qq_.js","assets/Session.html-C7UWc88l.js","assets/note.html-CidlTBYC.js","assets/spring-security-oauth2-authorization-server.html-D-4kWtlA.js","assets/Annotation.html-C9NZ7kgo.js","assets/BeanPostProcessor.html-Dp0QSMvM.js","assets/CircularDependency.html-B8HCeRou.js","assets/DesignPatternInSpring.html-CoJ7-0Dg.js","assets/OncePerRequestFilter.html-BCQqcD7O.js","assets/index.html-W9fvppVE.js","assets/SpringAOP.html-DLZP8sCz.js","assets/SpringCache.html-BsDFP-Bt.js","assets/SpringExtensionPoint.html-BwGgiHY7.js","assets/SpringIOC.html-Dv9Nuies.js","assets/SpringMVC.html-7ciGTcsU.js","assets/SpringSourceAnalize.html-BPAH3F93.js","assets/Validator.html-CVtkvwdc.js","assets/AOPLog.html-oemJ_ITV.js","assets/CollectionInject.html-FkTZBxrs.js","assets/Http2.html-NGu_jTQ-.js","assets/index.html-0aNaZ5Jx.js","assets/SpringBootAutoConfiguration.html-xbEEkjIP.js","assets/Swagger.html-CSZJriy0.js","assets/读写分离.html-DusZu70v.js","assets/部署.html-BUaaMgxp.js","assets/SpringCloudGateway.html-BsaY0McN.js","assets/index.html-B5xEb-9f.js","assets/wrapper.html-L3djvmiQ.js","assets/index.html-BhhaXmFF.js","assets/TooManyOpenFiles.html-DcMwQWIU.js","assets/Undertow.xxxNotFount.html-DNDQtsiq.js","assets/各种框架版本的坑.html-DwU6AsvJ.js","assets/各种问题.html-iZ7wZxcM.js","assets/java日志.html-4UsWfMdh.js","assets/logback.html-BexKq4m4.js","assets/build标签.html-Dc92dnvN.js","assets/import.html-CH_AORfR.js","assets/multiModule.html-KdXk5jwS.js","assets/problem.html-Xwm_z36q.js","assets/生命周期.html-DY214BI_.js","assets/404.html-Cj93pVFe.js","assets/index.html-DMdGsont.js","assets/index.html-CqZ8aMMm.js","assets/index.html-iCxZIK2p.js","assets/index.html-7a_DvzN8.js","assets/index.html-C7f-DwWo.js","assets/index.html-BfwZ7M0e.js","assets/index.html-LsLgqLJs.js","assets/index.html-Ccr7z95c.js","assets/index.html-C5QEB7Z1.js","assets/index.html-BpjUeNG2.js","assets/index.html-CsyDKx4g.js","assets/index.html-Z0st3i9J.js","assets/index.html-DNBA4gIn.js","assets/index.html-BOpkylY8.js","assets/index.html-DMm5nioB.js","assets/index.html-D-6TwFNB.js","assets/index.html-DLPru-AZ.js","assets/index.html-U3u26-xm.js","assets/index.html-Brnqxn8B.js","assets/index.html-COotBsqy.js","assets/index.html-D3P5gbcr.js","assets/index.html-B-qLo8KO.js","assets/index.html-txI6BFt1.js","assets/index.html-CioaVQa-.js","assets/index.html-DLNPWair.js","assets/index.html-BejOLJ2j.js","assets/index.html-CdNCPQJN.js","assets/index.html-x8UhOCrm.js","assets/index.html-C13ChgHV.js","assets/index.html-DjKeoveR.js","assets/index.html-BtI9HFtG.js","assets/index.html-64ghmh8T.js","assets/index.html-bGB8i1Bp.js","assets/index.html-BkAPtgx9.js","assets/index.html-r3r6Xe2X.js","assets/index.html-uOFyO4KX.js","assets/index.html-DOxnJP9F.js","assets/index.html-Bbe3YI-F.js","assets/index.html-CDqEgXQG.js","assets/index.html-CGegH2pR.js","assets/index.html-CW4w0zEj.js","assets/index.html-BT9FWv85.js","assets/index.html-bc7DDl1w.js","assets/index.html-BEh3yan4.js","assets/index.html-ClqrgpgA.js","assets/index.html-B6jGm4h5.js","assets/index.html-C0K4uyQT.js","assets/index.html-D0IWaeCP.js","assets/index.html-NRQgmwqP.js","assets/index.html-Cfr1VERK.js","assets/index.html-BEpChitr.js","assets/index.html-NsWiQWYZ.js","assets/index.html-BJDhEdQX.js","assets/index.html-xFKarS8b.js","assets/index.html-BgC1Uqo7.js","assets/index.html-BE3BiSH6.js","assets/index.html-DyUgClQe.js","assets/index.html-Bbyd6M1o.js","assets/index.html-a7FPOuSg.js","assets/index.html-IQR8utYP.js","assets/index.html-DHKrdhfw.js","assets/index.html-Sng5UTgU.js","assets/index.html-BOpWn6ez.js","assets/index.html-C-sunVIK.js","assets/index.html-C3d9thsv.js","assets/index.html-DkgwX5zY.js","assets/index.html-CvRj_9Cp.js","assets/index.html-Drxe7RQ8.js","assets/index.html-CxyHaGFC.js","assets/index.html-DCViU9NH.js","assets/index.html-ZrDWmiHj.js","assets/index.html-C3HSxeKn.js","assets/index.html--P4uqaDK.js","assets/index.html-C63jvqUL.js","assets/index.html-C_1bHi9D.js","assets/index.html-DDkBFrt6.js","assets/index.html-DcSDms4b.js","assets/index.html-C0IieY1i.js","assets/index.html-CTBcx_KY.js"])))=>i.map(i=>d[i]);
                       var Zu=Object.defineProperty;var i2=(i,s,e)=>s in i?Zu(i,s,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[s]=e;var xn=(i,s,e)=>i2(i,typeof s!="symbol"?s+"":s,e);const s2="modulepreload",e2=function(i){return"/"+i},ah={},g=function(s,e,t){let a=Promise.resolve();if(e&&e.length>0){document.getElementsByTagName("link");const l=document.querySelector("meta[property=csp-nonce]"),r=(l==null?void 0:l.nonce)||(l==null?void 0:l.getAttribute("nonce"));a=Promise.allSettled(e.map(h=>{if(h=e2(h),h in ah)return;ah[h]=!0;const d=h.endsWith(".css"),o=d?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${h}"]${o}`))return;const k=document.createElement("link");if(k.rel=d?"stylesheet":s2,d||(k.as="script"),k.crossOrigin="",k.href=h,r&&k.setAttribute("nonce",r),document.head.appendChild(k),d)return new Promise((c,u)=>{k.addEventListener("load",c),k.addEventListener("error",()=>u(new Error(`Unable to preload CSS for ${h}`)))})}))}function n(l){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=l,window.dispatchEvent(r),!r.defaultPrevented)throw l}return a.then(l=>{for(const r of l||[])r.status==="rejected"&&n(r.reason);return s().catch(n)})};/**
                       * @vue/shared v3.5.13
                       * (c) 2018-present Yuxi (Evan) You and Vue contributors
                      @@ -31,7 +31,7 @@ Server rendered element contains fewer child nodes than client vdom.`),P=!0),Fe(
                       * @vue/runtime-dom v3.5.13
                       * (c) 2018-present Yuxi (Evan) You and Vue contributors
                       * @license MIT
                      -**/let Al;const Dh=typeof window<"u"&&window.trustedTypes;if(Dh)try{Al=Dh.createPolicy("vue",{createHTML:i=>i})}catch{}const Zo=Al?i=>Al.createHTML(i):i=>i,_0="http://www.w3.org/2000/svg",C0="http://www.w3.org/1998/Math/MathML",Qs=typeof document<"u"?document:null,xh=Qs&&Qs.createElement("template"),w0={insert:(i,s,e)=>{s.insertBefore(i,e||null)},remove:i=>{const s=i.parentNode;s&&s.removeChild(i)},createElement:(i,s,e,t)=>{const a=s==="svg"?Qs.createElementNS(_0,i):s==="mathml"?Qs.createElementNS(C0,i):e?Qs.createElement(i,{is:e}):Qs.createElement(i);return i==="select"&&t&&t.multiple!=null&&a.setAttribute("multiple",t.multiple),a},createText:i=>Qs.createTextNode(i),createComment:i=>Qs.createComment(i),setText:(i,s)=>{i.nodeValue=s},setElementText:(i,s)=>{i.textContent=s},parentNode:i=>i.parentNode,nextSibling:i=>i.nextSibling,querySelector:i=>Qs.querySelector(i),setScopeId(i,s){i.setAttribute(s,"")},insertStaticContent(i,s,e,t,a,n){const l=e?e.previousSibling:s.lastChild;if(a&&(a===n||a.nextSibling))for(;s.insertBefore(a.cloneNode(!0),e),!(a===n||!(a=a.nextSibling)););else{xh.innerHTML=Zo(t==="svg"?`${i}`:t==="mathml"?`${i}`:i);const r=xh.content;if(t==="svg"||t==="mathml"){const h=r.firstChild;for(;h.firstChild;)r.appendChild(h.firstChild);r.removeChild(h)}s.insertBefore(r,e)}return[l?l.nextSibling:s.firstChild,e?e.previousSibling:s.lastChild]}},le="transition",wt="animation",lt=Symbol("_vtc"),ik={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},sk=Mi({},mo,ik),D0=i=>(i.displayName="Transition",i.props=sk,i),rt=D0((i,{slots:s})=>p(yg,ek(i),s)),Ce=(i,s=[])=>{ai(i)?i.forEach(e=>e(...s)):i&&i(...s)},Sh=i=>i?ai(i)?i.some(s=>s.length>1):i.length>1:!1;function ek(i){const s={};for(const P in i)P in ik||(s[P]=i[P]);if(i.css===!1)return s;const{name:e="v",type:t,duration:a,enterFromClass:n=`${e}-enter-from`,enterActiveClass:l=`${e}-enter-active`,enterToClass:r=`${e}-enter-to`,appearFromClass:h=n,appearActiveClass:d=l,appearToClass:o=r,leaveFromClass:k=`${e}-leave-from`,leaveActiveClass:c=`${e}-leave-active`,leaveToClass:u=`${e}-leave-to`}=i,v=x0(a),m=v&&v[0],B=v&&v[1],{onBeforeEnter:f,onEnter:b,onEnterCancelled:A,onLeave:_,onLeaveCancelled:L,onBeforeAppear:M=f,onAppear:x=b,onAppearCancelled:K=A}=s,j=(P,ii,ri,ki)=>{P._enterCancelled=ki,de(P,ii?o:r),de(P,ii?d:l),ri&&ri()},T=(P,ii)=>{P._isLeaving=!1,de(P,k),de(P,u),de(P,c),ii&&ii()},H=P=>(ii,ri)=>{const ki=P?x:b,W=()=>j(ii,P,ri);Ce(ki,[ii,W]),Th(()=>{de(ii,P?h:n),Ns(ii,P?o:r),Sh(ki)||Oh(ii,t,m,W)})};return Mi(s,{onBeforeEnter(P){Ce(f,[P]),Ns(P,n),Ns(P,l)},onBeforeAppear(P){Ce(M,[P]),Ns(P,h),Ns(P,d)},onEnter:H(!1),onAppear:H(!0),onLeave(P,ii){P._isLeaving=!0;const ri=()=>T(P,ii);Ns(P,k),P._enterCancelled?(Ns(P,c),yl()):(yl(),Ns(P,c)),Th(()=>{P._isLeaving&&(de(P,k),Ns(P,u),Sh(_)||Oh(P,t,B,ri))}),Ce(_,[P,ri])},onEnterCancelled(P){j(P,!1,void 0,!0),Ce(A,[P])},onAppearCancelled(P){j(P,!0,void 0,!0),Ce(K,[P])},onLeaveCancelled(P){T(P),Ce(L,[P])}})}function x0(i){if(i==null)return null;if(Li(i))return[$n(i.enter),$n(i.leave)];{const s=$n(i);return[s,s]}}function $n(i){return k2(i)}function Ns(i,s){s.split(/\s+/).forEach(e=>e&&i.classList.add(e)),(i[lt]||(i[lt]=new Set)).add(s)}function de(i,s){s.split(/\s+/).forEach(t=>t&&i.classList.remove(t));const e=i[lt];e&&(e.delete(s),e.size||(i[lt]=void 0))}function Th(i){requestAnimationFrame(()=>{requestAnimationFrame(i)})}let S0=0;function Oh(i,s,e,t){const a=i._endId=++S0,n=()=>{a===i._endId&&t()};if(e!=null)return setTimeout(n,e);const{type:l,timeout:r,propCount:h}=tk(i,s);if(!l)return t();const d=l+"end";let o=0;const k=()=>{i.removeEventListener(d,c),n()},c=u=>{u.target===i&&++o>=h&&k()};setTimeout(()=>{o(e[v]||"").split(", "),a=t(`${le}Delay`),n=t(`${le}Duration`),l=Lh(a,n),r=t(`${wt}Delay`),h=t(`${wt}Duration`),d=Lh(r,h);let o=null,k=0,c=0;s===le?l>0&&(o=le,k=l,c=n.length):s===wt?d>0&&(o=wt,k=d,c=h.length):(k=Math.max(l,d),o=k>0?l>d?le:wt:null,c=o?o===le?n.length:h.length:0);const u=o===le&&/\b(transform|all)(,|$)/.test(t(`${le}Property`).toString());return{type:o,timeout:k,propCount:c,hasTransform:u}}function Lh(i,s){for(;i.lengthIh(e)+Ih(i[t])))}function Ih(i){return i==="auto"?0:Number(i.slice(0,-1).replace(",","."))*1e3}function yl(){return document.body.offsetHeight}function T0(i,s,e){const t=i[lt];t&&(s=(s?[s,...t]:[...t]).join(" ")),s==null?i.removeAttribute("class"):e?i.setAttribute("class",s):i.className=s}const Ph=Symbol("_vod"),O0=Symbol("_vsh"),L0=Symbol(""),I0=/(^|;)\s*display\s*:/;function P0(i,s,e){const t=i.style,a=Oi(e);let n=!1;if(e&&!a){if(s)if(Oi(s))for(const l of s.split(";")){const r=l.slice(0,l.indexOf(":")).trim();e[r]==null&&Ua(t,r,"")}else for(const l in s)e[l]==null&&Ua(t,l,"");for(const l in e)l==="display"&&(n=!0),Ua(t,l,e[l])}else if(a){if(s!==e){const l=t[L0];l&&(e+=";"+l),t.cssText=e,n=I0.test(e)}}else s&&i.removeAttribute("style");Ph in i&&(i[Ph]=n?t.display:"",i[O0]&&(t.display="none"))}const Rh=/\s*!important$/;function Ua(i,s,e){if(ai(e))e.forEach(t=>Ua(i,s,t));else if(e==null&&(e=""),s.startsWith("--"))i.setProperty(s,e);else{const t=R0(i,s);Rh.test(e)?i.setProperty(Ee(t),e.replace(Rh,""),"important"):i[t]=e}}const jh=["Webkit","Moz","ms"],qn={};function R0(i,s){const e=qn[s];if(e)return e;let t=ps(s);if(t!=="filter"&&t in i)return qn[s]=t;t=da(t);for(let a=0;aHn||($0.then(()=>Hn=0),Hn=Date.now());function H0(i,s){const e=t=>{if(!t._vts)t._vts=Date.now();else if(t._vts<=e.attached)return;Ss(U0(t,e.value),s,5,[t])};return e.value=i,e.attached=q0(),e}function U0(i,s){if(ai(s)){const e=i.stopImmediatePropagation;return i.stopImmediatePropagation=()=>{e.call(i),i._stopped=!0},s.map(t=>a=>!a._stopped&&t&&t(a))}else return s}const Hh=i=>i.charCodeAt(0)===111&&i.charCodeAt(1)===110&&i.charCodeAt(2)>96&&i.charCodeAt(2)<123,z0=(i,s,e,t,a,n)=>{const l=a==="svg";s==="class"?T0(i,t,l):s==="style"?P0(i,e,t):pa(s)?Jl(s)||V0(i,s,e,t,n):(s[0]==="."?(s=s.slice(1),!0):s[0]==="^"?(s=s.slice(1),!1):W0(i,s,t,l))?(Nh(i,s,t),!i.tagName.includes("-")&&(s==="value"||s==="checked"||s==="selected")&&Vh(i,s,t,l,n,s!=="value")):i._isVueCE&&(/[A-Z]/.test(s)||!Oi(t))?Nh(i,ps(s),t,n,s):(s==="true-value"?i._trueValue=t:s==="false-value"&&(i._falseValue=t),Vh(i,s,t,l))};function W0(i,s,e,t){if(t)return!!(s==="innerHTML"||s==="textContent"||s in i&&Hh(s)&&ni(e));if(s==="spellcheck"||s==="draggable"||s==="translate"||s==="form"||s==="list"&&i.tagName==="INPUT"||s==="type"&&i.tagName==="TEXTAREA")return!1;if(s==="width"||s==="height"){const a=i.tagName;if(a==="IMG"||a==="VIDEO"||a==="CANVAS"||a==="SOURCE")return!1}return Hh(s)&&Oi(e)?!1:s in i}const ak=new WeakMap,nk=new WeakMap,tn=Symbol("_moveCb"),Uh=Symbol("_enterCb"),G0=i=>(delete i.props.mode,i),K0=G0({name:"TransitionGroup",props:Mi({},sk,{tag:String,moveClass:String}),setup(i,{slots:s}){const e=mt(),t=yo();let a,n;return Co(()=>{if(!a.length)return;const l=i.moveClass||`${i.name||"v"}-move`;if(!Q0(a[0].el,e.vnode.el,l))return;a.forEach(J0),a.forEach(Y0);const r=a.filter(X0);yl(),r.forEach(h=>{const d=h.el,o=d.style;Ns(d,l),o.transform=o.webkitTransform=o.transitionDuration="";const k=d[tn]=c=>{c&&c.target!==d||(!c||/transform$/.test(c.propertyName))&&(d.removeEventListener("transitionend",k),d[tn]=null,de(d,l))};d.addEventListener("transitionend",k)})}),()=>{const l=pi(i),r=ek(l);let h=l.tag||ts;if(a=[],n)for(let d=0;d{r.split(/\s+/).forEach(h=>h&&t.classList.remove(h))}),e.split(/\s+/).forEach(r=>r&&t.classList.add(r)),t.style.display="none";const n=s.nodeType===1?s:s.parentNode;n.appendChild(t);const{hasTransform:l}=tk(t);return n.removeChild(t),l}const Z0=Mi({patchProp:z0},w0);let Un,zh=!1;function i3(){return Un=zh?Un:Zg(Z0),zh=!0,Un}const s3=(...i)=>{const s=i3().createApp(...i),{mount:e}=s;return s.mount=t=>{const a=t3(t);if(a)return e(a,!0,e3(a))},s};function e3(i){if(i instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&i instanceof MathMLElement)return"mathml"}function t3(i){return Oi(i)?document.querySelector(i):i}var ga=i=>/^[a-z][a-z0-9+.-]*:/.test(i)||i.startsWith("//"),a3=/.md((\?|#).*)?$/,vr=(i,s="/")=>ga(i)||i.startsWith("/")&&!i.startsWith(s)&&!a3.test(i),Ve=i=>/^(https?:)?\/\//.test(i),Wh=i=>{if(!i||i.endsWith("/"))return i;let s=i.replace(/(^|\/)README.md$/i,"$1index.html");return s.endsWith(".md")?s=`${s.substring(0,s.length-3)}.html`:s.endsWith(".html")||(s=`${s}.html`),s.endsWith("/index.html")&&(s=s.substring(0,s.length-10)),s},n3="http://.",l3=(i,s)=>{if(!i.startsWith("/")&&s){const e=s.slice(0,s.lastIndexOf("/"));return Wh(new URL(`${e}/${i}`,n3).pathname)}return Wh(i)},r3=(i,s)=>{const e=Object.keys(i).sort((t,a)=>{const n=a.split("/").length-t.split("/").length;return n!==0?n:a.length-t.length});for(const t of e)if(s.startsWith(t))return t;return"/"},h3=/(#|\?)/,rk=i=>{const[s,...e]=i.split(h3);return{pathname:s,hashAndQueries:e.join("")}},p3=["link","meta","script","style","noscript","template"],d3=["title","base"],o3=([i,s,e])=>d3.includes(i)?i:p3.includes(i)?i==="meta"&&s.name?`${i}.${s.name}`:i==="template"&&s.id?`${i}.${s.id}`:JSON.stringify([i,Object.entries(s).map(([t,a])=>typeof a=="boolean"?a?[t,""]:null:[t,a]).filter(t=>t!=null).sort(([t],[a])=>t.localeCompare(a)),e]):null,k3=i=>{const s=new Set,e=[];return i.forEach(t=>{const a=o3(t);a&&!s.has(a)&&(s.add(a),e.push(t))}),e},c3=i=>i.startsWith("/")?i:`/${i}`,hk=i=>i.endsWith("/")||i.endsWith(".html")?i:`${i}/`,Ar=i=>i.endsWith("/")?i.slice(0,-1):i,pk=i=>i.startsWith("/")?i.slice(1):i,Bt=i=>Object.prototype.toString.call(i)==="[object Object]",Ti=i=>typeof i=="string";const u3=JSON.parse("{}"),g3=Object.fromEntries([["/",{loader:()=>g(()=>import("./index.html-KwmoBcRm.js"),__vite__mapDeps([0,1])),meta:{t:"ChenSino",i:"home"}}],["/home.html",{loader:()=>g(()=>import("./home.html-aGGYOR7m.js"),__vite__mapDeps([2,1])),meta:{t:"Home",i:"house"}}],["/slide.html",{loader:()=>g(()=>import("./slide.html-D-H9bRlk.js"),__vite__mapDeps([3,1])),meta:{d:1659087823e3,e:`
                      +**/let Al;const Dh=typeof window<"u"&&window.trustedTypes;if(Dh)try{Al=Dh.createPolicy("vue",{createHTML:i=>i})}catch{}const Zo=Al?i=>Al.createHTML(i):i=>i,_0="http://www.w3.org/2000/svg",C0="http://www.w3.org/1998/Math/MathML",Qs=typeof document<"u"?document:null,xh=Qs&&Qs.createElement("template"),w0={insert:(i,s,e)=>{s.insertBefore(i,e||null)},remove:i=>{const s=i.parentNode;s&&s.removeChild(i)},createElement:(i,s,e,t)=>{const a=s==="svg"?Qs.createElementNS(_0,i):s==="mathml"?Qs.createElementNS(C0,i):e?Qs.createElement(i,{is:e}):Qs.createElement(i);return i==="select"&&t&&t.multiple!=null&&a.setAttribute("multiple",t.multiple),a},createText:i=>Qs.createTextNode(i),createComment:i=>Qs.createComment(i),setText:(i,s)=>{i.nodeValue=s},setElementText:(i,s)=>{i.textContent=s},parentNode:i=>i.parentNode,nextSibling:i=>i.nextSibling,querySelector:i=>Qs.querySelector(i),setScopeId(i,s){i.setAttribute(s,"")},insertStaticContent(i,s,e,t,a,n){const l=e?e.previousSibling:s.lastChild;if(a&&(a===n||a.nextSibling))for(;s.insertBefore(a.cloneNode(!0),e),!(a===n||!(a=a.nextSibling)););else{xh.innerHTML=Zo(t==="svg"?`${i}`:t==="mathml"?`${i}`:i);const r=xh.content;if(t==="svg"||t==="mathml"){const h=r.firstChild;for(;h.firstChild;)r.appendChild(h.firstChild);r.removeChild(h)}s.insertBefore(r,e)}return[l?l.nextSibling:s.firstChild,e?e.previousSibling:s.lastChild]}},le="transition",wt="animation",lt=Symbol("_vtc"),ik={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},sk=Mi({},mo,ik),D0=i=>(i.displayName="Transition",i.props=sk,i),rt=D0((i,{slots:s})=>p(yg,ek(i),s)),Ce=(i,s=[])=>{ai(i)?i.forEach(e=>e(...s)):i&&i(...s)},Sh=i=>i?ai(i)?i.some(s=>s.length>1):i.length>1:!1;function ek(i){const s={};for(const P in i)P in ik||(s[P]=i[P]);if(i.css===!1)return s;const{name:e="v",type:t,duration:a,enterFromClass:n=`${e}-enter-from`,enterActiveClass:l=`${e}-enter-active`,enterToClass:r=`${e}-enter-to`,appearFromClass:h=n,appearActiveClass:d=l,appearToClass:o=r,leaveFromClass:k=`${e}-leave-from`,leaveActiveClass:c=`${e}-leave-active`,leaveToClass:u=`${e}-leave-to`}=i,v=x0(a),m=v&&v[0],B=v&&v[1],{onBeforeEnter:f,onEnter:b,onEnterCancelled:A,onLeave:_,onLeaveCancelled:L,onBeforeAppear:M=f,onAppear:x=b,onAppearCancelled:K=A}=s,j=(P,ii,ri,ki)=>{P._enterCancelled=ki,de(P,ii?o:r),de(P,ii?d:l),ri&&ri()},T=(P,ii)=>{P._isLeaving=!1,de(P,k),de(P,u),de(P,c),ii&&ii()},H=P=>(ii,ri)=>{const ki=P?x:b,W=()=>j(ii,P,ri);Ce(ki,[ii,W]),Th(()=>{de(ii,P?h:n),Ns(ii,P?o:r),Sh(ki)||Oh(ii,t,m,W)})};return Mi(s,{onBeforeEnter(P){Ce(f,[P]),Ns(P,n),Ns(P,l)},onBeforeAppear(P){Ce(M,[P]),Ns(P,h),Ns(P,d)},onEnter:H(!1),onAppear:H(!0),onLeave(P,ii){P._isLeaving=!0;const ri=()=>T(P,ii);Ns(P,k),P._enterCancelled?(Ns(P,c),yl()):(yl(),Ns(P,c)),Th(()=>{P._isLeaving&&(de(P,k),Ns(P,u),Sh(_)||Oh(P,t,B,ri))}),Ce(_,[P,ri])},onEnterCancelled(P){j(P,!1,void 0,!0),Ce(A,[P])},onAppearCancelled(P){j(P,!0,void 0,!0),Ce(K,[P])},onLeaveCancelled(P){T(P),Ce(L,[P])}})}function x0(i){if(i==null)return null;if(Li(i))return[$n(i.enter),$n(i.leave)];{const s=$n(i);return[s,s]}}function $n(i){return k2(i)}function Ns(i,s){s.split(/\s+/).forEach(e=>e&&i.classList.add(e)),(i[lt]||(i[lt]=new Set)).add(s)}function de(i,s){s.split(/\s+/).forEach(t=>t&&i.classList.remove(t));const e=i[lt];e&&(e.delete(s),e.size||(i[lt]=void 0))}function Th(i){requestAnimationFrame(()=>{requestAnimationFrame(i)})}let S0=0;function Oh(i,s,e,t){const a=i._endId=++S0,n=()=>{a===i._endId&&t()};if(e!=null)return setTimeout(n,e);const{type:l,timeout:r,propCount:h}=tk(i,s);if(!l)return t();const d=l+"end";let o=0;const k=()=>{i.removeEventListener(d,c),n()},c=u=>{u.target===i&&++o>=h&&k()};setTimeout(()=>{o(e[v]||"").split(", "),a=t(`${le}Delay`),n=t(`${le}Duration`),l=Lh(a,n),r=t(`${wt}Delay`),h=t(`${wt}Duration`),d=Lh(r,h);let o=null,k=0,c=0;s===le?l>0&&(o=le,k=l,c=n.length):s===wt?d>0&&(o=wt,k=d,c=h.length):(k=Math.max(l,d),o=k>0?l>d?le:wt:null,c=o?o===le?n.length:h.length:0);const u=o===le&&/\b(transform|all)(,|$)/.test(t(`${le}Property`).toString());return{type:o,timeout:k,propCount:c,hasTransform:u}}function Lh(i,s){for(;i.lengthIh(e)+Ih(i[t])))}function Ih(i){return i==="auto"?0:Number(i.slice(0,-1).replace(",","."))*1e3}function yl(){return document.body.offsetHeight}function T0(i,s,e){const t=i[lt];t&&(s=(s?[s,...t]:[...t]).join(" ")),s==null?i.removeAttribute("class"):e?i.setAttribute("class",s):i.className=s}const Ph=Symbol("_vod"),O0=Symbol("_vsh"),L0=Symbol(""),I0=/(^|;)\s*display\s*:/;function P0(i,s,e){const t=i.style,a=Oi(e);let n=!1;if(e&&!a){if(s)if(Oi(s))for(const l of s.split(";")){const r=l.slice(0,l.indexOf(":")).trim();e[r]==null&&Ua(t,r,"")}else for(const l in s)e[l]==null&&Ua(t,l,"");for(const l in e)l==="display"&&(n=!0),Ua(t,l,e[l])}else if(a){if(s!==e){const l=t[L0];l&&(e+=";"+l),t.cssText=e,n=I0.test(e)}}else s&&i.removeAttribute("style");Ph in i&&(i[Ph]=n?t.display:"",i[O0]&&(t.display="none"))}const Rh=/\s*!important$/;function Ua(i,s,e){if(ai(e))e.forEach(t=>Ua(i,s,t));else if(e==null&&(e=""),s.startsWith("--"))i.setProperty(s,e);else{const t=R0(i,s);Rh.test(e)?i.setProperty(Ee(t),e.replace(Rh,""),"important"):i[t]=e}}const jh=["Webkit","Moz","ms"],qn={};function R0(i,s){const e=qn[s];if(e)return e;let t=ps(s);if(t!=="filter"&&t in i)return qn[s]=t;t=da(t);for(let a=0;aHn||($0.then(()=>Hn=0),Hn=Date.now());function H0(i,s){const e=t=>{if(!t._vts)t._vts=Date.now();else if(t._vts<=e.attached)return;Ss(U0(t,e.value),s,5,[t])};return e.value=i,e.attached=q0(),e}function U0(i,s){if(ai(s)){const e=i.stopImmediatePropagation;return i.stopImmediatePropagation=()=>{e.call(i),i._stopped=!0},s.map(t=>a=>!a._stopped&&t&&t(a))}else return s}const Hh=i=>i.charCodeAt(0)===111&&i.charCodeAt(1)===110&&i.charCodeAt(2)>96&&i.charCodeAt(2)<123,z0=(i,s,e,t,a,n)=>{const l=a==="svg";s==="class"?T0(i,t,l):s==="style"?P0(i,e,t):pa(s)?Jl(s)||V0(i,s,e,t,n):(s[0]==="."?(s=s.slice(1),!0):s[0]==="^"?(s=s.slice(1),!1):W0(i,s,t,l))?(Nh(i,s,t),!i.tagName.includes("-")&&(s==="value"||s==="checked"||s==="selected")&&Vh(i,s,t,l,n,s!=="value")):i._isVueCE&&(/[A-Z]/.test(s)||!Oi(t))?Nh(i,ps(s),t,n,s):(s==="true-value"?i._trueValue=t:s==="false-value"&&(i._falseValue=t),Vh(i,s,t,l))};function W0(i,s,e,t){if(t)return!!(s==="innerHTML"||s==="textContent"||s in i&&Hh(s)&&ni(e));if(s==="spellcheck"||s==="draggable"||s==="translate"||s==="form"||s==="list"&&i.tagName==="INPUT"||s==="type"&&i.tagName==="TEXTAREA")return!1;if(s==="width"||s==="height"){const a=i.tagName;if(a==="IMG"||a==="VIDEO"||a==="CANVAS"||a==="SOURCE")return!1}return Hh(s)&&Oi(e)?!1:s in i}const ak=new WeakMap,nk=new WeakMap,tn=Symbol("_moveCb"),Uh=Symbol("_enterCb"),G0=i=>(delete i.props.mode,i),K0=G0({name:"TransitionGroup",props:Mi({},sk,{tag:String,moveClass:String}),setup(i,{slots:s}){const e=mt(),t=yo();let a,n;return Co(()=>{if(!a.length)return;const l=i.moveClass||`${i.name||"v"}-move`;if(!Q0(a[0].el,e.vnode.el,l))return;a.forEach(J0),a.forEach(Y0);const r=a.filter(X0);yl(),r.forEach(h=>{const d=h.el,o=d.style;Ns(d,l),o.transform=o.webkitTransform=o.transitionDuration="";const k=d[tn]=c=>{c&&c.target!==d||(!c||/transform$/.test(c.propertyName))&&(d.removeEventListener("transitionend",k),d[tn]=null,de(d,l))};d.addEventListener("transitionend",k)})}),()=>{const l=pi(i),r=ek(l);let h=l.tag||ts;if(a=[],n)for(let d=0;d{r.split(/\s+/).forEach(h=>h&&t.classList.remove(h))}),e.split(/\s+/).forEach(r=>r&&t.classList.add(r)),t.style.display="none";const n=s.nodeType===1?s:s.parentNode;n.appendChild(t);const{hasTransform:l}=tk(t);return n.removeChild(t),l}const Z0=Mi({patchProp:z0},w0);let Un,zh=!1;function i3(){return Un=zh?Un:Zg(Z0),zh=!0,Un}const s3=(...i)=>{const s=i3().createApp(...i),{mount:e}=s;return s.mount=t=>{const a=t3(t);if(a)return e(a,!0,e3(a))},s};function e3(i){if(i instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&i instanceof MathMLElement)return"mathml"}function t3(i){return Oi(i)?document.querySelector(i):i}var ga=i=>/^[a-z][a-z0-9+.-]*:/.test(i)||i.startsWith("//"),a3=/.md((\?|#).*)?$/,vr=(i,s="/")=>ga(i)||i.startsWith("/")&&!i.startsWith(s)&&!a3.test(i),Ve=i=>/^(https?:)?\/\//.test(i),Wh=i=>{if(!i||i.endsWith("/"))return i;let s=i.replace(/(^|\/)README.md$/i,"$1index.html");return s.endsWith(".md")?s=`${s.substring(0,s.length-3)}.html`:s.endsWith(".html")||(s=`${s}.html`),s.endsWith("/index.html")&&(s=s.substring(0,s.length-10)),s},n3="http://.",l3=(i,s)=>{if(!i.startsWith("/")&&s){const e=s.slice(0,s.lastIndexOf("/"));return Wh(new URL(`${e}/${i}`,n3).pathname)}return Wh(i)},r3=(i,s)=>{const e=Object.keys(i).sort((t,a)=>{const n=a.split("/").length-t.split("/").length;return n!==0?n:a.length-t.length});for(const t of e)if(s.startsWith(t))return t;return"/"},h3=/(#|\?)/,rk=i=>{const[s,...e]=i.split(h3);return{pathname:s,hashAndQueries:e.join("")}},p3=["link","meta","script","style","noscript","template"],d3=["title","base"],o3=([i,s,e])=>d3.includes(i)?i:p3.includes(i)?i==="meta"&&s.name?`${i}.${s.name}`:i==="template"&&s.id?`${i}.${s.id}`:JSON.stringify([i,Object.entries(s).map(([t,a])=>typeof a=="boolean"?a?[t,""]:null:[t,a]).filter(t=>t!=null).sort(([t],[a])=>t.localeCompare(a)),e]):null,k3=i=>{const s=new Set,e=[];return i.forEach(t=>{const a=o3(t);a&&!s.has(a)&&(s.add(a),e.push(t))}),e},c3=i=>i.startsWith("/")?i:`/${i}`,hk=i=>i.endsWith("/")||i.endsWith(".html")?i:`${i}/`,Ar=i=>i.endsWith("/")?i.slice(0,-1):i,pk=i=>i.startsWith("/")?i.slice(1):i,Bt=i=>Object.prototype.toString.call(i)==="[object Object]",Ti=i=>typeof i=="string";const u3=JSON.parse("{}"),g3=Object.fromEntries([["/",{loader:()=>g(()=>import("./index.html-BIyQQ4YT.js"),__vite__mapDeps([0,1])),meta:{t:"ChenSino",i:"home"}}],["/home.html",{loader:()=>g(()=>import("./home.html-NeR1GMte.js"),__vite__mapDeps([2,1])),meta:{t:"Home",i:"house"}}],["/slide.html",{loader:()=>g(()=>import("./slide.html-Y7bXu_We.js"),__vite__mapDeps([3,1])),meta:{d:1659087823e3,e:`
                       

                      @slidestart

                      幻灯片演示

                      @@ -43,9 +43,9 @@ Server rendered element contains fewer child nodes than client vdom.`),P=!0),Fe(

                      标注幻灯片

                      -`,r:{minutes:4.5,words:1349},t:"幻灯片页",i:"slides",y:"a"}}],["/designpattern/",{loader:()=>g(()=>import("./index.html-BRFnlh32.js"),__vite__mapDeps([4,1])),meta:{d:16523136e5,l:"2022年5月12日",o:!0,e:`

                      说明

                      +`,r:{minutes:4.5,words:1349},t:"幻灯片页",i:"slides",y:"a"}}],["/designpattern/",{loader:()=>g(()=>import("./index.html-pcaAAK7Q.js"),__vite__mapDeps([4,1])),meta:{d:16523136e5,l:"2022年5月12日",o:!0,e:`

                      说明

                      java设计模式代码请参考开源项目java-design-patterns,本博客只是基于自己的理解做简单介绍,让设计模式不再那么难理解,另外看到开源框架中的设计模式我也会慢慢摘取出来,主要是学习spring家族是如何使用设计模式的。

                      -`,r:{minutes:.34,words:101},t:"README",y:"a"}}],["/designpattern/%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F.html",{loader:()=>g(()=>import("./建造者模式.html-BFH9gyJJ.js"),__vite__mapDeps([5,1])),meta:{d:16523136e5,l:"2022年5月12日",c:["设计模式"],o:!0,e:`

                      建造者模式

                      +`,r:{minutes:.34,words:101},t:"README",y:"a"}}],["/designpattern/%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F.html",{loader:()=>g(()=>import("./建造者模式.html-BewjSrrw.js"),__vite__mapDeps([5,1])),meta:{d:16523136e5,l:"2022年5月12日",c:["设计模式"],o:!0,e:`

                      建造者模式

                      这种模式太常见了,开源框架中的各种builder都是

                      mybatis中

                      org.apache.ibatis.mapping.Environment

                      @@ -112,8 +112,8 @@ Server rendered element contains fewer child nodes than client vdom.`),P=!0),Fe( } }
                      -
                      `,r:{minutes:.61,words:183},t:"Builder Pattern",y:"a"}}],["/designpattern/%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F.html",{loader:()=>g(()=>import("./模板方法模式.html-BHkIE83-.js"),__vite__mapDeps([6,1])),meta:{d:16538688e5,l:"2022年5月30日",c:["设计模式"],o:!0,e:`

                      定义

                      -

                      模板方法模式(Template Method Pattern)是一种行为型设计模式,定义一个操作中的算法骨架,而将一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。这种设计模式是一种控制反转的实现方式。因为高层代码不再确定(控制)算法的处理流程。模板方法模式多用在某些类别的算法中,实现了相同的方法,造成代码的重复。这个设计模式和策略模式很像,不同的是,模板方法会有一些通用的逻辑,而策略模式是整个方法重写。从类的继承结构也可以看出来,模板方法是提供一个抽象类,有一个通用的方法,不通用的逻辑放到子类去实现,而策略模式是子类直接继承自接口,要重写整个方法。

                      `,r:{minutes:1.87,words:562},t:"Template Method",y:"a"}}],["/designpattern/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.html",{loader:()=>g(()=>import("./策略模式.html-6Sf1ytqy.js"),__vite__mapDeps([7,1])),meta:{d:16523136e5,l:"2022年5月12日",c:["设计模式"],o:!0,e:`

                      策略模式【CHATGPT回答】

                      +
                      `,r:{minutes:.61,words:183},t:"Builder Pattern",y:"a"}}],["/designpattern/%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F.html",{loader:()=>g(()=>import("./模板方法模式.html-B7vHULWV.js"),__vite__mapDeps([6,1])),meta:{d:16538688e5,l:"2022年5月30日",c:["设计模式"],o:!0,e:`

                      定义

                      +

                      模板方法模式(Template Method Pattern)是一种行为型设计模式,定义一个操作中的算法骨架,而将一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。这种设计模式是一种控制反转的实现方式。因为高层代码不再确定(控制)算法的处理流程。模板方法模式多用在某些类别的算法中,实现了相同的方法,造成代码的重复。这个设计模式和策略模式很像,不同的是,模板方法会有一些通用的逻辑,而策略模式是整个方法重写。从类的继承结构也可以看出来,模板方法是提供一个抽象类,有一个通用的方法,不通用的逻辑放到子类去实现,而策略模式是子类直接继承自接口,要重写整个方法。

                      `,r:{minutes:1.87,words:562},t:"Template Method",y:"a"}}],["/designpattern/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.html",{loader:()=>g(()=>import("./策略模式.html-BMf3v7YI.js"),__vite__mapDeps([7,1])),meta:{d:16523136e5,l:"2022年5月12日",c:["设计模式"],o:!0,e:`

                      策略模式【CHATGPT回答】

                      策略模式(Strategy Pattern)是一种行为型设计模式,它将一组可相互替换的算法封装成独立的对象,并对外暴露相同的接口,从而使得它们可以根据需要动态地替换,以实现不同的行为。
                       
                       策略模式由三个角色组成,分别是:
                      @@ -127,11 +127,11 @@ Server rendered element contains fewer child nodes than client vdom.`),P=!0),Fe(
                       在策略模式中,不同的算法被封装在具体的策略中,这些策略可以根据上下文的需要进行灵活替换,从而实现不同的功能。因此,策略模式具有高度的可扩展性和灵活性,并且可以有效地减少代码冗余。
                       
                       例如,在订单系统中,可以使用策略模式来处理收费方式的问题。具体而言,可以定义一个ChargeStrategy接口,用于实现不同的收费算法。然后,定义多个具体的收费算法,分别实现ChargeStrategy接口。最后,在订单系统中,可以根据订单的不同情况,动态地设置不同的收费算法,以实现灵活的收费功能。
                      -
                      `,r:{minutes:2.41,words:722},t:"Strategy Pattern",y:"a"}}],["/designpattern/%E8%A3%85%E9%A5%B0%E6%A8%A1%E5%BC%8F.html",{loader:()=>g(()=>import("./装饰模式.html-CiXRBiu9.js"),__vite__mapDeps([8,1])),meta:{d:16543872e5,l:"2022年6月5日",c:["设计模式"],o:!0,e:`

                      定义

                      +
                      `,r:{minutes:2.41,words:722},t:"Strategy Pattern",y:"a"}}],["/designpattern/%E8%A3%85%E9%A5%B0%E6%A8%A1%E5%BC%8F.html",{loader:()=>g(()=>import("./装饰模式.html-B1IPnvpl.js"),__vite__mapDeps([8,1])),meta:{d:16543872e5,l:"2022年6月5日",c:["设计模式"],o:!0,e:`

                      定义

                      装饰者模式是一种常用的设计模式,它动态地给一个对象添加一些额外的职责1。装饰者模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为2。

                      框架中使用

                      mybatis中:

                      -

                      打开sqlSession时,会创建Executor,最终会进入org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)

                      `,r:{minutes:.9,words:270},t:"Decorator Design Pattern",y:"a"}}],["/designpattern/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F.html",{loader:()=>g(()=>import("./责任链模式.html-BnEIojaU.js"),__vite__mapDeps([9,1])),meta:{d:16523136e5,l:"2022年5月12日",c:["设计模式"],o:!0,e:`

                      定义

                      +

                      打开sqlSession时,会创建Executor,最终会进入org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)

                      `,r:{minutes:.9,words:270},t:"Decorator Design Pattern",y:"a"}}],["/designpattern/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F.html",{loader:()=>g(()=>import("./责任链模式.html-BiFSpWx5.js"),__vite__mapDeps([9,1])),meta:{d:16523136e5,l:"2022年5月12日",c:["设计模式"],o:!0,e:`

                      定义

                      责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它可以将多个对象组合成一条链,并按照事先规定的顺序依次处理请求。每个对象都可以选择处理请求,或者将请求传递给链中的下一个对象。这种模式将请求的发送者和接收者解耦,使得多个对象都有机会处理请求,从而提高了系统的灵活性和可扩展性。
                       
                       具体来说,责任链模式包含以下几个角色:
                      @@ -143,8 +143,8 @@ Server rendered element contains fewer child nodes than client vdom.`),P=!0),Fe(
                       3. Client:客户端,它向链中的第一个ConcreteHandler对象发起请求,然后等待链条响应请求。
                       
                       责任链模式的优点在于它可以简化对象之间的耦合关系,增加系统的灵活性。它对系统进行解耦,使得请求发送者不必关心请求的具体处理者,发起请求后,请求会在链条中依次被多个对象处理。同时,责任链模式还可以方便地进行动态的链式调整,即在运行时往链中添加或删除具体处理者,以便满足实时的业务需求。
                      -
                      `,r:{minutes:2.14,words:641},t:"Chain of Responsibility Pattern",y:"a"}}],["/frontweb/",{loader:()=>g(()=>import("./index.html-DGOKX5rc.js"),__vite__mapDeps([10,1])),meta:{d:1496016e6,l:"2017年5月29日",e:`

                      基础打牢

                      -`,r:{minutes:.05,words:14},t:"JavaScript成神之路",y:"a"}}],["/guide/",{loader:()=>g(()=>import("./index.html-DcYo10ls.js"),__vite__mapDeps([11,1])),meta:{d:1659087823e3,c:["使用指南"],e:`

                      目录

                      +
                      `,r:{minutes:2.14,words:641},t:"Chain of Responsibility Pattern",y:"a"}}],["/frontweb/",{loader:()=>g(()=>import("./index.html-Cp5ukHy1.js"),__vite__mapDeps([10,1])),meta:{d:1496016e6,l:"2017年5月29日",e:`

                      基础打牢

                      +`,r:{minutes:.05,words:14},t:"JavaScript成神之路",y:"a"}}],["/guide/",{loader:()=>g(()=>import("./index.html-DTTN-fqR.js"),__vite__mapDeps([11,1])),meta:{d:1659087823e3,c:["使用指南"],e:`

                      目录

                      `,r:{minutes:.13,words:40},t:"主要功能与配置演示",i:"creative",y:"a"}}],["/guide/disable.html",{loader:()=>g(()=>import("./disable.html-Cyr3Xy2m.js"),__vite__mapDeps([12,1])),meta:{d:1659087823e3,c:["使用指南"],g:["禁用"],e:`

                      你可以通过设置页面的 Frontmatter,在页面禁用功能与布局。

                      -`,r:{minutes:.43,words:128},t:"布局与功能禁用",i:"config",O:3,y:"a"}}],["/guide/encrypt.html",{loader:()=>g(()=>import("./encrypt.html-g93oybTM.js"),__vite__mapDeps([13,1])),meta:{d:1659087823e3,c:["使用指南"],g:["文章加密"],n:!0,r:{minutes:.52,words:156},t:"密码加密的文章",i:"lock",y:"a"}}],["/guide/markdown.html",{loader:()=>g(()=>import("./markdown.html-DEfZHKXr.js"),__vite__mapDeps([14,1])),meta:{d:1659087823e3,c:["使用指南"],g:["Markdown"],e:`

                      VuePress 主要从 Markdown 文件生成页面。因此,你可以使用它轻松生成文档或博客站点。

                      +
    `,r:{minutes:.13,words:40},t:"主要功能与配置演示",i:"creative",y:"a"}}],["/guide/disable.html",{loader:()=>g(()=>import("./disable.html-BIHRTCCC.js"),__vite__mapDeps([12,1])),meta:{d:1659087823e3,c:["使用指南"],g:["禁用"],e:`

    你可以通过设置页面的 Frontmatter,在页面禁用功能与布局。

    +`,r:{minutes:.43,words:128},t:"布局与功能禁用",i:"config",O:3,y:"a"}}],["/guide/encrypt.html",{loader:()=>g(()=>import("./encrypt.html-DiZeFQ0R.js"),__vite__mapDeps([13,1])),meta:{d:1659087823e3,c:["使用指南"],g:["文章加密"],n:!0,r:{minutes:.52,words:156},t:"密码加密的文章",i:"lock",y:"a"}}],["/guide/markdown.html",{loader:()=>g(()=>import("./markdown.html-qweyEKPp.js"),__vite__mapDeps([14,1])),meta:{d:1659087823e3,c:["使用指南"],g:["Markdown"],e:`

    VuePress 主要从 Markdown 文件生成页面。因此,你可以使用它轻松生成文档或博客站点。

    你应该创建和编写 Markdown 文件,以便 VuePress 可以根据文件结构将它们转换为不同的页面。

    -`,r:{minutes:3.58,words:1073},t:"Markdown 展示",i:"markdown",O:2,y:"a"}}],["/guide/page.html",{loader:()=>g(()=>import("./page.html-CGr9yG-I.js"),__vite__mapDeps([15,1])),meta:{a:"Ms.Hope",d:15463008e5,l:"2019年1月1日",c:["使用指南"],g:["页面配置","使用指南"],u:!0,e:`

    more 注释之前的内容被视为文章摘要。

    -`,r:{minutes:1.46,words:438},t:"页面配置",i:"page",O:1,y:"a"}}],["/java/",{loader:()=>g(()=>import("./index.html-M6ciET1C.js"),__vite__mapDeps([16,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.04,words:11},t:"Java入门到放弃",y:"a"}}],["/myserver/Jellyfin%E6%90%AD%E5%BB%BA.html",{loader:()=>g(()=>import("./Jellyfin搭建.html-CMT6qNWD.js"),__vite__mapDeps([17,1])),meta:{a:"chensino",d:17110656e5,l:"2024年3月22日",o:!0,e:`

    家庭影院

    -`,r:{minutes:.06,words:18},t:"jellyfin搭建",y:"a"}}],["/myserver/OpenWRT.html",{loader:()=>g(()=>import("./OpenWRT.html-DzrmVEsx.js"),__vite__mapDeps([18,1])),meta:{a:"chensino",d:17337888e5,l:"2024年12月10日",o:!0,e:`

    问题1:新安装的openwrt没有网络

    +`,r:{minutes:3.58,words:1073},t:"Markdown 展示",i:"markdown",O:2,y:"a"}}],["/guide/page.html",{loader:()=>g(()=>import("./page.html-CiuNj2kU.js"),__vite__mapDeps([15,1])),meta:{a:"Ms.Hope",d:15463008e5,l:"2019年1月1日",c:["使用指南"],g:["页面配置","使用指南"],u:!0,e:`

    more 注释之前的内容被视为文章摘要。

    +`,r:{minutes:1.46,words:438},t:"页面配置",i:"page",O:1,y:"a"}}],["/java/",{loader:()=>g(()=>import("./index.html-C-EyMnTc.js"),__vite__mapDeps([16,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.04,words:11},t:"Java入门到放弃",y:"a"}}],["/myserver/Jellyfin%E6%90%AD%E5%BB%BA.html",{loader:()=>g(()=>import("./Jellyfin搭建.html-BxsZZcjM.js"),__vite__mapDeps([17,1])),meta:{a:"chensino",d:17110656e5,l:"2024年3月22日",o:!0,e:`

    家庭影院

    +`,r:{minutes:.06,words:18},t:"jellyfin搭建",y:"a"}}],["/myserver/OpenWRT.html",{loader:()=>g(()=>import("./OpenWRT.html-DJ_JkW49.js"),__vite__mapDeps([18,1])),meta:{a:"chensino",d:17337888e5,l:"2024年12月10日",o:!0,e:`

    问题1:新安装的openwrt没有网络

    表现为/etc/resolv.conf中的dns解析配置nameserver是127.0.0.1,这时ping baidu.com是不通的,但是ping外网ip是可以的,如果手动修改nameserver为 192.168.1.1则发现openwrt网络恢复,但是过一会这个nameserver会被还原为127.0.0.1。说明问题就出现在dns解析上

    解决方法:

    -

    在openwrt设置页面,网络——DHCP/DNS设置页面,把dns转发填写为192.168.1.1,或者其他114.114.114.114之类的dns服务即可。

    `,r:{minutes:.6,words:181},t:"OpenWRT",y:"a"}}],["/myserver/PVE%E8%99%9A%E6%8B%9F%E6%9C%BA.html",{loader:()=>g(()=>import("./PVE虚拟机.html-t1tsCGLO.js"),__vite__mapDeps([19,1])),meta:{a:"chensino",d:1701216e6,l:"2023年11月29日",o:!0,e:`

    PVE安装

    +

    在openwrt设置页面,网络——DHCP/DNS设置页面,把dns转发填写为192.168.1.1,或者其他114.114.114.114之类的dns服务即可。

    `,r:{minutes:.6,words:181},t:"OpenWRT",y:"a"}}],["/myserver/PVE%E8%99%9A%E6%8B%9F%E6%9C%BA.html",{loader:()=>g(()=>import("./PVE虚拟机.html-D-5k4U5W.js"),__vite__mapDeps([19,1])),meta:{a:"chensino",d:1701216e6,l:"2023年11月29日",o:!0,e:`

    PVE安装

    虚拟机系统安装

    UBUNTU

    OPENWRT旁路由

    @@ -181,14 +181,14 @@ Server rendered element contains fewer child nodes than client vdom.`),P=!0),Fe(

    防火墙

    SSL证书

    挂载硬盘

    -`,r:{minutes:.19,words:58},t:"PVE虚拟机",y:"a"}}],["/myserver/",{loader:()=>g(()=>import("./index.html-CvNGtBHP.js"),__vite__mapDeps([20,1])),meta:{a:"chensino",d:17110656e5,l:"2024年3月22日",e:`

    个人服务器搭建

    +`,r:{minutes:.19,words:58},t:"PVE虚拟机",y:"a"}}],["/myserver/",{loader:()=>g(()=>import("./index.html-BDFAfq7g.js"),__vite__mapDeps([20,1])),meta:{a:"chensino",d:17110656e5,l:"2024年3月22日",e:`

    个人服务器搭建

    自建NAS

    自己搭建nas

    实现nas外网访问

    使用外部网络访问nas

    搭建家庭影院

    下载,刮削,展示

    -`,r:{minutes:.19,words:57},t:"个人服务器",y:"a"}}],["/myserver/Ubuntu%E6%9C%8D%E5%8A%A1%E6%90%AD%E5%BB%BA.html",{loader:()=>g(()=>import("./Ubuntu服务搭建.html-B5ncaYIz.js"),__vite__mapDeps([21,1])),meta:{a:"chensino",d:17328384e5,l:"2024年11月29日",o:!0,e:`
    +`,r:{minutes:.19,words:57},t:"个人服务器",y:"a"}}],["/myserver/Ubuntu%E6%9C%8D%E5%8A%A1%E6%90%AD%E5%BB%BA.html",{loader:()=>g(()=>import("./Ubuntu服务搭建.html-Dn60vq3V.js"),__vite__mapDeps([21,1])),meta:{a:"chensino",d:17328384e5,l:"2024年11月29日",o:!0,e:`

    介绍

    本篇介绍我的ubuntu系统上搭建的服务

    @@ -198,8 +198,8 @@ Server rendered element contains fewer child nodes than client vdom.`),P=!0),Fe(

    nginx

    ELK

    JAVA后端

    -`,r:{minutes:.15,words:46},t:"UBUNTU系统服务搭建",y:"a"}}],["/myserver/ddns%E7%94%B3%E8%AF%B7%E8%AF%81%E4%B9%A6.html",{loader:()=>g(()=>import("./ddns申请证书.html-CQQa8cy8.js"),__vite__mapDeps([22,1])),meta:{a:"chensino",d:17276544e5,l:"2024年9月30日",o:!0,e:`

    acme官方文档:https://github.com/acmesh-official/acme.sh -pve官方文档:https://pve-doc-cn.readthedocs.io/zh-cn/latest/chapter_system_administration/certmgr.html

    `,r:{minutes:2.15,words:645},t:"ddns证书",y:"a"}}],["/myserver/firewall.html",{loader:()=>g(()=>import("./firewall.html-CDNDwMUM.js"),__vite__mapDeps([23,1])),meta:{a:"chensino",d:1727568e6,l:"2024年9月29日",o:!0,e:`

    参考:https://www.xh86.me/?p=11324

    +`,r:{minutes:.15,words:46},t:"UBUNTU系统服务搭建",y:"a"}}],["/myserver/ddns%E7%94%B3%E8%AF%B7%E8%AF%81%E4%B9%A6.html",{loader:()=>g(()=>import("./ddns申请证书.html-9E4jX9TA.js"),__vite__mapDeps([22,1])),meta:{a:"chensino",d:17276544e5,l:"2024年9月30日",o:!0,e:`

    acme官方文档:https://github.com/acmesh-official/acme.sh +pve官方文档:https://pve-doc-cn.readthedocs.io/zh-cn/latest/chapter_system_administration/certmgr.html

    `,r:{minutes:2.15,words:645},t:"ddns证书",y:"a"}}],["/myserver/firewall.html",{loader:()=>g(()=>import("./firewall.html-BD988R_L.js"),__vite__mapDeps([23,1])),meta:{a:"chensino",d:1727568e6,l:"2024年9月29日",o:!0,e:`

    参考:https://www.xh86.me/?p=11324

    一、防火墙简介

    pve有3个层级防火墙:

      @@ -207,14 +207,14 @@ pve官方文档:
      20240929173211
      `,r:{minutes:2.2,words:660},t:"pve防火墙",y:"a"}}],["/myserver/fnos.html",{loader:()=>g(()=>import("./fnos.html-BbY5Cor0.js"),__vite__mapDeps([24,1])),meta:{a:"chensino",d:17311104e5,l:"2024年11月9日",o:!0,e:`

      背景

      +
      20240929173211
      20240929173211
      `,r:{minutes:2.2,words:660},t:"pve防火墙",y:"a"}}],["/myserver/fnos.html",{loader:()=>g(()=>import("./fnos.html-CdbKMm57.js"),__vite__mapDeps([24,1])),meta:{a:"chensino",d:17311104e5,l:"2024年11月9日",o:!0,e:`

      背景

      nas端口暴露到公网,存在被扫描爆破的风险,使用fail2ban对账号进行保护

      1.安装fail2ban

      #1. 安装fail2ban
       sudo apt install fail2ban
      -
      `,r:{minutes:1.14,words:342},t:"飞牛NAS",y:"a"}}],["/myserver/nginx%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86ddns%E8%A7%A3%E6%9E%90%E9%97%AE%E9%A2%98.html",{loader:()=>g(()=>import("./nginx反向代理ddns解析问题.html-DmX4Ir6j.js"),__vite__mapDeps([25,1])),meta:{a:"chensino",d:1733184e6,l:"2024年12月3日",o:!0,e:`

      问题

      +
    `,r:{minutes:1.14,words:342},t:"飞牛NAS",y:"a"}}],["/myserver/nginx%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86ddns%E8%A7%A3%E6%9E%90%E9%97%AE%E9%A2%98.html",{loader:()=>g(()=>import("./nginx反向代理ddns解析问题.html-DaFsuZUP.js"),__vite__mapDeps([25,1])),meta:{a:"chensino",d:1733184e6,l:"2024年12月3日",o:!0,e:`

    问题

    nginx反向代理到ddns服务器,当ddns域名绑定ip变化时,ngixn就无法反代了,默认情况下是nginx启动时解析一次域名,缓存下来后面就从缓存获取,所以当ddns绑定ip变化就会出现无法访问

    解决方法

    设置resovlver,并且把ddns域名要设置为变量形式,在各自的location @@ -230,7 +230,7 @@ pve官方文档: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }

    -
    `,r:{minutes:1.49,words:448},t:"nginx反向代理ddns问题",y:"a"}}],["/myserver/x86_openwrt.html",{loader:()=>g(()=>import("./x86_openwrt.html-Cq0wSqzW.js"),__vite__mapDeps([26,1])),meta:{a:"chensino",d:17116704e5,l:"2024年3月29日",o:!0,e:`

    vmware中使用openwrt做旁路由网关

    +
`,r:{minutes:1.49,words:448},t:"nginx反向代理ddns问题",y:"a"}}],["/myserver/x86_openwrt.html",{loader:()=>g(()=>import("./x86_openwrt.html-BMoOjmEk.js"),__vite__mapDeps([26,1])),meta:{a:"chensino",d:17116704e5,l:"2024年3月29日",o:!0,e:`

vmware中使用openwrt做旁路由网关

注意!!

在windows下,如果宿主机是通过无线网卡连接的网络,vmware中的openwrt是无法作为一个正常的旁路由网关的,具体表现如下: @@ -238,16 +238,16 @@ vmware中其他系统可以正常使用openwrt做旁路由网关,但是局域 解决方式: 要么不要用windows系统,要么把宿主机切换为有线连接

-

参考

`,r:{minutes:.91,words:272},t:"旁路由网关",y:"a"}}],["/myserver/%E7%BD%91%E7%BB%9C%E8%AE%BE%E7%BD%AE.html",{loader:()=>g(()=>import("./网络设置.html-BJTIC__2.js"),__vite__mapDeps([27,1])),meta:{a:"chensino",d:17110656e5,l:"2024年3月22日",o:!0,e:`

本篇介绍我的网络配置过程,如何获得公网ip,以及升级我的家庭路由,mesh组网,我的网络是湖北电信500m,光猫一开始是天翼网关3.0, +

参考

`,r:{minutes:.91,words:272},t:"旁路由网关",y:"a"}}],["/myserver/%E7%BD%91%E7%BB%9C%E8%AE%BE%E7%BD%AE.html",{loader:()=>g(()=>import("./网络设置.html-DwuvRbKr.js"),__vite__mapDeps([27,1])),meta:{a:"chensino",d:17110656e5,l:"2024年3月22日",o:!0,e:`

本篇介绍我的网络配置过程,如何获得公网ip,以及升级我的家庭路由,mesh组网,我的网络是湖北电信500m,光猫一开始是天翼网关3.0, 后来咸鱼买了一个二手的4.0的花了50,曾打电话给电信客服升级4.0的是不是免费,他说199元。 家庭组网章节适合所有用户阅读,公网部分适合程序员朋友参考。

家庭组网

为什么要组网

我家套内93平,一个无线路由器无法覆盖整个房间,起初是在每个房间放一个路由器,每次从一个房间到另一房间都要切换网络,即使手机有
 自动切换网络功能,但是总是不丝滑,我想做到wifi切换无感知,所以就生出组网的想法。
-
`,r:{minutes:5.99,words:1797},t:"网络设置",y:"a"}}],["/myserver/%E8%87%AA%E5%BB%BAnas.html",{loader:()=>g(()=>import("./自建nas.html-2XSY6hdf.js"),__vite__mapDeps([28,1])),meta:{a:"chensino",d:17110656e5,l:"2024年3月22日",o:!0,e:`

nas介绍

+
`,r:{minutes:5.99,words:1797},t:"网络设置",y:"a"}}],["/myserver/%E8%87%AA%E5%BB%BAnas.html",{loader:()=>g(()=>import("./自建nas.html-CK_-5RCz.js"),__vite__mapDeps([28,1])),meta:{a:"chensino",d:17110656e5,l:"2024年3月22日",o:!0,e:`

nas介绍

nas是什么

-`,r:{minutes:.07,words:21},t:"自建nas",y:"a"}}],["/myserver/%E8%AF%81%E4%B9%A6%E8%87%AA%E5%8A%A8%E7%BB%AD%E7%AD%BE.html",{loader:()=>g(()=>import("./证书自动续签.html-R5o--Pmc.js"),__vite__mapDeps([29,1])),meta:{a:"chensino",d:1668384e6,l:"2022年11月14日",o:!0,e:`

1.场景

+`,r:{minutes:.07,words:21},t:"自建nas",y:"a"}}],["/myserver/%E8%AF%81%E4%B9%A6%E8%87%AA%E5%8A%A8%E7%BB%AD%E7%AD%BE.html",{loader:()=>g(()=>import("./证书自动续签.html-DTlcMbEd.js"),__vite__mapDeps([29,1])),meta:{a:"chensino",d:1668384e6,l:"2022年11月14日",o:!0,e:`

1.场景

我有一个pve虚拟机系统,需要部署ssl证书,同时我还有其他的云服务器以及内网服务器都需要证书,这些证书的根域名都是chensina.cn,而使用acme.sh申请的证书一般都是免费90天,到期就需要重新生成。重新生成后又要复制到其他远程服务器,然后重启对应的服务, 比较麻烦,所以这里写一个脚本自动完成。

2. 实现流程

@@ -256,20 +256,20 @@ vmware中其他系统可以正常使用openwrt做旁路由网关,但是局域
  • 安装证书到pve机器本地(需要重启pveproxy服务)
  • scp复制到其他远程服务器上
  • 远程服务器上的服务重启(使用脚本监听证书,自动执行重启)
  • -`,r:{minutes:2.7,words:809},t:"acme.sh证书自动续签",y:"a"}}],["/other/",{loader:()=>g(()=>import("./index.html-CXgHAvGT.js"),__vite__mapDeps([30,1])),meta:{a:"chenkun",d:1627776e6,l:"2021年8月1日",r:{minutes:.03,words:9},t:"其他",y:"a"}}],["/cpp/other/1.html",{loader:()=>g(()=>import("./1.html-CERq7o11.js"),__vite__mapDeps([31,1])),meta:{d:1642032e6,l:"2022年1月13日",e:`

    文章来源:https://zhuanlan.zhihu.com/p/111110992

    +`,r:{minutes:2.7,words:809},t:"acme.sh证书自动续签",y:"a"}}],["/other/",{loader:()=>g(()=>import("./index.html-DR85PmVq.js"),__vite__mapDeps([30,1])),meta:{a:"chenkun",d:1627776e6,l:"2021年8月1日",r:{minutes:.03,words:9},t:"其他",y:"a"}}],["/cpp/other/1.html",{loader:()=>g(()=>import("./1.html-BKAh0zdH.js"),__vite__mapDeps([31,1])),meta:{d:1642032e6,l:"2022年1月13日",e:`

    文章来源:https://zhuanlan.zhihu.com/p/111110992

    gcc

    它是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)。

    我们的程序只有一个源文件时,直接就可以用gcc命令编译它。

    可是,如果我们的程序包含很多个源文件时,该咋整?用gcc命令逐个去编译时,就发现很容易混乱而且工作量大,所以出现了下面make工具。

    gcc命令在链接时默认使用C的库,只有添加了-lstdc++选项才会使用 C++ 的库。 -不过 GCC 中还有一个g++命令,它专门用来编译 C++ 程序,广大 C++ 开发人员也都使用这个命令。g++命令和gcc命令的用法是一样的。

    `,r:{minutes:3.17,words:951},t:"c++中使用的编译工具介绍",y:"a"}}],["/cpp/other/2.html",{loader:()=>g(()=>import("./2.html-CMg5IU4s.js"),__vite__mapDeps([32,1])),meta:{d:1642032e6,l:"2022年1月13日",e:`

    占坑

    +不过 GCC 中还有一个g++命令,它专门用来编译 C++ 程序,广大 C++ 开发人员也都使用这个命令。g++命令和gcc命令的用法是一样的。

    `,r:{minutes:3.17,words:951},t:"c++中使用的编译工具介绍",y:"a"}}],["/cpp/other/2.html",{loader:()=>g(()=>import("./2.html-DYUUXpT6.js"),__vite__mapDeps([32,1])),meta:{d:1642032e6,l:"2022年1月13日",e:`

    占坑

    https://juejin.cn/post/6969389200416178213#heading-11

    https://blog.jetbrains.com/clion/2020/03/openjdk-with-clion/

    -`,r:{minutes:.09,words:27},t:"使用Clion搭建jdk源码调试环境",y:"a"}}],["/cpp/study/",{loader:()=>g(()=>import("./index.html-DkjjOyEZ.js"),__vite__mapDeps([33,1])),meta:{d:1642032e6,l:"2022年1月13日",e:`

    c++基础学习

    +`,r:{minutes:.09,words:27},t:"使用Clion搭建jdk源码调试环境",y:"a"}}],["/cpp/study/",{loader:()=>g(()=>import("./index.html-Cm_vJXFf.js"),__vite__mapDeps([33,1])),meta:{d:1642032e6,l:"2022年1月13日",e:`

    c++基础学习

    c++的进阶之路

    -`,r:{minutes:.06,words:17},t:"index",y:"a"}}],["/frontweb/es5/",{loader:()=>g(()=>import("./index.html-CI5-E8zh.js"),__vite__mapDeps([34,1])),meta:{a:"Zxf",d:16193088e5,l:"2021年4月25日",g:["你所不了解的JavaScript"],e:`

    目录

    +`,r:{minutes:.06,words:17},t:"index",y:"a"}}],["/frontweb/es5/",{loader:()=>g(()=>import("./index.html-4zQyDuTf.js"),__vite__mapDeps([34,1])),meta:{a:"Zxf",d:16193088e5,l:"2021年4月25日",g:["你所不了解的JavaScript"],e:`

    目录

    `,r:{minutes:.23,words:69},t:"ECMAScript 5",y:"a"}}],["/frontweb/es5/aboutAsync.html",{loader:()=>g(()=>import("./aboutAsync.html-D_L12OCl.js"),__vite__mapDeps([35,1])),meta:{a:"qianxun",d:15821568e5,l:"2020年2月20日",c:["vue知识点"],g:["必会"],e:`

    一,async函数的定义

    +`,r:{minutes:.23,words:69},t:"ECMAScript 5",y:"a"}}],["/frontweb/es5/aboutAsync.html",{loader:()=>g(()=>import("./aboutAsync.html-oq_Ucfhr.js"),__vite__mapDeps([35,1])),meta:{a:"qianxun",d:15821568e5,l:"2020年2月20日",c:["vue知识点"],g:["必会"],e:`

    一,async函数的定义

    async函数是使用async关键字声明的函数。 并且其中允许使用await关键字。async和await关键字让我们可以用一种更简洁的方式写出基于Promise异步行为,而无需刻意地链式调用promise。

    备注:async/await的目的为了简化使用基于 promise 的 API 时所需的语法。async/await的行为就好像搭配使用了生成器和 promise。

    -
    `,r:{minutes:4.97,words:1490},t:"异步async函数",y:"a"}}],["/frontweb/es5/aboutEvent.html",{loader:()=>g(()=>import("./aboutEvent.html-D7Q2TjUz.js"),__vite__mapDeps([36,1])),meta:{a:"qianxun",d:16263072e5,l:"2021年7月15日",c:["js基础"],g:["必会"],e:`

    一,事件注册的三种方式

    +`,r:{minutes:4.97,words:1490},t:"异步async函数",y:"a"}}],["/frontweb/es5/aboutEvent.html",{loader:()=>g(()=>import("./aboutEvent.html-CfGtwxE3.js"),__vite__mapDeps([36,1])),meta:{a:"qianxun",d:16263072e5,l:"2021年7月15日",c:["js基础"],g:["必会"],e:`

    一,事件注册的三种方式

    1,通过 HTML 元素指定事件属性来绑定

    <button onclick ="clickFu()">点我吧</button>
     <script>
    @@ -290,7 +290,7 @@ vmware中其他系统可以正常使用openwrt做旁路由网关,但是局域
            alert(3333)
        }
     </script>
    -
    `,r:{minutes:5.17,words:1550},t:"JS原生事件",y:"a"}}],["/frontweb/es5/aboutThis.html",{loader:()=>g(()=>import("./aboutThis.html-BzHERr3b.js"),__vite__mapDeps([37,1])),meta:{a:"qianxun",d:16193088e5,l:"2021年4月25日",c:["js基础"],g:["必会"],e:`

    一,函数内部的this指向

    +
    `,r:{minutes:5.17,words:1550},t:"JS原生事件",y:"a"}}],["/frontweb/es5/aboutThis.html",{loader:()=>g(()=>import("./aboutThis.html-CGTlZqFV.js"),__vite__mapDeps([37,1])),meta:{a:"qianxun",d:16193088e5,l:"2021年4月25日",c:["js基础"],g:["必会"],e:`

    一,函数内部的this指向

    函数内this指向,是当我们调用函数的时候才能确定。调用方式的不同决定this的指向不同。
     this的指向,是在调用函数时根据执行上下文所动态确定的。
     
    @@ -327,17 +327,17 @@ this的指向,是在调用函数时根据执行上下文所动态确定的。 window -`,r:{minutes:2.38,words:714},t:"this指向问题",y:"a"}}],["/frontweb/es5/asyncError.html",{loader:()=>g(()=>import("./asyncError.html-B7e9KI7M.js"),__vite__mapDeps([38,1])),meta:{a:"qianxun",d:16365024e5,l:"2021年11月10日",c:["vue知识点"],g:["必会"],e:`

    一,为什么要捕获异常

    +`,r:{minutes:2.38,words:714},t:"this指向问题",y:"a"}}],["/frontweb/es5/asyncError.html",{loader:()=>g(()=>import("./asyncError.html-DIaCTfUi.js"),__vite__mapDeps([38,1])),meta:{a:"qianxun",d:16365024e5,l:"2021年11月10日",c:["vue知识点"],g:["必会"],e:`

    一,为什么要捕获异常

    //确认提交
     const submitWorkloadSure = async () => {
       
         let data = await WorkloadSures()
          console.log(555)
     };
    -
    `,r:{minutes:2.56,words:769},t:"关于async/await的异常捕获",y:"a"}}],["/frontweb/es5/crossDomain.html",{loader:()=>g(()=>import("./crossDomain.html-D4cFVnCU.js"),__vite__mapDeps([39,1])),meta:{a:"qianxun",d:16193088e5,l:"2021年4月25日",c:["vue知识点"],g:["必会"],e:`

    一,同源策略

    +
    `,r:{minutes:2.56,words:769},t:"关于async/await的异常捕获",y:"a"}}],["/frontweb/es5/crossDomain.html",{loader:()=>g(()=>import("./crossDomain.html-DiNllvHl.js"),__vite__mapDeps([39,1])),meta:{a:"qianxun",d:16193088e5,l:"2021年4月25日",c:["vue知识点"],g:["必会"],e:`

    一,同源策略

    所谓同源是指:当浏览器向后端发送请求时其请求的协议、域名、端口要和当前服务完全一致。比如前端项目的服务位于http://localhost:8080,则其发送的所有请求必须是http://localhost:8080/xxx/xxx这种格式,否则就会被同源策略拦截。

    http://www.test.com:8000/  协议(http)、主域名(test)、子域名(www)、端口号(8000)
    -
    `,r:{minutes:6.18,words:1854},t:"前端跨域(一)之proxy配置",y:"a"}}],["/frontweb/es5/crossDomain2.html",{loader:()=>g(()=>import("./crossDomain2.html-DUOpY0k2.js"),__vite__mapDeps([40,1])),meta:{a:"qianxun",d:16193088e5,l:"2021年4月25日",c:["vue知识点"],g:["必会"],e:`

    一,什么是JSONP

    +
    `,r:{minutes:6.18,words:1854},t:"前端跨域(一)之proxy配置",y:"a"}}],["/frontweb/es5/crossDomain2.html",{loader:()=>g(()=>import("./crossDomain2.html-UM0H1KZK.js"),__vite__mapDeps([40,1])),meta:{a:"qianxun",d:16193088e5,l:"2021年4月25日",c:["vue知识点"],g:["必会"],e:`

    一,什么是JSONP

    Jsonp(JSON with Padding) 是 json 的一种"使用模式",可以让网页从别的域名(网站)那获取资料,即跨域读取数据。

    @@ -345,8 +345,8 @@ this的指向,是在调用函数时根据执行上下文所动态确定的。 同源策略,它是由 Netscape 提出的一个著名的安全策略,现在所有支持 JavaScript 的浏览器都会使用这个策略。

    二,JSONP的原理

    我们知道,在页面上有三种资源是可以与页面本身不同源的。它们是:js脚本,css样式文件,图片。像taobao等大型网站,很定会将这些静态资源放入cdn中,然后在页面上链接。而jsonp就是利用了script标签可以链接到不同源的js脚本,来到达跨域目的。 -于是可以判断,当前阶段如果想通过纯web端(ActiveX控件、服务端代理、Web socket等方式不算)跨域访问数据就只有一种可能,那就是在远程服务器上设法把数据装进js格式的文件里,供客户端调用和进一步处理;

    `,r:{minutes:2.97,words:891},t:"前端跨域(二)之JSONP跨域",y:"a"}}],["/frontweb/es5/lazyLoad.html",{loader:()=>g(()=>import("./lazyLoad.html-CBB2VYk3.js"),__vite__mapDeps([41,1])),meta:{a:"qianxun",d:16193088e5,l:"2021年4月25日",c:["js基础"],g:["必会"],e:`

    一,为什么需要图片懒加载

    -

    在老版本的Chrome中,图片的加载其实是会阻塞DOM渲染的。在我们现代浏览器中,这一点基本不用担心了,也就是说现在的图片加载不会阻塞DOM渲染,但是每一个图片都会对应一个HTTP请求而浏览器允许同时并发请求的数量是有限的(数量为6),假设你的网站有大量的图片,那么加载的过程是很耗时的,尤其像那些电商类需要大量图片的网站,可想而知,网站的初始加载时间会很长,再加上网络等其它影响,用户体验会很差。为了解决这个问题,提高用户体验,所以就出现了懒加载这种方式来减轻服务器的压力,优先加载可视区域的内容,其他部分等进入了可视区域再加载,从而提高性能。

    `,r:{minutes:3.16,words:949},t:"图片懒加载",y:"a"}}],["/frontweb/es5/throttle.html",{loader:()=>g(()=>import("./throttle.html-BkFDlaU-.js"),__vite__mapDeps([42,1])),meta:{a:"qianxun",d:16166304e5,l:"2021年3月25日",c:["js基础"],g:["必会"],e:`

    一,节流概念(Throttle)

    +于是可以判断,当前阶段如果想通过纯web端(ActiveX控件、服务端代理、Web socket等方式不算)跨域访问数据就只有一种可能,那就是在远程服务器上设法把数据装进js格式的文件里,供客户端调用和进一步处理;

    `,r:{minutes:2.97,words:891},t:"前端跨域(二)之JSONP跨域",y:"a"}}],["/frontweb/es5/lazyLoad.html",{loader:()=>g(()=>import("./lazyLoad.html-x9qhamqy.js"),__vite__mapDeps([41,1])),meta:{a:"qianxun",d:16193088e5,l:"2021年4月25日",c:["js基础"],g:["必会"],e:`

    一,为什么需要图片懒加载

    +

    在老版本的Chrome中,图片的加载其实是会阻塞DOM渲染的。在我们现代浏览器中,这一点基本不用担心了,也就是说现在的图片加载不会阻塞DOM渲染,但是每一个图片都会对应一个HTTP请求而浏览器允许同时并发请求的数量是有限的(数量为6),假设你的网站有大量的图片,那么加载的过程是很耗时的,尤其像那些电商类需要大量图片的网站,可想而知,网站的初始加载时间会很长,再加上网络等其它影响,用户体验会很差。为了解决这个问题,提高用户体验,所以就出现了懒加载这种方式来减轻服务器的压力,优先加载可视区域的内容,其他部分等进入了可视区域再加载,从而提高性能。

    `,r:{minutes:3.16,words:949},t:"图片懒加载",y:"a"}}],["/frontweb/es5/throttle.html",{loader:()=>g(()=>import("./throttle.html-lVY8dAD-.js"),__vite__mapDeps([42,1])),meta:{a:"qianxun",d:16166304e5,l:"2021年3月25日",c:["js基础"],g:["必会"],e:`

    一,节流概念(Throttle)

    规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

    @@ -355,12 +355,12 @@ this的指向,是在调用函数时根据执行上下文所动态确定的。
  • 鼠标连续不断地触发某事件(如点击),只在单位时间内只触发一次;
  • 在页面的无限加载场景下,需要用户在滚动页面时,每隔一段时间发一次 ajax 请求,而不是在用户停下滚动页面操作时才去请求数据;
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断;
  • -`,r:{minutes:2.53,words:759},t:"节流与防抖",y:"a"}}],["/frontweb/es6/",{loader:()=>g(()=>import("./index.html-C5owgjh1.js"),__vite__mapDeps([43,1])),meta:{a:"qianxun",d:15821568e5,l:"2020年2月20日",r:{minutes:.03,words:9},t:"ECMAScript 6",y:"a"}}],["/frontweb/es6/js%E4%B8%AD%E6%95%B4%E6%95%B0%E7%9A%84%E6%9C%80%E5%A4%A7%E5%80%BC.html",{loader:()=>g(()=>import("./js中整数的最大值.html-iX2inHhn.js"),__vite__mapDeps([44,1])),meta:{d:16583616e5,l:"2022年7月21日",o:!0,e:`

    背景

    +`,r:{minutes:2.53,words:759},t:"节流与防抖",y:"a"}}],["/frontweb/es6/",{loader:()=>g(()=>import("./index.html-CKiw-mPF.js"),__vite__mapDeps([43,1])),meta:{a:"qianxun",d:15821568e5,l:"2020年2月20日",r:{minutes:.03,words:9},t:"ECMAScript 6",y:"a"}}],["/frontweb/es6/js%E4%B8%AD%E6%95%B4%E6%95%B0%E7%9A%84%E6%9C%80%E5%A4%A7%E5%80%BC.html",{loader:()=>g(()=>import("./js中整数的最大值.html-BTBkXAEb.js"),__vite__mapDeps([44,1])),meta:{d:16583616e5,l:"2022年7月21日",o:!0,e:`

    背景

    今天用Mybatis-plus插入数据,自动产生了一个19位的主键id,后来才发现这是Mybatis-plus3.0后更新的功能,默认使用雪花算法最终会产生一个19位数字。

    然后我在前端项目查询后端接口,后端接口返回的1682195717382606849,而到了前端一直打印的是另一个数字。

    原因,参考gpt回答

    问题1:数据库19位id,在浏览器控制台显示错误?

    -

    如果你的数据库使用的是19位的ID,而浏览器控制台显示错误,可能是因为浏览器默认将超过16位的整数值视为科学计数法表示。在科学计数法表示下,可能会丢失精度或显示不正确。

    `,r:{minutes:2.48,words:744},t:"js能识别最大的正整数",y:"a"}}],["/frontweb/es6/promise.html",{loader:()=>g(()=>import("./promise.html-B1x3cimS.js"),__vite__mapDeps([45,1])),meta:{d:16630272e5,l:"2022年9月13日",o:!0,e:`

    1. Promise介绍

    +

    如果你的数据库使用的是19位的ID,而浏览器控制台显示错误,可能是因为浏览器默认将超过16位的整数值视为科学计数法表示。在科学计数法表示下,可能会丢失精度或显示不正确。

    `,r:{minutes:2.48,words:744},t:"js能识别最大的正整数",y:"a"}}],["/frontweb/es6/promise.html",{loader:()=>g(()=>import("./promise.html-Dqg8o8wE.js"),__vite__mapDeps([45,1])),meta:{d:16630272e5,l:"2022年9月13日",o:!0,e:`

    1. Promise介绍

    romise 是 JavaScript 中用于异步编程的一种解决方案。Promise 可以将异步操作进行封装,并提供了更加灵活和强大的处理方式。

    Promise 有三种状态:

      @@ -368,7 +368,7 @@ this的指向,是在调用函数时根据执行上下文所动态确定的。
    • fulfilled(成功):意味着操作成功完成,Promise 实例的最终值可通过 then 方法获取到。
    • rejected(失败):意味着操作失败,Promise 实例的最终值可通过 catch 方法获取到。
    -

    Promise 实例可以使用 then、catch 和 finally 方法实现异步操作的链式调用:

    `,r:{minutes:4.59,words:1377},t:"Promise介绍",y:"a"}}],["/frontweb/es6/useModule.html",{loader:()=>g(()=>import("./useModule.html-BNNVuyQF.js"),__vite__mapDeps([46,1])),meta:{a:"qianxun",d:1635181021e3,l:"2021年10月25日",c:["vue知识点"],g:["必会"],e:`

    一,什么是前端模块化

    +

    Promise 实例可以使用 then、catch 和 finally 方法实现异步操作的链式调用:

    `,r:{minutes:4.59,words:1377},t:"Promise介绍",y:"a"}}],["/frontweb/es6/useModule.html",{loader:()=>g(()=>import("./useModule.html-DpfNyX4_.js"),__vite__mapDeps([46,1])),meta:{a:"qianxun",d:1635181021e3,l:"2021年10月25日",c:["vue知识点"],g:["必会"],e:`

    一,什么是前端模块化

    模块化就是将一个复杂的应用程序,按照规范拆分成几个相互独立的文件,这些文件里面完成共同的或者类似的逻辑,通过对外暴露一些数据或者调用方法,与外部整合。

    这样每个文件彼此独立,我们开发者更容易开发和维护代码,特别是当开发的项目越来越大,代码复杂性也不断增加,这对于模块化的需求也会越来越大。

    模块化主要特点是:可复用性、可组合性、独立性、中心化。

    @@ -379,12 +379,12 @@ this的指向,是在调用函数时根据执行上下文所动态确定的。
  • 性能优化:异步加载模块对页面性能会非常好
  • 模块的版本管理:通过别名等配置,配合构建工具,可以实现模块的版本管理
  • 跨环境共享模块:通过 Sea.jsNodeJS版本,可以实现模块的跨服务器和浏览器共享
  • -`,r:{minutes:9.33,words:2800},t:"前端模块化",y:"a"}}],["/frontweb/es6/useNpm.html",{loader:()=>g(()=>import("./useNpm.html-BN-sNads.js"),__vite__mapDeps([47,1])),meta:{a:"qianxun",d:1634230621e3,l:"2021年10月14日",c:["vue知识点"],g:["必会"],e:`

    一,什么是npm

    +`,r:{minutes:9.33,words:2800},t:"前端模块化",y:"a"}}],["/frontweb/es6/useNpm.html",{loader:()=>g(()=>import("./useNpm.html-CO34N8n0.js"),__vite__mapDeps([47,1])),meta:{a:"qianxun",d:1634230621e3,l:"2021年10月14日",c:["vue知识点"],g:["必会"],e:`

    一,什么是npm

    npm是node官方的包管理工具 cnpm 是淘宝NPM镜像官网,来自淘宝NPM镜像官网的说明:

    淘宝为我们搭建了一个国内的npm服务器,它目前是每隔10分钟将国外npm仓库的所有内容“搬运”回国内的服务器上,这样我们直接访问淘宝的国内服务器就可以了,它的地址是:https://registry.npm.taobao.org

    -
    `,r:{minutes:1.5,words:449},t:"搞懂npm与cnpm",y:"a"}}],["/frontweb/es6/usePnpm.html",{loader:()=>g(()=>import("./usePnpm.html-DUVqoXBg.js"),__vite__mapDeps([48,1])),meta:{a:"qianxun",d:1634230621e3,l:"2021年10月14日",c:["npm知识点"],g:["必会"],e:`

    一,什么是pnpm

    +`,r:{minutes:1.5,words:449},t:"搞懂npm与cnpm",y:"a"}}],["/frontweb/es6/usePnpm.html",{loader:()=>g(()=>import("./usePnpm.html-DeDjO0-6.js"),__vite__mapDeps([48,1])),meta:{a:"qianxun",d:1634230621e3,l:"2021年10月14日",c:["npm知识点"],g:["必会"],e:`

    一,什么是pnpm

    performant npm ,意味“高性能的 npm”。pnpm由npm/yarn衍生而来,解决了npm/yarn内部潜在的bug,极大的优化了性能,扩展了使用场景。被誉为“最先进的包管理工具”

    二,特点

      @@ -396,7 +396,7 @@ cnpm 是淘宝NPM镜像官网,来自g(()=>import("./useYarn.html-CGJq63Qp.js"),__vite__mapDeps([49,1])),meta:{a:"qianxun",d:1634230621e3,l:"2021年10月14日",c:["vue知识点"],g:["必会"],e:`

      一,yarn简介

      +
    `,r:{minutes:2.27,words:682},t:"搞懂npm与pnpm",y:"a"}}],["/frontweb/es6/useYarn.html",{loader:()=>g(()=>import("./useYarn.html-DWtrjvbO.js"),__vite__mapDeps([49,1])),meta:{a:"qianxun",d:1634230621e3,l:"2021年10月14日",c:["vue知识点"],g:["必会"],e:`

    一,yarn简介

    yarn 是由 Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具,yarn 是为了弥补 npm 的一些缺陷而出现的。

    二,npm的缺陷

    `,r:{minutes:1.06,words:319},t:"npm与yarn的区别",y:"a"}}],["/frontweb/nodejs/",{loader:()=>g(()=>import("./index.html-CQVuIZLy.js"),__vite__mapDeps([50,1])),meta:{a:"chenkun",d:17235936e5,l:"2024年8月14日",c:["问题定位"],e:`

    +`,r:{minutes:.05,words:16},t:"NodeJS",y:"a"}}],["/frontweb/nodejs/%E5%AE%89%E8%A3%85%E9%97%AE%E9%A2%98.html",{loader:()=>g(()=>import("./安装问题.html-fx1na5Iw.js"),__vite__mapDeps([51,1])),meta:{a:"chenkun",d:17235936e5,l:"2024年8月14日",e:`

    背景

    从nodejs18开始就不支持Centos7了,这是因为centos7的gilbc版本比较低,因此需要安装非官方构建的版本。

    Note:如果npm安装的包依赖于glibc,那得改用docker或者换操作系统了。

    @@ -425,9 +425,9 @@ cnpm 是淘宝NPM镜像官网,来自
    tar -zxvf node-v18.19.0-linux-x64-glibc-217.tar.gz
     mv node-v18.19.0-linux-x64-glibc-217  node-v18
    -`,r:{minutes:.39,words:117},t:"CentOS7安装node18",y:"a"}}],["/frontweb/typeScript/",{loader:()=>g(()=>import("./index.html-YtRDZV2L.js"),__vite__mapDeps([52,1])),meta:{a:"qianxun",d:1628182621e3,l:"2021年8月5日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    学习typeScript

    +`,r:{minutes:.39,words:117},t:"CentOS7安装node18",y:"a"}}],["/frontweb/typeScript/",{loader:()=>g(()=>import("./index.html-WPNLoBTh.js"),__vite__mapDeps([52,1])),meta:{a:"qianxun",d:1628182621e3,l:"2021年8月5日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    学习typeScript

    (1)如何进阶TypeScript功底?一文带你理解TS中各种高级语法

    -

    (2) TypeScript学习(十)——缩小类型限制范围

    `,r:{minutes:.37,words:112},t:"typeScript学习资料",y:"a"}}],["/frontweb/typeScript/action-usage.html",{loader:()=>g(()=>import("./action-usage.html-CMjNTcaP.js"),__vite__mapDeps([53,1])),meta:{a:"qianxun",d:1634749021e3,l:"2021年10月20日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    一,利用typeScript实现新增,删除一行数据

    +

    (2) TypeScript学习(十)——缩小类型限制范围

    `,r:{minutes:.37,words:112},t:"typeScript学习资料",y:"a"}}],["/frontweb/typeScript/action-usage.html",{loader:()=>g(()=>import("./action-usage.html-rUKU9K_y.js"),__vite__mapDeps([53,1])),meta:{a:"qianxun",d:1634749021e3,l:"2021年10月20日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    一,利用typeScript实现新增,删除一行数据

    这里基本涵盖了typeScript在项目中的实战用法

    1,html部分

    <!DOCTYPE html>
    @@ -464,7 +464,7 @@ cnpm 是淘宝NPM镜像官网,来自	<script src="./script.js"></script>
     </body>
     </html>
    -
    `,r:{minutes:1.33,words:399},t:"typeScript项目实战",y:"a"}}],["/frontweb/typeScript/axios.html",{loader:()=>g(()=>import("./axios.html-BLOOWYNb.js"),__vite__mapDeps([54,1])),meta:{a:"qianxun",d:1636390621e3,l:"2021年11月8日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    项目一

    +
    `,r:{minutes:1.33,words:399},t:"typeScript项目实战",y:"a"}}],["/frontweb/typeScript/axios.html",{loader:()=>g(()=>import("./axios.html-CMfeDl50.js"),__vite__mapDeps([54,1])),meta:{a:"qianxun",d:1636390621e3,l:"2021年11月8日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    项目一

    根据名称获取动漫列表

    1,定义api.type.ts (定义后台接口返回的类型)

    `,r:{minutes:1.4,words:421},t:"typeScript中使用axios",y:"a"}}],["/frontweb/typeScript/basic-usage.html",{loader:()=>g(()=>import("./basic-usage.html-B8UvuKMd.js"),__vite__mapDeps([55,1])),meta:{a:"qianxun",d:1628182621e3,l:"2021年8月5日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    一,模板中的 TypeScript

    +
    `,r:{minutes:1.4,words:421},t:"typeScript中使用axios",y:"a"}}],["/frontweb/typeScript/basic-usage.html",{loader:()=>g(()=>import("./basic-usage.html-CvR9Cwjv.js"),__vite__mapDeps([55,1])),meta:{a:"qianxun",d:1628182621e3,l:"2021年8月5日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    一,模板中的 TypeScript

    在使用了 <script lang="ts"> 或 <script setup lang="ts"> 后,<template> 在绑定表达式中也支持 TypeScript。

    `,r:{minutes:1.94,words:582},t:"typeScript在vue项目中常见用法",y:"a"}}],["/frontweb/typeScript/fanType.html",{loader:()=>g(()=>import("./fanType.html-BMxjGDLX.js"),__vite__mapDeps([56,1])),meta:{a:"qianxun",d:1628182621e3,l:"2021年8月5日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    一,泛型

    +
    `,r:{minutes:1.94,words:582},t:"typeScript在vue项目中常见用法",y:"a"}}],["/frontweb/typeScript/fanType.html",{loader:()=>g(()=>import("./fanType.html-DX1_TAT6.js"),__vite__mapDeps([56,1])),meta:{a:"qianxun",d:1628182621e3,l:"2021年8月5日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    一,泛型

    泛型就是通过给类型传参,得到一个更加通用的类型,就像给函数传参一样。 如下我们得到一个通用的泛型类型 T1,通过传参,可以得到 T2 类型 string[]、T3 类型 number[]; T 是变量,我们可以用任意其他变量名代替他。

    `,r:{minutes:2.13,words:640},t:"typeScript中的泛型",y:"a"}}],["/frontweb/typeScript/tsAndvue3.html",{loader:()=>g(()=>import("./tsAndvue3.html-CfNs7pkY.js"),__vite__mapDeps([57,1])),meta:{a:"qianxun",d:1628182621e3,l:"2021年8月5日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    ts在表单中的应用

    +
    `,r:{minutes:2.13,words:640},t:"typeScript中的泛型",y:"a"}}],["/frontweb/typeScript/tsAndvue3.html",{loader:()=>g(()=>import("./tsAndvue3.html-ulSXqKHP.js"),__vite__mapDeps([57,1])),meta:{a:"qianxun",d:1628182621e3,l:"2021年8月5日",c:["vue知识点"],g:["必会","vue中的 TypeScript"],e:`

    ts在表单中的应用

    `,r:{minutes:1.81,words:543},t:"typeScript在vue3中的实战",y:"a"}}],["/frontweb/vite/",{loader:()=>g(()=>import("./index.html-Cluv8BOI.js"),__vite__mapDeps([58,1])),meta:{a:"qianxun",d:15821568e5,l:"2020年2月20日",c:["vite"],g:["vite"],r:{minutes:.05,words:14},t:"Vite",y:"a"}}],["/frontweb/vue/",{loader:()=>g(()=>import("./index.html-Dpjcv5aI.js"),__vite__mapDeps([59,1])),meta:{a:"Zxf",d:1616544e6,l:"2021年3月24日",g:["你所不了解的JavaScript"],r:{minutes:.05,words:16},t:"Vue",y:"a"}}],["/frontweb/vue/elementui%E8%A1%A8%E5%8D%95%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%A1%E9%AA%8C.html",{loader:()=>g(()=>import("./elementui表单自定义校验.html-BU27BiWU.js"),__vite__mapDeps([60,1])),meta:{a:"chensino",d:17199648e5,l:"2024年7月3日",o:!0,e:`

    常规

    +
    `,r:{minutes:1.81,words:543},t:"typeScript在vue3中的实战",y:"a"}}],["/frontweb/vite/",{loader:()=>g(()=>import("./index.html-BTa4HD8h.js"),__vite__mapDeps([58,1])),meta:{a:"qianxun",d:15821568e5,l:"2020年2月20日",c:["vite"],g:["vite"],r:{minutes:.05,words:14},t:"Vite",y:"a"}}],["/frontweb/vue/",{loader:()=>g(()=>import("./index.html-ClGHx_x3.js"),__vite__mapDeps([59,1])),meta:{a:"Zxf",d:1616544e6,l:"2021年3月24日",g:["你所不了解的JavaScript"],r:{minutes:.05,words:16},t:"Vue",y:"a"}}],["/frontweb/vue/elementui%E8%A1%A8%E5%8D%95%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%A1%E9%AA%8C.html",{loader:()=>g(()=>import("./elementui表单自定义校验.html-Bd7ZB8Po.js"),__vite__mapDeps([60,1])),meta:{a:"chensino",d:17199648e5,l:"2024年7月3日",o:!0,e:`

    常规

    `,r:{minutes:1.18,words:354},t:"ElementUI表单自定义校验",y:"a"}}],["/frontweb/vue/eventBus.html",{loader:()=>g(()=>import("./eventBus.html-DDvVENca.js"),__vite__mapDeps([61,1])),meta:{a:"qianxun",d:1615309021e3,l:"2021年3月9日",c:["vue知识点"],g:["必会"],e:`

    1,安装mitt库

    +
    `,r:{minutes:1.18,words:354},t:"ElementUI表单自定义校验",y:"a"}}],["/frontweb/vue/eventBus.html",{loader:()=>g(()=>import("./eventBus.html-md5G45mS.js"),__vite__mapDeps([61,1])),meta:{a:"qianxun",d:1615309021e3,l:"2021年3月9日",c:["vue知识点"],g:["必会"],e:`

    1,安装mitt库

    npm install mitt
    -
    `,r:{minutes:.45,words:136},t:"事件总线Mitt",y:"a"}}],["/frontweb/vue/vue-Direactive.html",{loader:()=>g(()=>import("./vue-Direactive.html-DMCWSWu1.js"),__vite__mapDeps([62,1])),meta:{a:"qianxun",d:16261344e5,l:"2021年7月13日",c:["vue知识点"],g:["必会"],e:` +
    `,r:{minutes:.45,words:136},t:"事件总线Mitt",y:"a"}}],["/frontweb/vue/vue-Direactive.html",{loader:()=>g(()=>import("./vue-Direactive.html-Ddh2keS8.js"),__vite__mapDeps([62,1])),meta:{a:"qianxun",d:16261344e5,l:"2021年7月13日",c:["vue知识点"],g:["必会"],e:`

    一,自定义指令介绍

    除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。

    vue中重用代码,3种方式:

    @@ -580,7 +580,7 @@ cnpm 是淘宝NPM镜像官网,来自
    } } <input v-focus />
    -
    `,r:{minutes:2.99,words:898},t:"vue自定义指令控制按钮级别权限",y:"a"}}],["/frontweb/vue/vue-authority.html",{loader:()=>g(()=>import("./vue-authority.html-C-Omz_-1.js"),__vite__mapDeps([63,1])),meta:{a:"qianxun",d:16255296e5,l:"2021年7月6日",c:["vue知识点"],g:["必会"],e:`

    前端主要有哪些权限控制?

    +
    `,r:{minutes:2.99,words:898},t:"vue自定义指令控制按钮级别权限",y:"a"}}],["/frontweb/vue/vue-authority.html",{loader:()=>g(()=>import("./vue-authority.html-CuO4qx38.js"),__vite__mapDeps([63,1])),meta:{a:"qianxun",d:16255296e5,l:"2021年7月6日",c:["vue知识点"],g:["必会"],e:`

    前端主要有哪些权限控制?

    一,接口访问权限

    接口权限目前一般采用通用的形式来验证(用户是否登录系统),没有的话一般返回401,跳转到登录页面重新进行登录 ,登录成功后拿到token,将token存起来,通过axios请求拦截器进行拦截,每次请求的时候头部携带token

    
    @@ -593,9 +593,9 @@ cnpm 是淘宝NPM镜像官网,来自        router.push('/login')
         }
     })
    -
    `,r:{minutes:2.54,words:761},t:"vue中权限相关的问题",y:"a"}}],["/frontweb/vue/vue-in-action.html",{loader:()=>g(()=>import("./vue-in-action.html-pzeMhg7a.js"),__vite__mapDeps([64,1])),meta:{a:"qianxun",d:16255296e5,l:"2021年7月6日",c:["vue知识点"],g:["必会"],e:`

    一、创建一个新项目

    +`,r:{minutes:2.54,words:761},t:"vue中权限相关的问题",y:"a"}}],["/frontweb/vue/vue-in-action.html",{loader:()=>g(()=>import("./vue-in-action.html-CmgeKTEO.js"),__vite__mapDeps([64,1])),meta:{a:"qianxun",d:16255296e5,l:"2021年7月6日",c:["vue知识点"],g:["必会"],e:`

    一、创建一个新项目

    vue create vue2-admin
    -
    `,r:{minutes:1.15,words:346},t:"利用vue-cli搭建项目",y:"a"}}],["/frontweb/vue/vue-nextTick.html",{loader:()=>g(()=>import("./vue-nextTick.html-a7ywGpHU.js"),__vite__mapDeps([65,1])),meta:{a:"qianxun",d:1615248e6,l:"2021年3月9日",c:["vue知识点"],g:["必会"],e:` +`,r:{minutes:1.15,words:346},t:"利用vue-cli搭建项目",y:"a"}}],["/frontweb/vue/vue-nextTick.html",{loader:()=>g(()=>import("./vue-nextTick.html-DoRZxd81.js"),__vite__mapDeps([65,1])),meta:{a:"qianxun",d:1615248e6,l:"2021年3月9日",c:["vue知识点"],g:["必会"],e:`

    一,什么是nextTick

    定义:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

    @@ -603,14 +603,14 @@ cnpm 是淘宝NPM镜像官网,来自
    g(()=>import("./vue-pic.html-BNNPKouB.js"),__vite__mapDeps([66,1])),meta:{a:"qianxun",d:1615309021e3,l:"2021年3月9日",c:["vue知识点"],g:["必会"],e:`

    一,在vue中静态导入相对路径

    +

    理解:nextTick(),是将回调函数延迟在下一次dom更新数据后调用,简单的理解是:当你通过数据更新了页面后,想获 取更新后的DOM,就需要使用到nextTick.

    `,r:{minutes:2.82,words:846},t:"关于vue-nextTick",y:"a"}}],["/frontweb/vue/vue-pic.html",{loader:()=>g(()=>import("./vue-pic.html-D3e779S-.js"),__vite__mapDeps([66,1])),meta:{a:"qianxun",d:1615309021e3,l:"2021年3月9日",c:["vue知识点"],g:["必会"],e:`

    一,在vue中静态导入相对路径

    <img src="../../assets/1.png" />
     <!-- 或者如下 -->
     <img src="@/assets/1.png" />
    -
    `,r:{minutes:1.47,words:441},t:"vue图片路径问题",y:"a"}}],["/frontweb/vue/vue-router1.html",{loader:()=>g(()=>import("./vue-router1.html-CUUy_5yC.js"),__vite__mapDeps([67,1])),meta:{a:"qianxun",d:16336512e5,l:"2021年10月8日",c:["vue知识点"],g:["必会"],e:` +`,r:{minutes:1.47,words:441},t:"vue图片路径问题",y:"a"}}],["/frontweb/vue/vue-router1.html",{loader:()=>g(()=>import("./vue-router1.html-BsuAzZy1.js"),__vite__mapDeps([67,1])),meta:{a:"qianxun",d:16336512e5,l:"2021年10月8日",c:["vue知识点"],g:["必会"],e:`

    一,安装

    npm install vue-router@4
    -
    `,r:{minutes:1.34,words:402},t:"vue-router4.0的基本使用",y:"a"}}],["/frontweb/vue/vue-router2.html",{loader:()=>g(()=>import("./vue-router2.html-wszoFzb3.js"),__vite__mapDeps([68,1])),meta:{a:"qianxun",d:16336512e5,l:"2021年10月8日",c:["vue知识点"],g:["必会"],e:"\n

    一, 路由的本质

    \n

    简单来说,浏览器端路由其实并不是真实的网页跳转(和服务器没有任何交互),而是纯粹在浏览器端发生的一系列行为,本质上来说前端路由就是:

    \n

    对 url 进行改变和监听,来让某个 dom 节点显示对应的视图

    \n

    二, 路由的区别

    \n

    一般来说,浏览器端的路由分为两种:

    \n
    1. hash 路由,特征是` url` 后面会有 # 号,如` baidu.com/#foo/bar/baz`。\n2. history路由,`url` 和普通路径没有差异。如 `baidu.com/foo/bar/baz`。\n
    ",r:{minutes:13.85,words:4154},t:"vue-router源码浅析",y:"a"}}],["/frontweb/vue/vue3Emit.html",{loader:()=>g(()=>import("./vue3Emit.html-dWosLNLo.js"),__vite__mapDeps([69,1])),meta:{d:16497216e5,l:"2022年4月12日",g:["ts","vue3"],o:!0,e:`

    vue3 使用组合式api时如何进行父子组件通信

    +`,r:{minutes:1.34,words:402},t:"vue-router4.0的基本使用",y:"a"}}],["/frontweb/vue/vue-router2.html",{loader:()=>g(()=>import("./vue-router2.html-CFBH3XYr.js"),__vite__mapDeps([68,1])),meta:{a:"qianxun",d:16336512e5,l:"2021年10月8日",c:["vue知识点"],g:["必会"],e:"\n

    一, 路由的本质

    \n

    简单来说,浏览器端路由其实并不是真实的网页跳转(和服务器没有任何交互),而是纯粹在浏览器端发生的一系列行为,本质上来说前端路由就是:

    \n

    对 url 进行改变和监听,来让某个 dom 节点显示对应的视图

    \n

    二, 路由的区别

    \n

    一般来说,浏览器端的路由分为两种:

    \n
    1. hash 路由,特征是` url` 后面会有 # 号,如` baidu.com/#foo/bar/baz`。\n2. history路由,`url` 和普通路径没有差异。如 `baidu.com/foo/bar/baz`。\n
    ",r:{minutes:13.85,words:4154},t:"vue-router源码浅析",y:"a"}}],["/frontweb/vue/vue3Emit.html",{loader:()=>g(()=>import("./vue3Emit.html-Bi1THuTe.js"),__vite__mapDeps([69,1])),meta:{d:16497216e5,l:"2022年4月12日",g:["ts","vue3"],o:!0,e:`

    vue3 使用组合式api时如何进行父子组件通信

    `,r:{minutes:.55,words:164},t:"vue3使用emit进行父子组件传值",y:"a"}}],["/frontweb/vue/vue3LifeTime.html",{loader:()=>g(()=>import("./vue3LifeTime.html-CTIvTlow.js"),__vite__mapDeps([70,1])),meta:{a:"qianxun",d:16255296e5,l:"2021年7月6日",c:["vue知识点"],g:["必会"],e:`

    一,vue生命周期钩子函数

    +`,r:{minutes:.55,words:164},t:"vue3使用emit进行父子组件传值",y:"a"}}],["/frontweb/vue/vue3LifeTime.html",{loader:()=>g(()=>import("./vue3LifeTime.html-raVaHUQD.js"),__vite__mapDeps([70,1])),meta:{a:"qianxun",d:16255296e5,l:"2021年7月6日",c:["vue知识点"],g:["必会"],e:`

    一,vue生命周期钩子函数

    d0c8a786c9177f3e668177cd4bfcf9c19e3d5676
    d0c8a786c9177f3e668177cd4bfcf9c19e3d5676
    -

    1.1 beforeCreate

    `,r:{minutes:5.35,words:1604},t:"vue中组件的生命周期",y:"a"}}],["/frontweb/vue/vueExtend.html",{loader:()=>g(()=>import("./vueExtend.html-DMEGEpFb.js"),__vite__mapDeps([71,1])),meta:{a:"qianxun",d:16255296e5,l:"2021年7月6日",c:["vue知识点"],g:["必会"],e:` +

    1.1 beforeCreate

    `,r:{minutes:5.35,words:1604},t:"vue中组件的生命周期",y:"a"}}],["/frontweb/vue/vueExtend.html",{loader:()=>g(()=>import("./vueExtend.html-DXi8EFTR.js"),__vite__mapDeps([71,1])),meta:{a:"qianxun",d:16255296e5,l:"2021年7月6日",c:["vue知识点"],g:["必会"],e:`

    一,vue.extend基本概念和用法

    使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

    data 选项是特例,需要注意 - 在 Vue.extend() 中它必须是函数。

    <div id="mount-point"></div>
    -
    `,r:{minutes:2.69,words:807},t:"利用Vue.extend定义全局组件",y:"a"}}],["/frontweb/vue/vue%E9%A1%B9%E7%9B%AE%E6%9C%AC%E5%9C%B0%E4%B8%80%E7%9B%B4%E5%8F%91websocket.html",{loader:()=>g(()=>import("./vue项目本地一直发websocket.html-wKQRhvFR.js"),__vite__mapDeps([72,1])),meta:{d:15221952e5,l:"2018年3月28日",e:`
    1. 背景
    +`,r:{minutes:2.69,words:807},t:"利用Vue.extend定义全局组件",y:"a"}}],["/frontweb/vue/vue%E9%A1%B9%E7%9B%AE%E6%9C%AC%E5%9C%B0%E4%B8%80%E7%9B%B4%E5%8F%91websocket.html",{loader:()=>g(()=>import("./vue项目本地一直发websocket.html-BJoQpGdp.js"),__vite__mapDeps([72,1])),meta:{d:15221952e5,l:"2018年3月28日",e:`
    1. 背景

    去年开始学习vue,用vue写了远程视频+聊天的程序,其中聊天用的是websocket。但是最近我发现vue项目即使不引入websocket包,在依赖中也会自动下载,并且每次在浏览器控制台会多一个websocket的请求,因为之前是用的socket聊天,所以我以为这个websocket是我引入的,知道最近我才发现不是这样的。 下图是一个直接用webpack初始化的一个项目,没有手动添加任何包,可以看到webpack自动下载了websocket包,并且在项目启动后,在控制台可以看到自动发送了websocket请求。 -

    `,r:{minutes:2.12,words:637},t:"探索vue本地启动一直发送websocket请求",y:"a"}}],["/frontweb/vue/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8C%87%E4%BB%A4%E7%9A%84%E5%AE%9E%E8%B7%B5.html",{loader:()=>g(()=>import("./自定义指令的实践.html-NCx1Pibh.js"),__vite__mapDeps([73,1])),meta:{a:"chensino",d:17201376e5,l:"2024年7月5日",o:!0,e:`

    需求

    +

    `,r:{minutes:2.12,words:637},t:"探索vue本地启动一直发送websocket请求",y:"a"}}],["/frontweb/vue/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8C%87%E4%BB%A4%E7%9A%84%E5%AE%9E%E8%B7%B5.html",{loader:()=>g(()=>import("./自定义指令的实践.html-hhrMuLV0.js"),__vite__mapDeps([73,1])),meta:{a:"chensino",d:17201376e5,l:"2024年7月5日",o:!0,e:`

    需求

    需求是: 在前端页面有很多文本,要求提取文本,根据规则判断文本是否和规则互斥,比如“女”和“前列腺”就是互斥的。当互斥时,需要高亮,同时当鼠标 指在文本上,弹出提示框,提示“女和前列腺规则互斥” 在页面中,通过自定义指令,实现对文本的局部高亮,其中高亮内容是通过接口动态获取。

    @@ -713,7 +713,7 @@ cnpm 是淘宝NPM镜像官网,来自
    } }, } -`,r:{minutes:1.94,words:581},t:"使用自定义指令实现高亮",y:"a"}}],["/java/advance/Arthas.html",{loader:()=>g(()=>import("./Arthas.html-CX1s5XJ4.js"),__vite__mapDeps([74,1])),meta:{a:"chenkun",d:1619568e6,l:"2021年4月28日",c:["问题定位"],e:` +`,r:{minutes:1.94,words:581},t:"使用自定义指令实现高亮",y:"a"}}],["/java/advance/Arthas.html",{loader:()=>g(()=>import("./Arthas.html-iIzMcD21.js"),__vite__mapDeps([74,1])),meta:{a:"chenkun",d:1619568e6,l:"2021年4月28日",c:["问题定位"],e:`

    背景:有一个导出PDF的功能,在本地运行正常,在线上异常,抛出的异常无法直接定位到问题。

    @@ -730,15 +730,15 @@ cnpm 是淘宝NPM镜像官网,来自
    [1]: 134242 ccs-gateway.jar [2]: 47802 ccs-data-biz.jar 2 -`,r:{minutes:1.04,words:313},t:"使用Arthas定位线上问题",y:"a"}}],["/java/advance/Collection.html",{loader:()=>g(()=>import("./Collection.html-UjyrsFMx.js"),__vite__mapDeps([75,1])),meta:{d:15221952e5,l:"2018年3月28日",c:["集合"],g:["集合"],e:`

    分析集合数据结构,HashMap、ArrayList、LinkedList扩容原理等

    +`,r:{minutes:1.04,words:313},t:"使用Arthas定位线上问题",y:"a"}}],["/java/advance/Collection.html",{loader:()=>g(()=>import("./Collection.html-DcRVdFY1.js"),__vite__mapDeps([75,1])),meta:{d:15221952e5,l:"2018年3月28日",c:["集合"],g:["集合"],e:`

    分析集合数据结构,HashMap、ArrayList、LinkedList扩容原理等

    1、Queue 队列

    1.1 queue类图

    -

    queue是java中的队列,可以实现队列特性,即:先进先出,先进先出这里就说明了要从队列中移除元素,只能从头部移除,因为要保证先进先出

    `,r:{minutes:3.12,words:937},t:"java集合",y:"a"}}],["/java/advance/CompileJdk11.html",{loader:()=>g(()=>import("./CompileJdk11.html-B-6Zabjd.js"),__vite__mapDeps([76,1])),meta:{a:"chenkun",d:1642032e6,l:"2022年1月13日",c:["jdk"],e:` +

    queue是java中的队列,可以实现队列特性,即:先进先出,先进先出这里就说明了要从队列中移除元素,只能从头部移除,因为要保证先进先出

    `,r:{minutes:3.12,words:937},t:"java集合",y:"a"}}],["/java/advance/CompileJdk11.html",{loader:()=>g(()=>import("./CompileJdk11.html-DBmPA9mE.js"),__vite__mapDeps([76,1])),meta:{a:"chenkun",d:1642032e6,l:"2022年1月13日",c:["jdk"],e:`

    1 下载源码

    网上根据关键词查找jdk源码,查找出来很多可以下载源码的链接,这里我们使用github去官方仓库,openjdk是托管在github的OpenJDK组织下,该组织下有各个版本的openjdk源码,不要直接使用jdk仓库,这个仓库存放的是当前正在开发的最新版本代码,我们要用的是jdk11,因此我们搜索jdk11仓库,我这里选择的是jdk11u这个库。

    -
    20230113095229
    20230113095229
    `,r:{minutes:4.54,words:1363},t:"在Manjaro中编译JDK11",y:"a"}}],["/java/advance/Concurrent.html",{loader:()=>g(()=>import("./Concurrent.html-DRLYghtT.js"),__vite__mapDeps([77,1])),meta:{d:1645056e6,l:"2022年2月17日",g:"-- 并发",o:!0,e:`

    1、背景

    +
    20230113095229
    20230113095229
    `,r:{minutes:4.54,words:1363},t:"在Manjaro中编译JDK11",y:"a"}}],["/java/advance/Concurrent.html",{loader:()=>g(()=>import("./Concurrent.html-iguxRgDL.js"),__vite__mapDeps([77,1])),meta:{d:1645056e6,l:"2022年2月17日",g:"-- 并发",o:!0,e:`

    1、背景

    因没做过大项目,并发经验匮乏,所以很多时候考虑不到并发问题,今天做接口的压力测试,无意间发现一个并发的问题,就是判断数据库是否存在某个记录时,如果没做并发处理,就会出现并发的问题,比如以下代码,给某个医院添加科室,当医院已经有这个科室则不允许重复添加重名的科室

    public void addHospitalDept(SysHospitalDept sysHospitalDept) {
             //根据医院id,科室名,校验科室唯一性
    @@ -752,25 +752,25 @@ cnpm 是淘宝NPM镜像官网,来自		sysHospitalDept.setCreateTime(LocalDateTime.now());
     		this.save(sysHospitalDept);
     	}
    -
    `,r:{minutes:1.57,words:471},t:"并发问题",y:"a"}}],["/java/advance/Future.html",{loader:()=>g(()=>import("./Future.html-axBzZo7a.js"),__vite__mapDeps([78,1])),meta:{a:"chenkun",d:16176672e5,l:"2021年4月6日",c:["线程池","多线程"],g:["多线程","线程池"],e:` +`,r:{minutes:1.57,words:471},t:"并发问题",y:"a"}}],["/java/advance/Future.html",{loader:()=>g(()=>import("./Future.html-CSv-67Ma.js"),__vite__mapDeps([78,1])),meta:{a:"chenkun",d:16176672e5,l:"2021年4月6日",c:["线程池","多线程"],g:["多线程","线程池"],e:`

    1、Future的作用

    Future可以用来获取一个异步执行的结果,可以使用isDone方法检查异步任务是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。

    -`,r:{minutes:.36,words:107},t:"多线程中的Future",y:"a"}}],["/java/advance/IO-Model.html",{loader:()=>g(()=>import("./IO-Model.html-BT2SPf7N.js"),__vite__mapDeps([79,1])),meta:{a:"勤劳的小手",d:16720992e5,l:"2022年12月27日",e:`

    本文是转载于知乎100%弄明白5种IO模型,原作者勤劳的小手,在原博客基础上整理了其他 +`,r:{minutes:.36,words:107},t:"多线程中的Future",y:"a"}}],["/java/advance/IO-Model.html",{loader:()=>g(()=>import("./IO-Model.html-AwEDHwi6.js"),__vite__mapDeps([79,1])),meta:{a:"勤劳的小手",d:16720992e5,l:"2022年12月27日",e:`

    本文是转载于知乎100%弄明白5种IO模型,原作者勤劳的小手,在原博客基础上整理了其他 与IO模型相关的优质内容。

    1、从TCP发送数据的流程说起

    所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
     
     需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。
    -
    `,r:{minutes:22.98,words:6893},t:"100%搞懂5中I/O模型",y:"a"}}],["/java/advance/IO-model1.html",{loader:()=>g(()=>import("./IO-model1.html-B5naQOu0.js"),__vite__mapDeps([80,1])),meta:{d:16443648e5,l:"2022年2月9日",e:`

    1 BIO

    +`,r:{minutes:22.98,words:6893},t:"100%搞懂5中I/O模型",y:"a"}}],["/java/advance/IO-model1.html",{loader:()=>g(()=>import("./IO-model1.html-CW-n6NiB.js"),__vite__mapDeps([80,1])),meta:{d:16443648e5,l:"2022年2月9日",e:`

    1 BIO

    当给BIO的accept和read方法加上超时机制后,可以在代码层面解决阻塞问题,但这不是真正的非阻塞,通常我们说的非阻塞是指的操作系统层面的非阻塞,就是当accept通过jni调用native方法后,最终系统不会一直被阻塞。真正的非阻塞是操作系统增加非阻塞功能后,java同步添加java.nio出现以后才有的,因此通过java实现非阻塞,需要调用nio中的类。

    2 NIO

    3 IO多路复用

    4 异步IO

    5 事件驱动的io

    6 reactor线程模型

    -

    reactor线程模型可参考Scalable IO in java,该书作者也是java.nio的作者

    `,r:{minutes:2.64,words:791},t:"I/O模型总结",y:"a"}}],["/java/advance/ImplementSameInterface.html",{loader:()=>g(()=>import("./ImplementSameInterface.html-kaw5w828.js"),__vite__mapDeps([81,1])),meta:{d:1641168e6,l:"2022年1月3日",g:["oauth","sso"],e:`

    1、背景

    +

    reactor线程模型可参考Scalable IO in java,该书作者也是java.nio的作者

    `,r:{minutes:2.64,words:791},t:"I/O模型总结",y:"a"}}],["/java/advance/ImplementSameInterface.html",{loader:()=>g(()=>import("./ImplementSameInterface.html-_LYgzeMO.js"),__vite__mapDeps([81,1])),meta:{d:1641168e6,l:"2022年1月3日",g:["oauth","sso"],e:`

    1、背景

    今天看Securfity的源码,其中org.springframework.security.config.annotation.web.builders.HttpSecurity类的UML看着很奇怪,如下图所示,命名其父类和父接口都实现过SecurityBuilder,为什么自己要再次实现呢?

    -
    20230105160622
    20230105160622
    `,r:{minutes:1.69,words:506},t:"子类和父类(或者父接口)实现同一个接口",y:"a"}}],["/java/advance/MysqlMasterSlave.html",{loader:()=>g(()=>import("./MysqlMasterSlave.html-5i2FdJ-q.js"),__vite__mapDeps([82,1])),meta:{d:15346368e5,l:"2018年8月19日",c:["java"],g:["部署搭建"],e:`

    1、mysql主从复制

    +
    20230105160622
    20230105160622
    `,r:{minutes:1.69,words:506},t:"子类和父类(或者父接口)实现同一个接口",y:"a"}}],["/java/advance/MysqlMasterSlave.html",{loader:()=>g(()=>import("./MysqlMasterSlave.html-IEFzX7F8.js"),__vite__mapDeps([82,1])),meta:{d:15346368e5,l:"2018年8月19日",c:["java"],g:["部署搭建"],e:`

    1、mysql主从复制

    1.1 搭建主从复制目的?

    为了实现读写分离,解决数据库性能问题,读写分离中,“读”的数据是从哪里来呢?其实他是从“写”库copy过来的

    @@ -879,7 +879,7 @@ cnpm 是淘宝NPM镜像官网,来自g(()=>import("./NativeMethod.html-B7wItcKZ.js"),__vite__mapDeps([83,1])),meta:{d:16418592e5,l:"2022年1月11日",e:`

    使用c++实现一个native方法供java调用

    +`,r:{minutes:6.67,words:2e3},t:"mysql8搭建主从复制",y:"a"}}],["/java/advance/NativeMethod.html",{loader:()=>g(()=>import("./NativeMethod.html-ClQ8_R4Z.js"),__vite__mapDeps([83,1])),meta:{d:16418592e5,l:"2022年1月11日",e:`

    使用c++实现一个native方法供java调用

    `,r:{minutes:2.44,words:733},t:"自定义native方法",y:"a"}}],["/java/advance/ParentDelegationClassLoader.html",{loader:()=>g(()=>import("./ParentDelegationClassLoader.html-DD8LdvjV.js"),__vite__mapDeps([84,1])),meta:{a:"chenkun",d:16170624e5,l:"2021年3月30日",u:1,e:`

    为什么说spi打破了双亲委派机制?

    +`,r:{minutes:2.44,words:733},t:"自定义native方法",y:"a"}}],["/java/advance/ParentDelegationClassLoader.html",{loader:()=>g(()=>import("./ParentDelegationClassLoader.html-BTelhs6W.js"),__vite__mapDeps([84,1])),meta:{a:"chenkun",d:16170624e5,l:"2021年3月30日",u:1,e:`

    为什么说spi打破了双亲委派机制?

    1、什么是双亲委派?

    image-20220330170731913
    image-20220330170731913
    -

    注:此处直接摘抄周志明老师的《深入理解java虚拟机》

    `,r:{minutes:12.88,words:3864},t:"证明SPI打破双亲委派模式",y:"a"}}],["/java/advance/ProxyInJava.html",{loader:()=>g(()=>import("./ProxyInJava.html-Bw3KCeSC.js"),__vite__mapDeps([85,1])),meta:{d:161568e7,l:"2021年3月14日",c:["java"],u:1,e:`

    是兄弟就来看我的博客

    -`,r:{minutes:8.46,words:2539},t:"彻底理清Java中的几种代理",y:"a"}}],["/java/advance/",{loader:()=>g(()=>import("./index.html-CuUIxB-V.js"),__vite__mapDeps([86,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.03,words:8},t:"Java进阶",y:"a"}}],["/java/advance/Synchronized.html",{loader:()=>g(()=>import("./Synchronized.html-C99mj8uR.js"),__vite__mapDeps([87,1])),meta:{d:16435872e5,l:"2022年1月31日",c:["对象锁"],e:`

    1、

    -`,r:{minutes:.05,words:15},t:"synchronized实现 原理",y:"a"}}],["/java/advance/ThreadLocal.html",{loader:()=>g(()=>import("./ThreadLocal.html-u61CIRhJ.js"),__vite__mapDeps([88,1])),meta:{a:"chenkun",d:16590528e5,l:"2022年7月29日",e:`

    1、介绍

    +

    注:此处直接摘抄周志明老师的《深入理解java虚拟机》

    `,r:{minutes:12.88,words:3864},t:"证明SPI打破双亲委派模式",y:"a"}}],["/java/advance/ProxyInJava.html",{loader:()=>g(()=>import("./ProxyInJava.html-D4L5WYNV.js"),__vite__mapDeps([85,1])),meta:{d:161568e7,l:"2021年3月14日",c:["java"],u:1,e:`

    是兄弟就来看我的博客

    +`,r:{minutes:8.46,words:2539},t:"彻底理清Java中的几种代理",y:"a"}}],["/java/advance/",{loader:()=>g(()=>import("./index.html-CiCX_JhU.js"),__vite__mapDeps([86,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.03,words:8},t:"Java进阶",y:"a"}}],["/java/advance/Synchronized.html",{loader:()=>g(()=>import("./Synchronized.html-B9MlqwzL.js"),__vite__mapDeps([87,1])),meta:{d:16435872e5,l:"2022年1月31日",c:["对象锁"],e:`

    1、

    +`,r:{minutes:.05,words:15},t:"synchronized实现 原理",y:"a"}}],["/java/advance/ThreadLocal.html",{loader:()=>g(()=>import("./ThreadLocal.html-BAQekg_Z.js"),__vite__mapDeps([88,1])),meta:{a:"chenkun",d:16590528e5,l:"2022年7月29日",e:`

    1、介绍

    2、使用

    3、 使用反射在当前线程获取所有的ThreadLocal

    @@ -970,7 +970,7 @@ cnpm 是淘宝NPM镜像官网,来自 } } } -`,r:{minutes:.77,words:232},t:"ThreadLocal",y:"a"}}],["/java/advance/ThreadPool.html",{loader:()=>g(()=>import("./ThreadPool.html-ChF6p8sJ.js"),__vite__mapDeps([89,1])),meta:{a:"chenkun",d:16168896e5,l:"2021年3月28日",c:["线程池","多线程"],g:["多线程","线程池"],e:`

    线程池

    +`,r:{minutes:.77,words:232},t:"ThreadLocal",y:"a"}}],["/java/advance/ThreadPool.html",{loader:()=>g(()=>import("./ThreadPool.html-xcxHOFbM.js"),__vite__mapDeps([89,1])),meta:{a:"chenkun",d:16168896e5,l:"2021年3月28日",c:["线程池","多线程"],g:["多线程","线程池"],e:`

    线程池

    前言:

    @@ -990,12 +990,12 @@ cnpm 是淘宝NPM镜像官网,来自 Thread.sleep(2000000); } -`,r:{minutes:11.83,words:3548},t:"线程池总结",y:"a"}}],["/java/advance/fail-fast.html",{loader:()=>g(()=>import("./fail-fast.html-DkT_nq5X.js"),__vite__mapDeps([90,1])),meta:{a:"chensino",d:16712352e5,l:"2022年12月17日",o:!1,e:`

    转自https://www.cnblogs.com/54chensongxia/p/12470446.html

    +`,r:{minutes:11.83,words:3548},t:"线程池总结",y:"a"}}],["/java/advance/fail-fast.html",{loader:()=>g(()=>import("./fail-fast.html-CIJb3icO.js"),__vite__mapDeps([90,1])),meta:{a:"chensino",d:16712352e5,l:"2022年12月17日",o:!1,e:`

    转自https://www.cnblogs.com/54chensongxia/p/12470446.html

    fail-fast

    在网上搜了下fail-fast的解释,很多人说fail-fast是Java中集合的一种错误检测机制,比如下面这个网友的解释:

    fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。

    -
    `,r:{minutes:6.9,words:2070},t:"集合Fail-Fast",y:"a"}}],["/java/advance/io.html",{loader:()=>g(()=>import("./io.html-6grZaOsg.js"),__vite__mapDeps([91,1])),meta:{d:16598304e5,l:"2022年8月7日",e:`

    1、参考

    +
    `,r:{minutes:6.9,words:2070},t:"集合Fail-Fast",y:"a"}}],["/java/advance/io.html",{loader:()=>g(()=>import("./io.html-DnJ9HVIs.js"),__vite__mapDeps([91,1])),meta:{d:16598304e5,l:"2022年8月7日",e:`

    1、参考

    `,r:{minutes:5.79,words:1737},t:"I/O模型",y:"a"}}],["/java/base/ConstantPool.html",{loader:()=>g(()=>import("./ConstantPool.html-DqztC09o.js"),__vite__mapDeps([92,1])),meta:{d:1615248e6,l:"2021年3月9日",c:["java基础"],g:["必会"],e:`

    1. Integer常量池默认的范围

    +`,r:{minutes:5.79,words:1737},t:"I/O模型",y:"a"}}],["/java/base/ConstantPool.html",{loader:()=>g(()=>import("./ConstantPool.html-DQ4ql_mB.js"),__vite__mapDeps([92,1])),meta:{d:1615248e6,l:"2021年3月9日",c:["java基础"],g:["必会"],e:`

    1. Integer常量池默认的范围

    范围:[-128,127],Integer内部有个缓存池,最小值-128是固定的,最大的值127是可以调整的,看源码知道, 最大值是和integerCacheHighPropValue有关,这个值是可以通过~java.lang.Integer.IntegerCache.high~ 属性指定,实际测试~~~System.setProperty("java.lang.Integer.IntegerCache.high","300")~~~不生效, 因为他是jvm参数应该在启动时设置vm参数,-Djava.lang.Integer.IntegerCache.high=300 -使用-XX:AutoBoxCacheMax=300也可以。

    `,r:{minutes:.99,words:298},t:"ConstantPool",y:"a"}}],["/java/base/CustomLRU.html",{loader:()=>g(()=>import("./CustomLRU.html-BdYIfH0T.js"),__vite__mapDeps([93,1])),meta:{d:16163712e5,l:"2021年3月22日",c:["java基础"],g:["必会"],e:` +使用-XX:AutoBoxCacheMax=300也可以。

    `,r:{minutes:.99,words:298},t:"ConstantPool",y:"a"}}],["/java/base/CustomLRU.html",{loader:()=>g(()=>import("./CustomLRU.html-DerU6oev.js"),__vite__mapDeps([93,1])),meta:{d:16163712e5,l:"2021年3月22日",c:["java基础"],g:["必会"],e:`

    LRU介绍

    lru(latest recently used)最近最少使用,在缓存中可以使用LRU算法移除最近最少使用的

    自定义lru算法

    @@ -1036,7 +1036,7 @@ cnpm 是淘宝NPM镜像官网,来自 return size() > cacheCount; } } -`,r:{minutes:1.42,words:426},t:"自定义LRU实现",y:"a"}}],["/java/base/IntegerConstantPool.html",{loader:()=>g(()=>import("./IntegerConstantPool.html-BpYi02Iy.js"),__vite__mapDeps([94,1])),meta:{d:15336864e5,l:"2018年8月8日",c:["java基础"],e:`

    1. Integer常量池默认的范围

    +`,r:{minutes:1.42,words:426},t:"自定义LRU实现",y:"a"}}],["/java/base/IntegerConstantPool.html",{loader:()=>g(()=>import("./IntegerConstantPool.html-qcjVgBE7.js"),__vite__mapDeps([94,1])),meta:{d:15336864e5,l:"2018年8月8日",c:["java基础"],e:`

    1. Integer常量池默认的范围

    范围:[-128,127],Integer内部有个缓存池,最小值-128是固定的,最大的值127是可以调整的,看源码知道,最大值是和integerCacheHighPropValue有关,这个值是可以通过~java.lang.Integer.IntegerCache.high~属性指定,实际测试~~~System.setProperty("java.lang.Integer.IntegerCache.high","300")~~~不生效,使用-XX:AutoBoxCacheMax=300可以。

    `,r:{minutes:.92,words:277},t:"Integer常量池",y:"a"}}],["/java/base/",{loader:()=>g(()=>import("./index.html-ckEdUTqL.js"),__vite__mapDeps([95,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.03,words:8},t:"Java基础",y:"a"}}],["/java/base/Serialization.html",{loader:()=>g(()=>import("./Serialization.html-BSKOYFIh.js"),__vite__mapDeps([96,1])),meta:{a:"ChenSino",d:15336864e5,l:"2018年8月8日",c:["java基础"],e:`

    1、序列化、反序列化是什么?

    +`,r:{minutes:.92,words:277},t:"Integer常量池",y:"a"}}],["/java/base/",{loader:()=>g(()=>import("./index.html-D7mz958k.js"),__vite__mapDeps([95,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.03,words:8},t:"Java基础",y:"a"}}],["/java/base/Serialization.html",{loader:()=>g(()=>import("./Serialization.html-OnvjGjMN.js"),__vite__mapDeps([96,1])),meta:{a:"ChenSino",d:15336864e5,l:"2018年8月8日",c:["java基础"],e:`

    1、序列化、反序列化是什么?

    `,r:{minutes:3.98,words:1195},t:"给对象设置null的意义",y:"a"}}],["/java/jvm/StringAdd.html",{loader:()=>g(()=>import("./StringAdd.html-DiHKxvsE.js"),__vite__mapDeps([104,1])),meta:{d:161568e7,l:"2021年3月14日",c:["java","jvm"],g:["字节码","反汇编"],u:2,e:`

    1、先看问题,以下结果是什么?

    `,r:{minutes:3.06,words:917},t:"通过反汇编来看String的拼接",y:"a"}}],["/java/jvm/volatile.html",{loader:()=>g(()=>import("./volatile.html-BgiFSb0F.js"),__vite__mapDeps([105,1])),meta:{d:16436736e5,l:"2022年2月1日",e:`

    1、双重检查的单例模式是否真的线程安全?

    +`,r:{minutes:3.06,words:917},t:"通过反汇编来看String的拼接",y:"a"}}],["/java/jvm/volatile.html",{loader:()=>g(()=>import("./volatile.html-DQhpE7DJ.js"),__vite__mapDeps([105,1])),meta:{d:16436736e5,l:"2022年2月1日",e:`

    1、双重检查的单例模式是否真的线程安全?

    `,r:{minutes:4.63,words:1390},t:"volatile关键字",y:"a"}}],["/java/jvm/%E5%90%8D%E8%AF%8D%E8%A7%A3%E9%87%8A.html",{loader:()=>g(()=>import("./名词解释.html-DixGDuzA.js"),__vite__mapDeps([106,1])),meta:{a:"chenkun",d:17248032e5,l:"2024年8月28日",e:`

    vm中说的字面量和符号引用是什么

    +`,r:{minutes:4.63,words:1390},t:"volatile关键字",y:"a"}}],["/java/jvm/%E5%90%8D%E8%AF%8D%E8%A7%A3%E9%87%8A.html",{loader:()=>g(()=>import("./名词解释.html-gUVey7Ns.js"),__vite__mapDeps([106,1])),meta:{a:"chenkun",d:17248032e5,l:"2024年8月28日",e:`

    vm中说的字面量和符号引用是什么

    在Java虚拟机(JVM)中,字面量和符号引用是与Java程序的编译和执行密切相关的两个概念。它们在Java类文件中表示变量、方法、常量等的不同方式。

    字面量(Literal)

    字面量是程序代码中直接书写的固定值,比如整数、浮点数、字符、字符串等。它们在程序运行时直接表示实际的数据值。

    @@ -1344,7 +1344,7 @@ cnpm 是淘宝NPM镜像官网,来自
    -`,r:{minutes:1.68,words:504},t:"名词解释",y:"a"}}],["/java/other/JdkVersion.html",{loader:()=>g(()=>import("./JdkVersion.html-C-a3tlfZ.js"),__vite__mapDeps([107,1])),meta:{a:"chenkun",d:16606944e5,l:"2022年8月17日",e:`
    +`,r:{minutes:1.68,words:504},t:"名词解释",y:"a"}}],["/java/other/JdkVersion.html",{loader:()=>g(()=>import("./JdkVersion.html-tQ6JXAtC.js"),__vite__mapDeps([107,1])),meta:{a:"chenkun",d:16606944e5,l:"2022年8月17日",e:`

    开发者注意

    Java版本现在6个月发布一个大版本,更新很快,但是对于web开发人员来说,代码层面其实新增的功能并不是那么多,开发人员只需要掌握几个主要的更新内容即可,重点关注LTS版本。

    @@ -1379,15 +1379,15 @@ List、Set、Map 接口中,提供新的静态工厂方法直接创建不可变

    引入响应式流 API Java 9 引入了新的响应式流 API。

    -`,r:{minutes:1.54,words:463},t:"Jdk版本",y:"a"}}],["/other/books/",{loader:()=>g(()=>import("./index.html-DfVsPsyN.js"),__vite__mapDeps([108,1])),meta:{d:16276896e5,l:"2021年7月31日",e:`

    目录

    +`,r:{minutes:1.54,words:463},t:"Jdk版本",y:"a"}}],["/other/books/",{loader:()=>g(()=>import("./index.html-3T2vvqio.js"),__vite__mapDeps([108,1])),meta:{d:16276896e5,l:"2021年7月31日",e:`

    目录

    -`,r:{minutes:.07,words:20},t:"电子书资源",y:"a"}}],["/other/books/ebooks.html",{loader:()=>g(()=>import("./ebooks.html-CmQ0pdLk.js"),__vite__mapDeps([109,1])),meta:{d:16271712e5,l:"2021年7月25日",c:["电子书"],e:`

    1、springboot

    +`,r:{minutes:.07,words:20},t:"电子书资源",y:"a"}}],["/other/books/ebooks.html",{loader:()=>g(()=>import("./ebooks.html-Bvqqu9te.js"),__vite__mapDeps([109,1])),meta:{d:16271712e5,l:"2021年7月25日",c:["电子书"],e:`

    1、springboot

    SpringBoot基本原理-黑马程序员

    2、Nacos原理

    -

    阿里巴巴官方出品

    `,r:{minutes:.16,words:47},t:"电子书资源汇总",y:"a"}}],["/other/computerprinciple/TCP-IP.html",{loader:()=>g(()=>import("./TCP-IP.html-DBoM_5OE.js"),__vite__mapDeps([110,1])),meta:{d:16435008e5,l:"2022年1月30日",e:`

    1、TCP/IP与OSI的关系

    -

    网上学习网络模型时一会是七层osi模型,一会又五层,一会又是四层模型,着实五花八门让人费解,实际上七层模型是osi的标准,目前大家所讲的tcp/ip也是符合osi的标准的,不过是把其中几层合并到一层了而已,点击查看tcp/ip和osi的关系。TCP/IP实际上是一个协议簇,包含很多协议,比如TCP、IP、UDP、ICMP、RIP、TELNETFTP、SMTP、ARP、TFTP等,这些协议一起称为TCP/IP协议。常见协议简单介绍:

    `,r:{minutes:7.86,words:2359},t:"深入理解TCP/IP",y:"a"}}],["/other/database/CPUOverLoad.html",{loader:()=>g(()=>import("./CPUOverLoad.html-BlwjFiRj.js"),__vite__mapDeps([111,1])),meta:{a:"chenkun",d:16267392e5,l:"2021年7月20日",c:["数据库"],e:` +

    阿里巴巴官方出品

    `,r:{minutes:.16,words:47},t:"电子书资源汇总",y:"a"}}],["/other/computerprinciple/TCP-IP.html",{loader:()=>g(()=>import("./TCP-IP.html-CbApdGPu.js"),__vite__mapDeps([110,1])),meta:{d:16435008e5,l:"2022年1月30日",e:`

    1、TCP/IP与OSI的关系

    +

    网上学习网络模型时一会是七层osi模型,一会又五层,一会又是四层模型,着实五花八门让人费解,实际上七层模型是osi的标准,目前大家所讲的tcp/ip也是符合osi的标准的,不过是把其中几层合并到一层了而已,点击查看tcp/ip和osi的关系。TCP/IP实际上是一个协议簇,包含很多协议,比如TCP、IP、UDP、ICMP、RIP、TELNETFTP、SMTP、ARP、TFTP等,这些协议一起称为TCP/IP协议。常见协议简单介绍:

    `,r:{minutes:7.86,words:2359},t:"深入理解TCP/IP",y:"a"}}],["/other/database/CPUOverLoad.html",{loader:()=>g(()=>import("./CPUOverLoad.html-BuVMUTre.js"),__vite__mapDeps([111,1])),meta:{a:"chenkun",d:16267392e5,l:"2021年7月20日",c:["数据库"],e:`

    问题

    某天突然收到预警邮件,服务器CPU超过阈值,并且一直持续居高不下

    @@ -1396,7 +1396,7 @@ Java 9 引入了新的响应式流 API。

    1. 使用htop查看资源消耗,按照CPU使用率降序排列,发现都是mysqld进程占用CPU很高
    2. 进入mysql命令行使用show processlist;查看当前正在执行的命令,经过多次执行show processlist发现有几条固定的sql一直在执行,并且每次传递的参数害不一样
    3. -
    `,r:{minutes:2.12,words:636},t:"Mysql CPU负载过高",y:"a"}}],["/other/database/MysqlCollate.html",{loader:()=>g(()=>import("./MysqlCollate.html-Ddr0cLWB.js"),__vite__mapDeps([112,1])),meta:{d:1645056e6,l:"2022年2月17日",g:["mysql"],o:!0,e:`

    本文转载自此处

    +`,r:{minutes:2.12,words:636},t:"Mysql CPU负载过高",y:"a"}}],["/other/database/MysqlCollate.html",{loader:()=>g(()=>import("./MysqlCollate.html-FwkcBh96.js"),__vite__mapDeps([112,1])),meta:{d:1645056e6,l:"2022年2月17日",g:["mysql"],o:!0,e:`

    本文转载自此处

    在mysql中执行show create table &lt tablename>指令,可以看到一张表的建表语句,example如下:

    CREATE TABLE \`table1\` (
         \`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    @@ -1404,13 +1404,13 @@ Java 9 引入了新的响应式流 API。

    \`field2\` varchar(128) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '字段2', PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8_unicode_ci;
    -
    `,r:{minutes:5.68,words:1704},t:"Mysql中的Collate和charset",y:"a"}}],["/other/database/MysqlNote.html",{loader:()=>g(()=>import("./MysqlNote.html-B-D9CeHp.js"),__vite__mapDeps([113,1])),meta:{a:"chenkun",d:1623024e6,l:"2021年6月7日",e:` +
    `,r:{minutes:5.68,words:1704},t:"Mysql中的Collate和charset",y:"a"}}],["/other/database/MysqlNote.html",{loader:()=>g(()=>import("./MysqlNote.html-BbvwFbUm.js"),__vite__mapDeps([113,1])),meta:{a:"chenkun",d:1623024e6,l:"2021年6月7日",e:`

    1、批量插入速度慢

    项目使用的MyBatis-plus批量插入效率很低,遂百度一下原因

    在jdbc的链接上加上rewriteBatchedStatements=true参数,可以解决此问题。

    -

    默认情况下rewriteBatchedStatements=false,jdbc批量插入会判断rewriteBatchedStatements,当为true才会执行批量语句,以下从源码(以下jdbc驱动源码版本为8.0.20)角度分析:

    `,r:{minutes:1.56,words:468},t:"Mysql知识点记录",y:"a"}}],["/other/database/MysqlRemoteConnect.html",{loader:()=>g(()=>import("./MysqlRemoteConnect.html-xJMa8jPk.js"),__vite__mapDeps([114,1])),meta:{a:"chenkun",d:16206048e5,l:"2021年5月10日",e:` +

    默认情况下rewriteBatchedStatements=false,jdbc批量插入会判断rewriteBatchedStatements,当为true才会执行批量语句,以下从源码(以下jdbc驱动源码版本为8.0.20)角度分析:

    `,r:{minutes:1.56,words:468},t:"Mysql知识点记录",y:"a"}}],["/other/database/MysqlRemoteConnect.html",{loader:()=>g(()=>import("./MysqlRemoteConnect.html-BdtTYKvx.js"),__vite__mapDeps([114,1])),meta:{a:"chenkun",d:16206048e5,l:"2021年5月10日",e:`

    默认情况下,mysql只允许本地登录,如果要开启远程连接,则需要修改/etc/mysql/my.conf文件。Mariadb可以去/etc/my.cnf看看引用的目录配置

    @@ -1422,9 +1422,9 @@ Java 9 引入了新的响应式流 API。

    grant all on . to admin@'%' identified by '123456' with grant option; flush privileges; 允许任何ip地址(%表示允许任何ip地址)的电脑用admin帐户和密码(123456)来访问这个mysql server。 -注意admin账户不一定要存在。

    `,r:{minutes:.76,words:227},t:"Mysql开启远程连接权限",y:"a"}}],["/other/database/",{loader:()=>g(()=>import("./index.html-bDZGzk6b.js"),__vite__mapDeps([115,1])),meta:{a:"chenkun",d:1619568e6,l:"2021年4月28日",e:`

    学习数据库知识

    +注意admin账户不一定要存在。

    `,r:{minutes:.76,words:227},t:"Mysql开启远程连接权限",y:"a"}}],["/other/database/",{loader:()=>g(()=>import("./index.html-Ck4VzfbX.js"),__vite__mapDeps([115,1])),meta:{a:"chenkun",d:1619568e6,l:"2021年4月28日",e:`

    学习数据库知识

    -`,r:{minutes:.07,words:21},t:"数据库",y:"a"}}],["/other/database/Recurse.html",{loader:()=>g(()=>import("./Recurse.html-BYNa9G5A.js"),__vite__mapDeps([116,1])),meta:{d:16492032e5,l:"2022年4月6日",o:!0,e:`
    +`,r:{minutes:.07,words:21},t:"数据库",y:"a"}}],["/other/database/Recurse.html",{loader:()=>g(()=>import("./Recurse.html-Bux7wAbT.js"),__vite__mapDeps([116,1])),meta:{d:16492032e5,l:"2022年4月6日",o:!0,e:`

    注意!!!

    只有在mysql8.0之后才有递归,5.7及之前是不支持的

    @@ -1443,13 +1443,13 @@ flush privileges; JOIN cte ON t.parent_id = cte.id ) SELECT * FROM cte; -
    `,r:{minutes:1.2,words:360},t:"递归下钻",y:"a"}}],["/other/database/SQLOptimization.html",{loader:()=>g(()=>import("./SQLOptimization.html-Dm2zmzdd.js"),__vite__mapDeps([117,1])),meta:{a:"chenkun",d:1619568e6,l:"2021年4月28日",c:["数据库"],e:` +`,r:{minutes:1.2,words:360},t:"递归下钻",y:"a"}}],["/other/database/SQLOptimization.html",{loader:()=>g(()=>import("./SQLOptimization.html-CC5fHt2c.js"),__vite__mapDeps([117,1])),meta:{a:"chenkun",d:1619568e6,l:"2021年4月28日",c:["数据库"],e:`

    背景:

    最近在改一个老项目,其中使用到框架是mybatis,有一个业务表install_record,代表装机记录,一个accessory代表备件表,一个sys_file代表文件表,业务关系是一个install_record对应多个accessory、以及多个sys_file,在一开始使用的是mybatis的嵌套查询的方式,但此方式有N+1的问题,比如一个装机表对应10个accessory、20个sys_file,则就要查询1+10+20 = 31次数据库,效率是很低的,因此想改成嵌套查询的方式。

    -
    `,r:{minutes:1.01,words:303},t:"联合查询sql优化",y:"a"}}],["/other/database/mysql%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6.html",{loader:()=>g(()=>import("./mysql主从复制.html-COc1k69_.js"),__vite__mapDeps([118,1])),meta:{a:"chensino",d:16064352e5,l:"2020年11月27日",o:!0,e:`

    参考博客

    +
    `,r:{minutes:1.01,words:303},t:"联合查询sql优化",y:"a"}}],["/other/database/mysql%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6.html",{loader:()=>g(()=>import("./mysql主从复制.html-DtlLv9Nn.js"),__vite__mapDeps([118,1])),meta:{a:"chensino",d:16064352e5,l:"2020年11月27日",o:!0,e:`

    参考博客

    https://juejin.cn/post/7160580280682545166#heading-23

    -

    https://blog.csdn.net/qq_36357242/article/details/136211471

    `,r:{minutes:1.86,words:558},t:"Mysql主从复制部署",y:"a"}}],["/other/database/mysql%E7%B4%A2%E5%BC%95.html",{loader:()=>g(()=>import("./mysql索引.html-CBuRUHP0.js"),__vite__mapDeps([119,1])),meta:{a:"chensino",d:17330976e5,l:"2024年12月2日",o:!0,e:`

    1.索引分类

    +

    https://blog.csdn.net/qq_36357242/article/details/136211471

    `,r:{minutes:1.86,words:558},t:"Mysql主从复制部署",y:"a"}}],["/other/database/mysql%E7%B4%A2%E5%BC%95.html",{loader:()=>g(()=>import("./mysql索引.html-CuLUAXuj.js"),__vite__mapDeps([119,1])),meta:{a:"chensino",d:17330976e5,l:"2024年12月2日",o:!0,e:`

    1.索引分类

    1.1 主键索引(Primary Key Index)

    • 特点: @@ -1470,7 +1470,7 @@ flush privileges;
    • 在 InnoDB 中,主键索引是一种聚簇索引,存储完整的行数据。
    -`,r:{minutes:6.81,words:2044},t:"Mysql索引",y:"a"}}],["/other/database/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html",{loader:()=>g(()=>import("./数据库备份.html-Dyo0xHVz.js"),__vite__mapDeps([120,1])),meta:{a:"chensino",d:15074208e5,l:"2017年10月8日",o:!0,e:`
    +`,r:{minutes:6.81,words:2044},t:"Mysql索引",y:"a"}}],["/other/database/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD.html",{loader:()=>g(()=>import("./数据库备份.html-DzmVZIdZ.js"),__vite__mapDeps([120,1])),meta:{a:"chensino",d:15074208e5,l:"2017年10月8日",o:!0,e:`

    重要提示

    废弃此方式,直接使用gobackup,具体的参考官网

    @@ -1522,23 +1522,23 @@ flush privileges; #写删除文件日志 echo "【\`date +%Y%m%d-%H:%M:%S\`】delete $delfile" >> $backup_dir/log.txt fi -
    `,r:{minutes:1.04,words:313},t:"定时备份数据库",y:"a"}}],["/other/distributeservice/DistributeLock.html",{loader:()=>g(()=>import("./DistributeLock.html-DphO16c2.js"),__vite__mapDeps([121,1])),meta:{d:14897952e5,l:"2017年3月18日",e:`

    1、写在前面

    +`,r:{minutes:1.04,words:313},t:"定时备份数据库",y:"a"}}],["/other/distributeservice/DistributeLock.html",{loader:()=>g(()=>import("./DistributeLock.html-CblYS_y1.js"),__vite__mapDeps([121,1])),meta:{d:14897952e5,l:"2017年3月18日",e:`

    1、写在前面

    以前一直没搞清楚分布式锁和分布式事务,对其概念以及使用场景很模糊,今天查查资料,好好总结一下分布式事务和分布式锁。另外提前说一句,使用redis来解决分布式、高并发问题存在一些困难,Redis 分布式锁只能作为一种缓解并发的手段,如果要完全解决并发问题,仍需要数据库的防并发手段。

    2、锁和分布式锁解决了什么问题?

    单机锁:解决单进程中多线程同时操作共有数据(比如java堆数据)带来的安全问题,强调的是单机服务中线程安全问题,这种场景很常见,比如单机多线程售票的例子。这种锁直接通过java的本地锁实现即可,可以使用java自带的synchroized和Lock

    -
    `,r:{minutes:9.8,words:2940},t:"分布式锁",y:"a"}}],["/other/distributeservice/Nacos.html",{loader:()=>g(()=>import("./Nacos.html-CHmBYmVv.js"),__vite__mapDeps([122,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",e:`

    1、配置管理

    +
    `,r:{minutes:9.8,words:2940},t:"分布式锁",y:"a"}}],["/other/distributeservice/Nacos.html",{loader:()=>g(()=>import("./Nacos.html-KVubxHn8.js"),__vite__mapDeps([122,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",e:`

    1、配置管理

    命名空间——>Group——>Data Id——>配置项

    命名空间:

    默认使用的命名空间是public,一个命名空间下可以有多个Group,多个服务。命名空间使用场景是租户粒度的隔离,可以给不同租户不同的命名空间,还可以用于隔离不同环境比如dev、test、prod。

    如下图,我给dev环境设置了两个Group,CCS代表中控系统,BOM代表bom系统。

    -
    image-20220602103505994
    image-20220602103505994
    `,r:{minutes:.7,words:209},t:"Nacos学习",y:"a"}}],["/other/distributeservice/",{loader:()=>g(()=>import("./index.html-CiezHrmK.js"),__vite__mapDeps([123,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",e:` -`,r:{minutes:.06,words:17},t:"分布式微服务",y:"a"}}],["/other/docker/Docker.html",{loader:()=>g(()=>import("./Docker.html-DfENlTQt.js"),__vite__mapDeps([124,1])),meta:{a:"chenkun",d:1633824e6,l:"2021年10月10日",c:["docker"],e:`

    1 命令速查

    -`,r:{minutes:.07,words:22},t:"Docker常用命令",y:"a"}}],["/other/docker/ServiceInstall.html",{loader:()=>g(()=>import("./ServiceInstall.html-a8eSZceK.js"),__vite__mapDeps([125,1])),meta:{d:16695936e5,l:"2022年11月28日",c:["docker"],e:`

    1. docker安装Redis

    +
    image-20220602103505994
    image-20220602103505994
    `,r:{minutes:.7,words:209},t:"Nacos学习",y:"a"}}],["/other/distributeservice/",{loader:()=>g(()=>import("./index.html-E9Wkx9cd.js"),__vite__mapDeps([123,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",e:` +`,r:{minutes:.06,words:17},t:"分布式微服务",y:"a"}}],["/other/docker/Docker.html",{loader:()=>g(()=>import("./Docker.html-DRMULpQx.js"),__vite__mapDeps([124,1])),meta:{a:"chenkun",d:1633824e6,l:"2021年10月10日",c:["docker"],e:`

    1 命令速查

    +`,r:{minutes:.07,words:22},t:"Docker常用命令",y:"a"}}],["/other/docker/ServiceInstall.html",{loader:()=>g(()=>import("./ServiceInstall.html-IjUvufHh.js"),__vite__mapDeps([125,1])),meta:{d:16695936e5,l:"2022年11月28日",c:["docker"],e:`

    1. docker安装Redis

    起初的需求是用docker启动一个redis,并且指定一个配置,死活不成功,主要是少设置了data目录的映射

    @@ -1546,7 +1546,7 @@ flush privileges;

    注意

    使用docker-compose安装redis时,若指定外置redis.conf配置文件,要切记同时设置data存储目录,默认docker中的数据存在/data下,所以 使用卷映射时需要映射到容器内的/data docker-compose.yml

    -`,r:{minutes:.7,words:210},t:"使用docker安装常见服务",y:"a"}}],["/other/docker/%E9%80%9A%E8%BF%87%E4%BB%A3%E7%90%86%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F.html",{loader:()=>g(()=>import("./通过代理拉取镜像.html-DlMILAav.js"),__vite__mapDeps([126,1])),meta:{a:"chensino",d:17324928e5,l:"2024年11月25日",o:!1,e:`
    +
    `,r:{minutes:.7,words:210},t:"使用docker安装常见服务",y:"a"}}],["/other/docker/%E9%80%9A%E8%BF%87%E4%BB%A3%E7%90%86%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F.html",{loader:()=>g(()=>import("./通过代理拉取镜像.html-DDfh6M-Q.js"),__vite__mapDeps([126,1])),meta:{a:"chensino",d:17324928e5,l:"2024年11月25日",o:!1,e:` @@ -1554,7 +1554,7 @@ flush privileges;

    docker官方源被屏蔽,国内用户无法直接拉取镜像,很多国内的镜像源又不稳定,还是想用官方源,这里提供http代理的方式下载官方源, 当然还有其他系统全局代理,网关代理等等不在本次讨论范围,本次使用http代理docker的pull,从官方获取镜像

    -
    `,r:{minutes:.77,words:231},t:"Docker通过代理拉取镜像",y:"a"}}],["/other/essay/2022-04-12.html",{loader:()=>g(()=>import("./2022-04-12.html-VXRZFk6l.js"),__vite__mapDeps([127,1])),meta:{a:"chenkun",d:16173216e5,l:"2021年4月2日",c:["数据库"],g:["数据库"],e:` +
    `,r:{minutes:.77,words:231},t:"Docker通过代理拉取镜像",y:"a"}}],["/other/essay/2022-04-12.html",{loader:()=>g(()=>import("./2022-04-12.html-COB6RKZC.js"),__vite__mapDeps([127,1])),meta:{a:"chenkun",d:16173216e5,l:"2021年4月2日",c:["数据库"],g:["数据库"],e:`

    背景:

    我有一个库A,这个库同时被两个服务使用(serviceA、serviceB),某天,因serviceA业务需要,必须更改数据库结构,导致serviceB无法使用,但是serviceB又不能停机,所以就考虑把数据库克隆一份给serviceB使用。在克隆使用的是mysqldump直接导出,然后再新建另一个库B,在库B导入sql,结果就悲剧了。

    @@ -1562,14 +1562,14 @@ flush privileges;

    1、库A导出

    # 在服务器把写了个定时备份脚本,这个databaseA是未更新前备份的库
     mysqldump -h localhost -uroot -p123456 databaseA>databaseA.sql
    -
    `,r:{minutes:1.18,words:355},t:"2022-04-02",y:"a"}}],["/other/essay/BTree.html",{loader:()=>g(()=>import("./BTree.html-CLTFqYVA.js"),__vite__mapDeps([128,1])),meta:{d:15821568e5,l:"2020年2月20日",g:["数据结构","二叉树"],u:8,e:`

    二叉树进化图

    +`,r:{minutes:1.18,words:355},t:"2022-04-02",y:"a"}}],["/other/essay/BTree.html",{loader:()=>g(()=>import("./BTree.html-B6mC9nUD.js"),__vite__mapDeps([128,1])),meta:{d:15821568e5,l:"2020年2月20日",g:["数据结构","二叉树"],u:8,e:`

    二叉树进化图

    树结构大道
    树结构大道
    -`,r:{minutes:.11,words:32},t:"二叉树",y:"a"}}],["/other/essay/CDN.html",{loader:()=>g(()=>import("./CDN.html-BrUklWPJ.js"),__vite__mapDeps([129,1])),meta:{a:"John",d:15506208e5,l:"2019年2月20日",g:["运维"],u:8,e:`

    1、什么是CDN?

    +`,r:{minutes:.11,words:32},t:"二叉树",y:"a"}}],["/other/essay/CDN.html",{loader:()=>g(()=>import("./CDN.html-DEIMFq2d.js"),__vite__mapDeps([129,1])),meta:{a:"John",d:15506208e5,l:"2019年2月20日",g:["运维"],u:8,e:`

    1、什么是CDN?

    CDN(Content Delivery Network,内容分发网络)是构建在现有互联网基础之上的一层智能虚拟网络,通过在网络各处部署节点服务器,实现将源站内容分发至所有CDN节点,使用户可以就近获得所需的内容。CDN服务缩短了用户查看内容的访问延迟,提高了用户访问网站的响应速度与网站的可用性,解决了网络带宽小、用户访问量大、网点分布不均等问题。
    -
    `,r:{minutes:7.17,words:2152},t:"CDN静态资源加速",y:"a"}}],["/other/essay/ChromeDevTools.html",{loader:()=>g(()=>import("./ChromeDevTools.html-DIFEaEZF.js"),__vite__mapDeps([130,1])),meta:{a:"陈老师",d:16241472e5,l:"2021年6月20日",g:["工具使用"],e:`

    学会ChromeDevtool,高效率定位问题

    +`,r:{minutes:7.17,words:2152},t:"CDN静态资源加速",y:"a"}}],["/other/essay/ChromeDevTools.html",{loader:()=>g(()=>import("./ChromeDevTools.html-CzA-EmEC.js"),__vite__mapDeps([130,1])),meta:{a:"陈老师",d:16241472e5,l:"2021年6月20日",g:["工具使用"],e:`

    学会ChromeDevtool,高效率定位问题

    https://developer.chrome.com/docs/devtools/evaluate-performance/

    -`,r:{minutes:.1,words:31},t:"ChromeDevTools学习",y:"a"}}],["/other/essay/CloudService.html",{loader:()=>g(()=>import("./CloudService.html-CjT4ijCP.js"),__vite__mapDeps([131,1])),meta:{a:"陈老师",d:16241472e5,l:"2021年6月20日",g:["运维"],e:`

    1、我的云服务使用场景

    +`,r:{minutes:.1,words:31},t:"ChromeDevTools学习",y:"a"}}],["/other/essay/CloudService.html",{loader:()=>g(()=>import("./CloudService.html-VcDoz7-V.js"),__vite__mapDeps([131,1])),meta:{a:"陈老师",d:16241472e5,l:"2021年6月20日",g:["运维"],e:`

    1、我的云服务使用场景

    1. @@ -1585,19 +1585,19 @@ flush privileges;
    2. 一开始没有上cdn,用户反应慢,后来配置了cdn加速obs中的图片,这样第一次打开图片会把图片资源拉到就近的服务器,问题就是第一次打开依然慢;
    3. 导出PDF很慢,经常出现504Timeout
    -

    3、问题分析

    `,r:{minutes:7.92,words:2375},t:"云服务问题分析及总结",y:"a"}}],["/other/essay/DeployGithubPage.html",{loader:()=>g(()=>import("./DeployGithubPage.html-C3CJu8PT.js"),__vite__mapDeps([132,1])),meta:{a:"Sino",d:16157664e5,l:"2021年3月15日",g:["部署搭建"],u:5,e:'

    1、 GitHubPage介绍

    \n

    1.1 ok

    \n

    1.2 搭建个人githubpage

    \n
    个人page和项目page的区别就是个人page只有一个,所谓的个人Page说白了也是一个特殊的项目Page,无非就是它的仓库名字比较特殊,必须为<username>.github.io,比如java框架`spring-cloud.github.io`、`facebook.github.io`,注意个人page的仓库名一定要加上 `.github.io`才算个人Page,不加的话就是一个普通项目了。\n个人page有啥特殊之处呢?\n在访问页面时可以直接使用https://<username>.github.io,不用加仓库名,普通的项目page,访问时需要加仓库名,比如https://<username>.github.io/<reponame>
    \n
    ',r:{minutes:2.08,words:625},t:"如何在github部署静态网站",y:"a"}}],["/other/essay/IM%E5%8D%B3%E6%97%B6%E9%80%9A%E4%BF%A1%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B.html",{loader:()=>g(()=>import("./IM即时通信技术选型.html-BrFdlOJV.js"),__vite__mapDeps([133,1])),meta:{a:"chensino",d:17234208e5,l:"2024年8月12日",o:!0,e:`

    1. 需求

    +

    3、问题分析

    `,r:{minutes:7.92,words:2375},t:"云服务问题分析及总结",y:"a"}}],["/other/essay/DeployGithubPage.html",{loader:()=>g(()=>import("./DeployGithubPage.html-BMfBhB20.js"),__vite__mapDeps([132,1])),meta:{a:"Sino",d:16157664e5,l:"2021年3月15日",g:["部署搭建"],u:5,e:'

    1、 GitHubPage介绍

    \n

    1.1 ok

    \n

    1.2 搭建个人githubpage

    \n
    个人page和项目page的区别就是个人page只有一个,所谓的个人Page说白了也是一个特殊的项目Page,无非就是它的仓库名字比较特殊,必须为<username>.github.io,比如java框架`spring-cloud.github.io`、`facebook.github.io`,注意个人page的仓库名一定要加上 `.github.io`才算个人Page,不加的话就是一个普通项目了。\n个人page有啥特殊之处呢?\n在访问页面时可以直接使用https://<username>.github.io,不用加仓库名,普通的项目page,访问时需要加仓库名,比如https://<username>.github.io/<reponame>
    \n
    ',r:{minutes:2.08,words:625},t:"如何在github部署静态网站",y:"a"}}],["/other/essay/IM%E5%8D%B3%E6%97%B6%E9%80%9A%E4%BF%A1%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B.html",{loader:()=>g(()=>import("./IM即时通信技术选型.html-veogyBQt.js"),__vite__mapDeps([133,1])),meta:{a:"chensino",d:17234208e5,l:"2024年8月12日",o:!0,e:`

    1. 需求

    服务端开发语言:Java

    终端:Android、iOS、Web、小程序

    核心需求:发送文字、图片、文件、语音、视频、消息缓存、消息存储、消息未读、已读、撤回,离线消息、历史消息、单聊、群聊,多端同步,以及其他一些需求。

    用户管理:添加好友、查看好友列表、删除好友、查看好友信息、创建群聊、加入群聊、查看群成员信息、退出群聊、修改群昵称、拉人进群、踢人出群、解散群聊、填写群公告、修改群备注以及其他用户相关的需求等。

    -

    权限管理: 针对群聊不同用户有不同的权限,比如群主、管理员、普通成员,普通成员可以发送消息、撤回消息、删除消息、修改消息、查看消息等,管理员可以管理群成员,修改群昵称、拉人进群、踢人出群、解散群聊、填写群公告、修改群备注等。

    `,r:{minutes:19.44,words:5832},t:"IM即时通信选型",y:"a"}}],["/other/essay/Jenkins.html",{loader:()=>g(()=>import("./Jenkins.html-BlEG3ZLM.js"),__vite__mapDeps([134,1])),meta:{a:"chenkun",d:16184448e5,l:"2021年4月15日",c:["运维"],e:` +

    权限管理: 针对群聊不同用户有不同的权限,比如群主、管理员、普通成员,普通成员可以发送消息、撤回消息、删除消息、修改消息、查看消息等,管理员可以管理群成员,修改群昵称、拉人进群、踢人出群、解散群聊、填写群公告、修改群备注等。

    `,r:{minutes:19.44,words:5832},t:"IM即时通信选型",y:"a"}}],["/other/essay/Jenkins.html",{loader:()=>g(()=>import("./Jenkins.html-BlvN7BiZ.js"),__vite__mapDeps([134,1])),meta:{a:"chenkun",d:16184448e5,l:"2021年4月15日",c:["运维"],e:`

    1、jenkins插件更新报错

    1.1 报错如下,ssl证书问题

    image-20220415144539319
    image-20220415144539319
    -

    Jenkins(2020年及以后版本,2.260以上)安装后,插件下载时失败,网上找了各种解决方法,修改jenkins插件的下载源地址:

    `,r:{minutes:5.09,words:1528},t:"jenkins部署及使用",y:"a"}}],["/other/essay/",{loader:()=>g(()=>import("./index.html-Ci0fk4NS.js"),__vite__mapDeps([135,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",r:{minutes:.05,words:14},t:"随笔分享",y:"a"}}],["/other/essay/TyporaPicgo.html",{loader:()=>g(()=>import("./TyporaPicgo.html-BFOgiNZO.js"),__vite__mapDeps([136,1])),meta:{d:15520896e5,l:"2019年3月9日",e:`

    前言

    +

    Jenkins(2020年及以后版本,2.260以上)安装后,插件下载时失败,网上找了各种解决方法,修改jenkins插件的下载源地址:

    `,r:{minutes:5.09,words:1528},t:"jenkins部署及使用",y:"a"}}],["/other/essay/",{loader:()=>g(()=>import("./index.html-CkTmkVqY.js"),__vite__mapDeps([135,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",r:{minutes:.05,words:14},t:"随笔分享",y:"a"}}],["/other/essay/TyporaPicgo.html",{loader:()=>g(()=>import("./TyporaPicgo.html-DiCAdyNh.js"),__vite__mapDeps([136,1])),meta:{d:15520896e5,l:"2019年3月9日",e:`

    前言

    平时用MarkDown写博客少不了需要截图,我用的是Typora,刚开始截图是保存在本地,有时想把博客分享到网上,就发现各种图全挂了,需要手动一个一个再复制一下,着实麻烦,今天无意间发现有个叫PicGo的工具,此工具专门上传图到各大图床,着实方便。

    picgo配置

    -

    picgo-core

    `,r:{minutes:1.65,words:494},t:"在Typora中使用Picgo",y:"a"}}],["/other/essay/elasticSearch%E6%93%8D%E4%BD%9C.html",{loader:()=>g(()=>import("./elasticSearch操作.html-hVwrnBmy.js"),__vite__mapDeps([137,1])),meta:{a:"chensino",d:1722816e6,l:"2024年8月5日",o:!0,e:`

    1.索引库操作

    +

    picgo-core

    `,r:{minutes:1.65,words:494},t:"在Typora中使用Picgo",y:"a"}}],["/other/essay/elasticSearch%E6%93%8D%E4%BD%9C.html",{loader:()=>g(()=>import("./elasticSearch操作.html-B7JMEaBD.js"),__vite__mapDeps([137,1])),meta:{a:"chensino",d:1722816e6,l:"2024年8月5日",o:!0,e:`

    1.索引库操作

    索引库就类似数据库表,mapping映射就类似表的结构。

    我们要向es中存储数据,必须先创建“库”和“表”

    1.1 Mapping映射属性

    @@ -1623,7 +1623,7 @@ flush privileges;
  • properties:该字段的子字段

  • -`,r:{minutes:21.3,words:6391},t:"elasticsearch操作",y:"a"}}],["/other/essay/elk%E9%83%A8%E7%BD%B2.html",{loader:()=>g(()=>import("./elk部署.html-D33Pj7hN.js"),__vite__mapDeps([138,1])),meta:{a:"chensino",d:1722816e6,l:"2024年8月5日",o:!0,e:`

    1. 目录结构

    +`,r:{minutes:21.3,words:6391},t:"elasticsearch操作",y:"a"}}],["/other/essay/elk%E9%83%A8%E7%BD%B2.html",{loader:()=>g(()=>import("./elk部署.html-mVatN34N.js"),__vite__mapDeps([138,1])),meta:{a:"chensino",d:1722816e6,l:"2024年8月5日",o:!0,e:`

    1. 目录结构

    elk
     ├── config
        ├── es
    @@ -1641,7 +1641,7 @@ flush privileges;
            ├── data
     └── logs
         └── logstash
    -
    `,r:{minutes:3.3,words:990},t:"ELK 部署",y:"a"}}],["/other/essay/im%E5%8D%B3%E6%97%B6%E9%80%9A%E4%BF%A1%E7%9A%84%E9%9C%80%E6%B1%82.html",{loader:()=>g(()=>import("./im即时通信的需求.html-jBUJLx5g.js"),__vite__mapDeps([139,1])),meta:{a:"chenkun",d:1724112e6,l:"2024年8月20日",o:!0,e:` +`,r:{minutes:3.3,words:990},t:"ELK 部署",y:"a"}}],["/other/essay/im%E5%8D%B3%E6%97%B6%E9%80%9A%E4%BF%A1%E7%9A%84%E9%9C%80%E6%B1%82.html",{loader:()=>g(()=>import("./im即时通信的需求.html-DyFOtblG.js"),__vite__mapDeps([139,1])),meta:{a:"chenkun",d:1724112e6,l:"2024年8月20日",o:!0,e:`

    1. 概述

    1.1 项目背景

    随着医院规模的扩大及医疗服务的复杂化,医疗人员之间的信息交流需求日益增加。传统的电话、短信等方式已难以满足医院内部高效、及时、保密的沟通需求。因此,开发一款专门面向医院人员的即时通信软件势在必行。

    @@ -1653,10 +1653,10 @@ flush privileges;
  • 医生:科室主任、主治医师、住院医师等。
  • 护士:护士长、责任护士、实习护士等。
  • 行政人员:医院管理层、科室秘书、后勤人员等。
  • -`,r:{minutes:10.31,words:3093},t:"即时通信软件需求",y:"a"}}],["/other/essay/windows%E4%B8%8B%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C.html",{loader:()=>g(()=>import("./windows下服务注册.html-Dcz3rIgQ.js"),__vite__mapDeps([140,1])),meta:{a:"chensino",d:17116704e5,l:"2024年3月29日",o:!0,e:`

    1.需求

    +`,r:{minutes:10.31,words:3093},t:"即时通信软件需求",y:"a"}}],["/other/essay/windows%E4%B8%8B%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C.html",{loader:()=>g(()=>import("./windows下服务注册.html-Hl5CGc6m.js"),__vite__mapDeps([140,1])),meta:{a:"chensino",d:17116704e5,l:"2024年3月29日",o:!0,e:`

    1.需求

        1. windows下没有像linux的\`nohup xxx &\`的后台启动命令,springboot项目就无法后台运行
         2. 把任意一个命令注册为服务
    -
    `,r:{minutes:.99,words:297},t:"windows下服务注册",y:"a"}}],["/other/essay/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%BC%95%E5%AF%BC.html",{loader:()=>g(()=>import("./操作系统引导.html-Dkn3DOho.js"),__vite__mapDeps([141,1])),meta:{a:"chensino",d:17121024e5,l:"2024年4月3日",o:!0,e:`

    对应关系

    +`,r:{minutes:.99,words:297},t:"windows下服务注册",y:"a"}}],["/other/essay/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%BC%95%E5%AF%BC.html",{loader:()=>g(()=>import("./操作系统引导.html-CEnMYqvl.js"),__vite__mapDeps([141,1])),meta:{a:"chensino",d:17121024e5,l:"2024年4月3日",o:!0,e:`

    对应关系

    @@ -1686,13 +1686,13 @@ flush privileges; -
    `,r:{minutes:5.29,words:1586},t:"系统引导基本名词BIOS/EFI/MBR/GPT/GRUB",y:"a"}}],["/other/git/BatchDeleteGitHubRepo.html",{loader:()=>g(()=>import("./BatchDeleteGitHubRepo.html-DsXldxOW.js"),__vite__mapDeps([142,1])),meta:{d:1645488e6,l:"2022年2月22日",o:!0,e:`

    背景

    +`,r:{minutes:5.29,words:1586},t:"系统引导基本名词BIOS/EFI/MBR/GPT/GRUB",y:"a"}}],["/other/git/BatchDeleteGitHubRepo.html",{loader:()=>g(()=>import("./BatchDeleteGitHubRepo.html-i5eo4HNC.js"),__vite__mapDeps([142,1])),meta:{d:1645488e6,l:"2022年2月22日",o:!0,e:`

    背景

    github上fork了很多仓库,但是平时又没看,所以索性删除,一个个删除又很慢,所以搞个脚本批量删除

    方法

    1. 到github配置一个token,https://github.com/settings/tokens
     2. 准备一个文件放置待删除的仓库,每行一个仓库名
     3. 用脚本批量删除
    -
    `,r:{minutes:.56,words:168},t:"批量删除github仓库",y:"a"}}],["/other/git/GitCommands.html",{loader:()=>g(()=>import("./GitCommands.html-BbarE5Z9.js"),__vite__mapDeps([143,1])),meta:{a:"ChenSino",d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    一、善用手册

    +`,r:{minutes:.56,words:168},t:"批量删除github仓库",y:"a"}}],["/other/git/GitCommands.html",{loader:()=>g(()=>import("./GitCommands.html-OTAFexBu.js"),__vite__mapDeps([143,1])),meta:{a:"ChenSino",d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    一、善用手册

    $ git --help
     用法:git [--version] [--help] [-C <路径>] [-c <名称>=<取值>]
                [--exec-path[=<路径>]] [--html-path] [--man-path] [--info-path]
    @@ -1739,50 +1739,50 @@ flush privileges;
     查看 'git help <命令>' 'git help <概念>' 以获取给定子命令或概念的
     帮助。
     有关系统的概述,查看 'git help git'。
    -
    `,r:{minutes:11.64,words:3491},t:"git命令",y:"a"}}],["/other/git/",{loader:()=>g(()=>import("./index.html-DaMYstTJ.js"),__vite__mapDeps([144,1])),meta:{d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],r:{minutes:.05,words:16},t:"Git",y:"a"}}],["/other/git/branch01.html",{loader:()=>g(()=>import("./branch01.html-DjNYYC56.js"),__vite__mapDeps([145,1])),meta:{d:1628009821e3,l:"2021年8月3日",c:["git 操作"],g:["必会"],e:`

    1,查看远程分支

    +`,r:{minutes:11.64,words:3491},t:"git命令",y:"a"}}],["/other/git/",{loader:()=>g(()=>import("./index.html-CsoAqmHA.js"),__vite__mapDeps([144,1])),meta:{d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],r:{minutes:.05,words:16},t:"Git",y:"a"}}],["/other/git/branch01.html",{loader:()=>g(()=>import("./branch01.html-Cyy-Y0us.js"),__vite__mapDeps([145,1])),meta:{d:1628009821e3,l:"2021年8月3日",c:["git 操作"],g:["必会"],e:`

    1,查看远程分支

    可以查看远程分支名,查看要切换的远程分支是否存在。

    git branch -a
    -
    `,r:{minutes:.49,words:147},t:"git 拉取远程分支到本地",y:"a"}}],["/other/git/branch02.html",{loader:()=>g(()=>import("./branch02.html-DC9xWnxN.js"),__vite__mapDeps([146,1])),meta:{d:1628009821e3,l:"2021年8月3日",c:["git 操作"],g:["必会"],e:`

    一,推送本地分支到远程

    +`,r:{minutes:.49,words:147},t:"git 拉取远程分支到本地",y:"a"}}],["/other/git/branch02.html",{loader:()=>g(()=>import("./branch02.html-DzulXNoK.js"),__vite__mapDeps([146,1])),meta:{d:1628009821e3,l:"2021年8月3日",c:["git 操作"],g:["必会"],e:`

    一,推送本地分支到远程

    昨天我在自家电脑创建了一个分支search_dev.写搜索功能。

    1,在本地创建并切换到search_dev分支

    git checkout -b search_dev
    -
    `,r:{minutes:1.63,words:489},t:"git分支操作",y:"a"}}],["/other/git/fatal.html",{loader:()=>g(()=>import("./fatal.html-BMpaFBLz.js"),__vite__mapDeps([147,1])),meta:{a:"zxf",d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    如何解决 fatal: unable to access...的问题

    +`,r:{minutes:1.63,words:489},t:"git分支操作",y:"a"}}],["/other/git/fatal.html",{loader:()=>g(()=>import("./fatal.html-1jDaL0H2.js"),__vite__mapDeps([147,1])),meta:{a:"zxf",d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    如何解决 fatal: unable to access...的问题

    现象

    浏览器可正常访问github,终端无法clone

    确定是否是因为代理问题

    ## 查看git所有配置,检查是否使用了代理
     git config --list
    -
    `,r:{minutes:.98,words:294},t:"Git克隆出现连接错误",y:"a"}}],["/other/git/gitConflict.html",{loader:()=>g(()=>import("./gitConflict.html-DVdpn_6G.js"),__vite__mapDeps([148,1])),meta:{a:"zxf",d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    一,git冲突出现的原因及解决方案

    +`,r:{minutes:.98,words:294},t:"Git克隆出现连接错误",y:"a"}}],["/other/git/gitConflict.html",{loader:()=>g(()=>import("./gitConflict.html-BGZttGsy.js"),__vite__mapDeps([148,1])),meta:{a:"zxf",d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    一,git冲突出现的原因及解决方案

    简单来说就是本地修改的文件和目标远程库的同一个文件都有修改,这时无论是pull,push,merge时都会产生冲突。

    1.1 本地代码没有commit. 修改同一文件的不同处,依旧会产生冲突。

    • 修改component.html本地文件第11行,既没有add,也没有commit
    • 然后修改远程component.html远程文件第14行
    • 本地执行git pull,拉取代码提示,同时修改component.html,提示发生冲突
    • -
    `,r:{minutes:2.72,words:815},t:"git冲突出现的原因",y:"a"}}],["/other/git/gitRebase.html",{loader:()=>g(()=>import("./gitRebase.html-BC1KUd03.js"),__vite__mapDeps([149,1])),meta:{a:"zxf",d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    一,用于合并当前分支的多个commit记录

    +`,r:{minutes:2.72,words:815},t:"git冲突出现的原因",y:"a"}}],["/other/git/gitRebase.html",{loader:()=>g(()=>import("./gitRebase.html-Dz5w3eml.js"),__vite__mapDeps([149,1])),meta:{a:"zxf",d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    一,用于合并当前分支的多个commit记录

    应用场景,如下第2-4次提交是对同一功能的代码提交记录,完全可以合并成一次提交记录。这个时候rebase就很有用了。

    -
    image-20220724180044950
    image-20220724180044950
    `,r:{minutes:3.03,words:909},t:"git rebase的使用",y:"a"}}],["/other/git/gitwork.html",{loader:()=>g(()=>import("./gitwork.html-Ck1n4MrT.js"),__vite__mapDeps([150,1])),meta:{a:"zxf",d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    一,基本概念

    +
    image-20220724180044950
    image-20220724180044950
    `,r:{minutes:3.03,words:909},t:"git rebase的使用",y:"a"}}],["/other/git/gitwork.html",{loader:()=>g(()=>import("./gitwork.html-DbshE2ks.js"),__vite__mapDeps([150,1])),meta:{a:"zxf",d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    一,基本概念

    • 工作区:就是你在电脑里能看到的目录。
    • 暂存区:英文叫 stage 或 index。一般存放在 .git 目录下的 index 文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。
    • 版本库:工作区有一个隐藏目录 .git,这个不算工作区,而是 Git 的版本库。

    下面这个图展示了工作区、版本库中的暂存区和版本库之间的关系:

    -
    image-20220803144042656
    image-20220803144042656
    `,r:{minutes:2.15,words:645},t:"git工作区、暂存区、和版本库",y:"a"}}],["/other/git/mergeBranch.html",{loader:()=>g(()=>import("./mergeBranch.html-DLnnhDxz.js"),__vite__mapDeps([151,1])),meta:{d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    一,问题

    +
    image-20220803144042656
    image-20220803144042656
    `,r:{minutes:2.15,words:645},t:"git工作区、暂存区、和版本库",y:"a"}}],["/other/git/mergeBranch.html",{loader:()=>g(()=>import("./mergeBranch.html-CJReB61K.js"),__vite__mapDeps([151,1])),meta:{d:1615309021e3,l:"2021年3月9日",c:["git 操作"],g:["必会"],e:`

    一,问题

    ​ 在git log中往往会发现在log中出现Merge branch 'master of .....'这种合并节点,造成日志的污染

    -
    image-20220726115529360
    image-20220726115529360
    `,r:{minutes:2.67,words:800},t:"git pull产生临时Merge branch的问题",y:"a"}}],["/other/git/proxy.html",{loader:()=>g(()=>import("./proxy.html-Q237dBqx.js"),__vite__mapDeps([152,1])),meta:{a:"chenkun",d:16606944e5,l:"2022年8月17日",e:`

    1、全局代理

    +
    image-20220726115529360
    image-20220726115529360
    `,r:{minutes:2.67,words:800},t:"git pull产生临时Merge branch的问题",y:"a"}}],["/other/git/proxy.html",{loader:()=>g(()=>import("./proxy.html-Ciypu-jF.js"),__vite__mapDeps([152,1])),meta:{a:"chenkun",d:16606944e5,l:"2022年8月17日",e:`

    1、全局代理

    git config --global https.proxy http://127.0.0.1:1080
     git config --global https.proxy https://127.0.0.1:1080
     ##取消代理
     git config --global --unset http.proxy
     git config --global --unset https.proxy
    -
    `,r:{minutes:.27,words:80},t:"git代理",y:"a"}}],["/other/git/rebaseAndMerge.html",{loader:()=>g(()=>import("./rebaseAndMerge.html-BU7yi9md.js"),__vite__mapDeps([153,1])),meta:{d:1627318621e3,l:"2021年7月26日",c:["git 操作"],g:["必会"],e:`

    在codehub上有多个分支,每次的提交都会生成一个新的ID。如下图,假设开始各个分支都是根据ID2的提交更新后的代码进行修改,(ID号仅代表生成的时间顺序,实际的ID号是根据算法生成的)

    +`,r:{minutes:.27,words:80},t:"git代理",y:"a"}}],["/other/git/rebaseAndMerge.html",{loader:()=>g(()=>import("./rebaseAndMerge.html-qrvKpauy.js"),__vite__mapDeps([153,1])),meta:{d:1627318621e3,l:"2021年7月26日",c:["git 操作"],g:["必会"],e:`

    在codehub上有多个分支,每次的提交都会生成一个新的ID。如下图,假设开始各个分支都是根据ID2的提交更新后的代码进行修改,(ID号仅代表生成的时间顺序,实际的ID号是根据算法生成的)

    img
    img
    -

    如果我们需要将绿色分支修改的代码更新到蓝色分支,本地远程分支内与个人工作分支已经是蓝色分支对应库内最新代码,那么在绿色远程分支代码更新到个人的库后(fetch),需要将本地远程分支代码更新到个人工作分支,这时有两种方法,rebase和merge。

    `,r:{minutes:1.28,words:383},t:"git rebase与merge的区别",y:"a"}}],["/other/git/reset.html",{loader:()=>g(()=>import("./reset.html-C034UaNH.js"),__vite__mapDeps([154,1])),meta:{a:"chenkun",d:16590528e5,l:"2022年7月29日",e:`

    1、几种reset介绍

    +

    如果我们需要将绿色分支修改的代码更新到蓝色分支,本地远程分支内与个人工作分支已经是蓝色分支对应库内最新代码,那么在绿色远程分支代码更新到个人的库后(fetch),需要将本地远程分支代码更新到个人工作分支,这时有两种方法,rebase和merge。

    `,r:{minutes:1.28,words:383},t:"git rebase与merge的区别",y:"a"}}],["/other/git/reset.html",{loader:()=>g(()=>import("./reset.html-BnywMEUr.js"),__vite__mapDeps([154,1])),meta:{a:"chenkun",d:16590528e5,l:"2022年7月29日",e:`

    1、几种reset介绍

    git reset 命令用于回退版本,可以指定退回某一次提交的版本。

    git reset 命令语法格式如下:

    git reset [--soft | --mixed | --hard] [HEAD]
    -
    `,r:{minutes:1.78,words:533},t:"git reset命令使用",y:"a"}}],["/other/git/stash.html",{loader:()=>g(()=>import("./stash.html-B-CQ6rNg.js"),__vite__mapDeps([155,1])),meta:{d:1615309021e3,l:"2021年3月9日",c:["git 操作","必会"],e:`

    一,使用场景

    +`,r:{minutes:1.78,words:533},t:"git reset命令使用",y:"a"}}],["/other/git/stash.html",{loader:()=>g(()=>import("./stash.html-DDps8CE5.js"),__vite__mapDeps([155,1])),meta:{d:1615309021e3,l:"2021年3月9日",c:["git 操作","必会"],e:`

    一,使用场景

    在开发的过程中,经常会遇到,几个分支并行进行。当在A分支开发,突然发现有个线上bug,需要临时切换到B分支进行处理,同时,A分支上的代码还未编写完整,不想提交上去。这个时候,git stash的好处就提现出来了。

    二,stash的作用

    stash会跟踪文件的修改与暂存的改动——然后将未完成的修改保存到一个栈上, 而你可以在任何时候重新应用这些改动(甚至在不同的分支上)。

    @@ -1810,22 +1810,22 @@ flush privileges;
  • git stash drop stash@{$num} :丢弃stash@{$num}存储,从列表中删除这个存储

  • -`,r:{minutes:1.52,words:456},t:"git stash 暂存",y:"a"}}],["/other/hardware/CPU.html",{loader:()=>g(()=>import("./CPU.html-CrPccORk.js"),__vite__mapDeps([156,1])),meta:{d:16600032e5,l:"2022年8月9日",o:!0,e:`

    intel cpu型号

    +`,r:{minutes:1.52,words:456},t:"git stash 暂存",y:"a"}}],["/other/hardware/CPU.html",{loader:()=>g(()=>import("./CPU.html-BZbWMqTq.js"),__vite__mapDeps([156,1])),meta:{d:16600032e5,l:"2022年8月9日",o:!0,e:`

    intel cpu型号

    官方说明

    cpu型号命名

    -
    20230809094135
    20230809094135
    `,r:{minutes:1.46,words:438},t:"cpu介绍",y:"a"}}],["/other/linux/CentOS.html",{loader:()=>g(()=>import("./CentOS.html-BdaPB0Ti.js"),__vite__mapDeps([157,1])),meta:{a:"chenkun",d:16600896e5,l:"2022年8月10日",e:`

    1、安装JDK11

    +
    20230809094135
    20230809094135
    `,r:{minutes:1.46,words:438},t:"cpu介绍",y:"a"}}],["/other/linux/CentOS.html",{loader:()=>g(()=>import("./CentOS.html-CvNBcUZo.js"),__vite__mapDeps([157,1])),meta:{a:"chenkun",d:16600896e5,l:"2022年8月10日",e:`

    1、安装JDK11

    sudo yum -y install java-11-openjdk java-11-openjdk-devel
    -
    `,r:{minutes:.18,words:55},t:"RedHat系",y:"a"}}],["/other/linux/CommonUsedCMD.html",{loader:()=>g(()=>import("./CommonUsedCMD.html-CyjonT_t.js"),__vite__mapDeps([158,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",e:` +`,r:{minutes:.18,words:55},t:"RedHat系",y:"a"}}],["/other/linux/CommonUsedCMD.html",{loader:()=>g(()=>import("./CommonUsedCMD.html-tJHiFr9g.js"),__vite__mapDeps([158,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",e:`

    1、查找多个文件中是否包含字符串

    grep -r targetString targetDirectory
     # -r 表示递归查询
     # targetString  表示目标字符串
     # targetDirectory 表示目录
    -
    `,r:{minutes:3.29,words:986},t:"常用命令",y:"a"}}],["/other/linux/Curl.html",{loader:()=>g(()=>import("./Curl.html-Bmo3iaqW.js"),__vite__mapDeps([159,1])),meta:{a:"chenkun",d:16602624e5,l:"2022年8月12日",c:["linux"],g:["linux"],e:`

    1、使用CURL分析接口请求耗时

    +`,r:{minutes:3.29,words:986},t:"常用命令",y:"a"}}],["/other/linux/Curl.html",{loader:()=>g(()=>import("./Curl.html-7Jrb-SD3.js"),__vite__mapDeps([159,1])),meta:{a:"chenkun",d:16602624e5,l:"2022年8月12日",c:["linux"],g:["linux"],e:`

    1、使用CURL分析接口请求耗时

    `,r:{minutes:4.79,words:1438},t:"Curl命令",y:"a"}}],["/other/linux/InstallMysqlWithDocker.html",{loader:()=>g(()=>import("./InstallMysqlWithDocker.html-BJNnL9bp.js"),__vite__mapDeps([160,1])),meta:{a:"chenkun",d:16336512e5,l:"2021年10月8日",e:`

    1. 不要用太新的版本

    +`,r:{minutes:4.79,words:1438},t:"Curl命令",y:"a"}}],["/other/linux/InstallMysqlWithDocker.html",{loader:()=>g(()=>import("./InstallMysqlWithDocker.html-Bj1rp9yg.js"),__vite__mapDeps([160,1])),meta:{a:"chenkun",d:16336512e5,l:"2021年10月8日",e:`

    1. 不要用太新的版本

    安装一定要选对版本,刚开始我使用的8.0.30镜像,一直无法启动,日志报错大概意思是我映射的目录有问题,应该是新版本mysql的安装文件有变动, 我也懒得去深究,直接换了一个版本就好了了。

    2 安装

    @@ -1839,7 +1839,7 @@ flush privileges; -v /usr/local/docker/mysql/data:/var/lib/mysql \\ -e MYSQL_ROOT_PASSWORD=root \\ -d mysql:8.0.23 -`,r:{minutes:.75,words:224},t:"docker安装mysql",y:"a"}}],["/other/linux/Manjaro.html",{loader:()=>g(()=>import("./Manjaro.html-Bb8Od0IV.js"),__vite__mapDeps([161,1])),meta:{a:"chenkun",d:16163712e5,l:"2021年3月22日",e:`

    1、降级软件包

    +`,r:{minutes:.75,words:224},t:"docker安装mysql",y:"a"}}],["/other/linux/Manjaro.html",{loader:()=>g(()=>import("./Manjaro.html-e_kr8ypm.js"),__vite__mapDeps([161,1])),meta:{a:"chenkun",d:16163712e5,l:"2021年3月22日",e:`

    1、降级软件包

    安装downgrade程序 sudo pacman -S downgrade 降级 @@ -1847,7 +1847,7 @@ flush privileges; 注意DOWNGRADE_FROM_ALA=1一定要按照我上边这样写,不能单独export DOWNGRADE_FROM_ALA=1 设置忽略升级的包 第二步会让你选择更新的时候是否要忽略更新,选择y的话,它会在/etc/pacman.conf添加一个忽略,如果不想湖绿,把下面的IgnorePkg注释即可

    -
    image-20220322171440300
    image-20220322171440300
    `,r:{minutes:11.52,words:3457},t:"Manjaro问题搜集",y:"a"}}],["/other/linux/MountDisk.html",{loader:()=>g(()=>import("./MountDisk.html-BzHOCk5h.js"),__vite__mapDeps([162,1])),meta:{a:"chenkun",d:16432416e5,l:"2022年1月27日",e:`

    参考

    +
    image-20220322171440300
    image-20220322171440300
    `,r:{minutes:11.52,words:3457},t:"Manjaro问题搜集",y:"a"}}],["/other/linux/MountDisk.html",{loader:()=>g(()=>import("./MountDisk.html-BzdT4jwB.js"),__vite__mapDeps([162,1])),meta:{a:"chenkun",d:16432416e5,l:"2022年1月27日",e:`

    参考

    初始化Linux数据盘(fdisk)

    挂载

    划分分区并挂载磁盘

    @@ -1855,7 +1855,7 @@ flush privileges;
    1. fdisk -l 回显类似如下信息:
    2. -
    `,r:{minutes:7.62,words:2285},t:"系统挂载磁盘",y:"a"}}],["/other/linux/MultiNetworkCard.html",{loader:()=>g(()=>import("./MultiNetworkCard.html-CK4m44eU.js"),__vite__mapDeps([163,1])),meta:{a:"chenkun",d:1661472e6,l:"2022年8月26日",e:`

    1.1 kde桌面双网卡内外网设置

    +`,r:{minutes:7.62,words:2285},t:"系统挂载磁盘",y:"a"}}],["/other/linux/MultiNetworkCard.html",{loader:()=>g(()=>import("./MultiNetworkCard.html-Ckes4Awq.js"),__vite__mapDeps([163,1])),meta:{a:"chenkun",d:1661472e6,l:"2022年8月26日",e:`

    1.1 kde桌面双网卡内外网设置

    环境如下:

    $ screenfetch   
     
    @@ -1874,9 +1874,9 @@ flush privileges;
      ████████  ████████  ████████     CPU: 12th Gen Intel Core i5-12600K @ 16x 4.9GHz [29.0°C]
      ████████  ████████  ████████     GPU: Mesa Intel(R) Graphics (ADL-S GT1)
                                                        RAM: 16498MiB / 23813MiB
    -
    `,r:{minutes:1.9,words:570},t:"双网卡问题",y:"a"}}],["/other/linux/",{loader:()=>g(()=>import("./index.html-DURvABzp.js"),__vite__mapDeps([164,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",c:["运维"],e:`

    好记性不如烂笔头

    +`,r:{minutes:1.9,words:570},t:"双网卡问题",y:"a"}}],["/other/linux/",{loader:()=>g(()=>import("./index.html-Ckf605kx.js"),__vite__mapDeps([164,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",c:["运维"],e:`

    好记性不如烂笔头

    -`,r:{minutes:.08,words:23},t:"Linux",y:"a"}}],["/other/linux/Samba.html",{loader:()=>g(()=>import("./Samba.html-CBqUaBWb.js"),__vite__mapDeps([165,1])),meta:{a:"chenkun",d:1624752e6,l:"2021年6月27日",e:`

    1、安装过程省略

    +`,r:{minutes:.08,words:23},t:"Linux",y:"a"}}],["/other/linux/Samba.html",{loader:()=>g(()=>import("./Samba.html-DzYkwvBv.js"),__vite__mapDeps([165,1])),meta:{a:"chenkun",d:1624752e6,l:"2021年6月27日",e:`

    1、安装过程省略

    2、配置

    2.1 配置文件

    配置目录在 /etc/samba,修改smb.conf在最后加一组[test],同时修改[global]在里面加上ntlm auth = yes,最终加完如下

    @@ -1918,21 +1918,21 @@ flush privileges; writable = yes browseable = yes guest ok = yes -`,r:{minutes:.93,words:280},t:"部署Samba",y:"a"}}],["/other/linux/ShareBetweenWindowsAndLinux.html",{loader:()=>g(()=>import("./ShareBetweenWindowsAndLinux.html-ChX2AFxh.js"),__vite__mapDeps([166,1])),meta:{a:"chenkun",d:16982784e5,l:"2023年10月26日",e:`

    1、在windows设置共享目录

    +`,r:{minutes:.93,words:280},t:"部署Samba",y:"a"}}],["/other/linux/ShareBetweenWindowsAndLinux.html",{loader:()=>g(()=>import("./ShareBetweenWindowsAndLinux.html-B04nBB2Z.js"),__vite__mapDeps([166,1])),meta:{a:"chenkun",d:16982784e5,l:"2023年10月26日",e:`

    1、在windows设置共享目录

    设置过程省略……

    2、在linux下挂载

        ## 1. 创建空白目录
             mkdir /home/data/share
         ## 2. 修改/etc/fstab开机自动挂载
         //10.10.102.97/tempfile    /home/data/share  cifs    defaults,user=xxx,password=xxx,uid=1000,gid=1000  0 0
    -
    `,r:{minutes:.64,words:192},t:"Linux挂载windows共享目录",y:"a"}}],["/other/linux/TcpDump.html",{loader:()=>g(()=>import("./TcpDump.html-oAiwcaO7.js"),__vite__mapDeps([167,1])),meta:{a:"chenkun",d:16203456e5,l:"2021年5月7日",e:`

    简要介绍Linux抓报

    +`,r:{minutes:.64,words:192},t:"Linux挂载windows共享目录",y:"a"}}],["/other/linux/TcpDump.html",{loader:()=>g(()=>import("./TcpDump.html-mCRY-l5s.js"),__vite__mapDeps([167,1])),meta:{a:"chenkun",d:16203456e5,l:"2021年5月7日",e:`

    简要介绍Linux抓报

    作为web开发我抓报主要是针对http请求,主要是看请求行,请求头,请求体,以及响应

    1、所用抓报命令

    sudo tcpdump tcp -i eth1 -t -s 0 -c 100 and dst port ! 22 and src net 192.168.1.0/24 -w ./target.cap
    -
    `,r:{minutes:1.17,words:351},t:"tcpdump抓包",y:"a"}}],["/other/linux/Wifi.html",{loader:()=>g(()=>import("./Wifi.html-CN2euKMr.js"),__vite__mapDeps([168,1])),meta:{d:16616448e5,l:"2022年8月28日",e:`

    1、需求场景

    +`,r:{minutes:1.17,words:351},t:"tcpdump抓包",y:"a"}}],["/other/linux/Wifi.html",{loader:()=>g(()=>import("./Wifi.html-cQsjtWBO.js"),__vite__mapDeps([168,1])),meta:{d:16616448e5,l:"2022年8月28日",e:`

    1、需求场景

    场景1:

    有一个没有wifi模块的电脑,想给他加装一个wifi模块,要求不仅支持windows系统,还需要支持Linux系统,最容易想到的方式是买一个USB无线网卡, @@ -1941,7 +1941,7 @@ flush privileges;

    场景2:

    有一个笔记本电脑,自带的有wifi模块,但是此无线网卡不支持5G、WIFI6等,此时可以根据情况升级无线网卡;

    -
    `,r:{minutes:.95,words:284},t:"Linux下加装wifi模块",y:"a"}}],["/other/linux/firewall.html",{loader:()=>g(()=>import("./firewall.html-v7dOSz8g.js"),__vite__mapDeps([169,1])),meta:{d:16595712e5,l:"2022年8月4日",o:!0,e:`

    集中常用的防火墙配置工具

    +
    `,r:{minutes:.95,words:284},t:"Linux下加装wifi模块",y:"a"}}],["/other/linux/firewall.html",{loader:()=>g(()=>import("./firewall.html-DhAgXCrJ.js"),__vite__mapDeps([169,1])),meta:{d:16595712e5,l:"2022年8月4日",o:!0,e:`

    集中常用的防火墙配置工具

    1. netfilter
         集成在linux内核中
     2. iptables
    @@ -1954,7 +1954,7 @@ flush privileges;
         基于ufw的图形化客户端
     5. firewalld
         底层也生成一个iptables表
    -
    `,r:{minutes:12.55,words:3764},t:"linux中防火墙",y:"a"}}],["/other/linux/wsl.html",{loader:()=>g(()=>import("./wsl.html-B09klqwv.js"),__vite__mapDeps([170,1])),meta:{a:"chenkun",d:1626912e6,l:"2021年7月22日",e:` +`,r:{minutes:12.55,words:3764},t:"linux中防火墙",y:"a"}}],["/other/linux/wsl.html",{loader:()=>g(()=>import("./wsl.html-q1A1mBMo.js"),__vite__mapDeps([170,1])),meta:{a:"chenkun",d:1626912e6,l:"2021年7月22日",e:`

    二、wsl使用

    2.1 更换国内镜像源

    2.2 从windows进入wsl

    @@ -1965,45 +1965,45 @@ flush privileges;

    2.3 wsl域名解析慢的问题

    域名解析慢会导致apt命令无法在线安装软件,更新等

    -
    `,r:{minutes:.79,words:236},t:"windows子系统wsl",y:"a"}}],["/other/linux/%E6%88%AA%E5%9B%BE.html",{loader:()=>g(()=>import("./截图.html-C375TSox.js"),__vite__mapDeps([171,1])),meta:{a:"chensino",d:1712016e6,l:"2024年4月2日",o:!0,e:`

    manjaro-kde为flameshot设置快捷截图

    +
    `,r:{minutes:.79,words:236},t:"windows子系统wsl",y:"a"}}],["/other/linux/%E6%88%AA%E5%9B%BE.html",{loader:()=>g(()=>import("./截图.html-DpqSuPpa.js"),__vite__mapDeps([171,1])),meta:{a:"chensino",d:1712016e6,l:"2024年4月2日",o:!0,e:`

    manjaro-kde为flameshot设置快捷截图

    1. 打开快捷键设置
     2. 添加命令
     3. 输入\`/usr/bin/flameshot gui\`
     4. 设置一个快捷键
     
     ![20240402111155](https://ddns.chensina.cn:29000/afatpig/blog/20240402111155.png)
    -
    `,r:{minutes:.19,words:57},t:"使用Flameshot截图",y:"a"}}],["/other/markdown/",{loader:()=>g(()=>import("./index.html-DNYsUsip.js"),__vite__mapDeps([172,1])),meta:{a:"chenkun",d:16259616e5,l:"2021年7月11日",c:["markdown"],g:["markdown"],e:`

    Emoji

    +`,r:{minutes:.19,words:57},t:"使用Flameshot截图",y:"a"}}],["/other/markdown/",{loader:()=>g(()=>import("./index.html-DFaAAH7O.js"),__vite__mapDeps([172,1])),meta:{a:"chenkun",d:16259616e5,l:"2021年7月11日",c:["markdown"],g:["markdown"],e:`

    Emoji

    Emoji

    vue-press-theme-hope Icon

    Icon

    -`,r:{minutes:.1,words:29},t:"MarkDown资源",y:"a"}}],["/other/oauth2/01.html",{loader:()=>g(()=>import("./01.html-BqSw6zV7.js"),__vite__mapDeps([173,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    你好,我是王新栋。 +`,r:{minutes:.1,words:29},t:"MarkDown资源",y:"a"}}],["/other/oauth2/01.html",{loader:()=>g(()=>import("./01.html-DTbWNLK7.js"),__vite__mapDeps([173,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    你好,我是王新栋。 在课程正式开始之前,我想先问你个问题。第一次使用极客时间 App 的时候,你是直接使用了第三方帐号(比如微信、微博)登录,还是选择了重新注册新用户?如果你选择了重新注册用户,那你还得上传头像、输入用户名等信息。但如果你选择了使用第三方帐号微信来登录,那极客时间会直接使用你微信的这些信息作为基础信息,你就能省心很多。

    到这里,我估计你会问,这是怎么实现的?微信把我的个人信息给了极客时间,它又是怎么保证我的数据安全的呢?

    -

    其实,微信这一系列授权背后的原理都可以归到一个词上,那就是 OAuth 2.0。今天这节课,我们就来看看 OAuth 2.0 到底是什么、能干什么以及它是怎么干的。

    `,r:{minutes:10.36,words:3109},t:"01 | OAuth 2.0是要通过什么方式解决什么问题?",y:"a"}}],["/other/oauth2/02.html",{loader:()=>g(()=>import("./02.html-hTc330ny.js"),__vite__mapDeps([174,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    你好,我是王新栋。

    +

    其实,微信这一系列授权背后的原理都可以归到一个词上,那就是 OAuth 2.0。今天这节课,我们就来看看 OAuth 2.0 到底是什么、能干什么以及它是怎么干的。

    `,r:{minutes:10.36,words:3109},t:"01 | OAuth 2.0是要通过什么方式解决什么问题?",y:"a"}}],["/other/oauth2/02.html",{loader:()=>g(()=>import("./02.html-0inoDdd1.js"),__vite__mapDeps([174,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    你好,我是王新栋。

    在上一讲,我提到了 OAuth 2.0 的授权码许可类型,在小兔打单软件的例子里面,小兔最终是通过访问令牌请求到小明的店铺里的订单数据。同时呢,我还提到了,这个访问令牌是通过授权码换来的。到这里估计你会问了,为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢?

    你可以先停下来想想这个问题。今天咱们这节课,我会带着你深入探究下其中的逻辑。

    为什么需要授权码?

    -

    在讲这个问题之前,我先要和你同步下,在 OAuth 2.0 的体系里面有 4 种角色,按照官方的称呼它们分别是资源拥有者、客户端、授权服务和受保护资源。不过,这里的客户端,我更愿意称其为第三方软件,而且在咱们这个课程中,都是以第三方软件在举例子。所以,在后续的讲解中我统一把它称为第三方软件。

    `,r:{minutes:12.79,words:3838},t:"02 | 授权码许可类型中,为什么一定要有授权码?",y:"a"}}],["/other/oauth2/03.html",{loader:()=>g(()=>import("./03.html-BlykeApW.js"),__vite__mapDeps([175,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    你好,我是王新栋。

    +

    在讲这个问题之前,我先要和你同步下,在 OAuth 2.0 的体系里面有 4 种角色,按照官方的称呼它们分别是资源拥有者、客户端、授权服务和受保护资源。不过,这里的客户端,我更愿意称其为第三方软件,而且在咱们这个课程中,都是以第三方软件在举例子。所以,在后续的讲解中我统一把它称为第三方软件。

    `,r:{minutes:12.79,words:3838},t:"02 | 授权码许可类型中,为什么一定要有授权码?",y:"a"}}],["/other/oauth2/03.html",{loader:()=>g(()=>import("./03.html-0MOLhHcg.js"),__vite__mapDeps([175,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    你好,我是王新栋。

    在上一讲,我从为什么需要授权码这个问题开始,为你串了一遍授权码许可流程整体的通信过程。在接下来的三讲中,我会着重为你讲解关于授权服务的工作流程、授权过程中的令牌,以及如何接入 OAuth 2.0。这样一来,你就可以吃透授权码许可这一最经典、最完备、最常用的授权流程了,以后再处理授权相关的逻辑就更得心应手了。现在呢,让我们开始这一讲。

    在介绍授权码许可类型时,我提到了很多次 “授权服务”。一句话概括,授权服务就是负责颁发访问令牌的服务。更进一步地讲,OAuth 2.0 的核心是授权服务,而授权服务的核心

    -

    为什么这么说呢?当第三方软件比如小兔,要想获取小明在京东店铺的订单,就必须先从京东商家开放平台的授权服务那里获取访问令牌,进而通过访问令牌来 “代表” 小明去请求小明的订单数据。这不恰恰就是整个 OAuth 2.0 授权体系的核心吗?

    `,r:{minutes:18.59,words:5577},t:"03 | 授权服务:授权码和访问令牌的颁发流程是怎样",y:"a"}}],["/other/oauth2/04.html",{loader:()=>g(()=>import("./04.html-Bb3h6Bb4.js"),__vite__mapDeps([176,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    在上一讲,我们讲到了授权服务的核心就是颁发访问令牌,而 OAuth 2.0 规范并没有约束访问令牌内容的生成规则,只要符合唯一性、不连续性、不可猜性就够了。这就意味着,我们可以灵活选择令牌的形式,既可以是没有内部结构且不包含任何信息含义的随机字符串,也可以是具有内部结构且包含有信息含义的字符串。

    +

    为什么这么说呢?当第三方软件比如小兔,要想获取小明在京东店铺的订单,就必须先从京东商家开放平台的授权服务那里获取访问令牌,进而通过访问令牌来 “代表” 小明去请求小明的订单数据。这不恰恰就是整个 OAuth 2.0 授权体系的核心吗?

    `,r:{minutes:18.59,words:5577},t:"03 | 授权服务:授权码和访问令牌的颁发流程是怎样",y:"a"}}],["/other/oauth2/04.html",{loader:()=>g(()=>import("./04.html-CODd576A.js"),__vite__mapDeps([176,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    在上一讲,我们讲到了授权服务的核心就是颁发访问令牌,而 OAuth 2.0 规范并没有约束访问令牌内容的生成规则,只要符合唯一性、不连续性、不可猜性就够了。这就意味着,我们可以灵活选择令牌的形式,既可以是没有内部结构且不包含任何信息含义的随机字符串,也可以是具有内部结构且包含有信息含义的字符串。

    随机字符串这样的方式我就不再介绍了,之前课程中我们生成令牌的方式都是默认一个随机字符串。而在结构化令牌这方面,目前用得最多的就是 JWT 令牌了。

    -

    接下来,我就要和你详细讲讲,JWT 是什么、原理是怎样的、优势是什么,以及怎么使用,同时我还会讲到令牌生命周期的问题。

    `,r:{minutes:14.53,words:4359},t:"04 | 在OAuth 2.0中,如何使用JWT结构化令牌?",y:"a"}}],["/other/oauth2/05.html",{loader:()=>g(()=>import("./05.html-Cxt8rLap.js"),__vite__mapDeps([177,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    第 3 讲,我已经讲了授权服务的流程,如果你还记得的话,当时我特意强调了一点,就是授权服务将 OAuth 2.0 的复杂性都揽在了自己身上,这也是授权服务为什么是 OAuth 2.0 体系的核心的原因之一。

    -

    虽然授权服务做了大部分工作,但是呢,在 OAuth 2.0 的体系里面,除了资源拥有者是作为用户参与,还有另外两个系统角色,也就是第三方软件和受保护资源服务。那么今天这一讲,我们就站在这两个角色的角度,看看它们应该做哪些工作,才能接入到 OAuth 2.0 的体系里面呢?

    `,r:{minutes:14.76,words:4428},t:"05 | 如何安全、快速地接入OAuth 2.0?",y:"a"}}],["/other/oauth2/06.html",{loader:()=>g(()=>import("./06.html-Bcc4cjIC.js"),__vite__mapDeps([178,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    在前面几讲学习授权码许可类型的原理与工作流程时,不知道你是不是一直有这样一个疑问:授权码许可的流程最完备、最安全没错儿,但它适合所有的授权场景吗?在有些场景下使用授权码许可授权,是不是过于复杂了,是不是根本就没必要这样?

    +

    接下来,我就要和你详细讲讲,JWT 是什么、原理是怎样的、优势是什么,以及怎么使用,同时我还会讲到令牌生命周期的问题。

    `,r:{minutes:14.53,words:4359},t:"04 | 在OAuth 2.0中,如何使用JWT结构化令牌?",y:"a"}}],["/other/oauth2/05.html",{loader:()=>g(()=>import("./05.html-BlGcSftZ.js"),__vite__mapDeps([177,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    第 3 讲,我已经讲了授权服务的流程,如果你还记得的话,当时我特意强调了一点,就是授权服务将 OAuth 2.0 的复杂性都揽在了自己身上,这也是授权服务为什么是 OAuth 2.0 体系的核心的原因之一。

    +

    虽然授权服务做了大部分工作,但是呢,在 OAuth 2.0 的体系里面,除了资源拥有者是作为用户参与,还有另外两个系统角色,也就是第三方软件和受保护资源服务。那么今天这一讲,我们就站在这两个角色的角度,看看它们应该做哪些工作,才能接入到 OAuth 2.0 的体系里面呢?

    `,r:{minutes:14.76,words:4428},t:"05 | 如何安全、快速地接入OAuth 2.0?",y:"a"}}],["/other/oauth2/06.html",{loader:()=>g(()=>import("./06.html-FuByxaV8.js"),__vite__mapDeps([178,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    在前面几讲学习授权码许可类型的原理与工作流程时,不知道你是不是一直有这样一个疑问:授权码许可的流程最完备、最安全没错儿,但它适合所有的授权场景吗?在有些场景下使用授权码许可授权,是不是过于复杂了,是不是根本就没必要这样?

    比如,小兔打单软件是京东官方开发的一款软件,那么小明在使用小兔的时候,还需要小兔再走一遍授权码许可类型的流程吗?估计你也猜到答案了,肯定是不需要了。

    你还记得授权码许可流程的特点么?它通过授权码这种临时的中间值,让小明这样的用户参与进来,从而让小兔软件和京东之间建立联系,进而让小兔代表小明去访问他在京东店铺的订单数据。

    -

    现在小兔被“招安”了,是京东自家的了,是被京东充分信任的,没有“第三方软件”的概念了。同时,小明也是京东店铺的商家,也就是说软件和用户都是京东的资产。这时,显然没有必要再使用授权码许可类型进行授权了。但是呢,小兔依然要通过互联网访问订单数据的 Web API,来提供为小明打单的功能。

    `,r:{minutes:11.08,words:3325},t:"06 | 除了授权码许可类型,OAuth 2.0还支持什么授权流程?",y:"a"}}],["/other/oauth2/07.html",{loader:()=>g(()=>import("./07.html-CN-w1qxn.js"),__vite__mapDeps([179,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    在前面几讲中,我都是基于 Web 应用的场景来讲解的 OAuth 2.0。除了 Web 应用外,现实环境中还有非常多的移动 App。那么,在移动 App 中,能不能使用 OAuth 2.0 ,又该如何使用 OAuth 2.0 呢?

    +

    现在小兔被“招安”了,是京东自家的了,是被京东充分信任的,没有“第三方软件”的概念了。同时,小明也是京东店铺的商家,也就是说软件和用户都是京东的资产。这时,显然没有必要再使用授权码许可类型进行授权了。但是呢,小兔依然要通过互联网访问订单数据的 Web API,来提供为小明打单的功能。

    `,r:{minutes:11.08,words:3325},t:"06 | 除了授权码许可类型,OAuth 2.0还支持什么授权流程?",y:"a"}}],["/other/oauth2/07.html",{loader:()=>g(()=>import("./07.html-Cg_BlXZ8.js"),__vite__mapDeps([179,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    在前面几讲中,我都是基于 Web 应用的场景来讲解的 OAuth 2.0。除了 Web 应用外,现实环境中还有非常多的移动 App。那么,在移动 App 中,能不能使用 OAuth 2.0 ,又该如何使用 OAuth 2.0 呢?

    没错,OAuth 2.0 最初的应用场景确实是 Web 应用,但是它的伟大之处就在于,它把自己的核心协议定位成了一个框架而不是单个的协议。这样做的好处是,我们可以基于这个基本的框架协议,在一些特定的领域进行扩展。

    -

    因此,到了桌面或者移动的场景下,OAuth 2.0 的协议一样适用。考虑到授权码许可是最完备、最安全的许可类型,所以我在讲移动 App 如何使用 OAuth 2.0 的时候,依然会用授权码许可来讲解,毕竟“要用就用最好的”。

    `,r:{minutes:9.48,words:2845},t:"07 | 如何在移动App中使用OAuth 2.0?",y:"a"}}],["/other/oauth2/08.html",{loader:()=>g(()=>import("./08.html-BdpbItx3.js"),__vite__mapDeps([180,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    当知道这一讲的主题是 OAuth 2.0 的安全漏洞时,你可能要问了:“OAuth 2.0 不是一种安全协议吗,不是保护 Web API 的吗?为啥 OAuth 2.0 自己还有安全的问题了呢?”

    +

    因此,到了桌面或者移动的场景下,OAuth 2.0 的协议一样适用。考虑到授权码许可是最完备、最安全的许可类型,所以我在讲移动 App 如何使用 OAuth 2.0 的时候,依然会用授权码许可来讲解,毕竟“要用就用最好的”。

    `,r:{minutes:9.48,words:2845},t:"07 | 如何在移动App中使用OAuth 2.0?",y:"a"}}],["/other/oauth2/08.html",{loader:()=>g(()=>import("./08.html-C2-4HV62.js"),__vite__mapDeps([180,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    当知道这一讲的主题是 OAuth 2.0 的安全漏洞时,你可能要问了:“OAuth 2.0 不是一种安全协议吗,不是保护 Web API 的吗?为啥 OAuth 2.0 自己还有安全的问题了呢?”

    首先,OAuth 2.0 的确是一种安全协议。这没啥问题,但是它有很多使用规范,比如授权码是一个临时凭据只能被使用一次,要对重定向 URI 做校验等。那么,如果使用的时候你没有按照这样的规范来实施,就会有安全漏洞了。

    -

    其次,OAuth 2.0 既然是“生长”在互联网这个大环境中,就一样会面对互联网上常见安全风险的攻击,比如跨站请求伪造(Cross-site request forgery,CSRF)、跨站脚本攻击(Cross Site Scripting,XSS)。最后,除了这些常见攻击类型外,OAuth 2.0 自身也有可被利用的安全漏洞,比如授权码失窃、重定向 URI 伪造。

    `,r:{minutes:15.51,words:4652},t:"08 | 实践OAuth 2.0时,使用不当可能会导致哪些安全漏洞?",y:"a"}}],["/other/oauth2/09.html",{loader:()=>g(()=>import("./09.html-FHq4Vqms.js"),__vite__mapDeps([181,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    如果你是一个第三方软件开发者,在实现用户登录的逻辑时,除了可以让用户新注册一个账号再登录外,还可以接入微信、微博等平台,让用户使用自己的微信、微博账号去登录。同时,如果你的应用下面又有多个子应用,还可以让用户只登录一次就能访问所有的子应用,来提升用户体验。这就是联合登录和单点登录了。再继续深究,它们其实都是 OpenID Connect(简称 OIDC)的应用场景的实现。那 OIDC 又是什么呢?今天,我们就来学习下 OIDC 和 OAuth 2.0 的关系,以及如何用 OAuth 2.0 来实现一个 OIDC 用户身份认证协议。

    +

    其次,OAuth 2.0 既然是“生长”在互联网这个大环境中,就一样会面对互联网上常见安全风险的攻击,比如跨站请求伪造(Cross-site request forgery,CSRF)、跨站脚本攻击(Cross Site Scripting,XSS)。最后,除了这些常见攻击类型外,OAuth 2.0 自身也有可被利用的安全漏洞,比如授权码失窃、重定向 URI 伪造。

    `,r:{minutes:15.51,words:4652},t:"08 | 实践OAuth 2.0时,使用不当可能会导致哪些安全漏洞?",y:"a"}}],["/other/oauth2/09.html",{loader:()=>g(()=>import("./09.html-CYHAg00J.js"),__vite__mapDeps([181,1])),meta:{a:"王新栋",d:16363296e5,l:"2021年11月8日",e:`

    如果你是一个第三方软件开发者,在实现用户登录的逻辑时,除了可以让用户新注册一个账号再登录外,还可以接入微信、微博等平台,让用户使用自己的微信、微博账号去登录。同时,如果你的应用下面又有多个子应用,还可以让用户只登录一次就能访问所有的子应用,来提升用户体验。这就是联合登录和单点登录了。再继续深究,它们其实都是 OpenID Connect(简称 OIDC)的应用场景的实现。那 OIDC 又是什么呢?今天,我们就来学习下 OIDC 和 OAuth 2.0 的关系,以及如何用 OAuth 2.0 来实现一个 OIDC 用户身份认证协议。

    OIDC 是什么?

    -

    OIDC 其实就是一种用户身份认证的开放标准。使用微信账号登录极客时间的场景,就是这种开放标准的实践。

    `,r:{minutes:12.38,words:3713},t:"09 | 实战:利用OAuth 2.0实现一个OpenID Connect用户身份认证协议",y:"a"}}],["/other/oauth2/10.html",{loader:()=>g(()=>import("./10.html-CxJyQyMC.js"),__vite__mapDeps([182,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0 的整个知识体系。

    +

    OIDC 其实就是一种用户身份认证的开放标准。使用微信账号登录极客时间的场景,就是这种开放标准的实践。

    `,r:{minutes:12.38,words:3713},t:"09 | 实战:利用OAuth 2.0实现一个OpenID Connect用户身份认证协议",y:"a"}}],["/other/oauth2/10.html",{loader:()=>g(()=>import("./10.html-N4UaRZCw.js"),__vite__mapDeps([182,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0 的整个知识体系。

    当然了,我也会在这个过程中,与你分享我在实践 OAuth 2.0 的过程中,积累的最值得分享的经验。

    好,接下来就让我们先串一串 OAuth 2.0 的工作流程吧。

    OAuth 2.0 工作流程串讲

    -
    img
    img
    `,r:{minutes:10.17,words:3052},t:"10 | 串讲:OAuth 2.0的工作流程与安全问题",y:"a"}}],["/other/oauth2/11.html",{loader:()=>g(()=>import("./11.html-C-Hi5p0c.js"),__vite__mapDeps([183,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    你好,我朱晔,是《Java 业务开发常见错误 100 例》专栏课程的作者。

    -

    《OAuth 2.0 实战课》上线之后,我也第一时间关注了这门课。在开篇词中,我看到有一些同学留言问道:“如何使用 Spring Security 来实现 OAuth 2.0?”这时,我想到之前自己写过一篇相关的文章,于是就直接在开篇词下留了言。后面我很快收到了不少用户的点赞和肯定,紧接着极客时间编辑也邀请我从自己的角度为专栏写篇加餐。好吧,功不唐捐,于是我就将之前我写的那篇老文章再次迭代、整理为今天的这一讲内容,希望可以帮助你掌握 OAuth 2.0。

    `,r:{minutes:25.57,words:7670},t:"11 | 实战案例:使用Spring Security搭建一套基于JWT的OAuth 2.0架构",y:"a"}}],["/other/oauth2/12.html",{loader:()=>g(()=>import("./12.html-BNyGNDL2.js"),__vite__mapDeps([184,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`
    +
    img
    img
    `,r:{minutes:10.17,words:3052},t:"10 | 串讲:OAuth 2.0的工作流程与安全问题",y:"a"}}],["/other/oauth2/11.html",{loader:()=>g(()=>import("./11.html-q32x2bnY.js"),__vite__mapDeps([183,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    你好,我朱晔,是《Java 业务开发常见错误 100 例》专栏课程的作者。

    +

    《OAuth 2.0 实战课》上线之后,我也第一时间关注了这门课。在开篇词中,我看到有一些同学留言问道:“如何使用 Spring Security 来实现 OAuth 2.0?”这时,我想到之前自己写过一篇相关的文章,于是就直接在开篇词下留了言。后面我很快收到了不少用户的点赞和肯定,紧接着极客时间编辑也邀请我从自己的角度为专栏写篇加餐。好吧,功不唐捐,于是我就将之前我写的那篇老文章再次迭代、整理为今天的这一讲内容,希望可以帮助你掌握 OAuth 2.0。

    `,r:{minutes:25.57,words:7670},t:"11 | 实战案例:使用Spring Security搭建一套基于JWT的OAuth 2.0架构",y:"a"}}],["/other/oauth2/12.html",{loader:()=>g(()=>import("./12.html-DGRJGE6K.js"),__vite__mapDeps([184,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    在前面几讲,我们一起学习了 OAuth 2.0 在开放环境中的使用过程。那么 OAuth 2.0 不仅仅可以用在开放的场景中,它可以应用到我们任何需要授权 / 鉴权的地方,包括微服务。

    @@ -2011,9 +2011,9 @@ flush privileges;

    其中,在携程工作期间,他负责过携程的 API 网关产品的研发工作,包括它和携程的令牌服务的集成;在拍拍贷工作期间,他负责过拍拍贷的令牌服务的研发和运维工作。这两家公司的令牌服务和 OAuth 2.0 类似,但要更简单些。

    -
    `,r:{minutes:19.48,words:5844},t:"12 | 架构案例:基于OAuth 2.0/JWT的微服务参考架构",y:"a"}}],["/other/oauth2/13.html",{loader:()=>g(()=>import("./13.html-DtCYSHOP.js"),__vite__mapDeps([185,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    在咱们这门课中,我提到了很多次“开放平台”,不难理解,它的作用就是企业把自己的业务能力主要以开放 API 的形式,赋能给外部开发者。而作为第三方开发者或者 ISV(独立软件供应商)在接入这些开放平台的时候,我们最应该关心的就是它们的官方文档,关注接入的流程是怎样的、对应的 API 是什么、每个 API 都传递哪些参数,也就

    +
    `,r:{minutes:19.48,words:5844},t:"12 | 架构案例:基于OAuth 2.0/JWT的微服务参考架构",y:"a"}}],["/other/oauth2/13.html",{loader:()=>g(()=>import("./13.html-DiPY3dlK.js"),__vite__mapDeps([185,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    在咱们这门课中,我提到了很多次“开放平台”,不难理解,它的作用就是企业把自己的业务能力主要以开放 API 的形式,赋能给外部开发者。而作为第三方开发者或者 ISV(独立软件供应商)在接入这些开放平台的时候,我们最应该关心的就是它们的官方文档,关注接入的流程是怎样的、对应的 API 是什么、每个 API 都传递哪些参数,也就

    到这里,你会发现“开放平台的官方文档”会是一个关键点。不过呢,当你去各大开放平台上面看这些文

    -

    其中的原因也很简单,那就是开放平台为了让已经具备 OAuth 2.0 知识的研发人员去快速地对接平台上面的业务,把各类对接流程做了分类归档。比如,你会发现微信开放平台上有使用授权码获取授权信息的文档,也有获取令牌的文档,但并没有一份整体的、能够串起来的文档说明。从我的角度来看,这其实也就间接提高了使用门槛,因为如果你不懂 OAuth 2.0,基本是

    `,r:{minutes:11.53,words:3458},t:"13 | 各大开放平台是如何使用OAuth 2.0的?",y:"a"}}],["/other/oauth2/14.html",{loader:()=>g(()=>import("./14.html-C7QC-4vS.js"),__vite__mapDeps([186,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    从 6 月 29 日这门课上线,到现在已经过去一个多月了。我看到了很多同学的留言,有思考,也有提出的问题。那我首先,在这里要感谢你对咱们这门课的支持、鼓励和反馈。在回复你们的留言时,我也把你们提出的问题记了下来。在梳理今天这期答疑的时候,我又从头到尾看了一遍这些问题,也进一步思考了每个问题背后的元认知,最后我归纳出了 6 个问题:

    +

    其中的原因也很简单,那就是开放平台为了让已经具备 OAuth 2.0 知识的研发人员去快速地对接平台上面的业务,把各类对接流程做了分类归档。比如,你会发现微信开放平台上有使用授权码获取授权信息的文档,也有获取令牌的文档,但并没有一份整体的、能够串起来的文档说明。从我的角度来看,这其实也就间接提高了使用门槛,因为如果你不懂 OAuth 2.0,基本是

    `,r:{minutes:11.53,words:3458},t:"13 | 各大开放平台是如何使用OAuth 2.0的?",y:"a"}}],["/other/oauth2/14.html",{loader:()=>g(()=>import("./14.html-Cm6GW1t2.js"),__vite__mapDeps([186,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    从 6 月 29 日这门课上线,到现在已经过去一个多月了。我看到了很多同学的留言,有思考,也有提出的问题。那我首先,在这里要感谢你对咱们这门课的支持、鼓励和反馈。在回复你们的留言时,我也把你们提出的问题记了下来。在梳理今天这期答疑的时候,我又从头到尾看了一遍这些问题,也进一步思考了每个问题背后的元认知,最后我归纳出了 6 个问题:

    1. 发明 OAuth 的目的到底是什么?

      @@ -2033,15 +2033,15 @@ flush privileges;
    2. PKCE 协议到底解决的是什么问题?

    3. -
    `,r:{minutes:10.29,words:3086},t:"14 | 查漏补缺:OAuth 2.0 常见问题答疑",y:"a"}}],["/other/oauth2/",{loader:()=>g(()=>import("./index.html-ByhguBAg.js"),__vite__mapDeps([187,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`
    +`,r:{minutes:10.29,words:3086},t:"14 | 查漏补缺:OAuth 2.0 常见问题答疑",y:"a"}}],["/other/oauth2/",{loader:()=>g(()=>import("./index.html-DoiJAPwv.js"),__vite__mapDeps([187,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    申明

    oauth2相关博客转载于极客时间

    你好,我是王新栋,是京东的资深架构师,主要负责京东商家开放平台的架构工作。在接下来的时间里,我将带你一起学习OAuth 2.0这个授权协议。

    -

    我从2014年加入京东,便开始接触开放平台相关的技术,主要包括网关、授权两块的内容。在刚开始的几年时间里面,我一直都认为网关是开放平台的核心,起到 “中流砥柱” 的作用,毕竟网关要承载整个开放平台的调用量,同时还要有足够的系统容错能力。

    `,r:{minutes:7.87,words:2360},t:"开篇词 | 为什么要学OAuth 2.0?",y:"a"}}],["/other/rabbitmq/",{loader:()=>g(()=>import("./index.html-00yXAu2E.js"),__vite__mapDeps([188,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    从入门到放弃

    +

    我从2014年加入京东,便开始接触开放平台相关的技术,主要包括网关、授权两块的内容。在刚开始的几年时间里面,我一直都认为网关是开放平台的核心,起到 “中流砥柱” 的作用,毕竟网关要承载整个开放平台的调用量,同时还要有足够的系统容错能力。

    `,r:{minutes:7.87,words:2360},t:"开篇词 | 为什么要学OAuth 2.0?",y:"a"}}],["/other/rabbitmq/",{loader:()=>g(()=>import("./index.html-ilrnDPtM.js"),__vite__mapDeps([188,1])),meta:{a:"ChenSino",d:16363296e5,l:"2021年11月8日",e:`

    从入门到放弃

    官网

    中文文档

    -

    在线 动态演示工具

    `,r:{minutes:.12,words:37},t:"rabbitmq",y:"a"}}],["/other/software/%E7%A0%B4%E8%A7%A3%E8%BD%AF%E4%BB%B6.html",{loader:()=>g(()=>import("./破解软件.html-ufEFOt5K.js"),__vite__mapDeps([189,1])),meta:{a:"chensino",d:1667088e6,l:"2022年10月30日",e:`

    smartsvn

    +

    在线 动态演示工具

    `,r:{minutes:.12,words:37},t:"rabbitmq",y:"a"}}],["/other/software/%E7%A0%B4%E8%A7%A3%E8%BD%AF%E4%BB%B6.html",{loader:()=>g(()=>import("./破解软件.html-tKbsG-Zr.js"),__vite__mapDeps([189,1])),meta:{a:"chensino",d:1667088e6,l:"2022年10月30日",e:`

    smartsvn

    安装过程略......

    创建smartsvn.license,把以下内容复制进去,激活时选择smartsvn.license就可以了

    Name=csdn  
    @@ -2053,7 +2053,7 @@ flush privileges;
     Addon-API=true  
     Enterprise=true  
     Key=4kl-<Zqcm-iUF7I-IVmYG-XAyvv-KYRoC-xlgsv-sSBds-VAnP6
    -
    `,r:{minutes:.25,words:75},t:"软件激活",y:"a"}}],["/other/tools/Idea.html",{loader:()=>g(()=>import("./Idea.html-BAc8p-a0.js"),__vite__mapDeps([190,1])),meta:{a:"chenkun",d:16590528e5,l:"2022年7月29日",e:`

    1、多线程debug遇到的问题

    +
    `,r:{minutes:.25,words:75},t:"软件激活",y:"a"}}],["/other/tools/Idea.html",{loader:()=>g(()=>import("./Idea.html-Bdp8uu0g.js"),__vite__mapDeps([190,1])),meta:{a:"chenkun",d:16590528e5,l:"2022年7月29日",e:`

    1、多线程debug遇到的问题

    多线程调试需要把Thread选上,至于Thread和All的区别请查看官方文档

    `,r:{minutes:2.72,words:817},t:"Idea",y:"a"}}],["/other/tools/",{loader:()=>g(()=>import("./index.html-CwfcZ1tZ.js"),__vite__mapDeps([191,1])),meta:{a:"chenkun",d:16161984e5,l:"2021年3月20日",e:` -`,r:{minutes:.05,words:15},t:"软件工具",y:"a"}}],["/other/tools/SoftWare.html",{loader:()=>g(()=>import("./SoftWare.html-BZYs1Hsf.js"),__vite__mapDeps([192,1])),meta:{a:"chenkun",d:1626912e6,l:"2021年7月22日",e:`

    1、Beyond Compare3

    +`,r:{minutes:2.72,words:817},t:"Idea",y:"a"}}],["/other/tools/",{loader:()=>g(()=>import("./index.html-BuYJ3g5z.js"),__vite__mapDeps([191,1])),meta:{a:"chenkun",d:16161984e5,l:"2021年3月20日",e:` +`,r:{minutes:.05,words:15},t:"软件工具",y:"a"}}],["/other/tools/SoftWare.html",{loader:()=>g(()=>import("./SoftWare.html-6ZCndbox.js"),__vite__mapDeps([192,1])),meta:{a:"chenkun",d:1626912e6,l:"2021年7月22日",e:`

    1、Beyond Compare3

    1.1、下载

    资源地址

    1.2 、集成到git 对比

    -

    参考此处配置,主要有两步,

    `,r:{minutes:.3,words:89},t:"软件分享",y:"a"}}],["/other/tools/VsCode.html",{loader:()=>g(()=>import("./VsCode.html-BydNEOJq.js"),__vite__mapDeps([193,1])),meta:{a:"chenkun",d:16263072e5,l:"2021年7月15日",e:` +

    参考此处配置,主要有两步,

    `,r:{minutes:.3,words:89},t:"软件分享",y:"a"}}],["/other/tools/VsCode.html",{loader:()=>g(()=>import("./VsCode.html-B0jpbkGz.js"),__vite__mapDeps([193,1])),meta:{a:"chenkun",d:16263072e5,l:"2021年7月15日",e:`

    版本: 1.69.1 (user setup) 提交: b06ae3b2d2dbfe28bca3134cc6be65935cdfea6a @@ -2082,7 +2082,7 @@ OS: Windows_NT x64 6.1.7601

    现象,使用vscod打开终端一直卡着。

    解决方法是禁用GPU加速,猜测是因为公司电脑没有独显的原因

    -
    `,r:{minutes:1.18,words:353},t:"vscode配置",y:"a"}}],["/other/training/CloudServiceTraining.html",{loader:()=>g(()=>import("./CloudServiceTraining.html-BbBqNR5J.js"),__vite__mapDeps([194,1])),meta:{a:"chenkun",d:1626048e6,l:"2021年7月12日",c:["小组分享"],e:` +
    `,r:{minutes:1.18,words:353},t:"vscode配置",y:"a"}}],["/other/training/CloudServiceTraining.html",{loader:()=>g(()=>import("./CloudServiceTraining.html-DFJMNjIC.js"),__vite__mapDeps([194,1])),meta:{a:"chenkun",d:1626048e6,l:"2021年7月12日",c:["小组分享"],e:`

    [TOC]

    问题:

    @@ -2096,11 +2096,11 @@ OS: Windows_NT x64 6.1.7601

    本章节参考DNS 原理入门——阮一峰

    1.1 什么是DNS

    DNS (Domain Name System 的缩写)的作用非常简单,就是根据域名查出IP地址。你可以把它想象成一本巨大的电话本。 -举例来说,如果你要访问域名math.stackexchange.com,首先要通过DNS查出它的IP地址是151.101.129.69。

    `,r:{minutes:9.66,words:2899},t:"小组分享-云服务",y:"a"}}],["/other/training/",{loader:()=>g(()=>import("./index.html-BNQrYStM.js"),__vite__mapDeps([195,1])),meta:{a:"chenkun",d:1626048e6,l:"2021年7月12日",e:"",r:{minutes:.05,words:15},t:"小组分享",y:"a"}}],["/other/training/SSO.html",{loader:()=>g(()=>import("./SSO.html-o59ROGA3.js"),__vite__mapDeps([196,1])),meta:{a:"chenkun",d:16337376e5,l:"2021年10月9日",c:["小组分享"],e:`

    1、SSO

    +举例来说,如果你要访问域名math.stackexchange.com,首先要通过DNS查出它的IP地址是151.101.129.69。

    `,r:{minutes:9.66,words:2899},t:"小组分享-云服务",y:"a"}}],["/other/training/",{loader:()=>g(()=>import("./index.html-oyZf_SP4.js"),__vite__mapDeps([195,1])),meta:{a:"chenkun",d:1626048e6,l:"2021年7月12日",e:"",r:{minutes:.05,words:15},t:"小组分享",y:"a"}}],["/other/training/SSO.html",{loader:()=>g(()=>import("./SSO.html-CT7CTtUX.js"),__vite__mapDeps([196,1])),meta:{a:"chenkun",d:16337376e5,l:"2021年10月9日",c:["小组分享"],e:`

    1、SSO

    1.1 SSO介绍

    单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    1.2 SSO使用场景(解决了什么问题)

    -

    很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影豆瓣日记......真的会让人崩溃的。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。怎么做?

    `,r:{minutes:7.15,words:2144},t:"OAuth2分享",y:"a"}}],["/other/training/draw.html",{loader:()=>g(()=>import("./draw.html-B6CWzEZ_.js"),__vite__mapDeps([197,1])),meta:{a:"chenkun",d:16375392e5,l:"2021年11月22日",c:["小组分享"],e:`

    1 示例

    +

    很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影豆瓣日记......真的会让人崩溃的。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。怎么做?

    `,r:{minutes:7.15,words:2144},t:"OAuth2分享",y:"a"}}],["/other/training/draw.html",{loader:()=>g(()=>import("./draw.html-Dr2RFJVL.js"),__vite__mapDeps([197,1])),meta:{a:"chenkun",d:16375392e5,l:"2021年11月22日",c:["小组分享"],e:`

    1 示例

    https://paper.pigx.vip/

    2 制图工具

    @@ -2144,12 +2144,12 @@ OS: Windows_NT x64 6.1.7601

    -
    ★★★
    `,r:{minutes:1.18,words:354},t:"画图工具",y:"a"}}],["/other/web/BuildWebProject.html",{loader:()=>g(()=>import("./BuildWebProject.html-C1h9cPZ9.js"),__vite__mapDeps([198,1])),meta:{a:"chenkun",d:16262208e5,l:"2021年7月14日",c:["web"],g:["web","springboot"],e:`
    +`,r:{minutes:1.18,words:354},t:"画图工具",y:"a"}}],["/other/web/BuildWebProject.html",{loader:()=>g(()=>import("./BuildWebProject.html-iK6Db4et.js"),__vite__mapDeps([198,1])),meta:{a:"chenkun",d:16262208e5,l:"2021年7月14日",c:["web"],g:["web","springboot"],e:`

    温馨提示

    项目地址,每个节点的代码使用commitId作为区分,想看某个节点代码,直接还原到对应commitid即可,执行git reset --hard commitId

    1、后端篇

    -

    1.1 初始化springboot项目

    `,r:{minutes:11.48,words:3445},t:"前后分离项目搭建",y:"a"}}],["/other/web/Cookie.html",{loader:()=>g(()=>import("./Cookie.html-CX4Rmbtp.js"),__vite__mapDeps([199,1])),meta:{a:"qianxun",d:1635181021e3,l:"2021年10月25日",g:["必会"],e:`

    Cookie的作用域domain

    +

    1.1 初始化springboot项目

    `,r:{minutes:11.48,words:3445},t:"前后分离项目搭建",y:"a"}}],["/other/web/Cookie.html",{loader:()=>g(()=>import("./Cookie.html-BtSuppuo.js"),__vite__mapDeps([199,1])),meta:{a:"qianxun",d:1635181021e3,l:"2021年10月25日",g:["必会"],e:`

    Cookie的作用域domain

    一级域名:aaa.com 二级域名:bbb.aaa.com 三级域名:ccc.bbb.aaa.com

    @@ -2160,17 +2160,17 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b

    划重点

    在当前域名下,只能设置当前域以及父域的cookie,不能设置子域下的cookie。例如我在浏览器访问后端服务的域名为bbb.aaa.com时,我在后端就只能把cookie的 域设置为当前域(缺省状态就是当前)或者设置为其父域名aaa.com,而不能设置为其子域名ccc.bbb.aaa.com,设置子域名前端SetCookie或有警告

    -
    `,r:{minutes:1.81,words:542},t:"Cookie",y:"a"}}],["/other/web/Http.html",{loader:()=>g(()=>import("./Http.html-Ban9xdQ_.js"),__vite__mapDeps([200,1])),meta:{a:"chensino",d:16357248e5,l:"2021年11月1日",g:["必会"],e:`
    +
    `,r:{minutes:1.81,words:542},t:"Cookie",y:"a"}}],["/other/web/Http.html",{loader:()=>g(()=>import("./Http.html-UGMp5Sj8.js"),__vite__mapDeps([200,1])),meta:{a:"chensino",d:16357248e5,l:"2021年11月1日",g:["必会"],e:`

    必看手册

    https://developer.mozilla.org/zh-CN/docs/Web/HTTP

    一、状态码

    1.1 3xx

    1.1.1 304

    -

    HTTP 304 Not Modified 说明无需再次传输请求的内容,也就是说可以使用缓存的内容。这通常是在一些安全的方法(safe),例如GET 或HEAD 或在请求中附带了头部信息: If-None-Match 或If-Modified-Since。

    `,r:{minutes:2.11,words:632},t:"Http",y:"a"}}],["/other/web/Jwt.html",{loader:()=>g(()=>import("./Jwt.html-7z10_PeO.js"),__vite__mapDeps([201,1])),meta:{a:"chenkun",d:1666687039e3,c:["web"],g:["web"],e:`

    1、jwt在服务端如何校验的?

    +

    HTTP 304 Not Modified 说明无需再次传输请求的内容,也就是说可以使用缓存的内容。这通常是在一些安全的方法(safe),例如GET 或HEAD 或在请求中附带了头部信息: If-None-Match 或If-Modified-Since。

    `,r:{minutes:2.11,words:632},t:"Http",y:"a"}}],["/other/web/Jwt.html",{loader:()=>g(()=>import("./Jwt.html-CIXzxbJn.js"),__vite__mapDeps([201,1])),meta:{a:"chenkun",d:1666687039e3,c:["web"],g:["web"],e:`

    1、jwt在服务端如何校验的?

    之前一直用jwt但是仅仅了解他的基本原理没有去思考一个问题——服务端是如何校验jwt的?

    了解过jwt原理的同学都知道jwt是可以自校验的,token里面有header,payload,我有个想法就是如果用户随便生成一个token,那后端是如何知道这个token不能用?或者我把开发环境的token拿到生产环境使用 是否可行?

    -

    jwt用户认证流程如下,因jwt的token是无状态的,所以每次请求都要经过过滤器进行校验,第一次登陆后把生成的token缓存到redis,当校验时如果找到对应token则继续,解析head中userid信息,根据userid查用户角色权限等信息,然后再设置到security的context中,具体代码如下

    `,r:{minutes:3,words:899},t:"jwt",y:"a"}}],["/other/web/OAUTH_LOGIN.html",{loader:()=>g(()=>import("./OAUTH_LOGIN.html-BU7CthLn.js"),__vite__mapDeps([202,1])),meta:{d:16510176e5,l:"2022年4月27日",c:["oauth"],g:["oauth"],o:!0,e:`
    +

    jwt用户认证流程如下,因jwt的token是无状态的,所以每次请求都要经过过滤器进行校验,第一次登陆后把生成的token缓存到redis,当校验时如果找到对应token则继续,解析head中userid信息,根据userid查用户角色权限等信息,然后再设置到security的context中,具体代码如下

    `,r:{minutes:3,words:899},t:"jwt",y:"a"}}],["/other/web/OAUTH_LOGIN.html",{loader:()=>g(()=>import("./OAUTH_LOGIN.html-CrucBoKd.js"),__vite__mapDeps([202,1])),meta:{d:16510176e5,l:"2022年4月27日",c:["oauth"],g:["oauth"],o:!0,e:`

    本博客介绍前后端分离项目的完整接入oauth的流程,本博客使用github来示范,因为github注册oauth应用无须审核,微信审核特别麻烦,并且个人用户无法注册

    @@ -2179,15 +2179,15 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b
        1. 社交帐号登录应用,比如使用微信、微博登录其它应用
         2. 从第三方获取用户资料比如:手机号、邮箱、头像等
         3. 从第三方获取业务数据,比如:通过自己的系统想从京东获取订单
    -
    `,r:{minutes:5.84,words:1752},t:"oauth第三方登录",y:"a"}}],["/other/web/",{loader:()=>g(()=>import("./index.html-CmAgccQa.js"),__vite__mapDeps([203,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",c:["web"],g:["web"],e:`
    +
    `,r:{minutes:5.84,words:1752},t:"oauth第三方登录",y:"a"}}],["/other/web/",{loader:()=>g(()=>import("./index.html-DDQpBdVL.js"),__vite__mapDeps([203,1])),meta:{a:"chenkun",d:16159392e5,l:"2021年3月17日",c:["web"],g:["web"],e:`

    此分类记录web开发通用知识

    -`,r:{minutes:.11,words:33},t:"web开发通用知识",y:"a"}}],["/other/web/RefreshToken.html",{loader:()=>g(()=>import("./RefreshToken.html-D2M8Zi5t.js"),__vite__mapDeps([204,1])),meta:{d:1651968e6,l:"2022年5月8日",o:!0,e:`

    1、 refresh_token介绍

    -

    refresh_token是OAuth2协议中的一个概念,用于在access_token过期时获取新的access_token,说道token续签大多博客上来就refresh_token巴拉巴拉。在OAuth2协议中,通过客户端以授权服务器的名义请求access_token,客户端会收到access_token和refresh_token,并在access_token过期时使用refresh_token申请新的access_token。refresh_token的安全性非常重要,授权服务器会根据设定的有效期对refresh_token进行管理,以确保对用户的信息和资源进行保护。同时,在使用refresh_token时,也需要进行国家或行业标准的数据保护和安全性认证,以避免出现不必要的风险和隐私泄漏。

    `,r:{minutes:2.1,words:631},t:"refresh_token",y:"a"}}],["/other/web/Restful.html",{loader:()=>g(()=>import("./Restful.html-DGA73Lg6.js"),__vite__mapDeps([205,1])),meta:{a:"chenkun",d:16259616e5,l:"2021年7月11日",c:["web"],g:["web"],e:`

    一、参考

    +`,r:{minutes:.11,words:33},t:"web开发通用知识",y:"a"}}],["/other/web/RefreshToken.html",{loader:()=>g(()=>import("./RefreshToken.html-DrzHWxB_.js"),__vite__mapDeps([204,1])),meta:{d:1651968e6,l:"2022年5月8日",o:!0,e:`

    1、 refresh_token介绍

    +

    refresh_token是OAuth2协议中的一个概念,用于在access_token过期时获取新的access_token,说道token续签大多博客上来就refresh_token巴拉巴拉。在OAuth2协议中,通过客户端以授权服务器的名义请求access_token,客户端会收到access_token和refresh_token,并在access_token过期时使用refresh_token申请新的access_token。refresh_token的安全性非常重要,授权服务器会根据设定的有效期对refresh_token进行管理,以确保对用户的信息和资源进行保护。同时,在使用refresh_token时,也需要进行国家或行业标准的数据保护和安全性认证,以避免出现不必要的风险和隐私泄漏。

    `,r:{minutes:2.1,words:631},t:"refresh_token",y:"a"}}],["/other/web/Restful.html",{loader:()=>g(()=>import("./Restful.html-DLGwYhd3.js"),__vite__mapDeps([205,1])),meta:{a:"chenkun",d:16259616e5,l:"2021年7月11日",c:["web"],g:["web"],e:`

    一、参考


    RESTful架构

    RESTful API 设计

    -`,r:{minutes:.1,words:30},t:"HTTP Restful",y:"a"}}],["/other/web/token%E8%87%AA%E5%8A%A8%E5%88%B7%E6%96%B0.html",{loader:()=>g(()=>import("./token自动刷新.html-tntla8SI.js"),__vite__mapDeps([206,1])),meta:{d:15221952e5,l:"2018年3月28日",e:`

    问题

    +`,r:{minutes:.1,words:30},t:"HTTP Restful",y:"a"}}],["/other/web/token%E8%87%AA%E5%8A%A8%E5%88%B7%E6%96%B0.html",{loader:()=>g(()=>import("./token自动刷新.html-CSV4znys.js"),__vite__mapDeps([206,1])),meta:{d:15221952e5,l:"2018年3月28日",e:`

    问题

    token设置过期时间后,如果不做其他处理,那么会存在一点小问题,比如把token设置有效期为一小时,有个用户登录系统后一直在操作,当到一小时后,突然该用户就被报401,这个体验是非常不好的。

    解决方法

      @@ -2197,14 +2197,14 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b
    1. 使用refresh_token 此方法需要前端来实现,后端需要在用户登陆成功后除了返回access_token外还要返回refresh_token,另外还要多提供一个刷新token的接口,前端去判断何时调用该接口。 参考:refresh_token来自动刷新token
    2. -
    `,r:{minutes:.91,words:272},t:"token自动刷新",y:"a"}}],["/other/web/%E5%89%8D%E5%90%8E%E5%88%86%E7%A6%BBoauth2.0.html",{loader:()=>g(()=>import("./前后分离oauth2.0.html-Be8XZSRK.js"),__vite__mapDeps([207,1])),meta:{d:15221952e5,l:"2018年3月28日",e:`

    OAUTH2.0 业务需求

    +`,r:{minutes:.91,words:272},t:"token自动刷新",y:"a"}}],["/other/web/%E5%89%8D%E5%90%8E%E5%88%86%E7%A6%BBoauth2.0.html",{loader:()=>g(()=>import("./前后分离oauth2.0.html-BjSMXlJY.js"),__vite__mapDeps([207,1])),meta:{d:15221952e5,l:"2018年3月28日",e:`

    OAUTH2.0 业务需求

    因业务需求,需要把两个不相干的系统帐号打通,要求A系统的帐号能登录B系统。从网上找的教程都千篇一律,廖雪峰老师的讲的算是比较透彻,不过没有提怎么实现前后端分离的项目。网上的教程都是前后端放一起,后端在oauth流程结束后很容易把token以及获取到的三方帐号给到前端,然后对于完全前后端分离的项目来说,前后端都是独立的服务,存在跨域,前端如果不主动请求后端,那么后端拿到的oauth信息是无法给到前端了。

    解决方法

    -

    最后通过强大的谷歌找到了方法,参考的OAUTH2.0前后分离,最终用的这个博主的方法实现了前端获取后端传送来的token,以及三方用户信息。

    `,r:{minutes:4.29,words:1288},t:"前后端分离项目OAUTH2.0",y:"a"}}],["/other/windows/WSL.html",{loader:()=>g(()=>import("./WSL.html-DOItsf24.js"),__vite__mapDeps([208,1])),meta:{a:"chensino",d:17270496e5,l:"2024年9月23日",o:!0,e:`

    1. wsl系统设置桥接网络

    +

    最后通过强大的谷歌找到了方法,参考的OAUTH2.0前后分离,最终用的这个博主的方法实现了前端获取后端传送来的token,以及三方用户信息。

    `,r:{minutes:4.29,words:1288},t:"前后端分离项目OAUTH2.0",y:"a"}}],["/other/windows/WSL.html",{loader:()=>g(()=>import("./WSL.html-C3w2j3-t.js"),__vite__mapDeps([208,1])),meta:{a:"chensino",d:17270496e5,l:"2024年9月23日",o:!0,e:`

    1. wsl系统设置桥接网络

    参考:https://www.cnblogs.com/cheyunhua/p/17577895.html

    1.1 开启hyper-v

    桥接功能需要windows的hyper-v组件支持,但是win10/11家庭版是不包含hyper-v的,专业版才包含。网上也有文章提到家庭版安装hyper-v的方法,但是我没有测试,以下内容都是在win11专业版上进行的测试,win10专业版应该也是一样的。 -首先,进入控制面板—程序—启用或关闭windows功能,勾选hyper-v,确认后重启电脑。

    `,r:{minutes:2.15,words:645},t:"wsl问题",y:"a"}}],["/other/windows/%E7%B3%BB%E7%BB%9F%E5%AE%89%E8%A3%85.html",{loader:()=>g(()=>import("./系统安装.html-B1sTpAMR.js"),__vite__mapDeps([209,1])),meta:{a:"chensino",d:17315424e5,l:"2024年11月14日",o:!0,n:!0,r:{minutes:.16,words:48},t:"系统安装激活",y:"a"}}],["/java/framework/mybatis/MybatisPlusDataSource.html",{loader:()=>g(()=>import("./MybatisPlusDataSource.html-Bql0giQE.js"),__vite__mapDeps([210,1])),meta:{a:"chenkun",d:16590528e5,l:"2022年7月29日",e:`

    1、问题的背景

    +首先,进入控制面板—程序—启用或关闭windows功能,勾选hyper-v,确认后重启电脑。

    `,r:{minutes:2.15,words:645},t:"wsl问题",y:"a"}}],["/other/windows/%E7%B3%BB%E7%BB%9F%E5%AE%89%E8%A3%85.html",{loader:()=>g(()=>import("./系统安装.html-Dtd-JGht.js"),__vite__mapDeps([209,1])),meta:{a:"chensino",d:17315424e5,l:"2024年11月14日",o:!0,n:!0,r:{minutes:.16,words:48},t:"系统安装激活",y:"a"}}],["/java/framework/mybatis/MybatisPlusDataSource.html",{loader:()=>g(()=>import("./MybatisPlusDataSource.html-CJvWq9xZ.js"),__vite__mapDeps([210,1])),meta:{a:"chenkun",d:16590528e5,l:"2022年7月29日",e:`

    1、问题的背景

    有两个库,ccsx_data、ccsx_weibao,默认库是ccsx_data,我在代码中使用了>mybatis-pulus的@DS()注解,想切换到ccsx_weibao这个库,但是切换一直失败,代码如下:

    @@ -2221,19 +2221,19 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b } return installRecordVOIPage; } -`,r:{minutes:2.32,words:696},t:"MybatisPlus多线程数据源切换问题",y:"a"}}],["/java/framework/mybatis/",{loader:()=>g(()=>import("./index.html-DDYvG21u.js"),__vite__mapDeps([211,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.02,words:6},t:"Mybatis",y:"a"}}],["/java/framework/mybatis/mybatis.html",{loader:()=>g(()=>import("./mybatis.html-C4UCNyV0.js"),__vite__mapDeps([212,1])),meta:{a:"chenkun",d:15401664e5,l:"2018年10月22日",c:["框架"],g:["框架"],e:`

    mybatis缓存

    +`,r:{minutes:2.32,words:696},t:"MybatisPlus多线程数据源切换问题",y:"a"}}],["/java/framework/mybatis/",{loader:()=>g(()=>import("./index.html-BY3G0JcQ.js"),__vite__mapDeps([211,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.02,words:6},t:"Mybatis",y:"a"}}],["/java/framework/mybatis/mybatis.html",{loader:()=>g(()=>import("./mybatis.html-lxSGPQZl.js"),__vite__mapDeps([212,1])),meta:{a:"chenkun",d:15401664e5,l:"2018年10月22日",c:["框架"],g:["框架"],e:`

    mybatis缓存

    缓存介绍

    一级缓存存在的问题

    1、 一级缓存

    -

    mybatis缓存有一级缓存也叫SelSession缓存,是强制打开的,也就是说Mybatis没有提供关闭一级缓存的方式。一级缓存只是相对于同一个SqlSession而言。所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其查询结果放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。一级缓存的存储位置是在JVM虚拟机内存,它放在一个Map中

    `,r:{minutes:11.71,words:3513},t:"Mybatis使用",y:"a"}}],["/java/framework/security/Authorization.html",{loader:()=>g(()=>import("./Authorization.html-CaZDVM9e.js"),__vite__mapDeps([213,1])),meta:{d:16714944e5,l:"2022年12月20日",c:["security"],e:`

    1、权限说明

    +

    mybatis缓存有一级缓存也叫SelSession缓存,是强制打开的,也就是说Mybatis没有提供关闭一级缓存的方式。一级缓存只是相对于同一个SqlSession而言。所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其查询结果放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。一级缓存的存储位置是在JVM虚拟机内存,它放在一个Map中

    `,r:{minutes:11.71,words:3513},t:"Mybatis使用",y:"a"}}],["/java/framework/security/Authorization.html",{loader:()=>g(()=>import("./Authorization.html-J2At3l0r.js"),__vite__mapDeps([213,1])),meta:{d:16714944e5,l:"2022年12月20日",c:["security"],e:`

    1、权限说明

    认证(Authentication):登录操作就是最常见的认证方式,就是提供用户名和密码来证明自己是某个系统的合法用户,当用户没有经过认证去访问一个受保护资源时,应当响应401
    授权(Authorization):授权是检验用户是否有权限访问某个资源,比如普通用户是无法看到管理员界面的,当用户无权访问某个资源,应当响应403

    2、Security中负责权限校验的类结构图

    -
    Security中权限类
    Security中权限类
    `,r:{minutes:1.07,words:322},t:"权限校验原理",y:"a"}}],["/java/framework/security/CustomAuthenticationProvider.html",{loader:()=>g(()=>import("./CustomAuthenticationProvider.html-C5viGtaI.js"),__vite__mapDeps([214,1])),meta:{a:"chensino",d:16716672e5,l:"2022年12月22日",c:["Security"],e:`

    1、需求

    +
    Security中权限类
    Security中权限类
    `,r:{minutes:1.07,words:322},t:"权限校验原理",y:"a"}}],["/java/framework/security/CustomAuthenticationProvider.html",{loader:()=>g(()=>import("./CustomAuthenticationProvider.html-BPtSrpyO.js"),__vite__mapDeps([214,1])),meta:{a:"chensino",d:16716672e5,l:"2022年12月22日",c:["Security"],e:`

    1、需求

    前后分离项目使用不同登录方式进行登录
         1. 使用帐号/密码登录
         2. 使用手机号/验证码登录
    -
    `,r:{minutes:3.64,words:1092},t:"Security扩展自定义登录方式",y:"a"}}],["/java/framework/security/CustomLoginPage.html",{loader:()=>g(()=>import("./CustomLoginPage.html-OUdaxTEw.js"),__vite__mapDeps([215,1])),meta:{a:"chenkun",d:16674336e5,l:"2022年11月3日",e:`
    +
    `,r:{minutes:3.64,words:1092},t:"Security扩展自定义登录方式",y:"a"}}],["/java/framework/security/CustomLoginPage.html",{loader:()=>g(()=>import("./CustomLoginPage.html-C7rOlHXp.js"),__vite__mapDeps([215,1])),meta:{a:"chenkun",d:16674336e5,l:"2022年11月3日",e:`

    注意

    本篇博客是用来配置前后不分离的项目,正常情况下现在的项目都是前后分离了,因此本篇内容 并没有太多学习价值,但是网上大多数教程都特别喜欢讲这一部分内容,就我目前了解到的内容, @@ -2243,7 +2243,7 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b

    官方文档

    https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html

    -
    `,r:{minutes:2.47,words:741},t:"自定义登陆页面",y:"a"}}],["/java/framework/security/CustomTokenAuthentication.html",{loader:()=>g(()=>import("./CustomTokenAuthentication.html-phBOu6dq.js"),__vite__mapDeps([216,1])),meta:{a:"chensino",d:16716672e5,l:"2022年12月22日",e:`
    +
    `,r:{minutes:2.47,words:741},t:"自定义登陆页面",y:"a"}}],["/java/framework/security/CustomTokenAuthentication.html",{loader:()=>g(()=>import("./CustomTokenAuthentication.html-D2kx6W0O.js"),__vite__mapDeps([216,1])),meta:{a:"chensino",d:16716672e5,l:"2022年12月22日",e:`

    注意

    网上很难精准找到一个前后端分离项目自定义Token认证的教程,找了很久终于找到,特此记录

    @@ -2254,7 +2254,7 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b 后不分离在Security中是大大的不一样!!大家学习的时候关键词检索要记得加上前后分离!! 2. 配置自定义过滤器,自定义过滤器用来处理请求,识别请求中是否有我们自定义的合法token,当然合不合法就要在过滤器的doFilter中自己写了。 -
    `,r:{minutes:2.32,words:696},t:"前后分离项目自定义token认证",y:"a"}}],["/java/framework/security/DelegatingFilterProxy.html",{loader:()=>g(()=>import("./DelegatingFilterProxy.html-CjzmnpRV.js"),__vite__mapDeps([217,1])),meta:{a:"chensino",d:17267904e5,l:"2024年9月20日",o:!0,e:`

    作用

    +`,r:{minutes:2.32,words:696},t:"前后分离项目自定义token认证",y:"a"}}],["/java/framework/security/DelegatingFilterProxy.html",{loader:()=>g(()=>import("./DelegatingFilterProxy.html-DFpCf46S.js"),__vite__mapDeps([217,1])),meta:{a:"chensino",d:17267904e5,l:"2024年9月20日",o:!0,e:`

    作用

    在典型的 Spring 应用中,ContextLoaderListener 通常用于加载 Spring 的应用上下文(ApplicationContext),并管理其中定义的 Spring Bean。然而,Servlet 容器在启动时,会优先初始化过滤器(Filter)实例,而此时 Spring 上下文可能还没有完全加载。

    具体解析:

      @@ -2263,19 +2263,19 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b
      • ContextLoaderListener 是 Spring 中常见的监听器,负责在应用启动时加载 Spring 的 ApplicationContext,从而初始化应用中的所有 Spring Bean。这通常是在应用启动的较晚阶段完成的。
      • ContextLoaderListener 会监听 Web 应用的启动事件,并在合适的时机加载和管理 Spring 上下文。
      • -
      `,r:{minutes:2.4,words:721},t:"DelegatingFilterProxy介绍",y:"a"}}],["/java/framework/security/OAuth2Authentication.html",{loader:()=>g(()=>import("./OAuth2Authentication.html-CAGOYH3n.js"),__vite__mapDeps([218,1])),meta:{a:"chenkun",d:1672704e6,l:"2023年1月3日",c:["Security","OAuth"],e:`

      1、时序图

      +`,r:{minutes:2.4,words:721},t:"DelegatingFilterProxy介绍",y:"a"}}],["/java/framework/security/OAuth2Authentication.html",{loader:()=>g(()=>import("./OAuth2Authentication.html-azcJcZ3I.js"),__vite__mapDeps([218,1])),meta:{a:"chenkun",d:1672704e6,l:"2023年1月3日",c:["Security","OAuth"],e:`

      1、时序图

      时序图
      时序图

      2、流程解析

      本流程是以使用Ruoyi对接Pig授权中心为例,进行讲解,其他网站的的oauth的原理都和这个一样,所以只要把这个流程搞懂了即可,接下来就按照真实的流程进行逐步解析。

      -
      `,r:{minutes:2.4,words:720},t:"Ruoyi使用oauth对接pig",y:"a"}}],["/java/framework/security/OncePerRequestFilter.html",{loader:()=>g(()=>import("./OncePerRequestFilter.html-B3TQmcGq.js"),__vite__mapDeps([219,1])),meta:{a:"chenkun",d:16043616e5,l:"2020年11月3日",c:["Security"],e:`

      1、OnecePerRequestFilter初识

      +
    `,r:{minutes:2.4,words:720},t:"Ruoyi使用oauth对接pig",y:"a"}}],["/java/framework/security/OncePerRequestFilter.html",{loader:()=>g(()=>import("./OncePerRequestFilter.html-B_WUT3Wi.js"),__vite__mapDeps([219,1])),meta:{a:"chenkun",d:16043616e5,l:"2020年11月3日",c:["Security"],e:`

    1、OnecePerRequestFilter初识

    第一次接触这个类,在SpringSecurity中,大概百度了一下,知道此类是限制一次请求只
     走一次过滤器,但是我不明白为啥要做这个限制,或者说难道还有一次请求会走两次过滤器?
    -
    `,r:{minutes:3.43,words:1028},t:"OnecePerRequestFilter",y:"a"}}],["/java/framework/security/PreAuthorize.html",{loader:()=>g(()=>import("./PreAuthorize.html-DzIfTkjN.js"),__vite__mapDeps([220,1])),meta:{d:16531776e5,l:"2022年5月22日",o:!0,e:`

    1、使用方式

    +`,r:{minutes:3.43,words:1028},t:"OnecePerRequestFilter",y:"a"}}],["/java/framework/security/PreAuthorize.html",{loader:()=>g(()=>import("./PreAuthorize.html-QEyDJ3Om.js"),__vite__mapDeps([220,1])),meta:{d:16531776e5,l:"2022年5月22日",o:!0,e:`

    1、使用方式

    比较简单,省略.....

    2、@PreAuthorize注解是何时生效的?

    2.1

    -`,r:{minutes:.11,words:33},t:"PreAuthorize注解",y:"a"}}],["/java/framework/security/",{loader:()=>g(()=>import("./index.html-BqsUMbdU.js"),__vite__mapDeps([221,1])),meta:{a:"chensino",d:16674336e5,l:"2022年11月3日",r:{minutes:.04,words:11},t:"Security学习",y:"a"}}],["/java/framework/security/SSO.html",{loader:()=>g(()=>import("./SSO.html-BQ6lKX33.js"),__vite__mapDeps([222,1])),meta:{d:1641168e6,l:"2022年1月3日",g:["oauth","sso"],e:`

    1、写在前面

    +`,r:{minutes:.11,words:33},t:"PreAuthorize注解",y:"a"}}],["/java/framework/security/",{loader:()=>g(()=>import("./index.html-CgoRxHB7.js"),__vite__mapDeps([221,1])),meta:{a:"chensino",d:16674336e5,l:"2022年11月3日",r:{minutes:.04,words:11},t:"Security学习",y:"a"}}],["/java/framework/security/SSO.html",{loader:()=>g(()=>import("./SSO.html-DntfVSpP.js"),__vite__mapDeps([222,1])),meta:{d:1641168e6,l:"2022年1月3日",g:["oauth","sso"],e:`

    1、写在前面

    一个系统一般都会有用户角色权限相关的概念,用户需要登录系统并且拥有相应的权限后才能访问对应的资源。

    也就是说,用户登录(认证)成功后,会获得此用户拥有的权限,用户在去访问资源时,系统会校验其拥有的权限是否可以访问这个资源。

    2、SSO和OAUTH2介绍

    -

    SSO(SingleSignOn)的出现是为了解决多系统认证的问题的,这里特别强调一下,它是解决认证问题的。就是通过用户的一次性鉴别登录,当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。

    `,r:{minutes:3.65,words:1094},t:"SSO协议",y:"a"}}],["/java/framework/security/SecurityFilterChain.html",{loader:()=>g(()=>import("./SecurityFilterChain.html-Brlhg7Xm.js"),__vite__mapDeps([223,1])),meta:{a:"chensino",d:16358976e5,l:"2021年11月3日",c:["security"],e:`

    1、配置类

    +

    SSO(SingleSignOn)的出现是为了解决多系统认证的问题的,这里特别强调一下,它是解决认证问题的。就是通过用户的一次性鉴别登录,当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。

    `,r:{minutes:3.65,words:1094},t:"SSO协议",y:"a"}}],["/java/framework/security/SecurityFilterChain.html",{loader:()=>g(()=>import("./SecurityFilterChain.html-CklH0qq_.js"),__vite__mapDeps([223,1])),meta:{a:"chensino",d:16358976e5,l:"2021年11月3日",c:["security"],e:`

    1、配置类

    1. 5.7版本后Security把WebSecurityConfigurerAdapter标记为废弃,鼓励程序员使用SecurityFilterChain进行配置,如果看过官网Security的架构图对SecurityFilterChain一定不会陌生,此类是Security过滤器的核心,所以用它来配置寓意更为明显。
    -
    `,r:{minutes:4.81,words:1444},t:"Security配置类",y:"a"}}],["/java/framework/security/Session.html",{loader:()=>g(()=>import("./Session.html-B4ArK8cx.js"),__vite__mapDeps([224,1])),meta:{a:"chenkun",d:1672704e6,l:"2023年1月3日",c:["Security"],e:`

    1、背景(问题复现)

    +`,r:{minutes:4.81,words:1444},t:"Security配置类",y:"a"}}],["/java/framework/security/Session.html",{loader:()=>g(()=>import("./Session.html-C7UWc88l.js"),__vite__mapDeps([224,1])),meta:{a:"chenkun",d:1672704e6,l:"2023年1月3日",c:["Security"],e:`

    1、背景(问题复现)

    
     1. 使用idea新建一个springboot项目,引入web和security框架,不做任何配置直接启动web项目
     2. 随便访问一个接口比如:http://localhost:8080/hello/aa,此时由于接口被security默认保护,会重定向到登录页面(如图一),此时查看sessionid(也就是name为JSESSIONID的cookie)是91E9629F748637154F86CCB44FB2B23D
     3. 然后输入用户名:user,密码:控制台随机生成的,登录后会重定向到之前访问的接口,但此时
        查看JSESSIONID发现变了,变成87957B71A3CEA4FA375CFFACA6AD425D
    -
    `,r:{minutes:1.69,words:507},t:"Security中的Session",y:"a"}}],["/java/framework/security/note.html",{loader:()=>g(()=>import("./note.html-YoblFkQS.js"),__vite__mapDeps([225,1])),meta:{a:"chenkun",d:1662336e6,l:"2022年9月5日",e:`

    1、加密解密流程

    -

    在项目中引入security依赖后,项目启动会自动生成一个账号密码,账号固定为user,密码为控制台随机生成的。账号密码可以自定义加解密方式,一般情况我们的账号密码都是放在数据库中,一个正常的业务流程是,先定义好密码加解密方式,加密方式需要实现PasswordEncoder接口,比如这里我用默认的加解密,然后在新增用户插入到数据库时,需要调用encode方法来加密密码。在请求后端接口时如果需要登陆权限,则会自动跳转到登陆页面,登陆接口会自动调用PasswordEncodermatches方法拿明文密码加密后和密文密码(根据用户名获取已经存到系统的密文)进行匹配,匹配成功则登陆通过。

    `,r:{minutes:3.75,words:1125},t:"Security入门笔记",y:"a"}}],["/java/framework/security/spring-security-oauth2-authorization-server.html",{loader:()=>g(()=>import("./spring-security-oauth2-authorization-server.html-BNQ7rVT9.js"),__vite__mapDeps([226,1])),meta:{d:16520544e5,l:"2022年5月9日",o:!0,e:`

    介绍

    +`,r:{minutes:1.69,words:507},t:"Security中的Session",y:"a"}}],["/java/framework/security/note.html",{loader:()=>g(()=>import("./note.html-CidlTBYC.js"),__vite__mapDeps([225,1])),meta:{a:"chenkun",d:1662336e6,l:"2022年9月5日",e:`

    1、加密解密流程

    +

    在项目中引入security依赖后,项目启动会自动生成一个账号密码,账号固定为user,密码为控制台随机生成的。账号密码可以自定义加解密方式,一般情况我们的账号密码都是放在数据库中,一个正常的业务流程是,先定义好密码加解密方式,加密方式需要实现PasswordEncoder接口,比如这里我用默认的加解密,然后在新增用户插入到数据库时,需要调用encode方法来加密密码。在请求后端接口时如果需要登陆权限,则会自动跳转到登陆页面,登陆接口会自动调用PasswordEncodermatches方法拿明文密码加密后和密文密码(根据用户名获取已经存到系统的密文)进行匹配,匹配成功则登陆通过。

    `,r:{minutes:3.75,words:1125},t:"Security入门笔记",y:"a"}}],["/java/framework/security/spring-security-oauth2-authorization-server.html",{loader:()=>g(()=>import("./spring-security-oauth2-authorization-server.html-D-4kWtlA.js"),__vite__mapDeps([226,1])),meta:{d:16520544e5,l:"2022年5月9日",o:!0,e:`

    介绍

    spring-security-oauth2已经被废弃,采用Security5.7 之后就用spring-security-oauth2-authorization-server,

    /oauth2/token

    在之前的老版本中请求token的端点是在一个叫做TokenEndpoint的类中,此类可以处理/oauth/token请求,这个类可看成是一个Controller,而在新版本中已经没有这个类,新版本中请求token的端点是/oauth2/token,我看源码没有找到对应的controller,直到我debug源码,才发现根本没有类似之前的专门处理/oauth/token的controller,其实新版本在Filter(实际是OAuth2TokenEndpointFilter)中就已经直接响应给客户端了, -20230509201806

    `,r:{minutes:1.02,words:305},t:"spring-security-oauth2-authorization-server",y:"a"}}],["/java/framework/spring/Annotation.html",{loader:()=>g(()=>import("./Annotation.html-DEhyElMf.js"),__vite__mapDeps([227,1])),meta:{d:16715808e5,l:"2022年12月21日",c:["Spring"],e:`

    1、依赖注入

    +20230509201806

    `,r:{minutes:1.02,words:305},t:"spring-security-oauth2-authorization-server",y:"a"}}],["/java/framework/spring/Annotation.html",{loader:()=>g(()=>import("./Annotation.html-C9NZ7kgo.js"),__vite__mapDeps([227,1])),meta:{d:16715808e5,l:"2022年12月21日",c:["Spring"],e:`

    1、依赖注入

     1. @Autowired
          是Spring中的注解,按照类型注入,此注解可以用于字段属性上、setter方法上、构造函数上,用在字段上则Spring底层会使用反射对字段进行赋值,用成员变量的在setter方法上,则会调用setter方法进行注入。从Spring4.3开始,如果只有一个有参的构造方法,则可以省略构造方法上的@Autowired
      2. @Autowired + @Qualifier 
         按照bean的名字注入
      3. @Resource
         是JavaEE规范中的注解在JSR250引入,默认是按照bean的名字注入,如果没指定名字则按照类型注入
    -
    `,r:{minutes:1.48,words:443},t:"注解",y:"a"}}],["/java/framework/spring/BeanPostProcessor.html",{loader:()=>g(()=>import("./BeanPostProcessor.html-B4kV_I9i.js"),__vite__mapDeps([228,1])),meta:{a:"chenkun",d:15401664e5,l:"2018年10月22日",e:` +`,r:{minutes:1.48,words:443},t:"注解",y:"a"}}],["/java/framework/spring/BeanPostProcessor.html",{loader:()=>g(()=>import("./BeanPostProcessor.html-Dp0QSMvM.js"),__vite__mapDeps([228,1])),meta:{a:"chenkun",d:15401664e5,l:"2018年10月22日",e:`

    1. BeanPostProcessor介绍

    打开源码里面有两个方法,分别是postProcessBeforeInitialization和postProcessAfterInitialization。

    
    @@ -2319,7 +2319,7 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b
     	}
     
     }
    -
    `,r:{minutes:3.63,words:1089},t:"BeanPostProcessor介绍",y:"a"}}],["/java/framework/spring/CircularDependency.html",{loader:()=>g(()=>import("./CircularDependency.html-CeopMWPo.js"),__vite__mapDeps([229,1])),meta:{a:"chensino",d:16717536e5,l:"2022年12月23日",c:["Spring"],e:`

    1、循环依赖的产生

    +`,r:{minutes:3.63,words:1089},t:"BeanPostProcessor介绍",y:"a"}}],["/java/framework/spring/CircularDependency.html",{loader:()=>g(()=>import("./CircularDependency.html-B8HCeRou.js"),__vite__mapDeps([229,1])),meta:{a:"chensino",d:16717536e5,l:"2022年12月23日",c:["Spring"],e:`

    1、循环依赖的产生

    A依赖B,B也依赖于A

    @@ -2346,12 +2346,12 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b return this.b; } } -`,r:{minutes:1.07,words:320},t:"Spring循环依赖",y:"a"}}],["/java/framework/spring/DesignPatternInSpring.html",{loader:()=>g(()=>import("./DesignPatternInSpring.html-nq8yHSdu.js"),__vite__mapDeps([230,1])),meta:{d:14950656e5,l:"2017年5月18日",e:`

    1、工厂模式

    +`,r:{minutes:1.07,words:320},t:"Spring循环依赖",y:"a"}}],["/java/framework/spring/DesignPatternInSpring.html",{loader:()=>g(()=>import("./DesignPatternInSpring.html-CoJ7-0Dg.js"),__vite__mapDeps([230,1])),meta:{d:14950656e5,l:"2017年5月18日",e:`

    1、工厂模式

    BeanFactory是典型的工厂方法模式,其有多个实现,不同的实现有不同的getBean方法,默认实现是DefaultListalbeBeanFactory,我们也可以定义自己的工厂实现BeanFactory接口,重写里面的getBean方法

    2、单例模式

    -

    在spring中使用singleton修饰的bean都是单例模式,org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#addSingleton,在spring实例化出真正的对象后,会把这个对象加到容器中

    `,r:{minutes:1.81,words:542},t:"Spring中的设计模式",y:"a"}}],["/java/framework/spring/OncePerRequestFilter.html",{loader:()=>g(()=>import("./OncePerRequestFilter.html-D66nF_K3.js"),__vite__mapDeps([231,1])),meta:{a:"chensino",d:1496016e6,l:"2017年5月29日",e:`

    1、OncePerRequestFilter

    +

    在spring中使用singleton修饰的bean都是单例模式,org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#addSingleton,在spring实例化出真正的对象后,会把这个对象加到容器中

    `,r:{minutes:1.81,words:542},t:"Spring中的设计模式",y:"a"}}],["/java/framework/spring/OncePerRequestFilter.html",{loader:()=>g(()=>import("./OncePerRequestFilter.html-BCQqcD7O.js"),__vite__mapDeps([231,1])),meta:{a:"chensino",d:1496016e6,l:"2017年5月29日",e:`

    1、OncePerRequestFilter

    org.springframework.web.filter.OncePerRequestFilter是springweb中的一个过滤器,是为了确保一个请求只被过滤器执行一次。什么?难道一个请求还能被同一个过滤器执行多次? -其实是有这中可能的,在不同版本下的servlet下,过滤器的行为是不一样的。比如在servlet-2.3中,Filter会过滤一切请求,包括服务器内部使用forward转发请求和<%@ include file="/index.jsp"%>的情况。到了servlet-2.4中Filter默认下只拦截外部提交的请求,forward和include这些内部转发都不会被过滤。因为Springweb无法保证每个用户使用的Servlet容器是一样的,因此,在有些场景如果我们要确保同一个请求只能被过滤器处理一次,那就需要spring自己来实现了,因此有了这个类。其注释下写的很清楚,就是为了要陈any servlet container都确保只执行一次。

    `,r:{minutes:1.26,words:378},t:"OncePerRequestFilter",y:"a"}}],["/java/framework/spring/",{loader:()=>g(()=>import("./index.html-AKycjFxt.js"),__vite__mapDeps([232,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.02,words:6},t:"Spring",y:"a"}}],["/java/framework/spring/SpringAOP.html",{loader:()=>g(()=>import("./SpringAOP.html-2cvojWKp.js"),__vite__mapDeps([233,1])),meta:{d:15060384e5,l:"2017年9月22日",u:100,e:`

    1、SpringAOP

    +其实是有这中可能的,在不同版本下的servlet下,过滤器的行为是不一样的。比如在servlet-2.3中,Filter会过滤一切请求,包括服务器内部使用forward转发请求和<%@ include file="/index.jsp"%>的情况。到了servlet-2.4中Filter默认下只拦截外部提交的请求,forward和include这些内部转发都不会被过滤。因为Springweb无法保证每个用户使用的Servlet容器是一样的,因此,在有些场景如果我们要确保同一个请求只能被过滤器处理一次,那就需要spring自己来实现了,因此有了这个类。其注释下写的很清楚,就是为了要陈any servlet container都确保只执行一次。

    `,r:{minutes:1.26,words:378},t:"OncePerRequestFilter",y:"a"}}],["/java/framework/spring/",{loader:()=>g(()=>import("./index.html-W9fvppVE.js"),__vite__mapDeps([232,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.02,words:6},t:"Spring",y:"a"}}],["/java/framework/spring/SpringAOP.html",{loader:()=>g(()=>import("./SpringAOP.html-DLZP8sCz.js"),__vite__mapDeps([233,1])),meta:{d:15060384e5,l:"2017年9月22日",u:100,e:`

    1、SpringAOP

    > SpringAOP的本质就是动态代理,底层使用JDK动态代理或者CGlib动态代理,通过代理框架生成代理类,实现对目标类的增强,Spring代理是方法级别的代理,是对方法增强,
     > 
     > 代理有四个要素:
    @@ -2359,8 +2359,8 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b
     > 2. 额外功能(增强)
     > 3. 切入点(被增强的方法)
     > 4. 组装(把切入点和额外功能进行整合,就是确认哪些方法需要增强)
    -
    `,r:{minutes:3.52,words:1056},t:"SpringAOP",y:"a"}}],["/java/framework/spring/SpringCache.html",{loader:()=>g(()=>import("./SpringCache.html-keNWZMJo.js"),__vite__mapDeps([234,1])),meta:{d:15375744e5,l:"2018年9月22日",u:100,e:`

    Spring缓存大揭秘

    -`,r:{minutes:1.97,words:592},t:"Spring缓存",y:"a"}}],["/java/framework/spring/SpringExtensionPoint.html",{loader:()=>g(()=>import("./SpringExtensionPoint.html-De7aTg35.js"),__vite__mapDeps([235,1])),meta:{d:14953248e5,l:"2017年5月21日",e:`

    [TOC]

    +`,r:{minutes:3.52,words:1056},t:"SpringAOP",y:"a"}}],["/java/framework/spring/SpringCache.html",{loader:()=>g(()=>import("./SpringCache.html-BsDFP-Bt.js"),__vite__mapDeps([234,1])),meta:{d:15375744e5,l:"2018年9月22日",u:100,e:`

    Spring缓存大揭秘

    +`,r:{minutes:1.97,words:592},t:"Spring缓存",y:"a"}}],["/java/framework/spring/SpringExtensionPoint.html",{loader:()=>g(()=>import("./SpringExtensionPoint.html-BwGgiHY7.js"),__vite__mapDeps([235,1])),meta:{d:14953248e5,l:"2017年5月21日",e:`

    [TOC]

    1. spring生命周期

    spring容器实例化一个对象往大说主要是分为两步

    1.1 第一步:根据配置生成BeanDefinition

    @@ -2371,8 +2371,8 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b
  • scope :可取singleton、prototype,即单例还是多例;
  • lazyInit:表明此bean是否为懒加载,如果是true代表容器启动后自动创建出bean真实对象,若为false,则代表需要显示的调用容器的getBean()方法时才会实例化出对象;
  • 其它字段不再介绍
  • -`,r:{minutes:3.59,words:1077},t:"Spring框架扩展点",y:"a"}}],["/java/framework/spring/SpringIOC.html",{loader:()=>g(()=>import("./SpringIOC.html-DvmNTL7R.js"),__vite__mapDeps([236,1])),meta:{d:15900192e5,l:"2020年5月21日",c:"open-source",e:`

    Spring 最重要的概念是 IOC 和 AOP,本篇文章其实就是要带领大家来分析下 Spring 的 IOC 容器。既然大家平时都要用到 Spring,怎么可以不好好了解 Spring 呢?阅读本文并不能让你成为 Spring 专家,不过一定有助于大家理解 Spring 的很多概念,帮助大家排查应用中和 Spring 相关的一些问题。

    -

    本文采用的源码版本是 4.3.11.RELEASE,算是 5.0.x 前比较新的版本了。为了降低难度,本文所说的所有的内容都是基于 xml 的配置的方式,实际使用已经很少人这么做了,至少不是纯 xml 配置,不过从理解源码的角度来看用这种方式来说无疑是最合适的。

    `,r:{minutes:62.1,words:18630},t:"Spring IOC 容器源码分析",y:"a"}}],["/java/framework/spring/SpringMVC.html",{loader:()=>g(()=>import("./SpringMVC.html-BSrJLxWA.js"),__vite__mapDeps([237,1])),meta:{d:16561152e5,l:"2022年6月25日",o:!0,e:`

    SpringMVC处理请求的流程

    +`,r:{minutes:3.59,words:1077},t:"Spring框架扩展点",y:"a"}}],["/java/framework/spring/SpringIOC.html",{loader:()=>g(()=>import("./SpringIOC.html-Dv9Nuies.js"),__vite__mapDeps([236,1])),meta:{d:15900192e5,l:"2020年5月21日",c:"open-source",e:`

    Spring 最重要的概念是 IOC 和 AOP,本篇文章其实就是要带领大家来分析下 Spring 的 IOC 容器。既然大家平时都要用到 Spring,怎么可以不好好了解 Spring 呢?阅读本文并不能让你成为 Spring 专家,不过一定有助于大家理解 Spring 的很多概念,帮助大家排查应用中和 Spring 相关的一些问题。

    +

    本文采用的源码版本是 4.3.11.RELEASE,算是 5.0.x 前比较新的版本了。为了降低难度,本文所说的所有的内容都是基于 xml 的配置的方式,实际使用已经很少人这么做了,至少不是纯 xml 配置,不过从理解源码的角度来看用这种方式来说无疑是最合适的。

    `,r:{minutes:62.1,words:18630},t:"Spring IOC 容器源码分析",y:"a"}}],["/java/framework/spring/SpringMVC.html",{loader:()=>g(()=>import("./SpringMVC.html-7ciGTcsU.js"),__vite__mapDeps([237,1])),meta:{d:16561152e5,l:"2022年6月25日",o:!0,e:`

    SpringMVC处理请求的流程

    20230625180034
    20230625180034

    前端控制器源码

    
    @@ -2437,7 +2437,7 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b
       }
             //无关代码省略。。。。。
      }
    -
    `,r:{minutes:3.77,words:1131},t:"SpringMVC基本原理",y:"a"}}],["/java/framework/spring/SpringSourceAnalize.html",{loader:()=>g(()=>import("./SpringSourceAnalize.html-CA7pc5uW.js"),__vite__mapDeps([238,1])),meta:{d:16447968e5,l:"2022年2月14日",r:{minutes:.03,words:10},t:"SpringIOC源码分析",y:"a"}}],["/java/framework/spring/Validator.html",{loader:()=>g(()=>import("./Validator.html-Cgd70-bx.js"),__vite__mapDeps([239,1])),meta:{d:16517952e5,l:"2022年5月6日",o:!0,e:`

    自定义校验

    +`,r:{minutes:3.77,words:1131},t:"SpringMVC基本原理",y:"a"}}],["/java/framework/spring/SpringSourceAnalize.html",{loader:()=>g(()=>import("./SpringSourceAnalize.html-BPAH3F93.js"),__vite__mapDeps([238,1])),meta:{d:16447968e5,l:"2022年2月14日",r:{minutes:.03,words:10},t:"SpringIOC源码分析",y:"a"}}],["/java/framework/spring/Validator.html",{loader:()=>g(()=>import("./Validator.html-CVtkvwdc.js"),__vite__mapDeps([239,1])),meta:{d:16517952e5,l:"2022年5月6日",o:!0,e:`

    自定义校验

    
     import com.chensino.core.api.dto.UserLoginDTO;
     import com.chensino.core.api.validate.group.PhoneLogin;
    @@ -2489,7 +2489,7 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b
             }
         }
     }
    -
    `,r:{minutes:1.07,words:322},t:"自定义validator分组检验",y:"a"}}],["/java/framework/springboot/AOPLog.html",{loader:()=>g(()=>import("./AOPLog.html-Bm02oOMf.js"),__vite__mapDeps([240,1])),meta:{d:16160256e5,l:"2021年3月18日",e:`

    1. 博客背景

    +`,r:{minutes:1.07,words:322},t:"自定义validator分组检验",y:"a"}}],["/java/framework/springboot/AOPLog.html",{loader:()=>g(()=>import("./AOPLog.html-oemJ_ITV.js"),__vite__mapDeps([240,1])),meta:{d:16160256e5,l:"2021年3月18日",e:`

    1. 博客背景

    最近业务提了一个需求,让记录每个用户的每个操作请求到数据库,保证每个操作都可追溯,这个需求很典型,实现起来也不难,一个自定义注解就搞定了。

    2. 实现

    @@ -2508,7 +2508,7 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b String value(); } -`,r:{minutes:2,words:601},t:"使用aop记录请求log",y:"a"}}],["/java/framework/springboot/CollectionInject.html",{loader:()=>g(()=>import("./CollectionInject.html-BvWa61Sp.js"),__vite__mapDeps([241,1])),meta:{d:16410816e5,l:"2022年1月2日",c:["Spring"],e:`

    1、测试

    +`,r:{minutes:2,words:601},t:"使用aop记录请求log",y:"a"}}],["/java/framework/springboot/CollectionInject.html",{loader:()=>g(()=>import("./CollectionInject.html-FkTZBxrs.js"),__vite__mapDeps([241,1])),meta:{d:16410816e5,l:"2022年1月2日",c:["Spring"],e:`

    1、测试

    加入有以下代码,MyProcessor是一个接口,没有提供任何实现,然后启动容器会发现执行Bean1的构造方法时并不会空指针,容器会自动提供一个Collection的实现类LinkedHashMap$LinkedValues,那么容器如何注入我自己的MyProcessor呢?

    @Component
     public class Bean1 {
    @@ -2519,9 +2519,9 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b
             System.out.println(processors.size());
         }
     }
    -
    `,r:{minutes:1.35,words:404},t:"注入集合",y:"a"}}],["/java/framework/springboot/Http2.html",{loader:()=>g(()=>import("./Http2.html-2WMDcdOZ.js"),__vite__mapDeps([242,1])),meta:{d:16434144e5,l:"2022年1月29日",e:`

    1、http2.0优势

    +`,r:{minutes:1.35,words:404},t:"注入集合",y:"a"}}],["/java/framework/springboot/Http2.html",{loader:()=>g(()=>import("./Http2.html-NGu_jTQ-.js"),__vite__mapDeps([242,1])),meta:{d:16434144e5,l:"2022年1月29日",e:`

    1、http2.0优势

    2、springboot开启http2.0

    -`,r:{minutes:.06,words:18},t:"springboot开启http2.0",y:"a"}}],["/java/framework/springboot/",{loader:()=>g(()=>import("./index.html-CFDA46HI.js"),__vite__mapDeps([243,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.02,words:6},t:"SpringBoot",y:"a"}}],["/java/framework/springboot/SpringBootAutoConfiguration.html",{loader:()=>g(()=>import("./SpringBootAutoConfiguration.html-BJXaC7pW.js"),__vite__mapDeps([244,1])),meta:{a:"chenkun",d:15393024e5,l:"2018年10月12日",e:`

    1. springboot自动配置的原理初探

    +`,r:{minutes:.06,words:18},t:"springboot开启http2.0",y:"a"}}],["/java/framework/springboot/",{loader:()=>g(()=>import("./index.html-0aNaZ5Jx.js"),__vite__mapDeps([243,1])),meta:{d:1496016e6,l:"2017年5月29日",r:{minutes:.02,words:6},t:"SpringBoot",y:"a"}}],["/java/framework/springboot/SpringBootAutoConfiguration.html",{loader:()=>g(()=>import("./SpringBootAutoConfiguration.html-xbEEkjIP.js"),__vite__mapDeps([244,1])),meta:{a:"chenkun",d:15393024e5,l:"2018年10月12日",e:`

    1. springboot自动配置的原理初探

    ​以下注解都在springboot的自动化配置包中:spring-boot-autoconfigure

    1. @@ -2566,21 +2566,21 @@ aaa.com 是 bbb.aaa.com 和 ccc.bbb.aaa.com 的父域名;bbb.aaa.com 是 ccc.b
    2. AutoConfigurationImportSelector类中的getCandidateConfigurations方法代码如上,其调用了SpringFactoriesLoader的loadFactoryNames方法,来获取

    3. -
    `,r:{minutes:9.25,words:2774},t:"springboot自动配置原理",y:"a"}}],["/java/framework/springboot/Swagger.html",{loader:()=>g(()=>import("./Swagger.html-BUArKIxO.js"),__vite__mapDeps([245,1])),meta:{d:16520544e5,l:"2022年5月9日",o:!0,e:`

    Swagger和SpringFox关系

    +`,r:{minutes:9.25,words:2774},t:"springboot自动配置原理",y:"a"}}],["/java/framework/springboot/Swagger.html",{loader:()=>g(()=>import("./Swagger.html-CSZJriy0.js"),__vite__mapDeps([245,1])),meta:{d:16520544e5,l:"2022年5月9日",o:!0,e:`

    Swagger和SpringFox关系

    java项目在引入swagger的时候,一般会引入如下依赖,当时只知道照着博客抄,也有好奇为啥叫springfox,怎么没有swagger,不知你是否有同样的疑问?直到今天问了一下GPT

        <dependency>
             <groupId>io.springfox</groupId>
             <artifactId>springfox-boot-starter</artifactId>
         </dependency>
    -
    `,r:{minutes:2.29,words:687},t:"Swagger",y:"a"}}],["/java/framework/springboot/%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB.html",{loader:()=>g(()=>import("./读写分离.html-C1zXUnSi.js"),__vite__mapDeps([246,1])),meta:{a:"chensino",d:1638144e6,l:"2021年11月29日",o:!0,e:`
    +
    `,r:{minutes:2.29,words:687},t:"Swagger",y:"a"}}],["/java/framework/springboot/%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB.html",{loader:()=>g(()=>import("./读写分离.html-DusZu70v.js"),__vite__mapDeps([246,1])),meta:{a:"chensino",d:1638144e6,l:"2021年11月29日",o:!0,e:`

    温馨提示

    源码:https://github.com/ChenSino/shardingsphere
    mysql读写分离搭建请参考mysql主从复制搭建

    -
    `,r:{minutes:1.09,words:328},t:"springboot3读写分离",y:"a"}}],["/java/framework/springboot/%E9%83%A8%E7%BD%B2.html",{loader:()=>g(()=>import("./部署.html-D4gxZ9sd.js"),__vite__mapDeps([247,1])),meta:{d:16641504e5,l:"2022年9月26日",o:!0,e:`

    参考

    +`,r:{minutes:1.09,words:328},t:"springboot3读写分离",y:"a"}}],["/java/framework/springboot/%E9%83%A8%E7%BD%B2.html",{loader:()=>g(()=>import("./部署.html-BUaaMgxp.js"),__vite__mapDeps([247,1])),meta:{d:16641504e5,l:"2022年9月26日",o:!0,e:`

    参考

    官方部署说明

    瘦身

    springboot项目本身依赖加上业务第三方依赖会导致打包特别大,可以把依赖包都放在外部,jar包路径通过springboot提供的参数进行配置实现加载。

    -

    spring-boot-maven-plugin插件配置一般如下,这会把所有的依赖都 包含到jar中,

    `,r:{minutes:.94,words:282},t:"springboot部署",y:"a"}}],["/java/framework/springcloud/SpringCloudGateway.html",{loader:()=>g(()=>import("./SpringCloudGateway.html-CGUU2vu8.js"),__vite__mapDeps([248,1])),meta:{d:16556832e5,l:"2022年6月20日",o:!0,e:`

    问题描述

    +

    spring-boot-maven-plugin插件配置一般如下,这会把所有的依赖都 包含到jar中,

    `,r:{minutes:.94,words:282},t:"springboot部署",y:"a"}}],["/java/framework/springcloud/SpringCloudGateway.html",{loader:()=>g(()=>import("./SpringCloudGateway.html-BsaY0McN.js"),__vite__mapDeps([248,1])),meta:{d:16556832e5,l:"2022年6月20日",o:!0,e:`

    问题描述

    有一个业务服务,启动了两个做成负载均衡,分别为10.6.6.11:2221,10.6.6.11:5221,为了调试,所以把route修改为只路由到5221,但是网关服务配置好route后,发送请求无法路由到指定的10.6.6.11:5221服务,一直路由到另一个服务10.6.6.11:2221上,并且连自定义的gatewayfilter都失效了

    请求路径为:http://gateway:port/mcs/test 配置如下

    @@ -2602,10 +2602,10 @@ mysql读写分离搭建请参考 - Path=/mcs/** filters: - StripPrefix=1 -`,r:{minutes:1.83,words:550},t:"网关路由失效",y:"a"}}],["/java/other/gradle/",{loader:()=>g(()=>import("./index.html-CL9jj7rw.js"),__vite__mapDeps([249,1])),meta:{a:"chenkun",d:16660512e5,l:"2022年10月18日",e:`

    1. Gradle的放弃之路

    +`,r:{minutes:1.83,words:550},t:"网关路由失效",y:"a"}}],["/java/other/gradle/",{loader:()=>g(()=>import("./index.html-B5xEb-9f.js"),__vite__mapDeps([249,1])),meta:{a:"chenkun",d:16660512e5,l:"2022年10月18日",e:`

    1. Gradle的放弃之路

    世上无难事,只要肯放弃

    -`,r:{minutes:.09,words:28},t:"Gradle",y:"a"}}],["/java/other/gradle/wrapper.html",{loader:()=>g(()=>import("./wrapper.html-BLTTVm9X.js"),__vite__mapDeps([250,1])),meta:{a:"chenkun",d:16660512e5,l:"2022年10月18日",r:{minutes:.04,words:11},t:"Wrapper",y:"a"}}],["/java/other/locateproblem/",{loader:()=>g(()=>import("./index.html-RViehNRf.js"),__vite__mapDeps([251,1])),meta:{a:"chenkun",d:16589664e5,l:"2022年7月28日",e:`

    此分类专门定位线上问题

    -`,r:{minutes:.09,words:27},t:"线上问题定位",y:"a"}}],["/java/other/locateproblem/TooManyOpenFiles.html",{loader:()=>g(()=>import("./TooManyOpenFiles.html-BYzQ7KMi.js"),__vite__mapDeps([252,1])),meta:{a:`chenkun +`,r:{minutes:.09,words:28},t:"Gradle",y:"a"}}],["/java/other/gradle/wrapper.html",{loader:()=>g(()=>import("./wrapper.html-L3djvmiQ.js"),__vite__mapDeps([250,1])),meta:{a:"chenkun",d:16660512e5,l:"2022年10月18日",r:{minutes:.04,words:11},t:"Wrapper",y:"a"}}],["/java/other/locateproblem/",{loader:()=>g(()=>import("./index.html-BhhaXmFF.js"),__vite__mapDeps([251,1])),meta:{a:"chenkun",d:16589664e5,l:"2022年7月28日",e:`

    此分类专门定位线上问题

    +`,r:{minutes:.09,words:27},t:"线上问题定位",y:"a"}}],["/java/other/locateproblem/TooManyOpenFiles.html",{loader:()=>g(()=>import("./TooManyOpenFiles.html-DcMwQWIU.js"),__vite__mapDeps([252,1])),meta:{a:`chenkun - 问题定位`,d:16613856e5,l:"2022年8月25日",e:`

    Linux服务器打开文件过多

    线上异常

    `,r:{minutes:1.72,words:516},t:"TooManyOpenFiles",y:"a"}}],["/java/other/locateproblem/Undertow.xxxNotFount.html",{loader:()=>g(()=>import("./Undertow.xxxNotFount.html-D6hJS7YH.js"),__vite__mapDeps([253,1])),meta:{d:16517952e5,l:"2022年5月6日",o:!0,e:`

    背景

    +`,r:{minutes:1.72,words:516},t:"TooManyOpenFiles",y:"a"}}],["/java/other/locateproblem/Undertow.xxxNotFount.html",{loader:()=>g(()=>import("./Undertow.xxxNotFount.html-DNDQtsiq.js"),__vite__mapDeps([253,1])),meta:{d:16517952e5,l:"2022年5月6日",o:!0,e:`

    背景

    用户反馈上传图片失败,查看日志报错上传文件提示:java.nio.file.NoSuchFileException: /tmp/undertow.xxxx.xxxxx,解决方案,问题是这个问题之前用户就反馈过,之前是直接重启就好了,之后也没深究。最近用户又反馈不行了,所以花了点时间深究一下从根源解决问题。

    原因

    在 Linux 系统中,Spring Boot 应用以 java -jar 命令启动时,会在操作系统的 /tmp 目录下随机生成一个 tomcat(或 undertow )临时目录,上传的文件先要转换成临时文件保存在这个文件夹中。 由于临时 /tmp 目录下的文件,在长时间(默认10天)没有使用的情况下,操作系统会执行 tmp 目录清理服务(systemd-tmpfiles-clean.service),导致 /tmp/undertow.xxxx.xxxxxxx 文件被清理; -导致在上传文件时,java调用 Files.createFile(…) 在目录/tmp/undertow.xxxx.xxxxxxx下创建临时文件时,发现找不到目录,就会抛出以上的错误。

    `,r:{minutes:1.59,words:478},t:"undertow.xxx not found",y:"a"}}],["/java/other/locateproblem/%E5%90%84%E7%A7%8D%E6%A1%86%E6%9E%B6%E7%89%88%E6%9C%AC%E7%9A%84%E5%9D%91.html",{loader:()=>g(()=>import("./各种框架版本的坑.html-COGDmhTZ.js"),__vite__mapDeps([254,1])),meta:{d:16520544e5,l:"2022年5月9日",o:!0,e:`

    1、Spring系列

    +导致在上传文件时,java调用 Files.createFile(…) 在目录/tmp/undertow.xxxx.xxxxxxx下创建临时文件时,发现找不到目录,就会抛出以上的错误。

    `,r:{minutes:1.59,words:478},t:"undertow.xxx not found",y:"a"}}],["/java/other/locateproblem/%E5%90%84%E7%A7%8D%E6%A1%86%E6%9E%B6%E7%89%88%E6%9C%AC%E7%9A%84%E5%9D%91.html",{loader:()=>g(()=>import("./各种框架版本的坑.html-DwU6AsvJ.js"),__vite__mapDeps([254,1])),meta:{d:16520544e5,l:"2022年5月9日",o:!0,e:`

    1、Spring系列

    1.1 坑1 spring.factories使用方式变更

    2.7版本已经不推荐使用spring.factories,在3.0废弃了,以前的spring.factories改成META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,同时在此文件中写入类全限定名字,多个类,每行一个,不用符号隔开

    -

    1.2 坑2 Spring Security OAuth2已被新版本security弃用

    `,r:{minutes:1.62,words:487},t:"各种版本的坑",y:"a"}}],["/java/other/locateproblem/%E5%90%84%E7%A7%8D%E9%97%AE%E9%A2%98.html",{loader:()=>g(()=>import("./各种问题.html-B7HHDH3m.js"),__vite__mapDeps([255,1])),meta:{d:16521408e5,l:"2022年5月10日",o:!0,e:`

    1. springcloudgateway负载均衡配置不生效

    +

    1.2 坑2 Spring Security OAuth2已被新版本security弃用

    `,r:{minutes:1.62,words:487},t:"各种版本的坑",y:"a"}}],["/java/other/locateproblem/%E5%90%84%E7%A7%8D%E9%97%AE%E9%A2%98.html",{loader:()=>g(()=>import("./各种问题.html-iZ7wZxcM.js"),__vite__mapDeps([255,1])),meta:{d:16521408e5,l:"2022年5月10日",o:!0,e:`

    1. springcloudgateway负载均衡配置不生效

    配置如下,使用lb/serviceid配置负载均衡,一直报错,服务找不到

    `,r:{minutes:.33,words:98},t:"开发中遇到的各种问题",y:"a"}}],["/java/other/log/logback.html",{loader:()=>g(()=>import("./logback.html-P_GvzsK2.js"),__vite__mapDeps([256,1])),meta:{d:16531776e5,l:"2022年5月22日",g:["日志"],o:!0,e:`

    配置

    +`,r:{minutes:.33,words:98},t:"开发中遇到的各种问题",y:"a"}}],["/java/other/log/java%E6%97%A5%E5%BF%97.html",{loader:()=>g(()=>import("./java日志.html-4UsWfMdh.js"),__vite__mapDeps([256,1])),meta:{d:15221952e5,l:"2018年3月28日",e:`
    1. 常见的日志接口
    +
      +
    1. Commons Logging(Apache Commons Logging、Jakarta Commons Logging maven),简称jcl
    2. +
    3. SLF4J +这两个相当于是定义的接口,类似JDBC
    4. +
    +
    2. 日志接口的具体实现
    +
      +
    1. log4j
    2. +
    3. log4j2
    4. +
    5. logback
    6. +
    7. jdk自带的logging(简称jul)
    8. +
    +
    3. 说明
    +
      +
    • 一般使用是需要接口+实现配合使用,可以自由选择组合方式
    • +
    • spring中使用的是jcl日志接口,如果直接从spring-core中排除jcl会报错,需要添加jcl-over-slf4j
    • +
    • xxx-over-yyy代表,把原来的xxx改成用yyy,jcl-over-slf4j就是代表spring本来用的jcl框架实现的日志,现把它改成用slf4j来实现
    • +
    `,r:{minutes:2.13,words:639},t:"Java日志框架",y:"a"}}],["/java/other/log/logback.html",{loader:()=>g(()=>import("./logback.html-BexKq4m4.js"),__vite__mapDeps([257,1])),meta:{d:16531776e5,l:"2022年5月22日",g:["日志"],o:!0,e:`

    配置

    `,r:{minutes:3.56,words:1068},t:"LogBack",y:"a"}}],["/java/other/maven/build%E6%A0%87%E7%AD%BE.html",{loader:()=>g(()=>import("./build标签.html-DY2foH_V.js"),__vite__mapDeps([257,1])),meta:{d:16546464e5,l:"2022年6月8日",c:["maven"],o:!0,e:`

    配置解释

    +`,r:{minutes:3.56,words:1068},t:"LogBack",y:"a"}}],["/java/other/maven/build%E6%A0%87%E7%AD%BE.html",{loader:()=>g(()=>import("./build标签.html-Dc92dnvN.js"),__vite__mapDeps([258,1])),meta:{d:16546464e5,l:"2022年6月8日",c:["maven"],o:!0,e:`

    配置解释

    resource标签的操作是通过maven-resource-plugin插件来实现的,通过配置标签来实现资源文件的过滤替换。这个标签的作用是告诉Maven,在拷贝源文件到目标路径之前,对源文件内容进行参数替换。

    `,r:{minutes:.8,words:241},t:"build标签",y:"a"}}],["/java/other/maven/import.html",{loader:()=>g(()=>import("./import.html-CiYx9gmq.js"),__vite__mapDeps([258,1])),meta:{a:"chenkun",d:16620768e5,l:"2022年9月2日",e:`

    1. import介绍

    +`,r:{minutes:.8,words:241},t:"build标签",y:"a"}}],["/java/other/maven/import.html",{loader:()=>g(()=>import("./import.html-CH_AORfR.js"),__vite__mapDeps([259,1])),meta:{a:"chenkun",d:16620768e5,l:"2022年9月2日",e:`

    1. import介绍

    官方介绍

    -

    maven中的import是解决maven只能单个继承的问题,有时候我们的maven项目已经有了一个公司所自定义parent,但是我们想引入另一个项目中的dependencyManagement,此时可以用import,可以理解为把另一个项目中的dependencyManagement内容直接复制到本项目。

    `,r:{minutes:1.6,words:480},t:"import使用",y:"a"}}],["/java/other/maven/multiModule.html",{loader:()=>g(()=>import("./multiModule.html-DxNg0iZD.js"),__vite__mapDeps([259,1])),meta:{a:"chenkun",d:16589664e5,l:"2022年7月28日",e:`

    1、一个真实的多模块maven项目

    +

    maven中的import是解决maven只能单个继承的问题,有时候我们的maven项目已经有了一个公司所自定义parent,但是我们想引入另一个项目中的dependencyManagement,此时可以用import,可以理解为把另一个项目中的dependencyManagement内容直接复制到本项目。

    `,r:{minutes:1.6,words:480},t:"import使用",y:"a"}}],["/java/other/maven/multiModule.html",{loader:()=>g(()=>import("./multiModule.html-KdXk5jwS.js"),__vite__mapDeps([260,1])),meta:{a:"chenkun",d:16589664e5,l:"2022年7月28日",e:`

    1、一个真实的多模块maven项目

    $ tree -d -L 2
     .———————————————————————————————————最顶级pom所在目录
     ├── ccs-auth
    @@ -2804,7 +2822,7 @@ mysql读写分离搭建请参考├── ccs-weibao
         ├── ccs-weibao-api
         └── ccs-weibao-biz
    -
    `,r:{minutes:.85,words:255},t:"多模块maven项目的搭建",y:"a"}}],["/java/other/maven/problem.html",{loader:()=>g(()=>import("./problem.html-BgKe_lWA.js"),__vite__mapDeps([260,1])),meta:{a:"chenkun",d:16589664e5,l:"2022年7月28日",e:`

    1、继承

    +`,r:{minutes:.85,words:255},t:"多模块maven项目的搭建",y:"a"}}],["/java/other/maven/problem.html",{loader:()=>g(()=>import("./problem.html-Xwm_z36q.js"),__vite__mapDeps([261,1])),meta:{a:"chenkun",d:16589664e5,l:"2022年7月28日",e:`

    1、继承

    继承使用场景
    把公共的依赖都放在父模块,在子模块通过parent引用,使用了parent后,在父模块中dependencies定义的依赖会自动传递到子模块。

    @@ -2814,18 +2832,18 @@ mysql读写分离搭建请参考
    <groupId>com.chensino</groupId> <version>0.0.1-SNAPSHOT</version> </parent> -`,r:{minutes:2.72,words:816},t:"使用问题记录",y:"a"}}],["/java/other/maven/%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.html",{loader:()=>g(()=>import("./生命周期.html-BF6EG3Sg.js"),__vite__mapDeps([261,1])),meta:{d:165456e7,l:"2022年6月7日",c:["maven"],o:!0,e:`

    Maven的生命周期

    +`,r:{minutes:2.72,words:816},t:"使用问题记录",y:"a"}}],["/java/other/maven/%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.html",{loader:()=>g(()=>import("./生命周期.html-DY214BI_.js"),__vite__mapDeps([262,1])),meta:{d:165456e7,l:"2022年6月7日",c:["maven"],o:!0,e:`

    Maven的生命周期

    There are three built-in build lifecycles: default, clean and site. The default lifecycle handles your project deployment, the clean lifecycle handles project cleaning, while the site lifecycle handles the creation of your project's web site.

    各个生命周期包含的phase

    -

    官方说明

    `,r:{minutes:5.36,words:1609},t:"Maven的生命周期",y:"a"}}],["/404.html",{loader:()=>g(()=>import("./404.html-BagFMDd7.js"),__vite__mapDeps([262,1])),meta:{t:""}}],["/cpp/other/",{loader:()=>g(()=>import("./index.html-BRm5Gvor.js"),__vite__mapDeps([263,1])),meta:{t:"Other"}}],["/cpp/",{loader:()=>g(()=>import("./index.html-BPC3zJSU.js"),__vite__mapDeps([264,1])),meta:{t:"Cpp"}}],["/java/other/",{loader:()=>g(()=>import("./index.html-DzNwwRfO.js"),__vite__mapDeps([265,1])),meta:{t:"Other"}}],["/other/computerprinciple/",{loader:()=>g(()=>import("./index.html-DOEKJOHw.js"),__vite__mapDeps([266,1])),meta:{t:"Computerprinciple"}}],["/other/docker/",{loader:()=>g(()=>import("./index.html-Dz1hbfvm.js"),__vite__mapDeps([267,1])),meta:{t:"Docker"}}],["/other/hardware/",{loader:()=>g(()=>import("./index.html-DNVny5DL.js"),__vite__mapDeps([268,1])),meta:{t:"Hardware"}}],["/other/software/",{loader:()=>g(()=>import("./index.html-D8BV5Mgu.js"),__vite__mapDeps([269,1])),meta:{t:"Software"}}],["/other/windows/",{loader:()=>g(()=>import("./index.html-DP3vVcUi.js"),__vite__mapDeps([270,1])),meta:{t:"Windows"}}],["/java/framework/",{loader:()=>g(()=>import("./index.html-Biz09ahT.js"),__vite__mapDeps([271,1])),meta:{t:"Framework"}}],["/java/framework/springcloud/",{loader:()=>g(()=>import("./index.html-CHMcR43A.js"),__vite__mapDeps([272,1])),meta:{t:"Springcloud"}}],["/java/other/log/",{loader:()=>g(()=>import("./index.html-BEpN4Kx1.js"),__vite__mapDeps([273,1])),meta:{t:"Log"}}],["/java/other/maven/",{loader:()=>g(()=>import("./index.html-DAgZUna0.js"),__vite__mapDeps([274,1])),meta:{t:"Maven"}}],["/category/",{loader:()=>g(()=>import("./index.html-B6W_yULn.js"),__vite__mapDeps([275,1])),meta:{t:"分类",I:!1}}],["/category/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/",{loader:()=>g(()=>import("./index.html-DU4GHQR6.js"),__vite__mapDeps([276,1])),meta:{t:"设计模式 分类",I:!1}}],["/category/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/",{loader:()=>g(()=>import("./index.html-CmzcNC6M.js"),__vite__mapDeps([277,1])),meta:{t:"使用指南 分类",I:!1}}],["/category/vue%E7%9F%A5%E8%AF%86%E7%82%B9/",{loader:()=>g(()=>import("./index.html-CFahR_Uz.js"),__vite__mapDeps([278,1])),meta:{t:"vue知识点 分类",I:!1}}],["/category/js%E5%9F%BA%E7%A1%80/",{loader:()=>g(()=>import("./index.html-bniV5DET.js"),__vite__mapDeps([279,1])),meta:{t:"js基础 分类",I:!1}}],["/category/npm%E7%9F%A5%E8%AF%86%E7%82%B9/",{loader:()=>g(()=>import("./index.html-CaaT6W-w.js"),__vite__mapDeps([280,1])),meta:{t:"npm知识点 分类",I:!1}}],["/category/%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D/",{loader:()=>g(()=>import("./index.html-CQ2SoX7l.js"),__vite__mapDeps([281,1])),meta:{t:"问题定位 分类",I:!1}}],["/category/vite/",{loader:()=>g(()=>import("./index.html-DAAyVcR0.js"),__vite__mapDeps([282,1])),meta:{t:"vite 分类",I:!1}}],["/category/%E9%9B%86%E5%90%88/",{loader:()=>g(()=>import("./index.html-BCmoJY04.js"),__vite__mapDeps([283,1])),meta:{t:"集合 分类",I:!1}}],["/category/jdk/",{loader:()=>g(()=>import("./index.html-D0V6L-UM.js"),__vite__mapDeps([284,1])),meta:{t:"jdk 分类",I:!1}}],["/category/%E7%BA%BF%E7%A8%8B%E6%B1%A0/",{loader:()=>g(()=>import("./index.html-35GNTpgO.js"),__vite__mapDeps([285,1])),meta:{t:"线程池 分类",I:!1}}],["/category/%E5%A4%9A%E7%BA%BF%E7%A8%8B/",{loader:()=>g(()=>import("./index.html-CDZvytPE.js"),__vite__mapDeps([286,1])),meta:{t:"多线程 分类",I:!1}}],["/category/java/",{loader:()=>g(()=>import("./index.html-DyVVIrWa.js"),__vite__mapDeps([287,1])),meta:{t:"java 分类",I:!1}}],["/category/%E5%AF%B9%E8%B1%A1%E9%94%81/",{loader:()=>g(()=>import("./index.html-C3RPpUVY.js"),__vite__mapDeps([288,1])),meta:{t:"对象锁 分类",I:!1}}],["/category/java%E5%9F%BA%E7%A1%80/",{loader:()=>g(()=>import("./index.html-m-Am85s9.js"),__vite__mapDeps([289,1])),meta:{t:"java基础 分类",I:!1}}],["/category/jvm/",{loader:()=>g(()=>import("./index.html-DmaFZnd1.js"),__vite__mapDeps([290,1])),meta:{t:"jvm 分类",I:!1}}],["/category/%E7%94%B5%E5%AD%90%E4%B9%A6/",{loader:()=>g(()=>import("./index.html-Cyfg2yKH.js"),__vite__mapDeps([291,1])),meta:{t:"电子书 分类",I:!1}}],["/category/%E6%95%B0%E6%8D%AE%E5%BA%93/",{loader:()=>g(()=>import("./index.html-WwuxfzLA.js"),__vite__mapDeps([292,1])),meta:{t:"数据库 分类",I:!1}}],["/category/docker/",{loader:()=>g(()=>import("./index.html-D1sJtXWQ.js"),__vite__mapDeps([293,1])),meta:{t:"docker 分类",I:!1}}],["/category/%E8%BF%90%E7%BB%B4/",{loader:()=>g(()=>import("./index.html-Ckn7YqIK.js"),__vite__mapDeps([294,1])),meta:{t:"运维 分类",I:!1}}],["/category/git-%E6%93%8D%E4%BD%9C/",{loader:()=>g(()=>import("./index.html-D48OAIhP.js"),__vite__mapDeps([295,1])),meta:{t:"git 操作 分类",I:!1}}],["/category/%E5%BF%85%E4%BC%9A/",{loader:()=>g(()=>import("./index.html-hFoavWQa.js"),__vite__mapDeps([296,1])),meta:{t:"必会 分类",I:!1}}],["/category/linux/",{loader:()=>g(()=>import("./index.html-CU-UF1rP.js"),__vite__mapDeps([297,1])),meta:{t:"linux 分类",I:!1}}],["/category/markdown/",{loader:()=>g(()=>import("./index.html-BGGeTkjF.js"),__vite__mapDeps([298,1])),meta:{t:"markdown 分类",I:!1}}],["/category/%E5%B0%8F%E7%BB%84%E5%88%86%E4%BA%AB/",{loader:()=>g(()=>import("./index.html-BArKTdeW.js"),__vite__mapDeps([299,1])),meta:{t:"小组分享 分类",I:!1}}],["/category/web/",{loader:()=>g(()=>import("./index.html-DecTW5Vq.js"),__vite__mapDeps([300,1])),meta:{t:"web 分类",I:!1}}],["/category/oauth/",{loader:()=>g(()=>import("./index.html-KsfGbA7X.js"),__vite__mapDeps([301,1])),meta:{t:"oauth 分类",I:!1}}],["/category/%E6%A1%86%E6%9E%B6/",{loader:()=>g(()=>import("./index.html-Cimzf8US.js"),__vite__mapDeps([302,1])),meta:{t:"框架 分类",I:!1}}],["/category/security/",{loader:()=>g(()=>import("./index.html-C---1nFZ.js"),__vite__mapDeps([303,1])),meta:{t:"security 分类",I:!1}}],["/category/security/",{loader:()=>g(()=>import("./index.html-C---1nFZ.js"),__vite__mapDeps([303,1])),meta:{t:"Security 分类",I:!1}}],["/category/oauth/",{loader:()=>g(()=>import("./index.html-KsfGbA7X.js"),__vite__mapDeps([301,1])),meta:{t:"OAuth 分类",I:!1}}],["/category/spring/",{loader:()=>g(()=>import("./index.html-ncHBdLCM.js"),__vite__mapDeps([304,1])),meta:{t:"Spring 分类",I:!1}}],["/category/open-source/",{loader:()=>g(()=>import("./index.html-DerE9RKl.js"),__vite__mapDeps([305,1])),meta:{t:"open-source 分类",I:!1}}],["/category/maven/",{loader:()=>g(()=>import("./index.html-DPMWiYpK.js"),__vite__mapDeps([306,1])),meta:{t:"maven 分类",I:!1}}],["/tag/",{loader:()=>g(()=>import("./index.html-DmIcEJVt.js"),__vite__mapDeps([307,1])),meta:{t:"标签",I:!1}}],["/tag/%E7%A6%81%E7%94%A8/",{loader:()=>g(()=>import("./index.html-CCYbeGLn.js"),__vite__mapDeps([308,1])),meta:{t:"标签: 禁用",I:!1}}],["/tag/%E6%96%87%E7%AB%A0%E5%8A%A0%E5%AF%86/",{loader:()=>g(()=>import("./index.html-DY5iuE1D.js"),__vite__mapDeps([309,1])),meta:{t:"标签: 文章加密",I:!1}}],["/tag/markdown/",{loader:()=>g(()=>import("./index.html-B26PLbnt.js"),__vite__mapDeps([310,1])),meta:{t:"标签: Markdown",I:!1}}],["/tag/%E9%A1%B5%E9%9D%A2%E9%85%8D%E7%BD%AE/",{loader:()=>g(()=>import("./index.html-CEVfOHM2.js"),__vite__mapDeps([311,1])),meta:{t:"标签: 页面配置",I:!1}}],["/tag/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/",{loader:()=>g(()=>import("./index.html-DRDdWuur.js"),__vite__mapDeps([312,1])),meta:{t:"标签: 使用指南",I:!1}}],["/tag/%E4%BD%A0%E6%89%80%E4%B8%8D%E4%BA%86%E8%A7%A3%E7%9A%84javascript/",{loader:()=>g(()=>import("./index.html-gC5jCLxE.js"),__vite__mapDeps([313,1])),meta:{t:"标签: 你所不了解的JavaScript",I:!1}}],["/tag/%E5%BF%85%E4%BC%9A/",{loader:()=>g(()=>import("./index.html-BCxXA6GU.js"),__vite__mapDeps([314,1])),meta:{t:"标签: 必会",I:!1}}],["/tag/vue%E4%B8%AD%E7%9A%84-typescript/",{loader:()=>g(()=>import("./index.html-Bg-N4ojs.js"),__vite__mapDeps([315,1])),meta:{t:"标签: vue中的 TypeScript",I:!1}}],["/tag/vite/",{loader:()=>g(()=>import("./index.html-E-Cx3csN.js"),__vite__mapDeps([316,1])),meta:{t:"标签: vite",I:!1}}],["/tag/ts/",{loader:()=>g(()=>import("./index.html-D7cnqetB.js"),__vite__mapDeps([317,1])),meta:{t:"标签: ts",I:!1}}],["/tag/vue3/",{loader:()=>g(()=>import("./index.html-CJX-cM8l.js"),__vite__mapDeps([318,1])),meta:{t:"标签: vue3",I:!1}}],["/tag/%E9%9B%86%E5%90%88/",{loader:()=>g(()=>import("./index.html-RbiR8Hxk.js"),__vite__mapDeps([319,1])),meta:{t:"标签: 集合",I:!1}}],["/tag/---%E5%B9%B6%E5%8F%91/",{loader:()=>g(()=>import("./index.html-CnSAmy10.js"),__vite__mapDeps([320,1])),meta:{t:"标签: -- 并发",I:!1}}],["/tag/%E5%A4%9A%E7%BA%BF%E7%A8%8B/",{loader:()=>g(()=>import("./index.html-CX-Lzl1D.js"),__vite__mapDeps([321,1])),meta:{t:"标签: 多线程",I:!1}}],["/tag/%E7%BA%BF%E7%A8%8B%E6%B1%A0/",{loader:()=>g(()=>import("./index.html-BYhgblfy.js"),__vite__mapDeps([322,1])),meta:{t:"标签: 线程池",I:!1}}],["/tag/oauth/",{loader:()=>g(()=>import("./index.html-CnvT0Mlm.js"),__vite__mapDeps([323,1])),meta:{t:"标签: oauth",I:!1}}],["/tag/sso/",{loader:()=>g(()=>import("./index.html-DOt2JR9K.js"),__vite__mapDeps([324,1])),meta:{t:"标签: sso",I:!1}}],["/tag/%E9%83%A8%E7%BD%B2%E6%90%AD%E5%BB%BA/",{loader:()=>g(()=>import("./index.html-4XNFUA_b.js"),__vite__mapDeps([325,1])),meta:{t:"标签: 部署搭建",I:!1}}],["/tag/%E5%AD%97%E8%8A%82%E7%A0%81/",{loader:()=>g(()=>import("./index.html-zRLBETDX.js"),__vite__mapDeps([326,1])),meta:{t:"标签: 字节码",I:!1}}],["/tag/%E5%8F%8D%E6%B1%87%E7%BC%96/",{loader:()=>g(()=>import("./index.html-Cn-vemYg.js"),__vite__mapDeps([327,1])),meta:{t:"标签: 反汇编",I:!1}}],["/tag/mysql/",{loader:()=>g(()=>import("./index.html-X1bIOYsb.js"),__vite__mapDeps([328,1])),meta:{t:"标签: mysql",I:!1}}],["/tag/%E6%95%B0%E6%8D%AE%E5%BA%93/",{loader:()=>g(()=>import("./index.html-BqzZCNMV.js"),__vite__mapDeps([329,1])),meta:{t:"标签: 数据库",I:!1}}],["/tag/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/",{loader:()=>g(()=>import("./index.html-DE-84drN.js"),__vite__mapDeps([330,1])),meta:{t:"标签: 数据结构",I:!1}}],["/tag/%E4%BA%8C%E5%8F%89%E6%A0%91/",{loader:()=>g(()=>import("./index.html-CLZJl0yu.js"),__vite__mapDeps([331,1])),meta:{t:"标签: 二叉树",I:!1}}],["/tag/%E8%BF%90%E7%BB%B4/",{loader:()=>g(()=>import("./index.html-JNXR2WUl.js"),__vite__mapDeps([332,1])),meta:{t:"标签: 运维",I:!1}}],["/tag/%E5%B7%A5%E5%85%B7%E4%BD%BF%E7%94%A8/",{loader:()=>g(()=>import("./index.html-B1T6Eb8n.js"),__vite__mapDeps([333,1])),meta:{t:"标签: 工具使用",I:!1}}],["/tag/linux/",{loader:()=>g(()=>import("./index.html-CbMP3z62.js"),__vite__mapDeps([334,1])),meta:{t:"标签: linux",I:!1}}],["/tag/markdown/",{loader:()=>g(()=>import("./index.html-B26PLbnt.js"),__vite__mapDeps([310,1])),meta:{t:"标签: markdown",I:!1}}],["/tag/web/",{loader:()=>g(()=>import("./index.html-BkiR12t2.js"),__vite__mapDeps([335,1])),meta:{t:"标签: web",I:!1}}],["/tag/springboot/",{loader:()=>g(()=>import("./index.html-gvETmnnw.js"),__vite__mapDeps([336,1])),meta:{t:"标签: springboot",I:!1}}],["/tag/%E6%A1%86%E6%9E%B6/",{loader:()=>g(()=>import("./index.html-BVhcyzEe.js"),__vite__mapDeps([337,1])),meta:{t:"标签: 框架",I:!1}}],["/tag/%E6%97%A5%E5%BF%97/",{loader:()=>g(()=>import("./index.html-Cv8nLQFM.js"),__vite__mapDeps([338,1])),meta:{t:"标签: 日志",I:!1}}],["/article/",{loader:()=>g(()=>import("./index.html-CIIy3e5B.js"),__vite__mapDeps([339,1])),meta:{t:"文章",I:!1}}],["/star/",{loader:()=>g(()=>import("./index.html-DmAZd2-G.js"),__vite__mapDeps([340,1])),meta:{t:"星标",I:!1}}],["/timeline/",{loader:()=>g(()=>import("./index.html-Cr1BJnp5.js"),__vite__mapDeps([341,1])),meta:{t:"时间轴",I:!1}}]]);function v3(){return dk().__VUE_DEVTOOLS_GLOBAL_HOOK__}function dk(){return typeof navigator<"u"&&typeof window<"u"?window:typeof globalThis<"u"?globalThis:{}}const A3=typeof Proxy=="function",y3="devtools-plugin:setup",m3="plugin:settings:set";let He,ml;function B3(){var i;return He!==void 0||(typeof window<"u"&&window.performance?(He=!0,ml=window.performance):typeof globalThis<"u"&&(!((i=globalThis.perf_hooks)===null||i===void 0)&&i.performance)?(He=!0,ml=globalThis.perf_hooks.performance):He=!1),He}function f3(){return B3()?ml.now():Date.now()}class E3{constructor(s,e){this.target=null,this.targetQueue=[],this.onQueue=[],this.plugin=s,this.hook=e;const t={};if(s.settings)for(const l in s.settings){const r=s.settings[l];t[l]=r.defaultValue}const a=`__vue-devtools-plugin-settings__${s.id}`;let n=Object.assign({},t);try{const l=localStorage.getItem(a),r=JSON.parse(l);Object.assign(n,r)}catch{}this.fallbacks={getSettings(){return n},setSettings(l){try{localStorage.setItem(a,JSON.stringify(l))}catch{}n=l},now(){return f3()}},e&&e.on(m3,(l,r)=>{l===this.plugin.id&&this.fallbacks.setSettings(r)}),this.proxiedOn=new Proxy({},{get:(l,r)=>this.target?this.target.on[r]:(...h)=>{this.onQueue.push({method:r,args:h})}}),this.proxiedTarget=new Proxy({},{get:(l,r)=>this.target?this.target[r]:r==="on"?this.proxiedOn:Object.keys(this.fallbacks).includes(r)?(...h)=>(this.targetQueue.push({method:r,args:h,resolve:()=>{}}),this.fallbacks[r](...h)):(...h)=>new Promise(d=>{this.targetQueue.push({method:r,args:h,resolve:d})})})}async setRealTarget(s){this.target=s;for(const e of this.onQueue)this.target.on[e.method](...e.args);for(const e of this.targetQueue)e.resolve(await this.target[e.method](...e.args))}}function b3(i,s){const e=i,t=dk(),a=v3(),n=A3&&e.enableEarlyProxy;if(a&&(t.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__||!n))a.emit(y3,i,s);else{const l=n?new E3(e,a):null;(t.__VUE_DEVTOOLS_PLUGINS__=t.__VUE_DEVTOOLS_PLUGINS__||[]).push({pluginDescriptor:e,setupFn:s,proxy:l}),l&&s(l.proxiedTarget)}}/*! +

    官方说明

    `,r:{minutes:5.36,words:1609},t:"Maven的生命周期",y:"a"}}],["/404.html",{loader:()=>g(()=>import("./404.html-Cj93pVFe.js"),__vite__mapDeps([263,1])),meta:{t:""}}],["/cpp/other/",{loader:()=>g(()=>import("./index.html-DMdGsont.js"),__vite__mapDeps([264,1])),meta:{t:"Other"}}],["/cpp/",{loader:()=>g(()=>import("./index.html-CqZ8aMMm.js"),__vite__mapDeps([265,1])),meta:{t:"Cpp"}}],["/java/other/",{loader:()=>g(()=>import("./index.html-iCxZIK2p.js"),__vite__mapDeps([266,1])),meta:{t:"Other"}}],["/other/computerprinciple/",{loader:()=>g(()=>import("./index.html-7a_DvzN8.js"),__vite__mapDeps([267,1])),meta:{t:"Computerprinciple"}}],["/other/docker/",{loader:()=>g(()=>import("./index.html-C7f-DwWo.js"),__vite__mapDeps([268,1])),meta:{t:"Docker"}}],["/other/hardware/",{loader:()=>g(()=>import("./index.html-BfwZ7M0e.js"),__vite__mapDeps([269,1])),meta:{t:"Hardware"}}],["/other/software/",{loader:()=>g(()=>import("./index.html-LsLgqLJs.js"),__vite__mapDeps([270,1])),meta:{t:"Software"}}],["/other/windows/",{loader:()=>g(()=>import("./index.html-Ccr7z95c.js"),__vite__mapDeps([271,1])),meta:{t:"Windows"}}],["/java/framework/",{loader:()=>g(()=>import("./index.html-C5QEB7Z1.js"),__vite__mapDeps([272,1])),meta:{t:"Framework"}}],["/java/framework/springcloud/",{loader:()=>g(()=>import("./index.html-BpjUeNG2.js"),__vite__mapDeps([273,1])),meta:{t:"Springcloud"}}],["/java/other/log/",{loader:()=>g(()=>import("./index.html-CsyDKx4g.js"),__vite__mapDeps([274,1])),meta:{t:"Log"}}],["/java/other/maven/",{loader:()=>g(()=>import("./index.html-Z0st3i9J.js"),__vite__mapDeps([275,1])),meta:{t:"Maven"}}],["/category/",{loader:()=>g(()=>import("./index.html-DNBA4gIn.js"),__vite__mapDeps([276,1])),meta:{t:"分类",I:!1}}],["/category/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/",{loader:()=>g(()=>import("./index.html-BOpkylY8.js"),__vite__mapDeps([277,1])),meta:{t:"设计模式 分类",I:!1}}],["/category/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/",{loader:()=>g(()=>import("./index.html-DMm5nioB.js"),__vite__mapDeps([278,1])),meta:{t:"使用指南 分类",I:!1}}],["/category/vue%E7%9F%A5%E8%AF%86%E7%82%B9/",{loader:()=>g(()=>import("./index.html-D-6TwFNB.js"),__vite__mapDeps([279,1])),meta:{t:"vue知识点 分类",I:!1}}],["/category/js%E5%9F%BA%E7%A1%80/",{loader:()=>g(()=>import("./index.html-DLPru-AZ.js"),__vite__mapDeps([280,1])),meta:{t:"js基础 分类",I:!1}}],["/category/npm%E7%9F%A5%E8%AF%86%E7%82%B9/",{loader:()=>g(()=>import("./index.html-U3u26-xm.js"),__vite__mapDeps([281,1])),meta:{t:"npm知识点 分类",I:!1}}],["/category/%E9%97%AE%E9%A2%98%E5%AE%9A%E4%BD%8D/",{loader:()=>g(()=>import("./index.html-Brnqxn8B.js"),__vite__mapDeps([282,1])),meta:{t:"问题定位 分类",I:!1}}],["/category/vite/",{loader:()=>g(()=>import("./index.html-COotBsqy.js"),__vite__mapDeps([283,1])),meta:{t:"vite 分类",I:!1}}],["/category/%E9%9B%86%E5%90%88/",{loader:()=>g(()=>import("./index.html-D3P5gbcr.js"),__vite__mapDeps([284,1])),meta:{t:"集合 分类",I:!1}}],["/category/jdk/",{loader:()=>g(()=>import("./index.html-B-qLo8KO.js"),__vite__mapDeps([285,1])),meta:{t:"jdk 分类",I:!1}}],["/category/%E7%BA%BF%E7%A8%8B%E6%B1%A0/",{loader:()=>g(()=>import("./index.html-txI6BFt1.js"),__vite__mapDeps([286,1])),meta:{t:"线程池 分类",I:!1}}],["/category/%E5%A4%9A%E7%BA%BF%E7%A8%8B/",{loader:()=>g(()=>import("./index.html-CioaVQa-.js"),__vite__mapDeps([287,1])),meta:{t:"多线程 分类",I:!1}}],["/category/java/",{loader:()=>g(()=>import("./index.html-DLNPWair.js"),__vite__mapDeps([288,1])),meta:{t:"java 分类",I:!1}}],["/category/%E5%AF%B9%E8%B1%A1%E9%94%81/",{loader:()=>g(()=>import("./index.html-BejOLJ2j.js"),__vite__mapDeps([289,1])),meta:{t:"对象锁 分类",I:!1}}],["/category/java%E5%9F%BA%E7%A1%80/",{loader:()=>g(()=>import("./index.html-CdNCPQJN.js"),__vite__mapDeps([290,1])),meta:{t:"java基础 分类",I:!1}}],["/category/jvm/",{loader:()=>g(()=>import("./index.html-x8UhOCrm.js"),__vite__mapDeps([291,1])),meta:{t:"jvm 分类",I:!1}}],["/category/%E7%94%B5%E5%AD%90%E4%B9%A6/",{loader:()=>g(()=>import("./index.html-C13ChgHV.js"),__vite__mapDeps([292,1])),meta:{t:"电子书 分类",I:!1}}],["/category/%E6%95%B0%E6%8D%AE%E5%BA%93/",{loader:()=>g(()=>import("./index.html-DjKeoveR.js"),__vite__mapDeps([293,1])),meta:{t:"数据库 分类",I:!1}}],["/category/docker/",{loader:()=>g(()=>import("./index.html-BtI9HFtG.js"),__vite__mapDeps([294,1])),meta:{t:"docker 分类",I:!1}}],["/category/%E8%BF%90%E7%BB%B4/",{loader:()=>g(()=>import("./index.html-64ghmh8T.js"),__vite__mapDeps([295,1])),meta:{t:"运维 分类",I:!1}}],["/category/git-%E6%93%8D%E4%BD%9C/",{loader:()=>g(()=>import("./index.html-bGB8i1Bp.js"),__vite__mapDeps([296,1])),meta:{t:"git 操作 分类",I:!1}}],["/category/%E5%BF%85%E4%BC%9A/",{loader:()=>g(()=>import("./index.html-BkAPtgx9.js"),__vite__mapDeps([297,1])),meta:{t:"必会 分类",I:!1}}],["/category/linux/",{loader:()=>g(()=>import("./index.html-r3r6Xe2X.js"),__vite__mapDeps([298,1])),meta:{t:"linux 分类",I:!1}}],["/category/markdown/",{loader:()=>g(()=>import("./index.html-uOFyO4KX.js"),__vite__mapDeps([299,1])),meta:{t:"markdown 分类",I:!1}}],["/category/%E5%B0%8F%E7%BB%84%E5%88%86%E4%BA%AB/",{loader:()=>g(()=>import("./index.html-DOxnJP9F.js"),__vite__mapDeps([300,1])),meta:{t:"小组分享 分类",I:!1}}],["/category/web/",{loader:()=>g(()=>import("./index.html-Bbe3YI-F.js"),__vite__mapDeps([301,1])),meta:{t:"web 分类",I:!1}}],["/category/oauth/",{loader:()=>g(()=>import("./index.html-CDqEgXQG.js"),__vite__mapDeps([302,1])),meta:{t:"oauth 分类",I:!1}}],["/category/%E6%A1%86%E6%9E%B6/",{loader:()=>g(()=>import("./index.html-CGegH2pR.js"),__vite__mapDeps([303,1])),meta:{t:"框架 分类",I:!1}}],["/category/security/",{loader:()=>g(()=>import("./index.html-CW4w0zEj.js"),__vite__mapDeps([304,1])),meta:{t:"security 分类",I:!1}}],["/category/security/",{loader:()=>g(()=>import("./index.html-CW4w0zEj.js"),__vite__mapDeps([304,1])),meta:{t:"Security 分类",I:!1}}],["/category/oauth/",{loader:()=>g(()=>import("./index.html-CDqEgXQG.js"),__vite__mapDeps([302,1])),meta:{t:"OAuth 分类",I:!1}}],["/category/spring/",{loader:()=>g(()=>import("./index.html-BT9FWv85.js"),__vite__mapDeps([305,1])),meta:{t:"Spring 分类",I:!1}}],["/category/open-source/",{loader:()=>g(()=>import("./index.html-bc7DDl1w.js"),__vite__mapDeps([306,1])),meta:{t:"open-source 分类",I:!1}}],["/category/maven/",{loader:()=>g(()=>import("./index.html-BEh3yan4.js"),__vite__mapDeps([307,1])),meta:{t:"maven 分类",I:!1}}],["/tag/",{loader:()=>g(()=>import("./index.html-ClqrgpgA.js"),__vite__mapDeps([308,1])),meta:{t:"标签",I:!1}}],["/tag/%E7%A6%81%E7%94%A8/",{loader:()=>g(()=>import("./index.html-B6jGm4h5.js"),__vite__mapDeps([309,1])),meta:{t:"标签: 禁用",I:!1}}],["/tag/%E6%96%87%E7%AB%A0%E5%8A%A0%E5%AF%86/",{loader:()=>g(()=>import("./index.html-C0K4uyQT.js"),__vite__mapDeps([310,1])),meta:{t:"标签: 文章加密",I:!1}}],["/tag/markdown/",{loader:()=>g(()=>import("./index.html-D0IWaeCP.js"),__vite__mapDeps([311,1])),meta:{t:"标签: Markdown",I:!1}}],["/tag/%E9%A1%B5%E9%9D%A2%E9%85%8D%E7%BD%AE/",{loader:()=>g(()=>import("./index.html-NRQgmwqP.js"),__vite__mapDeps([312,1])),meta:{t:"标签: 页面配置",I:!1}}],["/tag/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/",{loader:()=>g(()=>import("./index.html-Cfr1VERK.js"),__vite__mapDeps([313,1])),meta:{t:"标签: 使用指南",I:!1}}],["/tag/%E4%BD%A0%E6%89%80%E4%B8%8D%E4%BA%86%E8%A7%A3%E7%9A%84javascript/",{loader:()=>g(()=>import("./index.html-BEpChitr.js"),__vite__mapDeps([314,1])),meta:{t:"标签: 你所不了解的JavaScript",I:!1}}],["/tag/%E5%BF%85%E4%BC%9A/",{loader:()=>g(()=>import("./index.html-NsWiQWYZ.js"),__vite__mapDeps([315,1])),meta:{t:"标签: 必会",I:!1}}],["/tag/vue%E4%B8%AD%E7%9A%84-typescript/",{loader:()=>g(()=>import("./index.html-BJDhEdQX.js"),__vite__mapDeps([316,1])),meta:{t:"标签: vue中的 TypeScript",I:!1}}],["/tag/vite/",{loader:()=>g(()=>import("./index.html-xFKarS8b.js"),__vite__mapDeps([317,1])),meta:{t:"标签: vite",I:!1}}],["/tag/ts/",{loader:()=>g(()=>import("./index.html-BgC1Uqo7.js"),__vite__mapDeps([318,1])),meta:{t:"标签: ts",I:!1}}],["/tag/vue3/",{loader:()=>g(()=>import("./index.html-BE3BiSH6.js"),__vite__mapDeps([319,1])),meta:{t:"标签: vue3",I:!1}}],["/tag/%E9%9B%86%E5%90%88/",{loader:()=>g(()=>import("./index.html-DyUgClQe.js"),__vite__mapDeps([320,1])),meta:{t:"标签: 集合",I:!1}}],["/tag/---%E5%B9%B6%E5%8F%91/",{loader:()=>g(()=>import("./index.html-Bbyd6M1o.js"),__vite__mapDeps([321,1])),meta:{t:"标签: -- 并发",I:!1}}],["/tag/%E5%A4%9A%E7%BA%BF%E7%A8%8B/",{loader:()=>g(()=>import("./index.html-a7FPOuSg.js"),__vite__mapDeps([322,1])),meta:{t:"标签: 多线程",I:!1}}],["/tag/%E7%BA%BF%E7%A8%8B%E6%B1%A0/",{loader:()=>g(()=>import("./index.html-IQR8utYP.js"),__vite__mapDeps([323,1])),meta:{t:"标签: 线程池",I:!1}}],["/tag/oauth/",{loader:()=>g(()=>import("./index.html-DHKrdhfw.js"),__vite__mapDeps([324,1])),meta:{t:"标签: oauth",I:!1}}],["/tag/sso/",{loader:()=>g(()=>import("./index.html-Sng5UTgU.js"),__vite__mapDeps([325,1])),meta:{t:"标签: sso",I:!1}}],["/tag/%E9%83%A8%E7%BD%B2%E6%90%AD%E5%BB%BA/",{loader:()=>g(()=>import("./index.html-BOpWn6ez.js"),__vite__mapDeps([326,1])),meta:{t:"标签: 部署搭建",I:!1}}],["/tag/%E5%AD%97%E8%8A%82%E7%A0%81/",{loader:()=>g(()=>import("./index.html-C-sunVIK.js"),__vite__mapDeps([327,1])),meta:{t:"标签: 字节码",I:!1}}],["/tag/%E5%8F%8D%E6%B1%87%E7%BC%96/",{loader:()=>g(()=>import("./index.html-C3d9thsv.js"),__vite__mapDeps([328,1])),meta:{t:"标签: 反汇编",I:!1}}],["/tag/mysql/",{loader:()=>g(()=>import("./index.html-DkgwX5zY.js"),__vite__mapDeps([329,1])),meta:{t:"标签: mysql",I:!1}}],["/tag/%E6%95%B0%E6%8D%AE%E5%BA%93/",{loader:()=>g(()=>import("./index.html-CvRj_9Cp.js"),__vite__mapDeps([330,1])),meta:{t:"标签: 数据库",I:!1}}],["/tag/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/",{loader:()=>g(()=>import("./index.html-Drxe7RQ8.js"),__vite__mapDeps([331,1])),meta:{t:"标签: 数据结构",I:!1}}],["/tag/%E4%BA%8C%E5%8F%89%E6%A0%91/",{loader:()=>g(()=>import("./index.html-CxyHaGFC.js"),__vite__mapDeps([332,1])),meta:{t:"标签: 二叉树",I:!1}}],["/tag/%E8%BF%90%E7%BB%B4/",{loader:()=>g(()=>import("./index.html-DCViU9NH.js"),__vite__mapDeps([333,1])),meta:{t:"标签: 运维",I:!1}}],["/tag/%E5%B7%A5%E5%85%B7%E4%BD%BF%E7%94%A8/",{loader:()=>g(()=>import("./index.html-ZrDWmiHj.js"),__vite__mapDeps([334,1])),meta:{t:"标签: 工具使用",I:!1}}],["/tag/linux/",{loader:()=>g(()=>import("./index.html-C3HSxeKn.js"),__vite__mapDeps([335,1])),meta:{t:"标签: linux",I:!1}}],["/tag/markdown/",{loader:()=>g(()=>import("./index.html-D0IWaeCP.js"),__vite__mapDeps([311,1])),meta:{t:"标签: markdown",I:!1}}],["/tag/web/",{loader:()=>g(()=>import("./index.html--P4uqaDK.js"),__vite__mapDeps([336,1])),meta:{t:"标签: web",I:!1}}],["/tag/springboot/",{loader:()=>g(()=>import("./index.html-C63jvqUL.js"),__vite__mapDeps([337,1])),meta:{t:"标签: springboot",I:!1}}],["/tag/%E6%A1%86%E6%9E%B6/",{loader:()=>g(()=>import("./index.html-C_1bHi9D.js"),__vite__mapDeps([338,1])),meta:{t:"标签: 框架",I:!1}}],["/tag/%E6%97%A5%E5%BF%97/",{loader:()=>g(()=>import("./index.html-DDkBFrt6.js"),__vite__mapDeps([339,1])),meta:{t:"标签: 日志",I:!1}}],["/article/",{loader:()=>g(()=>import("./index.html-DcSDms4b.js"),__vite__mapDeps([340,1])),meta:{t:"文章",I:!1}}],["/star/",{loader:()=>g(()=>import("./index.html-C0IieY1i.js"),__vite__mapDeps([341,1])),meta:{t:"星标",I:!1}}],["/timeline/",{loader:()=>g(()=>import("./index.html-CTBcx_KY.js"),__vite__mapDeps([342,1])),meta:{t:"时间轴",I:!1}}]]);function v3(){return dk().__VUE_DEVTOOLS_GLOBAL_HOOK__}function dk(){return typeof navigator<"u"&&typeof window<"u"?window:typeof globalThis<"u"?globalThis:{}}const A3=typeof Proxy=="function",y3="devtools-plugin:setup",m3="plugin:settings:set";let He,ml;function B3(){var i;return He!==void 0||(typeof window<"u"&&window.performance?(He=!0,ml=window.performance):typeof globalThis<"u"&&(!((i=globalThis.perf_hooks)===null||i===void 0)&&i.performance)?(He=!0,ml=globalThis.perf_hooks.performance):He=!1),He}function f3(){return B3()?ml.now():Date.now()}class E3{constructor(s,e){this.target=null,this.targetQueue=[],this.onQueue=[],this.plugin=s,this.hook=e;const t={};if(s.settings)for(const l in s.settings){const r=s.settings[l];t[l]=r.defaultValue}const a=`__vue-devtools-plugin-settings__${s.id}`;let n=Object.assign({},t);try{const l=localStorage.getItem(a),r=JSON.parse(l);Object.assign(n,r)}catch{}this.fallbacks={getSettings(){return n},setSettings(l){try{localStorage.setItem(a,JSON.stringify(l))}catch{}n=l},now(){return f3()}},e&&e.on(m3,(l,r)=>{l===this.plugin.id&&this.fallbacks.setSettings(r)}),this.proxiedOn=new Proxy({},{get:(l,r)=>this.target?this.target.on[r]:(...h)=>{this.onQueue.push({method:r,args:h})}}),this.proxiedTarget=new Proxy({},{get:(l,r)=>this.target?this.target[r]:r==="on"?this.proxiedOn:Object.keys(this.fallbacks).includes(r)?(...h)=>(this.targetQueue.push({method:r,args:h,resolve:()=>{}}),this.fallbacks[r](...h)):(...h)=>new Promise(d=>{this.targetQueue.push({method:r,args:h,resolve:d})})})}async setRealTarget(s){this.target=s;for(const e of this.onQueue)this.target.on[e.method](...e.args);for(const e of this.targetQueue)e.resolve(await this.target[e.method](...e.args))}}function b3(i,s){const e=i,t=dk(),a=v3(),n=A3&&e.enableEarlyProxy;if(a&&(t.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__||!n))a.emit(y3,i,s);else{const l=n?new E3(e,a):null;(t.__VUE_DEVTOOLS_PLUGINS__=t.__VUE_DEVTOOLS_PLUGINS__||[]).push({pluginDescriptor:e,setupFn:s,proxy:l}),l&&s(l.proxiedTarget)}}/*! * vue-router v4.4.5 * (c) 2024 Eduardo San Martin Morote * @license MIT */const Zs=typeof document<"u";function ok(i){return typeof i=="object"||"displayName"in i||"props"in i||"__vccOpts"in i}function F3(i){return i.__esModule||i[Symbol.toStringTag]==="Module"||i.default&&ok(i.default)}const Ai=Object.assign;function zn(i,s){const e={};for(const t in s){const a=s[t];e[t]=As(a)?a.map(i):i(a)}return e}const Ht=()=>{},As=Array.isArray,kk=/#/g,_3=/&/g,C3=/\//g,w3=/=/g,D3=/\?/g,ck=/\+/g,x3=/%5B/g,S3=/%5D/g,uk=/%5E/g,T3=/%60/g,gk=/%7B/g,O3=/%7C/g,vk=/%7D/g,L3=/%20/g;function yr(i){return encodeURI(""+i).replace(O3,"|").replace(x3,"[").replace(S3,"]")}function I3(i){return yr(i).replace(gk,"{").replace(vk,"}").replace(uk,"^")}function Bl(i){return yr(i).replace(ck,"%2B").replace(L3,"+").replace(kk,"%23").replace(_3,"%26").replace(T3,"`").replace(gk,"{").replace(vk,"}").replace(uk,"^")}function P3(i){return Bl(i).replace(w3,"%3D")}function R3(i){return yr(i).replace(kk,"%23").replace(D3,"%3F")}function j3(i){return i==null?"":R3(i).replace(C3,"%2F")}function ht(i){try{return decodeURIComponent(""+i)}catch{}return""+i}const M3=/\/$/,V3=i=>i.replace(M3,"");function Wn(i,s,e="/"){let t,a={},n="",l="";const r=s.indexOf("#");let h=s.indexOf("?");return r=0&&(h=-1),h>-1&&(t=s.slice(0,h),n=s.slice(h+1,r>-1?r:s.length),a=i(n)),r>-1&&(t=t||s.slice(0,r),l=s.slice(r,s.length)),t=H3(t??s,e),{fullPath:t+(n&&"?")+n+l,path:t,query:a,hash:ht(l)}}function N3(i,s){const e=s.query?i(s.query):"";return s.path+(e&&"?")+e+(s.hash||"")}function Gh(i,s){return!s||!i.toLowerCase().startsWith(s.toLowerCase())?i:i.slice(s.length)||"/"}function $3(i,s,e){const t=s.matched.length-1,a=e.matched.length-1;return t>-1&&t===a&&Be(s.matched[t],e.matched[a])&&Ak(s.params,e.params)&&i(s.query)===i(e.query)&&s.hash===e.hash}function Be(i,s){return(i.aliasOf||i)===(s.aliasOf||s)}function Ak(i,s){if(Object.keys(i).length!==Object.keys(s).length)return!1;for(const e in i)if(!q3(i[e],s[e]))return!1;return!0}function q3(i,s){return As(i)?Kh(i,s):As(s)?Kh(s,i):i===s}function Kh(i,s){return As(s)?i.length===s.length&&i.every((e,t)=>e===s[t]):i.length===1&&i[0]===s}function H3(i,s){if(i.startsWith("/"))return i;if(!i)return s;const e=s.split("/"),t=i.split("/"),a=t[t.length-1];(a===".."||a===".")&&t.push("");let n=e.length-1,l,r;for(l=0;l1&&n--;else break;return e.slice(0,n).join("/")+"/"+t.slice(l).join("/")}const Xs={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};var Zt;(function(i){i.pop="pop",i.push="push"})(Zt||(Zt={}));var Ut;(function(i){i.back="back",i.forward="forward",i.unknown=""})(Ut||(Ut={}));function U3(i){if(!i)if(Zs){const s=document.querySelector("base");i=s&&s.getAttribute("href")||"/",i=i.replace(/^\w+:\/\/[^\/]+/,"")}else i="/";return i[0]!=="/"&&i[0]!=="#"&&(i="/"+i),V3(i)}const z3=/^[^#]+#/;function W3(i,s){return i.replace(z3,"#")+s}function G3(i,s){const e=document.documentElement.getBoundingClientRect(),t=i.getBoundingClientRect();return{behavior:s.behavior,left:t.left-e.left-(s.left||0),top:t.top-e.top-(s.top||0)}}const vn=()=>({left:window.scrollX,top:window.scrollY});function K3(i){let s;if("el"in i){const e=i.el,t=typeof e=="string"&&e.startsWith("#"),a=typeof e=="string"?t?document.getElementById(e.slice(1)):document.querySelector(e):e;if(!a)return;s=G3(a,i)}else s=i;"scrollBehavior"in document.documentElement.style?window.scrollTo(s):window.scrollTo(s.left!=null?s.left:window.scrollX,s.top!=null?s.top:window.scrollY)}function Jh(i,s){return(history.state?history.state.position-s:-1)+i}const fl=new Map;function J3(i,s){fl.set(i,s)}function Y3(i){const s=fl.get(i);return fl.delete(i),s}let X3=()=>location.protocol+"//"+location.host;function yk(i,s){const{pathname:e,search:t,hash:a}=s,n=i.indexOf("#");if(n>-1){let r=a.includes(i.slice(n))?i.slice(n).length:1,h=a.slice(r);return h[0]!=="/"&&(h="/"+h),Gh(h,"")}return Gh(e,i)+t+a}function Q3(i,s,e,t){let a=[],n=[],l=null;const r=({state:c})=>{const u=yk(i,location),v=e.value,m=s.value;let B=0;if(c){if(e.value=u,s.value=c,l&&l===v){l=null;return}B=m?c.position-m.position:0}else t(u);a.forEach(f=>{f(e.value,v,{delta:B,type:Zt.pop,direction:B?B>0?Ut.forward:Ut.back:Ut.unknown})})};function h(){l=e.value}function d(c){a.push(c);const u=()=>{const v=a.indexOf(c);v>-1&&a.splice(v,1)};return n.push(u),u}function o(){const{history:c}=window;c.state&&c.replaceState(Ai({},c.state,{scroll:vn()}),"")}function k(){for(const c of n)c();n=[],window.removeEventListener("popstate",r),window.removeEventListener("beforeunload",o)}return window.addEventListener("popstate",r),window.addEventListener("beforeunload",o,{passive:!0}),{pauseListeners:h,listen:d,destroy:k}}function Yh(i,s,e,t=!1,a=!1){return{back:i,current:s,forward:e,replaced:t,position:window.history.length,scroll:a?vn():null}}function Z3(i){const{history:s,location:e}=window,t={value:yk(i,e)},a={value:s.state};a.value||n(t.value,{back:null,current:t.value,forward:null,position:s.length-1,replaced:!0,scroll:null},!0);function n(h,d,o){const k=i.indexOf("#"),c=k>-1?(e.host&&document.querySelector("base")?i:i.slice(k))+h:X3()+i+h;try{s[o?"replaceState":"pushState"](d,"",c),a.value=d}catch(u){console.error(u),e[o?"replace":"assign"](c)}}function l(h,d){const o=Ai({},s.state,Yh(a.value.back,h,a.value.forward,!0),d,{position:a.value.position});n(h,o,!0),t.value=h}function r(h,d){const o=Ai({},a.value,s.state,{forward:h,scroll:vn()});n(o.current,o,!0);const k=Ai({},Yh(t.value,h,null),{position:o.position+1},d);n(h,k,!1),t.value=h}return{location:t,state:a,push:r,replace:l}}function i1(i){i=U3(i);const s=Z3(i),e=Q3(i,s.state,s.location,s.replace);function t(n,l=!0){l||e.pauseListeners(),history.go(n)}const a=Ai({location:"",base:i,go:t,createHref:W3.bind(null,i)},s,e);return Object.defineProperty(a,"location",{enumerable:!0,get:()=>s.location.value}),Object.defineProperty(a,"state",{enumerable:!0,get:()=>s.state.value}),a}function mk(i){return typeof i=="string"||i&&typeof i=="object"}function Bk(i){return typeof i=="string"||typeof i=="symbol"}const fk=Symbol("");var Xh;(function(i){i[i.aborted=4]="aborted",i[i.cancelled=8]="cancelled",i[i.duplicated=16]="duplicated"})(Xh||(Xh={}));function pt(i,s){return Ai(new Error,{type:i,[fk]:!0},s)}function Js(i,s){return i instanceof Error&&fk in i&&(s==null||!!(i.type&s))}const Qh="[^/]+?",s1={sensitive:!1,strict:!1,start:!0,end:!0},e1=/[.+*?^${}()[\]/\\]/g;function t1(i,s){const e=Ai({},s1,s),t=[];let a=e.start?"^":"";const n=[];for(const d of i){const o=d.length?[]:[90];e.strict&&!d.length&&(a+="/");for(let k=0;ks.length?s.length===1&&s[0]===80?1:-1:0}function Ek(i,s){let e=0;const t=i.score,a=s.score;for(;e0&&s[s.length-1]<0}const n1={type:0,value:""},l1=/[a-zA-Z0-9_]/;function r1(i){if(!i)return[[]];if(i==="/")return[[n1]];if(!i.startsWith("/"))throw new Error(`Invalid path "${i}"`);function s(u){throw new Error(`ERR (${e})/"${d}": ${u}`)}let e=0,t=e;const a=[];let n;function l(){n&&a.push(n),n=[]}let r=0,h,d="",o="";function k(){d&&(e===0?n.push({type:0,value:d}):e===1||e===2||e===3?(n.length>1&&(h==="*"||h==="+")&&s(`A repeatable param (${d}) must be alone in its segment. eg: '/:ids+.`),n.push({type:1,value:d,regexp:o,repeatable:h==="*"||h==="+",optional:h==="*"||h==="?"})):s("Invalid state to consume buffer"),d="")}function c(){d+=h}for(;r{l(A)}:Ht}function l(k){if(Bk(k)){const c=t.get(k);c&&(t.delete(k),e.splice(e.indexOf(c),1),c.children.forEach(l),c.alias.forEach(l))}else{const c=e.indexOf(k);c>-1&&(e.splice(c,1),k.record.name&&t.delete(k.record.name),k.children.forEach(l),k.alias.forEach(l))}}function r(){return e}function h(k){const c=k1(k,e);e.splice(c,0,k),k.record.name&&!ep(k)&&t.set(k.record.name,k)}function d(k,c){let u,v={},m,B;if("name"in k&&k.name){if(u=t.get(k.name),!u)throw pt(1,{location:k});B=u.record.name,v=Ai(ip(c.params,u.keys.filter(A=>!A.optional).concat(u.parent?u.parent.keys.filter(A=>A.optional):[]).map(A=>A.name)),k.params&&ip(k.params,u.keys.map(A=>A.name))),m=u.stringify(v)}else if(k.path!=null)m=k.path,u=e.find(A=>A.re.test(m)),u&&(v=u.parse(m),B=u.record.name);else{if(u=c.name?t.get(c.name):e.find(A=>A.re.test(c.path)),!u)throw pt(1,{location:k,currentLocation:c});B=u.record.name,v=Ai({},c.params,k.params),m=u.stringify(v)}const f=[];let b=u;for(;b;)f.unshift(b.record),b=b.parent;return{name:B,path:m,params:v,matched:f,meta:o1(f)}}i.forEach(k=>n(k));function o(){e.length=0,t.clear()}return{addRoute:n,resolve:d,removeRoute:l,clearRoutes:o,getRoutes:r,getRecordMatcher:a}}function ip(i,s){const e={};for(const t of s)t in i&&(e[t]=i[t]);return e}function sp(i){const s={path:i.path,redirect:i.redirect,name:i.name,meta:i.meta||{},aliasOf:i.aliasOf,beforeEnter:i.beforeEnter,props:d1(i),children:i.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in i?i.components||null:i.component&&{default:i.component}};return Object.defineProperty(s,"mods",{value:{}}),s}function d1(i){const s={},e=i.props||!1;if("component"in i)s.default=e;else for(const t in i.components)s[t]=typeof e=="object"?e[t]:e;return s}function ep(i){for(;i;){if(i.record.aliasOf)return!0;i=i.parent}return!1}function o1(i){return i.reduce((s,e)=>Ai(s,e.meta),{})}function tp(i,s){const e={};for(const t in i)e[t]=t in s?s[t]:i[t];return e}function k1(i,s){let e=0,t=s.length;for(;e!==t;){const n=e+t>>1;Ek(i,s[n])<0?t=n:e=n+1}const a=c1(i);return a&&(t=s.lastIndexOf(a,t-1)),t}function c1(i){let s=i;for(;s=s.parent;)if(bk(s)&&Ek(i,s)===0)return s}function bk({record:i}){return!!(i.name||i.components&&Object.keys(i.components).length||i.redirect)}function u1(i){const s={};if(i===""||i==="?")return s;const t=(i[0]==="?"?i.slice(1):i).split("&");for(let a=0;an&&Bl(n)):[t&&Bl(t)]).forEach(n=>{n!==void 0&&(s+=(s.length?"&":"")+e,n!=null&&(s+="="+n))})}return s}function g1(i){const s={};for(const e in i){const t=i[e];t!==void 0&&(s[e]=As(t)?t.map(a=>a==null?null:""+a):t==null?t:""+t)}return s}const v1=Symbol(""),np=Symbol(""),An=Symbol(""),mr=Symbol(""),El=Symbol("");function Dt(){let i=[];function s(t){return i.push(t),()=>{const a=i.indexOf(t);a>-1&&i.splice(a,1)}}function e(){i=[]}return{add:s,list:()=>i.slice(),reset:e}}function ue(i,s,e,t,a,n=l=>l()){const l=t&&(t.enterCallbacks[a]=t.enterCallbacks[a]||[]);return()=>new Promise((r,h)=>{const d=c=>{c===!1?h(pt(4,{from:e,to:s})):c instanceof Error?h(c):mk(c)?h(pt(2,{from:s,to:c})):(l&&t.enterCallbacks[a]===l&&typeof c=="function"&&l.push(c),r())},o=n(()=>i.call(t&&t.instances[a],s,e,d));let k=Promise.resolve(o);i.length<3&&(k=k.then(d)),k.catch(c=>h(c))})}function Gn(i,s,e,t,a=n=>n()){const n=[];for(const l of i)for(const r in l.components){let h=l.components[r];if(!(s!=="beforeRouteEnter"&&!l.instances[r]))if(ok(h)){const o=(h.__vccOpts||h)[s];o&&n.push(ue(o,e,t,l,r,a))}else{let d=h();n.push(()=>d.then(o=>{if(!o)throw new Error(`Couldn't resolve component "${r}" at "${l.path}"`);const k=F3(o)?o.default:o;l.mods[r]=o,l.components[r]=k;const u=(k.__vccOpts||k)[s];return u&&ue(u,e,t,l,r,a)()}))}}return n}function lp(i){const s=Si(An),e=Si(mr),t=F(()=>{const h=ve(i.to);return s.resolve(h)}),a=F(()=>{const{matched:h}=t.value,{length:d}=h,o=h[d-1],k=e.matched;if(!o||!k.length)return-1;const c=k.findIndex(Be.bind(null,o));if(c>-1)return c;const u=rp(h[d-2]);return d>1&&rp(o)===u&&k[k.length-1].path!==u?k.findIndex(Be.bind(null,h[d-2])):c}),n=F(()=>a.value>-1&&B1(e.params,t.value.params)),l=F(()=>a.value>-1&&a.value===e.matched.length-1&&Ak(e.params,t.value.params));function r(h={}){return m1(h)?s[ve(i.replace)?"replace":"push"](ve(i.to)).catch(Ht):Promise.resolve()}if(Zs){const h=mt();if(h){const d={route:t.value,isActive:n.value,isExactActive:l.value,error:null};h.__vrl_devtools=h.__vrl_devtools||[],h.__vrl_devtools.push(d),kr(()=>{d.route=t.value,d.isActive=n.value,d.isExactActive=l.value,d.error=mk(ve(i.to))?null:'Invalid "to" value'},{flush:"post"})}}return{route:t,href:F(()=>t.value.href),isActive:n,isExactActive:l,navigate:r}}const A1=q({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:lp,setup(i,{slots:s}){const e=oa(lp(i)),{options:t}=Si(An),a=F(()=>({[hp(i.activeClass,t.linkActiveClass,"router-link-active")]:e.isActive,[hp(i.exactActiveClass,t.linkExactActiveClass,"router-link-exact-active")]:e.isExactActive}));return()=>{const n=s.default&&s.default(e);return i.custom?n:p("a",{"aria-current":e.isExactActive?i.ariaCurrentValue:null,href:e.href,onClick:e.navigate,class:a.value},n)}}}),y1=A1;function m1(i){if(!(i.metaKey||i.altKey||i.ctrlKey||i.shiftKey)&&!i.defaultPrevented&&!(i.button!==void 0&&i.button!==0)){if(i.currentTarget&&i.currentTarget.getAttribute){const s=i.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(s))return}return i.preventDefault&&i.preventDefault(),!0}}function B1(i,s){for(const e in s){const t=s[e],a=i[e];if(typeof t=="string"){if(t!==a)return!1}else if(!As(a)||a.length!==t.length||t.some((n,l)=>n!==a[l]))return!1}return!0}function rp(i){return i?i.aliasOf?i.aliasOf.path:i.path:""}const hp=(i,s,e)=>i??s??e,f1=q({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(i,{attrs:s,slots:e}){const t=Si(El),a=F(()=>i.route||t.value),n=Si(np,0),l=F(()=>{let d=ve(n);const{matched:o}=a.value;let k;for(;(k=o[d])&&!k.components;)d++;return d}),r=F(()=>a.value.matched[l.value]);xs(np,F(()=>l.value+1)),xs(v1,r),xs(El,a);const h=Z();return ui(()=>[h.value,r.value,i.name],([d,o,k],[c,u,v])=>{o&&(o.instances[k]=d,u&&u!==o&&d&&d===c&&(o.leaveGuards.size||(o.leaveGuards=u.leaveGuards),o.updateGuards.size||(o.updateGuards=u.updateGuards))),d&&o&&(!u||!Be(o,u)||!c)&&(o.enterCallbacks[k]||[]).forEach(m=>m(d))},{flush:"post"}),()=>{const d=a.value,o=i.name,k=r.value,c=k&&k.components[o];if(!c)return pp(e.default,{Component:c,route:d});const u=k.props[o],v=u?u===!0?d.params:typeof u=="function"?u(d):u:null,B=p(c,Ai({},v,s,{onVnodeUnmounted:f=>{f.component.isUnmounted&&(k.instances[o]=null)},ref:h}));if(Zs&&B.ref){const f={depth:l.value,name:k.name,path:k.path,meta:k.meta};(As(B.ref)?B.ref.map(A=>A.i):[B.ref.i]).forEach(A=>{A.__vrv_devtools=f})}return pp(e.default,{Component:B,route:d})||B}}});function pp(i,s){if(!i)return null;const e=i(s);return e.length===1?e[0]:e}const E1=f1;function xt(i,s){const e=Ai({},i,{matched:i.matched.map(t=>L1(t,["instances","children","aliasOf"]))});return{_custom:{type:null,readOnly:!0,display:i.fullPath,tooltip:s,value:e}}}function Ra(i){return{_custom:{display:i}}}let b1=0;function F1(i,s,e){if(s.__hasDevtools)return;s.__hasDevtools=!0;const t=b1++;b3({id:"org.vuejs.router"+(t?"."+t:""),label:"Vue Router",packageName:"vue-router",homepage:"https://router.vuejs.org",logo:"https://router.vuejs.org/logo.png",componentStateTypes:["Routing"],app:i},a=>{typeof a.now!="function"&&console.warn("[Vue Router]: You seem to be using an outdated version of Vue Devtools. Are you still using the Beta release instead of the stable one? You can find the links at https://devtools.vuejs.org/guide/installation.html."),a.on.inspectComponent((o,k)=>{o.instanceData&&o.instanceData.state.push({type:"Routing",key:"$route",editable:!1,value:xt(s.currentRoute.value,"Current Route")})}),a.on.visitComponentTree(({treeNode:o,componentInstance:k})=>{if(k.__vrv_devtools){const c=k.__vrv_devtools;o.tags.push({label:(c.name?`${c.name.toString()}: `:"")+c.path,textColor:0,tooltip:"This component is rendered by <router-view>",backgroundColor:Fk})}As(k.__vrl_devtools)&&(k.__devtoolsApi=a,k.__vrl_devtools.forEach(c=>{let u=c.route.path,v=wk,m="",B=0;c.error?(u=c.error,v=x1,B=S1):c.isExactActive?(v=Ck,m="This is exactly active"):c.isActive&&(v=_k,m="This link is active"),o.tags.push({label:u,textColor:B,tooltip:m,backgroundColor:v})}))}),ui(s.currentRoute,()=>{h(),a.notifyComponentUpdate(),a.sendInspectorTree(r),a.sendInspectorState(r)});const n="router:navigations:"+t;a.addTimelineLayer({id:n,label:`Router${t?" "+t:""} Navigations`,color:4237508}),s.onError((o,k)=>{a.addTimelineEvent({layerId:n,event:{title:"Error during Navigation",subtitle:k.fullPath,logType:"error",time:a.now(),data:{error:o},groupId:k.meta.__navigationId}})});let l=0;s.beforeEach((o,k)=>{const c={guard:Ra("beforeEach"),from:xt(k,"Current Location during this navigation"),to:xt(o,"Target location")};Object.defineProperty(o.meta,"__navigationId",{value:l++}),a.addTimelineEvent({layerId:n,event:{time:a.now(),title:"Start of navigation",subtitle:o.fullPath,data:c,groupId:o.meta.__navigationId}})}),s.afterEach((o,k,c)=>{const u={guard:Ra("afterEach")};c?(u.failure={_custom:{type:Error,readOnly:!0,display:c?c.message:"",tooltip:"Navigation Failure",value:c}},u.status=Ra("❌")):u.status=Ra("✅"),u.from=xt(k,"Current Location during this navigation"),u.to=xt(o,"Target location"),a.addTimelineEvent({layerId:n,event:{title:"End of navigation",subtitle:o.fullPath,time:a.now(),data:u,logType:c?"warning":"default",groupId:o.meta.__navigationId}})});const r="router-inspector:"+t;a.addInspector({id:r,label:"Routes"+(t?" "+t:""),icon:"book",treeFilterPlaceholder:"Search routes"});function h(){if(!d)return;const o=d;let k=e.getRoutes().filter(c=>!c.parent||!c.parent.record.components);k.forEach(Sk),o.filter&&(k=k.filter(c=>bl(c,o.filter.toLowerCase()))),k.forEach(c=>xk(c,s.currentRoute.value)),o.rootNodes=k.map(Dk)}let d;a.on.getInspectorTree(o=>{d=o,o.app===i&&o.inspectorId===r&&h()}),a.on.getInspectorState(o=>{if(o.app===i&&o.inspectorId===r){const c=e.getRoutes().find(u=>u.record.__vd_id===o.nodeId);c&&(o.state={options:C1(c)})}}),a.sendInspectorTree(r),a.sendInspectorState(r)})}function _1(i){return i.optional?i.repeatable?"*":"?":i.repeatable?"+":""}function C1(i){const{record:s}=i,e=[{editable:!1,key:"path",value:s.path}];return s.name!=null&&e.push({editable:!1,key:"name",value:s.name}),e.push({editable:!1,key:"regexp",value:i.re}),i.keys.length&&e.push({editable:!1,key:"keys",value:{_custom:{type:null,readOnly:!0,display:i.keys.map(t=>`${t.name}${_1(t)}`).join(" "),tooltip:"Param keys",value:i.keys}}}),s.redirect!=null&&e.push({editable:!1,key:"redirect",value:s.redirect}),i.alias.length&&e.push({editable:!1,key:"aliases",value:i.alias.map(t=>t.record.path)}),Object.keys(i.record.meta).length&&e.push({editable:!1,key:"meta",value:i.record.meta}),e.push({key:"score",editable:!1,value:{_custom:{type:null,readOnly:!0,display:i.score.map(t=>t.join(", ")).join(" | "),tooltip:"Score used to sort routes",value:i.score}}}),e}const Fk=15485081,_k=2450411,Ck=8702998,w1=2282478,wk=16486972,D1=6710886,x1=16704226,S1=12131356;function Dk(i){const s=[],{record:e}=i;e.name!=null&&s.push({label:String(e.name),textColor:0,backgroundColor:w1}),e.aliasOf&&s.push({label:"alias",textColor:0,backgroundColor:wk}),i.__vd_match&&s.push({label:"matches",textColor:0,backgroundColor:Fk}),i.__vd_exactActive&&s.push({label:"exact",textColor:0,backgroundColor:Ck}),i.__vd_active&&s.push({label:"active",textColor:0,backgroundColor:_k}),e.redirect&&s.push({label:typeof e.redirect=="string"?`redirect: ${e.redirect}`:"redirects",textColor:16777215,backgroundColor:D1});let t=e.__vd_id;return t==null&&(t=String(T1++),e.__vd_id=t),{id:t,label:e.path,tags:s,children:i.children.map(Dk)}}let T1=0;const O1=/^\/(.*)\/([a-z]*)$/;function xk(i,s){const e=s.matched.length&&Be(s.matched[s.matched.length-1],i.record);i.__vd_exactActive=i.__vd_active=e,e||(i.__vd_active=s.matched.some(t=>Be(t,i.record))),i.children.forEach(t=>xk(t,s))}function Sk(i){i.__vd_match=!1,i.children.forEach(Sk)}function bl(i,s){const e=String(i.re).match(O1);if(i.__vd_match=!1,!e||e.length<3)return!1;if(new RegExp(e[1].replace(/\$$/,""),e[2]).test(s))return i.children.forEach(l=>bl(l,s)),i.record.path!=="/"||s==="/"?(i.__vd_match=i.re.test(s),!0):!1;const a=i.record.path.toLowerCase(),n=ht(a);return!s.startsWith("/")&&(n.includes(s)||a.includes(s))||n.startsWith(s)||a.startsWith(s)||i.record.name&&String(i.record.name).includes(s)?!0:i.children.some(l=>bl(l,s))}function L1(i,s){const e={};for(const t in i)s.includes(t)||(e[t]=i[t]);return e}function I1(i){const s=p1(i.routes,i),e=i.parseQuery||u1,t=i.stringifyQuery||ap,a=i.history,n=Dt(),l=Dt(),r=Dt(),h=Vi(Xs);let d=Xs;Zs&&i.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const o=zn.bind(null,w=>""+w),k=zn.bind(null,j3),c=zn.bind(null,ht);function u(w,G){let U,Q;return Bk(w)?(U=s.getRecordMatcher(w),Q=G):Q=w,s.addRoute(Q,U)}function v(w){const G=s.getRecordMatcher(w);G&&s.removeRoute(G)}function m(){return s.getRoutes().map(w=>w.record)}function B(w){return!!s.getRecordMatcher(w)}function f(w,G){if(G=Ai({},G||h.value),typeof w=="string"){const E=Wn(e,w,G.path),C=s.resolve({path:E.path},G),O=a.createHref(E.fullPath);return Ai(E,C,{params:c(C.params),hash:ht(E.hash),redirectedFrom:void 0,href:O})}let U;if(w.path!=null)U=Ai({},w,{path:Wn(e,w.path,G.path).path});else{const E=Ai({},w.params);for(const C in E)E[C]==null&&delete E[C];U=Ai({},w,{params:k(E)}),G.params=k(G.params)}const Q=s.resolve(U,G),ci=w.hash||"";Q.params=o(c(Q.params));const fi=N3(t,Ai({},w,{hash:I3(ci),path:Q.path})),y=a.createHref(fi);return Ai({fullPath:fi,hash:ci,query:t===ap?g1(w.query):w.query||{}},Q,{redirectedFrom:void 0,href:y})}function b(w){return typeof w=="string"?Wn(e,w,h.value.path):Ai({},w)}function A(w,G){if(d!==w)return pt(8,{from:G,to:w})}function _(w){return x(w)}function L(w){return _(Ai(b(w),{replace:!0}))}function M(w){const G=w.matched[w.matched.length-1];if(G&&G.redirect){const{redirect:U}=G;let Q=typeof U=="function"?U(w):U;return typeof Q=="string"&&(Q=Q.includes("?")||Q.includes("#")?Q=b(Q):{path:Q},Q.params={}),Ai({query:w.query,hash:w.hash,params:Q.path!=null?{}:w.params},Q)}}function x(w,G){const U=d=f(w),Q=h.value,ci=w.state,fi=w.force,y=w.replace===!0,E=M(U);if(E)return x(Ai(b(E),{state:typeof E=="object"?Ai({},ci,E.state):ci,force:fi,replace:y}),G||U);const C=U;C.redirectedFrom=G;let O;return!fi&&$3(t,Q,U)&&(O=pt(16,{to:C,from:Q}),Ri(Q,Q,!0,!1)),(O?Promise.resolve(O):T(C,Q)).catch(D=>Js(D)?Js(D,2)?D:Pi(D):X(D,C,Q)).then(D=>{if(D){if(Js(D,2))return x(Ai({replace:y},b(D.to),{state:typeof D.to=="object"?Ai({},ci,D.to.state):ci,force:fi}),G||C)}else D=P(C,Q,!0,y,ci);return H(C,Q,D),D})}function K(w,G){const U=A(w,G);return U?Promise.reject(U):Promise.resolve()}function j(w){const G=ys.values().next().value;return G&&typeof G.runWithContext=="function"?G.runWithContext(w):w()}function T(w,G){let U;const[Q,ci,fi]=P1(w,G);U=Gn(Q.reverse(),"beforeRouteLeave",w,G);for(const E of Q)E.leaveGuards.forEach(C=>{U.push(ue(C,w,G))});const y=K.bind(null,w,G);return U.push(y),Ki(U).then(()=>{U=[];for(const E of n.list())U.push(ue(E,w,G));return U.push(y),Ki(U)}).then(()=>{U=Gn(ci,"beforeRouteUpdate",w,G);for(const E of ci)E.updateGuards.forEach(C=>{U.push(ue(C,w,G))});return U.push(y),Ki(U)}).then(()=>{U=[];for(const E of fi)if(E.beforeEnter)if(As(E.beforeEnter))for(const C of E.beforeEnter)U.push(ue(C,w,G));else U.push(ue(E.beforeEnter,w,G));return U.push(y),Ki(U)}).then(()=>(w.matched.forEach(E=>E.enterCallbacks={}),U=Gn(fi,"beforeRouteEnter",w,G,j),U.push(y),Ki(U))).then(()=>{U=[];for(const E of l.list())U.push(ue(E,w,G));return U.push(y),Ki(U)}).catch(E=>Js(E,8)?E:Promise.reject(E))}function H(w,G,U){r.list().forEach(Q=>j(()=>Q(w,G,U)))}function P(w,G,U,Q,ci){const fi=A(w,G);if(fi)return fi;const y=G===Xs,E=Zs?history.state:{};U&&(Q||y?a.replace(w.fullPath,Ai({scroll:y&&E&&E.scroll},ci)):a.push(w.fullPath,ci)),h.value=w,Ri(w,G,U,y),Pi()}let ii;function ri(){ii||(ii=a.listen((w,G,U)=>{if(!ls.listening)return;const Q=f(w),ci=M(Q);if(ci){x(Ai(ci,{replace:!0}),Q).catch(Ht);return}d=Q;const fi=h.value;Zs&&J3(Jh(fi.fullPath,U.delta),vn()),T(Q,fi).catch(y=>Js(y,12)?y:Js(y,2)?(x(y.to,Q).then(E=>{Js(E,20)&&!U.delta&&U.type===Zt.pop&&a.go(-1,!1)}).catch(Ht),Promise.reject()):(U.delta&&a.go(-U.delta,!1),X(y,Q,fi))).then(y=>{y=y||P(Q,fi,!1),y&&(U.delta&&!Js(y,8)?a.go(-U.delta,!1):U.type===Zt.pop&&Js(y,20)&&a.go(-1,!1)),H(Q,fi,y)}).catch(Ht)}))}let ki=Dt(),W=Dt(),N;function X(w,G,U){Pi(w);const Q=W.list();return Q.length?Q.forEach(ci=>ci(w,G,U)):console.error(w),Promise.reject(w)}function li(){return N&&h.value!==Xs?Promise.resolve():new Promise((w,G)=>{ki.add([w,G])})}function Pi(w){return N||(N=!w,ri(),ki.list().forEach(([G,U])=>w?U(w):G()),ki.reset()),w}function Ri(w,G,U,Q){const{scrollBehavior:ci}=i;if(!Zs||!ci)return Promise.resolve();const fi=!U&&Y3(Jh(w.fullPath,0))||(Q||!U)&&history.state&&history.state.scroll||null;return zs().then(()=>ci(w,G,fi)).then(y=>y&&K3(y)).catch(y=>X(y,w,G))}const vi=w=>a.go(w);let $i;const ys=new Set,ls={currentRoute:h,listening:!0,addRoute:u,removeRoute:v,clearRoutes:s.clearRoutes,hasRoute:B,getRoutes:m,resolve:f,options:i,push:_,replace:L,go:vi,back:()=>vi(-1),forward:()=>vi(1),beforeEach:n.add,beforeResolve:l.add,afterEach:r.add,onError:W.add,isReady:li,install(w){const G=this;w.component("RouterLink",y1),w.component("RouterView",E1),w.config.globalProperties.$router=G,Object.defineProperty(w.config.globalProperties,"$route",{enumerable:!0,get:()=>ve(h)}),Zs&&!$i&&h.value===Xs&&($i=!0,_(a.location).catch(ci=>{}));const U={};for(const ci in Xs)Object.defineProperty(U,ci,{get:()=>h.value[ci],enumerable:!0});w.provide(An,G),w.provide(mr,ao(U)),w.provide(El,h);const Q=w.unmount;ys.add(w),w.unmount=function(){ys.delete(w),ys.size<1&&(d=Xs,ii&&ii(),ii=null,h.value=Xs,$i=!1,N=!1),Q()},Zs&&F1(w,G,s)}};function Ki(w){return w.reduce((G,U)=>G.then(()=>j(U)),Promise.resolve())}return ls}function P1(i,s){const e=[],t=[],a=[],n=Math.max(s.matched.length,i.matched.length);for(let l=0;lBe(d,r))?t.push(r):e.push(r));const h=i.matched[l];h&&(s.matched.find(d=>Be(d,h))||a.push(h))}return[e,t,a]}function ne(){return Si(An)}function Ws(i){return Si(mr)}var Br=Symbol(""),Ps=()=>{const i=Si(Br);if(!i)throw new Error("useClientData() is called without provider.");return i},R1=()=>Ps().pageComponent,xi=()=>Ps().pageData,mi=()=>Ps().pageFrontmatter,j1=()=>Ps().pageHead,fr=()=>Ps().pageLang,M1=()=>Ps().pageLayout,Rs=()=>Ps().routeLocale,Tk=()=>Ps().routePath,V1=()=>Ps().routes,Ok=()=>Ps().siteData,va=()=>Ps().siteLocaleData,N1=Symbol(""),Fl=Vi(u3),st=Vi(g3),Lk=(i,s)=>{const e=l3(i,s);if(st.value[e])return e;const t=encodeURI(e);if(st.value[t])return t;const a=Fl.value[e]||Fl.value[t];return a||e},Fs=(i,s)=>{const{pathname:e,hashAndQueries:t}=rk(i),a=Lk(e,s),n=a+t;return st.value[a]?{...st.value[a],path:n,notFound:!1}:{...st.value["/404.html"],path:n,notFound:!0}},$1=(i,s)=>{const{pathname:e,hashAndQueries:t}=rk(i);return Lk(e,s)+t},q1=i=>{if(!(i.metaKey||i.altKey||i.ctrlKey||i.shiftKey)&&!i.defaultPrevented&&!(i.button!==void 0&&i.button!==0)){if(i.currentTarget){const s=i.currentTarget.getAttribute("target");if(s!=null&&s.match(/\b_blank\b/i))return}return i.preventDefault(),!0}},Ui=q({name:"RouteLink",props:{to:{type:String,required:!0},active:Boolean,activeClass:{type:String,default:"route-link-active"}},slots:Object,setup(i,{slots:s}){const e=ne(),t=Ws(),a=F(()=>i.to.startsWith("#")||i.to.startsWith("?")?i.to:`/${$1(i.to,t.path).substring(1)}`);return()=>p("a",{class:["route-link",{[i.activeClass]:i.active}],href:a.value,onClick:(n={})=>{q1(n)&&e.push(i.to).catch()}},s.default())}}),H1=q({name:"AutoLink",props:{config:{type:Object,required:!0}},slots:Object,setup(i,{slots:s}){const e=vt(i,"config"),t=Ws(),a=Ok(),n=F(()=>ga(e.value.link)),l=F(()=>e.value.target||(n.value?"_blank":void 0)),r=F(()=>l.value==="_blank"),h=F(()=>!n.value&&!r.value),d=F(()=>e.value.rel||(r.value?"noopener noreferrer":null)),o=F(()=>e.value.ariaLabel??e.value.text),k=F(()=>{if(e.value.exact)return!1;const u=Object.keys(a.value.locales);return u.length?u.every(v=>v!==e.value.link):e.value.link!=="/"}),c=F(()=>h.value?e.value.activeMatch?(e.value.activeMatch instanceof RegExp?e.value.activeMatch:new RegExp(e.value.activeMatch,"u")).test(t.path):k.value?t.path.startsWith(e.value.link):t.path===e.value.link:!1);return()=>{const{before:u,after:v,default:m}=s,B=(m==null?void 0:m(e.value))??[u==null?void 0:u(e.value),e.value.text,v==null?void 0:v(e.value)];return h.value?p(Ui,{class:"auto-link",to:e.value.link,active:c.value,"aria-label":o.value},()=>B):p("a",{class:"auto-link external-link",href:e.value.link,"aria-label":o.value,rel:d.value,target:l.value},B)}}}),Ik=q({name:"ClientOnly",setup(i,s){const e=Z(!1);return Di(()=>{e.value=!0}),()=>{var t,a;return e.value?(a=(t=s.slots).default)==null?void 0:a.call(t):null}}}),Pk=q({name:"Content",props:{path:{type:String,required:!1,default:""}},setup(i){const s=R1(),e=F(()=>{if(!i.path)return s.value;const t=Fs(i.path);return Cg(async()=>t.loader().then(({comp:a})=>a))});return()=>p(e.value)}}),U1="Layout",z1="en-US",we=oa({resolveLayouts:i=>i.reduce((s,e)=>({...s,...e.layouts}),{}),resolvePageHead:(i,s,e)=>{const t=Ti(s.description)?s.description:e.description,a=[...Array.isArray(s.head)?s.head:[],...e.head,["title",{},i],["meta",{name:"description",content:t}]];return k3(a)},resolvePageHeadTitle:(i,s)=>[i.title,s.title].filter(e=>!!e).join(" | "),resolvePageLang:(i,s)=>i.lang||s.lang||z1,resolvePageLayout:(i,s)=>{const e=Ti(i.frontmatter.layout)?i.frontmatter.layout:U1;if(!s[e])throw new Error(`[vuepress] Cannot resolve layout: ${e}`);return s[e]},resolveRouteLocale:(i,s)=>r3(i,decodeURI(s)),resolveSiteLocaleData:({base:i,locales:s,...e},t)=>{var a;return{...e,...s[t],head:[...((a=s[t])==null?void 0:a.head)??[],...e.head]}}}),js=(i={})=>i,_i=i=>Ve(i)?i:`/${pk(i)}`;const W1=Object.freeze(Object.defineProperty({__proto__:null},Symbol.toStringTag,{value:"Module"})),G1=Object.freeze(Object.defineProperty({__proto__:null},Symbol.toStringTag,{value:"Module"})),Rk=({size:i=48,stroke:s=4,wrapper:e=!0,height:t=2*i})=>{const a=p("svg",{xmlns:"http://www.w3.org/2000/svg",width:i,height:i,preserveAspectRatio:"xMidYMid",viewBox:"25 25 50 50"},[p("animateTransform",{attributeName:"transform",type:"rotate",dur:"2s",keyTimes:"0;1",repeatCount:"indefinite",values:"0;360"}),p("circle",{cx:"50",cy:"50",r:"20",fill:"none",stroke:"currentColor","stroke-width":s,"stroke-linecap":"round"},[p("animate",{attributeName:"stroke-dasharray",dur:"1.5s",keyTimes:"0;0.5;1",repeatCount:"indefinite",values:"1,200;90,200;1,200"}),p("animate",{attributeName:"stroke-dashoffset",dur:"1.5s",keyTimes:"0;0.5;1",repeatCount:"indefinite",values:"0;-35px;-125px"})])]);return e?p("div",{class:"loading-icon-wrapper",style:`display:flex;align-items:center;justify-content:center;height:${t}px`},a):a};Rk.displayName="LoadingIcon";function ft(i){return Hd()?(_2(i),!0):!1}function Wi(i){return typeof i=="function"?i():ve(i)}const Aa=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const K1=Object.prototype.toString,J1=i=>K1.call(i)==="[object Object]",Re=()=>{},dp=Y1();function Y1(){var i,s;return Aa&&((i=window==null?void 0:window.navigator)==null?void 0:i.userAgent)&&(/iP(?:ad|hone|od)/.test(window.navigator.userAgent)||((s=window==null?void 0:window.navigator)==null?void 0:s.maxTouchPoints)>2&&/iPad|Macintosh/.test(window==null?void 0:window.navigator.userAgent))}function Er(i,s){function e(...t){return new Promise((a,n)=>{Promise.resolve(i(()=>s.apply(this,t),{fn:s,thisArg:this,args:t})).then(a).catch(n)})}return e}const jk=i=>i();function X1(i,s={}){let e,t,a=Re;const n=r=>{clearTimeout(r),a(),a=Re};return r=>{const h=Wi(i),d=Wi(s.maxWait);return e&&n(e),h<=0||d!==void 0&&d<=0?(t&&(n(t),t=null),Promise.resolve(r())):new Promise((o,k)=>{a=s.rejectOnCancel?k:o,d&&!t&&(t=setTimeout(()=>{e&&n(e),t=null,o(r())},d)),e=setTimeout(()=>{t&&n(t),t=null,o(r())},h)})}}function Q1(...i){let s=0,e,t=!0,a=Re,n,l,r,h,d;!ji(i[0])&&typeof i[0]=="object"?{delay:l,trailing:r=!0,leading:h=!0,rejectOnCancel:d=!1}=i[0]:[l,r=!0,h=!0,d=!1]=i;const o=()=>{e&&(clearTimeout(e),e=void 0,a(),a=Re)};return c=>{const u=Wi(l),v=Date.now()-s,m=()=>n=c();return o(),u<=0?(s=Date.now(),m()):(v>u&&(h||!t)?(s=Date.now(),m()):r&&(n=new Promise((B,f)=>{a=d?f:B,e=setTimeout(()=>{s=Date.now(),t=!0,B(m()),o()},Math.max(0,u-v))})),!h&&!e&&(e=setTimeout(()=>t=!0,u)),t=!1,n)}}function Z1(i=jk){const s=Z(!0);function e(){s.value=!1}function t(){s.value=!0}const a=(...n)=>{s.value&&i(...n)};return{isActive:Me(s),pause:e,resume:t,eventFilter:a}}function i4(i){let s;function e(){return s||(s=i()),s}return e.reset=async()=>{const t=s;s=void 0,t&&await t},e}function Mk(i){return mt()}function s4(...i){if(i.length!==1)return vt(...i);const s=i[0];return typeof s=="function"?Me(ro(()=>({get:s,set:Re}))):Z(s)}function e4(i,s=200,e={}){return Er(X1(s,e),i)}function t4(i,s=200,e=!1,t=!0,a=!1){return Er(Q1(s,e,t,a),i)}function a4(i,s,e={}){const{eventFilter:t=jk,...a}=e;return ui(i,Er(t,s),a)}function n4(i,s,e={}){const{eventFilter:t,...a}=e,{eventFilter:n,pause:l,resume:r,isActive:h}=Z1(t);return{stop:a4(i,s,{...a,eventFilter:n}),pause:l,resume:r,isActive:h}}function yn(i,s=!0,e){Mk()?Di(i,e):s?i():zs(i)}function l4(i,s){Mk()&&yt(i,s)}function r4(i,s,e={}){const{immediate:t=!0}=e,a=Z(!1);let n=null;function l(){n&&(clearTimeout(n),n=null)}function r(){a.value=!1,l()}function h(...d){l(),a.value=!0,n=setTimeout(()=>{a.value=!1,n=null,i(...d)},Wi(s))}return t&&(a.value=!0,Aa&&h()),ft(r),{isPending:Me(a),start:h,stop:r}}function ia(i=!1,s={}){const{truthyValue:e=!0,falsyValue:t=!1}=s,a=ji(i),n=Z(i);function l(r){if(arguments.length)return n.value=r,n.value;{const h=Wi(e);return n.value=n.value===h?Wi(t):h,n.value}}return a?l:[n,l]}const Os=Aa?window:void 0,Vk=Aa?window.document:void 0,Nk=Aa?window.navigator:void 0;function se(i){var s;const e=Wi(i);return(s=e==null?void 0:e.$el)!=null?s:e}function Ni(...i){let s,e,t,a;if(typeof i[0]=="string"||Array.isArray(i[0])?([e,t,a]=i,s=Os):[s,e,t,a]=i,!s)return Re;Array.isArray(e)||(e=[e]),Array.isArray(t)||(t=[t]);const n=[],l=()=>{n.forEach(o=>o()),n.length=0},r=(o,k,c,u)=>(o.addEventListener(k,c,u),()=>o.removeEventListener(k,c,u)),h=ui(()=>[se(s),Wi(a)],([o,k])=>{if(l(),!o)return;const c=J1(k)?{...k}:k;n.push(...e.flatMap(u=>t.map(v=>r(o,u,v,c))))},{immediate:!0,flush:"post"}),d=()=>{h(),l()};return ft(d),d}function h4(){const i=Z(!1),s=mt();return s&&Di(()=>{i.value=!0},s),i}function ya(i){const s=h4();return F(()=>(s.value,!!i()))}function br(i,s={}){const{window:e=Os}=s,t=ya(()=>e&&"matchMedia"in e&&typeof e.matchMedia=="function");let a;const n=Z(!1),l=d=>{n.value=d.matches},r=()=>{a&&("removeEventListener"in a?a.removeEventListener("change",l):a.removeListener(l))},h=kr(()=>{t.value&&(r(),a=e.matchMedia(Wi(i)),"addEventListener"in a?a.addEventListener("change",l):a.addListener(l),n.value=a.matches)});return ft(()=>{h(),r(),a=void 0}),n}function op(i,s={}){const{controls:e=!1,navigator:t=Nk}=s,a=ya(()=>t&&"permissions"in t),n=Vi(),l=typeof i=="string"?{name:i}:i,r=Vi(),h=()=>{var o,k;r.value=(k=(o=n.value)==null?void 0:o.state)!=null?k:"prompt"};Ni(n,"change",h);const d=i4(async()=>{if(a.value){if(!n.value)try{n.value=await t.permissions.query(l)}catch{n.value=void 0}finally{h()}if(e)return pi(n.value)}});return d(),e?{state:r,isSupported:a,query:d}:r}function p4(i={}){const{navigator:s=Nk,read:e=!1,source:t,copiedDuring:a=1500,legacy:n=!1}=i,l=ya(()=>s&&"clipboard"in s),r=op("clipboard-read"),h=op("clipboard-write"),d=F(()=>l.value||n),o=Z(""),k=Z(!1),c=r4(()=>k.value=!1,a);function u(){l.value&&f(r.value)?s.clipboard.readText().then(b=>{o.value=b}):o.value=B()}d.value&&e&&Ni(["copy","cut"],u);async function v(b=Wi(t)){d.value&&b!=null&&(l.value&&f(h.value)?await s.clipboard.writeText(b):m(b),o.value=b,k.value=!0,c.start())}function m(b){const A=document.createElement("textarea");A.value=b??"",A.style.position="absolute",A.style.opacity="0",document.body.appendChild(A),A.select(),document.execCommand("copy"),A.remove()}function B(){var b,A,_;return(_=(A=(b=document==null?void 0:document.getSelection)==null?void 0:b.call(document))==null?void 0:A.toString())!=null?_:""}function f(b){return b==="granted"||b==="prompt"}return{isSupported:d,text:o,copied:k,copy:v}}const ja=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},Ma="__vueuse_ssr_handlers__",d4=o4();function o4(){return Ma in ja||(ja[Ma]=ja[Ma]||{}),ja[Ma]}function k4(i,s){return d4[i]||s}function c4(i){return br("(prefers-color-scheme: dark)",i)}function u4(i){return i==null?"any":i instanceof Set?"set":i instanceof Map?"map":i instanceof Date?"date":typeof i=="boolean"?"boolean":typeof i=="string"?"string":typeof i=="object"?"object":Number.isNaN(i)?"any":"number"}const g4={boolean:{read:i=>i==="true",write:i=>String(i)},object:{read:i=>JSON.parse(i),write:i=>JSON.stringify(i)},number:{read:i=>Number.parseFloat(i),write:i=>String(i)},any:{read:i=>i,write:i=>String(i)},string:{read:i=>i,write:i=>String(i)},map:{read:i=>new Map(JSON.parse(i)),write:i=>JSON.stringify(Array.from(i.entries()))},set:{read:i=>new Set(JSON.parse(i)),write:i=>JSON.stringify(Array.from(i))},date:{read:i=>new Date(i),write:i=>i.toISOString()}},kp="vueuse-storage";function ma(i,s,e,t={}){var a;const{flush:n="pre",deep:l=!0,listenToStorageChanges:r=!0,writeDefaults:h=!0,mergeDefaults:d=!1,shallow:o,window:k=Os,eventFilter:c,onError:u=T=>{console.error(T)},initOnMounted:v}=t,m=(o?Vi:Z)(typeof s=="function"?s():s);if(!e)try{e=k4("getDefaultStorage",()=>{var T;return(T=Os)==null?void 0:T.localStorage})()}catch(T){u(T)}if(!e)return m;const B=Wi(s),f=u4(B),b=(a=t.serializer)!=null?a:g4[f],{pause:A,resume:_}=n4(m,()=>M(m.value),{flush:n,deep:l,eventFilter:c});k&&r&&yn(()=>{e instanceof Storage?Ni(k,"storage",K):Ni(k,kp,j),v&&K()}),v||K();function L(T,H){if(k){const P={key:i,oldValue:T,newValue:H,storageArea:e};k.dispatchEvent(e instanceof Storage?new StorageEvent("storage",P):new CustomEvent(kp,{detail:P}))}}function M(T){try{const H=e.getItem(i);if(T==null)L(H,null),e.removeItem(i);else{const P=b.write(T);H!==P&&(e.setItem(i,P),L(H,P))}}catch(H){u(H)}}function x(T){const H=T?T.newValue:e.getItem(i);if(H==null)return h&&B!=null&&e.setItem(i,b.write(B)),B;if(!T&&d){const P=b.read(H);return typeof d=="function"?d(P,B):f==="object"&&!Array.isArray(P)?{...B,...P}:P}else return typeof H!="string"?H:b.read(H)}function K(T){if(!(T&&T.storageArea!==e)){if(T&&T.key==null){m.value=B;return}if(!(T&&T.key!==i)){A();try{(T==null?void 0:T.newValue)!==b.write(m.value)&&(m.value=x(T))}catch(H){u(H)}finally{T?zs(_):_()}}}}function j(T){K(T.detail)}return m}function v4(i,s,e={}){const{window:t=Os,...a}=e;let n;const l=ya(()=>t&&"ResizeObserver"in t),r=()=>{n&&(n.disconnect(),n=void 0)},h=F(()=>{const k=Wi(i);return Array.isArray(k)?k.map(c=>se(c)):[se(k)]}),d=ui(h,k=>{if(r(),l.value&&t){n=new ResizeObserver(s);for(const c of k)c&&n.observe(c,a)}},{immediate:!0,flush:"post"}),o=()=>{r(),d()};return ft(o),{isSupported:l,stop:o}}function A4(i,s={width:0,height:0},e={}){const{window:t=Os,box:a="content-box"}=e,n=F(()=>{var k,c;return(c=(k=se(i))==null?void 0:k.namespaceURI)==null?void 0:c.includes("svg")}),l=Z(s.width),r=Z(s.height),{stop:h}=v4(i,([k])=>{const c=a==="border-box"?k.borderBoxSize:a==="content-box"?k.contentBoxSize:k.devicePixelContentBoxSize;if(t&&n.value){const u=se(i);if(u){const v=u.getBoundingClientRect();l.value=v.width,r.value=v.height}}else if(c){const u=Array.isArray(c)?c:[c];l.value=u.reduce((v,{inlineSize:m})=>v+m,0),r.value=u.reduce((v,{blockSize:m})=>v+m,0)}else l.value=k.contentRect.width,r.value=k.contentRect.height},e);yn(()=>{const k=se(i);k&&(l.value="offsetWidth"in k?k.offsetWidth:s.width,r.value="offsetHeight"in k?k.offsetHeight:s.height)});const d=ui(()=>se(i),k=>{l.value=k?s.width:0,r.value=k?s.height:0});function o(){h(),d()}return{width:l,height:r,stop:o}}const cp=["fullscreenchange","webkitfullscreenchange","webkitendfullscreen","mozfullscreenchange","MSFullscreenChange"];function mn(i,s={}){const{document:e=Vk,autoExit:t=!1}=s,a=F(()=>{var f;return(f=se(i))!=null?f:e==null?void 0:e.querySelector("html")}),n=Z(!1),l=F(()=>["requestFullscreen","webkitRequestFullscreen","webkitEnterFullscreen","webkitEnterFullScreen","webkitRequestFullScreen","mozRequestFullScreen","msRequestFullscreen"].find(f=>e&&f in e||a.value&&f in a.value)),r=F(()=>["exitFullscreen","webkitExitFullscreen","webkitExitFullScreen","webkitCancelFullScreen","mozCancelFullScreen","msExitFullscreen"].find(f=>e&&f in e||a.value&&f in a.value)),h=F(()=>["fullScreen","webkitIsFullScreen","webkitDisplayingFullscreen","mozFullScreen","msFullscreenElement"].find(f=>e&&f in e||a.value&&f in a.value)),d=["fullscreenElement","webkitFullscreenElement","mozFullScreenElement","msFullscreenElement"].find(f=>e&&f in e),o=ya(()=>a.value&&e&&l.value!==void 0&&r.value!==void 0&&h.value!==void 0),k=()=>d?(e==null?void 0:e[d])===a.value:!1,c=()=>{if(h.value){if(e&&e[h.value]!=null)return e[h.value];{const f=a.value;if((f==null?void 0:f[h.value])!=null)return!!f[h.value]}}return!1};async function u(){if(!(!o.value||!n.value)){if(r.value)if((e==null?void 0:e[r.value])!=null)await e[r.value]();else{const f=a.value;(f==null?void 0:f[r.value])!=null&&await f[r.value]()}n.value=!1}}async function v(){if(!o.value||n.value)return;c()&&await u();const f=a.value;l.value&&(f==null?void 0:f[l.value])!=null&&(await f[l.value](),n.value=!0)}async function m(){await(n.value?u():v())}const B=()=>{const f=c();(!f||f&&k())&&(n.value=f)};return Ni(e,cp,B,!1),Ni(()=>se(a),cp,B,!1),t&&ft(u),{isSupported:o,isFullscreen:n,enter:v,exit:u,toggle:m}}function Kn(i){return typeof Window<"u"&&i instanceof Window?i.document.documentElement:typeof Document<"u"&&i instanceof Document?i.documentElement:i}function y4(i,s,e={}){const{window:t=Os}=e;return ma(i,s,t==null?void 0:t.localStorage,e)}function m4(i={}){const{window:s=Os}=i;if(!s)return Z(["en"]);const e=s.navigator,t=Z(e.languages);return Ni(s,"languagechange",()=>{t.value=e.languages}),t}function up(i,s=Re,e={}){const{immediate:t=!0,manual:a=!1,type:n="text/javascript",async:l=!0,crossOrigin:r,referrerPolicy:h,noModule:d,defer:o,document:k=Vk,attrs:c={}}=e,u=Z(null);let v=null;const m=b=>new Promise((A,_)=>{const L=K=>(u.value=K,A(K),K);if(!k){A(!1);return}let M=!1,x=k.querySelector(`script[src="${Wi(i)}"]`);x?x.hasAttribute("data-loaded")&&L(x):(x=k.createElement("script"),x.type=n,x.async=l,x.src=Wi(i),o&&(x.defer=o),r&&(x.crossOrigin=r),d&&(x.noModule=d),h&&(x.referrerPolicy=h),Object.entries(c).forEach(([K,j])=>x==null?void 0:x.setAttribute(K,j)),M=!0),x.addEventListener("error",K=>_(K)),x.addEventListener("abort",K=>_(K)),x.addEventListener("load",()=>{x.setAttribute("data-loaded","true"),s(x),L(x)}),M&&(x=k.head.appendChild(x)),b||L(x)}),B=(b=!0)=>(v||(v=m(b)),v),f=()=>{if(!k)return;v=null,u.value&&(u.value=null);const b=k.querySelector(`script[src="${Wi(i)}"]`);b&&k.head.removeChild(b)};return t&&!a&&yn(B),a||l4(f),{scriptTag:u,load:B,unload:f}}function $k(i){const s=window.getComputedStyle(i);if(s.overflowX==="scroll"||s.overflowY==="scroll"||s.overflowX==="auto"&&i.clientWidth1?!0:(s.preventDefault&&s.preventDefault(),!1)}const Jn=new WeakMap;function Fr(i,s=!1){const e=Z(s);let t=null,a="";ui(s4(i),r=>{const h=Kn(Wi(r));if(h){const d=h;if(Jn.get(d)||Jn.set(d,d.style.overflow),d.style.overflow!=="hidden"&&(a=d.style.overflow),d.style.overflow==="hidden")return e.value=!0;if(e.value)return d.style.overflow="hidden"}},{immediate:!0});const n=()=>{const r=Kn(Wi(i));!r||e.value||(dp&&(t=Ni(r,"touchmove",h=>{B4(h)},{passive:!1})),r.style.overflow="hidden",e.value=!0)},l=()=>{const r=Kn(Wi(i));!r||!e.value||(dp&&(t==null||t()),r.style.overflow=a,Jn.delete(r),e.value=!1)};return ft(l),F({get(){return e.value},set(r){r?n():l()}})}function _r(i,s,e={}){const{window:t=Os}=e;return ma(i,s,t==null?void 0:t.sessionStorage,e)}function f4(i={}){const{window:s=Os,behavior:e="auto"}=i;if(!s)return{x:Z(0),y:Z(0)};const t=Z(s.scrollX),a=Z(s.scrollY),n=F({get(){return t.value},set(r){scrollTo({left:r,behavior:e})}}),l=F({get(){return a.value},set(r){scrollTo({top:r,behavior:e})}});return Ni(s,"scroll",()=>{t.value=s.scrollX,a.value=s.scrollY},{capture:!1,passive:!0}),{x:n,y:l}}function E4(i={}){const{window:s=Os,initialWidth:e=Number.POSITIVE_INFINITY,initialHeight:t=Number.POSITIVE_INFINITY,listenOrientation:a=!0,includeScrollbar:n=!0,type:l="inner"}=i,r=Z(e),h=Z(t),d=()=>{s&&(l==="outer"?(r.value=s.outerWidth,h.value=s.outerHeight):n?(r.value=s.innerWidth,h.value=s.innerHeight):(r.value=s.document.documentElement.clientWidth,h.value=s.document.documentElement.clientHeight))};if(d(),yn(d),Ni("resize",d,{passive:!0}),a){const o=br("(orientation: portrait)");ui(o,()=>d())}return{width:r,height:h}}var gs=Uint8Array,Ge=Uint16Array,b4=Int32Array,qk=new gs([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),Hk=new gs([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),F4=new gs([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),Uk=function(i,s){for(var e=new Ge(31),t=0;t<31;++t)e[t]=s+=1<>1|(wi&21845)<<1;re=(re&52428)>>2|(re&13107)<<2,re=(re&61680)>>4|(re&3855)<<4,_l[wi]=((re&65280)>>8|(re&255)<<8)>>1}var zt=function(i,s,e){for(var t=i.length,a=0,n=new Ge(s);a>h]=d}else for(r=new Ge(t),a=0;a>15-i[a]);return r},Ba=new gs(288);for(var wi=0;wi<144;++wi)Ba[wi]=8;for(var wi=144;wi<256;++wi)Ba[wi]=9;for(var wi=256;wi<280;++wi)Ba[wi]=7;for(var wi=280;wi<288;++wi)Ba[wi]=8;var Gk=new gs(32);for(var wi=0;wi<32;++wi)Gk[wi]=5;var D4=zt(Ba,9,1),x4=zt(Gk,5,1),Yn=function(i){for(var s=i[0],e=1;es&&(s=i[e]);return s},Cs=function(i,s,e){var t=s/8|0;return(i[t]|i[t+1]<<8)>>(s&7)&e},Xn=function(i,s){var e=s/8|0;return(i[e]|i[e+1]<<8|i[e+2]<<16)>>(s&7)},S4=function(i){return(i+7)/8|0},Kk=function(i,s,e){return(s==null||s<0)&&(s=0),(e==null||e>i.length)&&(e=i.length),new gs(i.subarray(s,e))},T4=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],fs=function(i,s,e){var t=new Error(s||T4[i]);if(t.code=i,Error.captureStackTrace&&Error.captureStackTrace(t,fs),!e)throw t;return t},O4=function(i,s,e,t){var a=i.length,n=0;if(!a||s.f&&!s.l)return e||new gs(0);var l=!e,r=l||s.i!=2,h=s.i;l&&(e=new gs(a*3));var d=function(ci){var fi=e.length;if(ci>fi){var y=new gs(Math.max(fi*2,ci));y.set(e),e=y}},o=s.f||0,k=s.p||0,c=s.b||0,u=s.l,v=s.d,m=s.m,B=s.n,f=a*8;do{if(!u){o=Cs(i,k,1);var b=Cs(i,k+1,3);if(k+=3,b)if(b==1)u=D4,v=x4,m=9,B=5;else if(b==2){var M=Cs(i,k,31)+257,x=Cs(i,k+10,15)+4,K=M+Cs(i,k+5,31)+1;k+=14;for(var j=new gs(K),T=new gs(19),H=0;H>4;if(A<16)j[H++]=A;else{var W=0,N=0;for(A==16?(N=3+Cs(i,k,3),k+=2,W=j[H-1]):A==17?(N=3+Cs(i,k,7),k+=3):A==18&&(N=11+Cs(i,k,127),k+=7);N--;)j[H++]=W}}var X=j.subarray(0,M),li=j.subarray(M);m=Yn(X),B=Yn(li),u=zt(X,m,1),v=zt(li,B,1)}else fs(1);else{var A=S4(k)+4,_=i[A-4]|i[A-3]<<8,L=A+_;if(L>a){h&&fs(0);break}r&&d(c+_),e.set(i.subarray(A,L),c),s.b=c+=_,s.p=k=L*8,s.f=o;continue}if(k>f){h&&fs(0);break}}r&&d(c+131072);for(var Pi=(1<>4;if(k+=W&15,k>f){h&&fs(0);break}if(W||fs(2),$i<256)e[c++]=$i;else if($i==256){vi=k,u=null;break}else{var ys=$i-254;if($i>264){var H=$i-257,ls=qk[H];ys=Cs(i,k,(1<>4;Ki||fs(3),k+=Ki&15;var li=w4[w];if(w>3){var ls=Hk[w];li+=Xn(i,k)&(1<f){h&&fs(0);break}r&&d(c+131072);var G=c+ys;if(c>4>7||(i[0]<<8|i[1])%31)&&fs(6,"invalid zlib data"),(i[1]>>5&1)==+!s&&fs(6,"invalid zlib data: "+(i[1]&32?"need":"unexpected")+" dictionary"),(i[1]>>3&4)+2};function P4(i,s){return O4(i.subarray(I4(i,s),-4),{i:2},s,s)}var Cl=typeof TextDecoder<"u"&&new TextDecoder,R4=0;try{Cl.decode(L4,{stream:!0}),R4=1}catch{}var j4=function(i){for(var s="",e=0;;){var t=i[e++],a=(t>127)+(t>223)+(t>239);if(e+a>i.length)return{s,r:Kk(i,e-1)};a?a==3?(t=((t&15)<<18|(i[e++]&63)<<12|(i[e++]&63)<<6|i[e++]&63)-65536,s+=String.fromCharCode(55296|t>>10,56320|t&1023)):a&1?s+=String.fromCharCode((t&31)<<6|i[e++]&63):s+=String.fromCharCode((t&15)<<12|(i[e++]&63)<<6|i[e++]&63):s+=String.fromCharCode(t)}};function M4(i,s){{for(var e=new gs(i.length),t=0;t{const s=atob(i);return V4(P4(M4(s)))},Ls=(i,s)=>{var t;const e=(t=(s==null?void 0:s._instance)??mt())==null?void 0:t.appContext.components;return e?i in e||ps(i)in e||da(ps(i))in e:!1},Cr=i=>new Promise(s=>{setTimeout(s,i)}),fa=i=>{const s=Rs();return F(()=>i[s.value]??{})},wr=i=>typeof i<"u",Qn=i=>typeof i=="number",{isArray:wl}=Array,sa=(i,s)=>Ti(i)&&i.startsWith(s),N4=(i,s)=>Ti(i)&&i.endsWith(s),{entries:Et}=Object,{keys:Is}=Object,Dr=i=>{if(i){if(typeof i=="number")return new Date(i);const s=Date.parse(i.toString());if(!Number.isNaN(s))return new Date(s)}return null},Bn=i=>sa(i,"/"),Jk=({type:i="info",text:s="",vertical:e,color:t,bgColor:a},{slots:n})=>{var l;return p("span",{class:["vp-badge",i,{diy:t||a}],style:{verticalAlign:e??!1,backgroundColor:a??!1,color:t??!1}},((l=n.default)==null?void 0:l.call(n))??s)};Jk.displayName="Badge";var $4=q({name:"FontIcon",props:{icon:{type:String,default:""},color:{type:String,default:""},size:{type:[String,Number],default:""}},setup(i){const s=F(()=>{const t=["font-icon icon"],a=`fas fa-${i.icon}`;return t.push("fa-fw fa-sm"),t.push(i.icon.includes(" ")?i.icon:a),t}),e=F(()=>{const t={};return i.color&&(t.color=i.color),i.size&&(t["font-size"]=Number.isNaN(Number(i.size))?i.size:`${i.size}px`),Is(t).length?t:null});return()=>i.icon?p("span",{key:i.icon,class:s.value,style:e.value}):null}});const q4={enhance:({app:i})=>{Ls("Badge")||i.component("Badge",Jk),Ls("FontIcon")||i.component("FontIcon",$4)},setup:()=>{up("https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6/js/solid.min.js",()=>{},{attrs:{"data-auto-replace-svg":"nest"}}),up("https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6/js/fontawesome.min.js",()=>{},{attrs:{"data-auto-replace-svg":"nest"}})},rootComponents:[]},H4=Object.freeze(Object.defineProperty({__proto__:null,default:q4},Symbol.toStringTag,{value:"Module"})),vp=async(i,s)=>{const{path:e,query:t}=i.currentRoute.value,{scrollBehavior:a}=i.options;i.options.scrollBehavior=void 0,await i.replace({path:e,query:t,hash:s}),i.options.scrollBehavior=a},U4=({headerLinkSelector:i,headerAnchorSelector:s,delay:e,offset:t=5})=>{const a=ne();Ni("scroll",e4(()=>{var v,m;const l=Math.max(window.scrollY,document.documentElement.scrollTop,document.body.scrollTop);if(Math.abs(l-0)k.some(f=>f.hash===B.hash));for(let B=0;B=(((v=f.parentElement)==null?void 0:v.offsetTop)??0)-t,_=!b||l<(((m=b.parentElement)==null?void 0:m.offsetTop)??0)-t;if(!(A&&_))continue;const M=decodeURIComponent(a.currentRoute.value.hash),x=decodeURIComponent(f.hash);if(M===x)return;if(o){for(let K=B+1;KTi(i.title)?{title:i.title}:null;const Xk=Symbol(""),X4=i=>{Yk=i},Q4=()=>Si(Xk),Z4=i=>{i.provide(Xk,Yk)};var iv={"/":{title:"目录",empty:"暂无目录"}},sv=q({name:"Catalog",props:{base:{type:String,default:""},level:{type:Number,default:3},index:Boolean,hideHeading:Boolean},setup(i){const s=Q4(),e=fa(iv),t=xi(),a=V1(),n=Ok(),l=Vi(Et(a.value).map(([h,{meta:d}])=>{const o=s(d);if(!o)return null;const k=h.split("/").length;return{level:N4(h,"/")?k-2:k-1,base:h.replace(/\/[^/]+\/?$/,"/"),path:h,...o}}).filter(h=>Bt(h)&&Ti(h.title))),r=F(()=>{const h=i.base?c3(hk(i.base)):t.value.path.replace(/\/[^/]+$/,"/"),d=h.split("/").length-2,o=[];return l.value.filter(({level:k,path:c})=>{if(!sa(c,h)||c===h)return!1;if(h==="/"){const u=Is(n.value.locales).filter(v=>v!=="/");if(c==="/404.html"||u.some(v=>sa(c,v)))return!1}return k-d<=i.level}).sort(({title:k,level:c,order:u},{title:v,level:m,order:B})=>c-m||(Qn(u)?Qn(B)?u>0?B>0?u-B:-1:B<0?u-B:1:u:Qn(B)?B:k.localeCompare(v))).forEach(k=>{var v;const{base:c,level:u}=k;switch(u-d){case 1:{o.push(k);break}case 2:{const m=o.find(B=>B.path===c);m&&(m.children??(m.children=[])).push(k);break}default:{const m=o.find(B=>B.path===c.replace(/\/[^/]+\/$/,"/"));if(m){const B=(v=m.children)==null?void 0:v.find(f=>f.path===c);B&&(B.children??(B.children=[])).push(k)}}}}),o});return()=>{const h=r.value.some(d=>d.children);return p("div",{class:["vp-catalog",{index:i.index}]},[i.hideHeading?null:p("h2",{class:"vp-catalog-main-title"},e.value.title),r.value.length?p(i.index?"ol":"ul",{class:["vp-catalog-list",{deep:h}]},r.value.map(({children:d=[],title:o,path:k,content:c})=>{const u=p(Ui,{class:"vp-catalog-title",to:k},()=>c?p(c):o);return p("li",{class:"vp-catalog-item"},h?[p("h3",{id:o,class:["vp-catalog-child-title",{"has-children":d.length}]},[p("a",{href:`#${o}`,class:"vp-catalog-header-anchor","aria-hidden":!0},"#"),u]),d.length?p(i.index?"ol":"ul",{class:"vp-child-catalogs"},d.map(({children:v=[],content:m,path:B,title:f})=>p("li",{class:"vp-child-catalog"},[p("div",{class:["vp-catalog-sub-title",{"has-children":v.length}]},[p("a",{href:`#${f}`,class:"vp-catalog-header-anchor"},"#"),p(Ui,{class:"vp-catalog-title",to:B},()=>m?p(m):f)]),v.length?p(i.index?"ol":"div",{class:i.index?"vp-sub-catalogs":"vp-sub-catalogs-wrapper"},v.map(({content:b,path:A,title:_})=>i.index?p("li",{class:"vp-sub-catalog"},p(Ui,{to:A},()=>b?p(b):_)):p(Ui,{class:"vp-sub-catalog-link",to:A},()=>b?p(b):_))):null]))):null]:p("div",{class:"vp-catalog-child-title"},u))})):p("p",{class:"vp-empty-catalog"},e.value.empty)])}}}),ev=js({enhance:({app:i})=>{Z4(i),Ls("Catalog",i)||i.component("Catalog",sv)}});const tv=Object.freeze(Object.defineProperty({__proto__:null,default:ev},Symbol.toStringTag,{value:"Module"}));var av={"/":{backToTop:"返回顶部"}};const nv=q({name:"BackToTop",setup(){const i=mi(),s=fa(av),e=Vi(),{height:t}=A4(e),{height:a}=E4(),{y:n}=f4(),l=F(()=>i.value.backToTop!==!1&&n.value>100),r=F(()=>n.value/(t.value-a.value)*100);return Di(()=>{e.value=document.body}),()=>p(rt,{name:"back-to-top"},()=>l.value?p("button",{type:"button",class:"vp-back-to-top-button","aria-label":s.value.backToTop,onClick:()=>{window.scrollTo({top:0,behavior:"smooth"})}},[p("span",{class:"vp-scroll-progress",role:"progressbar","aria-labelledby":"loadinglabel","aria-valuenow":r.value},p("svg",p("circle",{cx:"26",cy:"26",r:"24",fill:"none",stroke:"currentColor","stroke-width":"4","stroke-dasharray":`${Math.PI*r.value*.48} ${Math.PI*(100-r.value)*.48}`}))),p("div",{class:"back-to-top-icon"})]):null)}}),lv=js({rootComponents:[nv]}),rv=Object.freeze(Object.defineProperty({__proto__:null,default:lv},Symbol.toStringTag,{value:"Module"}));/** * NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress * @license MIT - */const Ap=(i,s)=>{i.classList.add(s)},yp=(i,s)=>{i.classList.remove(s)},hv=i=>{var s;(s=i==null?void 0:i.parentNode)==null||s.removeChild(i)},Zn=(i,s,e)=>ie?e:i,mp=i=>(-1+i)*100,pv=(()=>{const i=[],s=()=>{const e=i.shift();e&&e(s)};return e=>{i.push(e),i.length===1&&s()}})(),dv=i=>i.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(s,e)=>e.toUpperCase()),Va=(()=>{const i=["Webkit","O","Moz","ms"],s={},e=n=>{const{style:l}=document.body;if(n in l)return n;const r=n.charAt(0).toUpperCase()+n.slice(1);let h=i.length;for(;h--;){const d=`${i[h]}${r}`;if(d in l)return d}return n},t=n=>{const l=dv(n);return s[l]??(s[l]=e(l))},a=(n,l,r)=>{n.style[t(l)]=r};return(n,l)=>{for(const r in l){const h=l[r];Object.hasOwn(l,r)&&wr(h)&&a(n,r,h)}}})(),Ys={minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
    '},Ii={percent:null,isRendered:()=>!!document.getElementById("nprogress"),set:i=>{const{speed:s,easing:e}=Ys,t=Ii.isStarted(),a=Zn(i,Ys.minimum,1);Ii.percent=a===1?null:a;const n=Ii.render(!t),l=n.querySelector(Ys.barSelector);return n.offsetWidth,pv(r=>{Va(l,{transform:`translate3d(${mp(a)}%,0,0)`,transition:`all ${s}ms ${e}`}),a===1?(Va(n,{transition:"none",opacity:"1"}),n.offsetWidth,setTimeout(()=>{Va(n,{transition:`all ${s}ms linear`,opacity:"0"}),setTimeout(()=>{Ii.remove(),r()},s)},s)):setTimeout(()=>{r()},s)}),Ii},isStarted:()=>typeof Ii.percent=="number",start:()=>{Ii.percent||Ii.set(0);const i=()=>{setTimeout(()=>{Ii.percent&&(Ii.trickle(),i())},Ys.trickleSpeed)};return i(),Ii},done:i=>!i&&!Ii.percent?Ii:Ii.increase(.3+.5*Math.random()).set(1),increase:i=>{let{percent:s}=Ii;return s?(s=Zn(s+(typeof i=="number"?i:(1-s)*Zn(Math.random()*s,.1,.95)),0,.994),Ii.set(s)):Ii.start()},trickle:()=>Ii.increase(Math.random()*Ys.trickleRate),render:i=>{if(Ii.isRendered())return document.getElementById("nprogress");Ap(document.documentElement,"nprogress-busy");const s=document.createElement("div");s.id="nprogress",s.innerHTML=Ys.template;const e=s.querySelector(Ys.barSelector),t=document.querySelector(Ys.parent),a=i?"-100":mp(Ii.percent??0);return Va(e,{transition:"all 0 linear",transform:`translate3d(${a}%,0,0)`}),t&&(t!==document.body&&Ap(t,"nprogress-custom-parent"),t.appendChild(s)),s},remove:()=>{yp(document.documentElement,"nprogress-busy"),yp(document.querySelector(Ys.parent),"nprogress-custom-parent"),hv(document.getElementById("nprogress"))}},ov=()=>{Di(()=>{const i=ne(),s=new Set;s.add(i.currentRoute.value.path),i.beforeEach(e=>{s.has(e.path)||Ii.start()}),i.afterEach(e=>{s.add(e.path),Ii.done()})})},kv=js({setup(){ov()}}),cv=Object.freeze(Object.defineProperty({__proto__:null,default:kv},Symbol.toStringTag,{value:"Module"}));var uv=Object.create,Qk=Object.defineProperty,gv=Object.getOwnPropertyDescriptor,xr=Object.getOwnPropertyNames,vv=Object.getPrototypeOf,Av=Object.prototype.hasOwnProperty,yv=(i,s)=>function(){return i&&(s=(0,i[xr(i)[0]])(i=0)),s},mv=(i,s)=>function(){return s||(0,i[xr(i)[0]])((s={exports:{}}).exports,s),s.exports},Bv=(i,s,e,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let a of xr(s))!Av.call(i,a)&&a!==e&&Qk(i,a,{get:()=>s[a],enumerable:!(t=gv(s,a))||t.enumerable});return i},fv=(i,s,e)=>(e=i!=null?uv(vv(i)):{},Bv(Qk(e,"default",{value:i,enumerable:!0}),i)),Ea=yv({"../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.43.0_@types+node@22.9.0__@swc+core@1.5.29_jiti@2.0.0_po_lnt5yfvawfblpk67opvcdwbq7u/node_modules/tsup/assets/esm_shims.js"(){}}),Ev=mv({"../../node_modules/.pnpm/rfdc@1.4.1/node_modules/rfdc/index.js"(i,s){Ea(),s.exports=t;function e(n){return n instanceof Buffer?Buffer.from(n):new n.constructor(n.buffer.slice(),n.byteOffset,n.length)}function t(n){if(n=n||{},n.circles)return a(n);const l=new Map;if(l.set(Date,k=>new Date(k)),l.set(Map,(k,c)=>new Map(h(Array.from(k),c))),l.set(Set,(k,c)=>new Set(h(Array.from(k),c))),n.constructorHandlers)for(const k of n.constructorHandlers)l.set(k[0],k[1]);let r=null;return n.proto?o:d;function h(k,c){const u=Object.keys(k),v=new Array(u.length);for(let m=0;mnew Date(u)),h.set(Map,(u,v)=>new Map(o(Array.from(u),v))),h.set(Set,(u,v)=>new Set(o(Array.from(u),v))),n.constructorHandlers)for(const u of n.constructorHandlers)h.set(u[0],u[1]);let d=null;return n.proto?c:k;function o(u,v){const m=Object.keys(u),B=new Array(m.length);for(let f=0;f(l=xv(i,d,o),l.finally(()=>{if(l=null,e.trailing&&r&&!a){const k=h(d,r);return r=null,k}}),l);return function(...d){return l?(e.trailing&&(r=d),l):new Promise(o=>{const k=!a&&e.leading;clearTimeout(a),a=setTimeout(()=>{a=null;const c=e.leading?t:h(this,d);for(const u of n)u(c);n=[]},s),k?(t=h(this,d),o(t)):n.push(o)})}}async function xv(i,s,e){return await i.apply(s,e)}function Dl(i,s={},e){for(const t in i){const a=i[t],n=e?`${e}:${t}`:t;typeof a=="object"&&a!==null?Dl(a,s,n):typeof a=="function"&&(s[n]=a)}return s}const Sv={run:i=>i()},Tv=()=>Sv,ic=typeof console.createTask<"u"?console.createTask:Tv;function Ov(i,s){const e=s.shift(),t=ic(e);return i.reduce((a,n)=>a.then(()=>t.run(()=>n(...s))),Promise.resolve())}function Lv(i,s){const e=s.shift(),t=ic(e);return Promise.all(i.map(a=>t.run(()=>a(...s))))}function il(i,s){for(const e of[...i])e(s)}class Iv{constructor(){this._hooks={},this._before=void 0,this._after=void 0,this._deprecatedMessages=void 0,this._deprecatedHooks={},this.hook=this.hook.bind(this),this.callHook=this.callHook.bind(this),this.callHookWith=this.callHookWith.bind(this)}hook(s,e,t={}){if(!s||typeof e!="function")return()=>{};const a=s;let n;for(;this._deprecatedHooks[s];)n=this._deprecatedHooks[s],s=n.to;if(n&&!t.allowDeprecated){let l=n.message;l||(l=`${a} hook has been deprecated`+(n.to?`, please use ${n.to}`:"")),this._deprecatedMessages||(this._deprecatedMessages=new Set),this._deprecatedMessages.has(l)||(console.warn(l),this._deprecatedMessages.add(l))}if(!e.name)try{Object.defineProperty(e,"name",{get:()=>"_"+s.replace(/\W+/g,"_")+"_hook_cb",configurable:!0})}catch{}return this._hooks[s]=this._hooks[s]||[],this._hooks[s].push(e),()=>{e&&(this.removeHook(s,e),e=void 0)}}hookOnce(s,e){let t,a=(...n)=>(typeof t=="function"&&t(),t=void 0,a=void 0,e(...n));return t=this.hook(s,a),t}removeHook(s,e){if(this._hooks[s]){const t=this._hooks[s].indexOf(e);t!==-1&&this._hooks[s].splice(t,1),this._hooks[s].length===0&&delete this._hooks[s]}}deprecateHook(s,e){this._deprecatedHooks[s]=typeof e=="string"?{to:e}:e;const t=this._hooks[s]||[];delete this._hooks[s];for(const a of t)this.hook(s,a)}deprecateHooks(s){Object.assign(this._deprecatedHooks,s);for(const e in s)this.deprecateHook(e,s[e])}addHooks(s){const e=Dl(s),t=Object.keys(e).map(a=>this.hook(a,e[a]));return()=>{for(const a of t.splice(0,t.length))a()}}removeHooks(s){const e=Dl(s);for(const t in e)this.removeHook(t,e[t])}removeAllHooks(){for(const s in this._hooks)delete this._hooks[s]}callHook(s,...e){return e.unshift(s),this.callHookWith(Ov,s,...e)}callHookParallel(s,...e){return e.unshift(s),this.callHookWith(Lv,s,...e)}callHookWith(s,e,...t){const a=this._before||this._after?{name:e,args:t,context:{}}:void 0;this._before&&il(this._before,a);const n=s(e in this._hooks?[...this._hooks[e]]:[],t);return n instanceof Promise?n.finally(()=>{this._after&&a&&il(this._after,a)}):(this._after&&a&&il(this._after,a),n)}beforeEach(s){return this._before=this._before||[],this._before.push(s),()=>{if(this._before!==void 0){const e=this._before.indexOf(s);e!==-1&&this._before.splice(e,1)}}}afterEach(s){return this._after=this._after||[],this._after.push(s),()=>{if(this._after!==void 0){const e=this._after.indexOf(s);e!==-1&&this._after.splice(e,1)}}}}function sc(){return new Iv}var Pv=Object.create,ec=Object.defineProperty,Rv=Object.getOwnPropertyDescriptor,Sr=Object.getOwnPropertyNames,jv=Object.getPrototypeOf,Mv=Object.prototype.hasOwnProperty,Vv=(i,s)=>function(){return i&&(s=(0,i[Sr(i)[0]])(i=0)),s},tc=(i,s)=>function(){return s||(0,i[Sr(i)[0]])((s={exports:{}}).exports,s),s.exports},Nv=(i,s,e,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let a of Sr(s))!Mv.call(i,a)&&a!==e&&ec(i,a,{get:()=>s[a],enumerable:!(t=Rv(s,a))||t.enumerable});return i},$v=(i,s,e)=>(e=i!=null?Pv(jv(i)):{},Nv(ec(e,"default",{value:i,enumerable:!0}),i)),S=Vv({"../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.43.0_@types+node@22.9.0__@swc+core@1.5.29_jiti@2.0.0_po_lnt5yfvawfblpk67opvcdwbq7u/node_modules/tsup/assets/esm_shims.js"(){}}),qv=tc({"../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/lib/speakingurl.js"(i,s){S(),function(e){var t={À:"A",Á:"A",Â:"A",Ã:"A",Ä:"Ae",Å:"A",Æ:"AE",Ç:"C",È:"E",É:"E",Ê:"E",Ë:"E",Ì:"I",Í:"I",Î:"I",Ï:"I",Ð:"D",Ñ:"N",Ò:"O",Ó:"O",Ô:"O",Õ:"O",Ö:"Oe",Ő:"O",Ø:"O",Ù:"U",Ú:"U",Û:"U",Ü:"Ue",Ű:"U",Ý:"Y",Þ:"TH",ß:"ss",à:"a",á:"a",â:"a",ã:"a",ä:"ae",å:"a",æ:"ae",ç:"c",è:"e",é:"e",ê:"e",ë:"e",ì:"i",í:"i",î:"i",ï:"i",ð:"d",ñ:"n",ò:"o",ó:"o",ô:"o",õ:"o",ö:"oe",ő:"o",ø:"o",ù:"u",ú:"u",û:"u",ü:"ue",ű:"u",ý:"y",þ:"th",ÿ:"y","ẞ":"SS",ا:"a",أ:"a",إ:"i",آ:"aa",ؤ:"u",ئ:"e",ء:"a",ب:"b",ت:"t",ث:"th",ج:"j",ح:"h",خ:"kh",د:"d",ذ:"th",ر:"r",ز:"z",س:"s",ش:"sh",ص:"s",ض:"dh",ط:"t",ظ:"z",ع:"a",غ:"gh",ف:"f",ق:"q",ك:"k",ل:"l",م:"m",ن:"n",ه:"h",و:"w",ي:"y",ى:"a",ة:"h",ﻻ:"la",ﻷ:"laa",ﻹ:"lai",ﻵ:"laa",گ:"g",چ:"ch",پ:"p",ژ:"zh",ک:"k",ی:"y","َ":"a","ً":"an","ِ":"e","ٍ":"en","ُ":"u","ٌ":"on","ْ":"","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9",က:"k",ခ:"kh",ဂ:"g",ဃ:"ga",င:"ng",စ:"s",ဆ:"sa",ဇ:"z","စျ":"za",ည:"ny",ဋ:"t",ဌ:"ta",ဍ:"d",ဎ:"da",ဏ:"na",တ:"t",ထ:"ta",ဒ:"d",ဓ:"da",န:"n",ပ:"p",ဖ:"pa",ဗ:"b",ဘ:"ba",မ:"m",ယ:"y",ရ:"ya",လ:"l",ဝ:"w",သ:"th",ဟ:"h",ဠ:"la",အ:"a","ြ":"y","ျ":"ya","ွ":"w","ြွ":"yw","ျွ":"ywa","ှ":"h",ဧ:"e","၏":"-e",ဣ:"i",ဤ:"-i",ဉ:"u",ဦ:"-u",ဩ:"aw","သြော":"aw",ဪ:"aw","၀":"0","၁":"1","၂":"2","၃":"3","၄":"4","၅":"5","၆":"6","၇":"7","၈":"8","၉":"9","္":"","့":"","း":"",č:"c",ď:"d",ě:"e",ň:"n",ř:"r",š:"s",ť:"t",ů:"u",ž:"z",Č:"C",Ď:"D",Ě:"E",Ň:"N",Ř:"R",Š:"S",Ť:"T",Ů:"U",Ž:"Z",ހ:"h",ށ:"sh",ނ:"n",ރ:"r",ބ:"b",ޅ:"lh",ކ:"k",އ:"a",ވ:"v",މ:"m",ފ:"f",ދ:"dh",ތ:"th",ލ:"l",ގ:"g",ޏ:"gn",ސ:"s",ޑ:"d",ޒ:"z",ޓ:"t",ޔ:"y",ޕ:"p",ޖ:"j",ޗ:"ch",ޘ:"tt",ޙ:"hh",ޚ:"kh",ޛ:"th",ޜ:"z",ޝ:"sh",ޞ:"s",ޟ:"d",ޠ:"t",ޡ:"z",ޢ:"a",ޣ:"gh",ޤ:"q",ޥ:"w","ަ":"a","ާ":"aa","ި":"i","ީ":"ee","ު":"u","ޫ":"oo","ެ":"e","ޭ":"ey","ޮ":"o","ޯ":"oa","ް":"",ა:"a",ბ:"b",გ:"g",დ:"d",ე:"e",ვ:"v",ზ:"z",თ:"t",ი:"i",კ:"k",ლ:"l",მ:"m",ნ:"n",ო:"o",პ:"p",ჟ:"zh",რ:"r",ს:"s",ტ:"t",უ:"u",ფ:"p",ქ:"k",ღ:"gh",ყ:"q",შ:"sh",ჩ:"ch",ც:"ts",ძ:"dz",წ:"ts",ჭ:"ch",ხ:"kh",ჯ:"j",ჰ:"h",α:"a",β:"v",γ:"g",δ:"d",ε:"e",ζ:"z",η:"i",θ:"th",ι:"i",κ:"k",λ:"l",μ:"m",ν:"n",ξ:"ks",ο:"o",π:"p",ρ:"r",σ:"s",τ:"t",υ:"y",φ:"f",χ:"x",ψ:"ps",ω:"o",ά:"a",έ:"e",ί:"i",ό:"o",ύ:"y",ή:"i",ώ:"o",ς:"s",ϊ:"i",ΰ:"y",ϋ:"y",ΐ:"i",Α:"A",Β:"B",Γ:"G",Δ:"D",Ε:"E",Ζ:"Z",Η:"I",Θ:"TH",Ι:"I",Κ:"K",Λ:"L",Μ:"M",Ν:"N",Ξ:"KS",Ο:"O",Π:"P",Ρ:"R",Σ:"S",Τ:"T",Υ:"Y",Φ:"F",Χ:"X",Ψ:"PS",Ω:"O",Ά:"A",Έ:"E",Ί:"I",Ό:"O",Ύ:"Y",Ή:"I",Ώ:"O",Ϊ:"I",Ϋ:"Y",ā:"a",ē:"e",ģ:"g",ī:"i",ķ:"k",ļ:"l",ņ:"n",ū:"u",Ā:"A",Ē:"E",Ģ:"G",Ī:"I",Ķ:"k",Ļ:"L",Ņ:"N",Ū:"U",Ќ:"Kj",ќ:"kj",Љ:"Lj",љ:"lj",Њ:"Nj",њ:"nj",Тс:"Ts",тс:"ts",ą:"a",ć:"c",ę:"e",ł:"l",ń:"n",ś:"s",ź:"z",ż:"z",Ą:"A",Ć:"C",Ę:"E",Ł:"L",Ń:"N",Ś:"S",Ź:"Z",Ż:"Z",Є:"Ye",І:"I",Ї:"Yi",Ґ:"G",є:"ye",і:"i",ї:"yi",ґ:"g",ă:"a",Ă:"A",ș:"s",Ș:"S",ț:"t",Ț:"T",ţ:"t",Ţ:"T",а:"a",б:"b",в:"v",г:"g",д:"d",е:"e",ё:"yo",ж:"zh",з:"z",и:"i",й:"i",к:"k",л:"l",м:"m",н:"n",о:"o",п:"p",р:"r",с:"s",т:"t",у:"u",ф:"f",х:"kh",ц:"c",ч:"ch",ш:"sh",щ:"sh",ъ:"",ы:"y",ь:"",э:"e",ю:"yu",я:"ya",А:"A",Б:"B",В:"V",Г:"G",Д:"D",Е:"E",Ё:"Yo",Ж:"Zh",З:"Z",И:"I",Й:"I",К:"K",Л:"L",М:"M",Н:"N",О:"O",П:"P",Р:"R",С:"S",Т:"T",У:"U",Ф:"F",Х:"Kh",Ц:"C",Ч:"Ch",Ш:"Sh",Щ:"Sh",Ъ:"",Ы:"Y",Ь:"",Э:"E",Ю:"Yu",Я:"Ya",ђ:"dj",ј:"j",ћ:"c",џ:"dz",Ђ:"Dj",Ј:"j",Ћ:"C",Џ:"Dz",ľ:"l",ĺ:"l",ŕ:"r",Ľ:"L",Ĺ:"L",Ŕ:"R",ş:"s",Ş:"S",ı:"i",İ:"I",ğ:"g",Ğ:"G",ả:"a",Ả:"A",ẳ:"a",Ẳ:"A",ẩ:"a",Ẩ:"A",đ:"d",Đ:"D",ẹ:"e",Ẹ:"E",ẽ:"e",Ẽ:"E",ẻ:"e",Ẻ:"E",ế:"e",Ế:"E",ề:"e",Ề:"E",ệ:"e",Ệ:"E",ễ:"e",Ễ:"E",ể:"e",Ể:"E",ỏ:"o",ọ:"o",Ọ:"o",ố:"o",Ố:"O",ồ:"o",Ồ:"O",ổ:"o",Ổ:"O",ộ:"o",Ộ:"O",ỗ:"o",Ỗ:"O",ơ:"o",Ơ:"O",ớ:"o",Ớ:"O",ờ:"o",Ờ:"O",ợ:"o",Ợ:"O",ỡ:"o",Ỡ:"O",Ở:"o",ở:"o",ị:"i",Ị:"I",ĩ:"i",Ĩ:"I",ỉ:"i",Ỉ:"i",ủ:"u",Ủ:"U",ụ:"u",Ụ:"U",ũ:"u",Ũ:"U",ư:"u",Ư:"U",ứ:"u",Ứ:"U",ừ:"u",Ừ:"U",ự:"u",Ự:"U",ữ:"u",Ữ:"U",ử:"u",Ử:"ư",ỷ:"y",Ỷ:"y",ỳ:"y",Ỳ:"Y",ỵ:"y",Ỵ:"Y",ỹ:"y",Ỹ:"Y",ạ:"a",Ạ:"A",ấ:"a",Ấ:"A",ầ:"a",Ầ:"A",ậ:"a",Ậ:"A",ẫ:"a",Ẫ:"A",ắ:"a",Ắ:"A",ằ:"a",Ằ:"A",ặ:"a",Ặ:"A",ẵ:"a",Ẵ:"A","⓪":"0","①":"1","②":"2","③":"3","④":"4","⑤":"5","⑥":"6","⑦":"7","⑧":"8","⑨":"9","⑩":"10","⑪":"11","⑫":"12","⑬":"13","⑭":"14","⑮":"15","⑯":"16","⑰":"17","⑱":"18","⑲":"18","⑳":"18","⓵":"1","⓶":"2","⓷":"3","⓸":"4","⓹":"5","⓺":"6","⓻":"7","⓼":"8","⓽":"9","⓾":"10","⓿":"0","⓫":"11","⓬":"12","⓭":"13","⓮":"14","⓯":"15","⓰":"16","⓱":"17","⓲":"18","⓳":"19","⓴":"20","Ⓐ":"A","Ⓑ":"B","Ⓒ":"C","Ⓓ":"D","Ⓔ":"E","Ⓕ":"F","Ⓖ":"G","Ⓗ":"H","Ⓘ":"I","Ⓙ":"J","Ⓚ":"K","Ⓛ":"L","Ⓜ":"M","Ⓝ":"N","Ⓞ":"O","Ⓟ":"P","Ⓠ":"Q","Ⓡ":"R","Ⓢ":"S","Ⓣ":"T","Ⓤ":"U","Ⓥ":"V","Ⓦ":"W","Ⓧ":"X","Ⓨ":"Y","Ⓩ":"Z","ⓐ":"a","ⓑ":"b","ⓒ":"c","ⓓ":"d","ⓔ":"e","ⓕ":"f","ⓖ":"g","ⓗ":"h","ⓘ":"i","ⓙ":"j","ⓚ":"k","ⓛ":"l","ⓜ":"m","ⓝ":"n","ⓞ":"o","ⓟ":"p","ⓠ":"q","ⓡ":"r","ⓢ":"s","ⓣ":"t","ⓤ":"u","ⓦ":"v","ⓥ":"w","ⓧ":"x","ⓨ":"y","ⓩ":"z","“":'"',"”":'"',"‘":"'","’":"'","∂":"d",ƒ:"f","™":"(TM)","©":"(C)",œ:"oe",Œ:"OE","®":"(R)","†":"+","℠":"(SM)","…":"...","˚":"o",º:"o",ª:"a","•":"*","၊":",","။":".",$:"USD","€":"EUR","₢":"BRN","₣":"FRF","£":"GBP","₤":"ITL","₦":"NGN","₧":"ESP","₩":"KRW","₪":"ILS","₫":"VND","₭":"LAK","₮":"MNT","₯":"GRD","₱":"ARS","₲":"PYG","₳":"ARA","₴":"UAH","₵":"GHS","¢":"cent","¥":"CNY",元:"CNY",円:"YEN","﷼":"IRR","₠":"EWE","฿":"THB","₨":"INR","₹":"INR","₰":"PF","₺":"TRY","؋":"AFN","₼":"AZN",лв:"BGN","៛":"KHR","₡":"CRC","₸":"KZT",ден:"MKD",zł:"PLN","₽":"RUB","₾":"GEL"},a=["်","ް"],n={"ာ":"a","ါ":"a","ေ":"e","ဲ":"e","ိ":"i","ီ":"i","ို":"o","ု":"u","ူ":"u","ေါင်":"aung","ော":"aw","ော်":"aw","ေါ":"aw","ေါ်":"aw","်":"်","က်":"et","ိုက်":"aik","ောက်":"auk","င်":"in","ိုင်":"aing","ောင်":"aung","စ်":"it","ည်":"i","တ်":"at","ိတ်":"eik","ုတ်":"ok","ွတ်":"ut","ေတ်":"it","ဒ်":"d","ိုဒ်":"ok","ုဒ်":"ait","န်":"an","ာန်":"an","ိန်":"ein","ုန်":"on","ွန်":"un","ပ်":"at","ိပ်":"eik","ုပ်":"ok","ွပ်":"ut","န်ုပ်":"nub","မ်":"an","ိမ်":"ein","ုမ်":"on","ွမ်":"un","ယ်":"e","ိုလ်":"ol","ဉ်":"in","ံ":"an","ိံ":"ein","ုံ":"on","ައް":"ah","ަށް":"ah"},l={en:{},az:{ç:"c",ə:"e",ğ:"g",ı:"i",ö:"o",ş:"s",ü:"u",Ç:"C",Ə:"E",Ğ:"G",İ:"I",Ö:"O",Ş:"S",Ü:"U"},cs:{č:"c",ď:"d",ě:"e",ň:"n",ř:"r",š:"s",ť:"t",ů:"u",ž:"z",Č:"C",Ď:"D",Ě:"E",Ň:"N",Ř:"R",Š:"S",Ť:"T",Ů:"U",Ž:"Z"},fi:{ä:"a",Ä:"A",ö:"o",Ö:"O"},hu:{ä:"a",Ä:"A",ö:"o",Ö:"O",ü:"u",Ü:"U",ű:"u",Ű:"U"},lt:{ą:"a",č:"c",ę:"e",ė:"e",į:"i",š:"s",ų:"u",ū:"u",ž:"z",Ą:"A",Č:"C",Ę:"E",Ė:"E",Į:"I",Š:"S",Ų:"U",Ū:"U"},lv:{ā:"a",č:"c",ē:"e",ģ:"g",ī:"i",ķ:"k",ļ:"l",ņ:"n",š:"s",ū:"u",ž:"z",Ā:"A",Č:"C",Ē:"E",Ģ:"G",Ī:"i",Ķ:"k",Ļ:"L",Ņ:"N",Š:"S",Ū:"u",Ž:"Z"},pl:{ą:"a",ć:"c",ę:"e",ł:"l",ń:"n",ó:"o",ś:"s",ź:"z",ż:"z",Ą:"A",Ć:"C",Ę:"e",Ł:"L",Ń:"N",Ó:"O",Ś:"S",Ź:"Z",Ż:"Z"},sv:{ä:"a",Ä:"A",ö:"o",Ö:"O"},sk:{ä:"a",Ä:"A"},sr:{љ:"lj",њ:"nj",Љ:"Lj",Њ:"Nj",đ:"dj",Đ:"Dj"},tr:{Ü:"U",Ö:"O",ü:"u",ö:"o"}},r={ar:{"∆":"delta","∞":"la-nihaya","♥":"hob","&":"wa","|":"aw","<":"aqal-men",">":"akbar-men","∑":"majmou","¤":"omla"},az:{},ca:{"∆":"delta","∞":"infinit","♥":"amor","&":"i","|":"o","<":"menys que",">":"mes que","∑":"suma dels","¤":"moneda"},cs:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"nebo","<":"mensi nez",">":"vetsi nez","∑":"soucet","¤":"mena"},de:{"∆":"delta","∞":"unendlich","♥":"Liebe","&":"und","|":"oder","<":"kleiner als",">":"groesser als","∑":"Summe von","¤":"Waehrung"},dv:{"∆":"delta","∞":"kolunulaa","♥":"loabi","&":"aai","|":"noonee","<":"ah vure kuda",">":"ah vure bodu","∑":"jumula","¤":"faisaa"},en:{"∆":"delta","∞":"infinity","♥":"love","&":"and","|":"or","<":"less than",">":"greater than","∑":"sum","¤":"currency"},es:{"∆":"delta","∞":"infinito","♥":"amor","&":"y","|":"u","<":"menos que",">":"mas que","∑":"suma de los","¤":"moneda"},fa:{"∆":"delta","∞":"bi-nahayat","♥":"eshgh","&":"va","|":"ya","<":"kamtar-az",">":"bishtar-az","∑":"majmooe","¤":"vahed"},fi:{"∆":"delta","∞":"aarettomyys","♥":"rakkaus","&":"ja","|":"tai","<":"pienempi kuin",">":"suurempi kuin","∑":"summa","¤":"valuutta"},fr:{"∆":"delta","∞":"infiniment","♥":"Amour","&":"et","|":"ou","<":"moins que",">":"superieure a","∑":"somme des","¤":"monnaie"},ge:{"∆":"delta","∞":"usasruloba","♥":"siqvaruli","&":"da","|":"an","<":"naklebi",">":"meti","∑":"jami","¤":"valuta"},gr:{},hu:{"∆":"delta","∞":"vegtelen","♥":"szerelem","&":"es","|":"vagy","<":"kisebb mint",">":"nagyobb mint","∑":"szumma","¤":"penznem"},it:{"∆":"delta","∞":"infinito","♥":"amore","&":"e","|":"o","<":"minore di",">":"maggiore di","∑":"somma","¤":"moneta"},lt:{"∆":"delta","∞":"begalybe","♥":"meile","&":"ir","|":"ar","<":"maziau nei",">":"daugiau nei","∑":"suma","¤":"valiuta"},lv:{"∆":"delta","∞":"bezgaliba","♥":"milestiba","&":"un","|":"vai","<":"mazak neka",">":"lielaks neka","∑":"summa","¤":"valuta"},my:{"∆":"kwahkhyaet","∞":"asaonasme","♥":"akhyait","&":"nhin","|":"tho","<":"ngethaw",">":"kyithaw","∑":"paungld","¤":"ngwekye"},mk:{},nl:{"∆":"delta","∞":"oneindig","♥":"liefde","&":"en","|":"of","<":"kleiner dan",">":"groter dan","∑":"som","¤":"valuta"},pl:{"∆":"delta","∞":"nieskonczonosc","♥":"milosc","&":"i","|":"lub","<":"mniejsze niz",">":"wieksze niz","∑":"suma","¤":"waluta"},pt:{"∆":"delta","∞":"infinito","♥":"amor","&":"e","|":"ou","<":"menor que",">":"maior que","∑":"soma","¤":"moeda"},ro:{"∆":"delta","∞":"infinit","♥":"dragoste","&":"si","|":"sau","<":"mai mic ca",">":"mai mare ca","∑":"suma","¤":"valuta"},ru:{"∆":"delta","∞":"beskonechno","♥":"lubov","&":"i","|":"ili","<":"menshe",">":"bolshe","∑":"summa","¤":"valjuta"},sk:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"alebo","<":"menej ako",">":"viac ako","∑":"sucet","¤":"mena"},sr:{},tr:{"∆":"delta","∞":"sonsuzluk","♥":"ask","&":"ve","|":"veya","<":"kucuktur",">":"buyuktur","∑":"toplam","¤":"para birimi"},uk:{"∆":"delta","∞":"bezkinechnist","♥":"lubov","&":"i","|":"abo","<":"menshe",">":"bilshe","∑":"suma","¤":"valjuta"},vn:{"∆":"delta","∞":"vo cuc","♥":"yeu","&":"va","|":"hoac","<":"nho hon",">":"lon hon","∑":"tong","¤":"tien te"}},h=[";","?",":","@","&","=","+","$",",","/"].join(""),d=[";","?",":","@","&","=","+","$",","].join(""),o=[".","!","~","*","'","(",")"].join(""),k=function(B,f){var b="-",A="",_="",L=!0,M={},x,K,j,T,H,P,ii,ri,ki,W,N,X,li,Pi,Ri="";if(typeof B!="string")return"";if(typeof f=="string"&&(b=f),ii=r.en,ri=l.en,typeof f=="object"){x=f.maintainCase||!1,M=f.custom&&typeof f.custom=="object"?f.custom:M,j=+f.truncate>1&&f.truncate||!1,T=f.uric||!1,H=f.uricNoSlash||!1,P=f.mark||!1,L=!(f.symbols===!1||f.lang===!1),b=f.separator||b,T&&(Ri+=h),H&&(Ri+=d),P&&(Ri+=o),ii=f.lang&&r[f.lang]&&L?r[f.lang]:L?r.en:{},ri=f.lang&&l[f.lang]?l[f.lang]:f.lang===!1||f.lang===!0?{}:l.en,f.titleCase&&typeof f.titleCase.length=="number"&&Array.prototype.toString.call(f.titleCase)?(f.titleCase.forEach(function(vi){M[vi+""]=vi+""}),K=!0):K=!!f.titleCase,f.custom&&typeof f.custom.length=="number"&&Array.prototype.toString.call(f.custom)&&f.custom.forEach(function(vi){M[vi+""]=vi+""}),Object.keys(M).forEach(function(vi){var $i;vi.length>1?$i=new RegExp("\\b"+u(vi)+"\\b","gi"):$i=new RegExp(u(vi),"gi"),B=B.replace($i,M[vi])});for(N in M)Ri+=N}for(Ri+=b,Ri=u(Ri),B=B.replace(/(^\s+|\s+$)/g,""),li=!1,Pi=!1,W=0,X=B.length;W=0?(_+=N,N=""):Pi===!0?(N=n[_]+t[N],_=""):N=li&&t[N].match(/[A-Za-z0-9]/)?" "+t[N]:t[N],li=!1,Pi=!1):N in n?(_+=N,N="",W===X-1&&(N=n[_]),Pi=!0):ii[N]&&!(T&&h.indexOf(N)!==-1)&&!(H&&d.indexOf(N)!==-1)?(N=li||A.substr(-1).match(/[A-Za-z0-9]/)?b+ii[N]:ii[N],N+=B[W+1]!==void 0&&B[W+1].match(/[A-Za-z0-9]/)?b:"",li=!0):(Pi===!0?(N=n[_]+N,_="",Pi=!1):li&&(/[A-Za-z0-9]/.test(N)||A.substr(-1).match(/A-Za-z0-9]/))&&(N=" "+N),li=!1),A+=N.replace(new RegExp("[^\\w\\s"+Ri+"_-]","g"),b);return K&&(A=A.replace(/(\w)(\S*)/g,function(vi,$i,ys){var ls=$i.toUpperCase()+(ys!==null?ys:"");return Object.keys(M).indexOf(ls.toLowerCase())<0?ls:ls.toLowerCase()})),A=A.replace(/\s+/g,b).replace(new RegExp("\\"+b+"+","g"),b).replace(new RegExp("(^\\"+b+"+|\\"+b+"+$)","g"),""),j&&A.length>j&&(ki=A.charAt(j)===b,A=A.slice(0,j),ki||(A=A.slice(0,A.lastIndexOf(b)))),!x&&!K&&(A=A.toLowerCase()),A},c=function(B){return function(b){return k(b,B)}},u=function(B){return B.replace(/[-\\^$*+?.()|[\]{}\/]/g,"\\$&")},v=function(m,B){for(var f in B)if(B[f]===m)return!0};if(typeof s<"u"&&s.exports)s.exports=k,s.exports.createSlug=c;else if(typeof define<"u"&&define.amd)define([],function(){return k});else try{if(e.getSlug||e.createSlug)throw"speakingurl: globals exists /(getSlug|createSlug)/";e.getSlug=k,e.createSlug=c}catch{}}(i)}}),Hv=tc({"../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/index.js"(i,s){S(),s.exports=qv()}});S();S();S();S();S();S();S();function Uv(i){return!!(i&&i.__v_isReadonly)}function ac(i){return Uv(i)?ac(i.__v_raw):!!(i&&i.__v_isReactive)}function sl(i){return!!(i&&i.__v_isRef===!0)}function It(i){const s=i&&i.__v_raw;return s?It(s):i}S();function zv(i){var s;const e=i.name||i._componentTag||i.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__||i.__name;return e==="index"&&((s=i.__file)!=null&&s.endsWith("index.vue"))?"":e}function Wv(i){const s=i.__file;if(s)return Cv(wv(s,".vue"))}function Ep(i,s){return i.type.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__=s,s}function fn(i){if(i.__VUE_DEVTOOLS_NEXT_APP_RECORD__)return i.__VUE_DEVTOOLS_NEXT_APP_RECORD__;if(i.root)return i.appContext.app.__VUE_DEVTOOLS_NEXT_APP_RECORD__}async function Gv(i){const{app:s,uid:e,instance:t}=i;try{if(t.__VUE_DEVTOOLS_NEXT_UID__)return t.__VUE_DEVTOOLS_NEXT_UID__;const a=await fn(s);if(!a)return null;const n=a.rootInstance===t;return`${a.id}:${n?"root":e}`}catch{}}function nc(i){var s,e;const t=(s=i.subTree)==null?void 0:s.type,a=fn(i);return a?((e=a==null?void 0:a.types)==null?void 0:e.Fragment)===t:!1}function En(i){var s,e,t;const a=zv((i==null?void 0:i.type)||{});if(a)return a;if((i==null?void 0:i.root)===i)return"Root";for(const l in(e=(s=i.parent)==null?void 0:s.type)==null?void 0:e.components)if(i.parent.type.components[l]===(i==null?void 0:i.type))return Ep(i,l);for(const l in(t=i.appContext)==null?void 0:t.components)if(i.appContext.components[l]===(i==null?void 0:i.type))return Ep(i,l);const n=Wv((i==null?void 0:i.type)||{});return n||"Anonymous Component"}function xl(i,s){return s=s||`${i.id}:root`,i.instanceMap.get(s)||i.instanceMap.get(":root")}var Kv=class{constructor(){this.refEditor=new Jv}set(i,s,e,t){const a=Array.isArray(s)?s:s.split(".");for(;a.length>1;){const r=a.shift();i instanceof Map&&(i=i.get(r)),i instanceof Set?i=Array.from(i.values())[r]:i=i[r],this.refEditor.isRef(i)&&(i=this.refEditor.get(i))}const n=a[0],l=this.refEditor.get(i)[n];t?t(i,n,e):this.refEditor.isRef(l)?this.refEditor.set(l,e):i[n]=e}get(i,s){const e=Array.isArray(s)?s:s.split(".");for(let t=0;t"u")return!1;const t=Array.isArray(s)?s.slice():s.split("."),a=e?2:1;for(;i&&t.length>a;){const n=t.shift();i=i[n],this.refEditor.isRef(i)&&(i=this.refEditor.get(i))}return i!=null&&Object.prototype.hasOwnProperty.call(i,t[0])}createDefaultSetCallback(i){return(s,e,t)=>{if((i.remove||i.newKey)&&(Array.isArray(s)?s.splice(e,1):It(s)instanceof Map?s.delete(e):It(s)instanceof Set?s.delete(Array.from(s.values())[e]):Reflect.deleteProperty(s,e)),!i.remove){const a=s[i.newKey||e];this.refEditor.isRef(a)?this.refEditor.set(a,t):It(s)instanceof Map?s.set(i.newKey||e,t):It(s)instanceof Set?s.add(t):s[i.newKey||e]=t}}}},Jv=class{set(i,s){if(sl(i))i.value=s;else{if(i instanceof Set&&Array.isArray(s)){i.clear(),s.forEach(a=>i.add(a));return}const e=Object.keys(s);if(i instanceof Map){const a=new Set(i.keys());e.forEach(n=>{i.set(n,Reflect.get(s,n)),a.delete(n)}),a.forEach(n=>i.delete(n));return}const t=new Set(Object.keys(i));e.forEach(a=>{Reflect.set(i,a,Reflect.get(s,a)),t.delete(a)}),t.forEach(a=>Reflect.deleteProperty(i,a))}}get(i){return sl(i)?i.value:i}isRef(i){return sl(i)||ac(i)}};S();function Tr(i){return nc(i)?Yv(i.subTree):i.subTree?[i.subTree.el]:[]}function Yv(i){if(!i.children)return[];const s=[];return i.children.forEach(e=>{e.component?s.push(...Tr(e.component)):e!=null&&e.el&&s.push(e.el)}),s}S();S();function Xv(){const i={top:0,bottom:0,left:0,right:0,get width(){return i.right-i.left},get height(){return i.bottom-i.top}};return i}var Na;function Qv(i){return Na||(Na=document.createRange()),Na.selectNode(i),Na.getBoundingClientRect()}function Zv(i){const s=Xv();if(!i.children)return s;for(let e=0,t=i.children.length;ei.bottom)&&(i.bottom=s.bottom),(!i.left||s.lefti.right)&&(i.right=s.right),i}var bp={top:0,left:0,right:0,bottom:0,width:0,height:0};function je(i){const s=i.subTree.el;return typeof window>"u"?bp:nc(i)?Zv(i.subTree):(s==null?void 0:s.nodeType)===1?s==null?void 0:s.getBoundingClientRect():i.subTree.component?je(i.subTree.component):bp}var lc="__vue-devtools-component-inspector__",rc="__vue-devtools-component-inspector__card__",hc="__vue-devtools-component-inspector__name__",pc="__vue-devtools-component-inspector__indicator__",dc={display:"block",zIndex:2147483640,position:"fixed",backgroundColor:"#42b88325",border:"1px solid #42b88350",borderRadius:"5px",transition:"all 0.1s ease-in",pointerEvents:"none"},s8={fontFamily:"Arial, Helvetica, sans-serif",padding:"5px 8px",borderRadius:"4px",textAlign:"left",position:"absolute",left:0,color:"#e9e9e9",fontSize:"14px",fontWeight:600,lineHeight:"24px",backgroundColor:"#42b883",boxShadow:"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)"},e8={display:"inline-block",fontWeight:400,fontStyle:"normal",fontSize:"12px",opacity:.7};function bt(){return document.getElementById(lc)}function t8(){return document.getElementById(rc)}function a8(){return document.getElementById(pc)}function n8(){return document.getElementById(hc)}function Or(i){return{left:`${Math.round(i.left*100)/100}px`,top:`${Math.round(i.top*100)/100}px`,width:`${Math.round(i.width*100)/100}px`,height:`${Math.round(i.height*100)/100}px`}}function Lr(i){var s;const e=document.createElement("div");e.id=(s=i.elementId)!=null?s:lc,Object.assign(e.style,{...dc,...Or(i.bounds),...i.style});const t=document.createElement("span");t.id=rc,Object.assign(t.style,{...s8,top:i.bounds.top<35?0:"-35px"});const a=document.createElement("span");a.id=hc,a.innerHTML=`<${i.name}>  `;const n=document.createElement("i");return n.id=pc,n.innerHTML=`${Math.round(i.bounds.width*100)/100} x ${Math.round(i.bounds.height*100)/100}`,Object.assign(n.style,e8),t.appendChild(a),t.appendChild(n),e.appendChild(t),document.body.appendChild(e),e}function Ir(i){const s=bt(),e=t8(),t=n8(),a=a8();s&&(Object.assign(s.style,{...dc,...Or(i.bounds)}),Object.assign(e.style,{top:i.bounds.top<35?0:"-35px"}),t.innerHTML=`<${i.name}>  `,a.innerHTML=`${Math.round(i.bounds.width*100)/100} x ${Math.round(i.bounds.height*100)/100}`)}function l8(i){const s=je(i);if(!s.width&&!s.height)return;const e=En(i);bt()?Ir({bounds:s,name:e}):Lr({bounds:s,name:e})}function oc(){const i=bt();i&&(i.style.display="none")}var Sl=null;function Tl(i){const s=i.target;if(s){const e=s.__vueParentComponent;if(e&&(Sl=e,e.vnode.el)){const a=je(e),n=En(e);bt()?Ir({bounds:a,name:n}):Lr({bounds:a,name:n})}}}function r8(i,s){var e;if(i.preventDefault(),i.stopPropagation(),Sl){const t=(e=Xi.value)==null?void 0:e.app;Gv({app:t,uid:t.uid,instance:Sl}).then(a=>{s(a)})}}var an=null;function h8(){oc(),window.removeEventListener("mouseover",Tl),window.removeEventListener("click",an,!0),an=null}function p8(){return window.addEventListener("mouseover",Tl),new Promise(i=>{function s(e){e.preventDefault(),e.stopPropagation(),r8(e,t=>{window.removeEventListener("click",s,!0),an=null,window.removeEventListener("mouseover",Tl);const a=bt();a&&(a.style.display="none"),i(JSON.stringify({id:t}))})}an=s,window.addEventListener("click",s,!0)})}function d8(i){const s=xl(Xi.value,i.id);if(s){const[e]=Tr(s);if(typeof e.scrollIntoView=="function")e.scrollIntoView({behavior:"smooth"});else{const t=je(s),a=document.createElement("div"),n={...Or(t),position:"absolute"};Object.assign(a.style,n),document.body.appendChild(a),a.scrollIntoView({behavior:"smooth"}),setTimeout(()=>{document.body.removeChild(a)},2e3)}setTimeout(()=>{const t=je(s);if(t.width||t.height){const a=En(s),n=bt();n?Ir({...i,name:a,bounds:t}):Lr({...i,name:a,bounds:t}),setTimeout(()=>{n&&(n.style.display="none")},1500)}},1200)}}S();var Fp,_p;(_p=(Fp=Y).__VUE_DEVTOOLS_COMPONENT_INSPECTOR_ENABLED__)!=null||(Fp.__VUE_DEVTOOLS_COMPONENT_INSPECTOR_ENABLED__=!0);function o8(i){let s=0;const e=setInterval(()=>{Y.__VUE_INSPECTOR__&&(clearInterval(e),s+=30,i()),s>=5e3&&clearInterval(e)},30)}function k8(){const i=Y.__VUE_INSPECTOR__,s=i.openInEditor;i.openInEditor=async(...e)=>{i.disable(),s(...e)}}function c8(){return new Promise(i=>{function s(){k8(),i(Y.__VUE_INSPECTOR__)}Y.__VUE_INSPECTOR__?s():o8(()=>{s()})})}S();S();S();var u8="__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS_STATE__";function g8(){if(!Zk||typeof localStorage>"u"||localStorage===null)return{recordingState:!1,mouseEventEnabled:!1,keyboardEventEnabled:!1,componentEventEnabled:!1,performanceEventEnabled:!1,selected:""};const i=localStorage.getItem(u8);return i?JSON.parse(i):{recordingState:!1,mouseEventEnabled:!1,keyboardEventEnabled:!1,componentEventEnabled:!1,performanceEventEnabled:!1,selected:""}}S();S();S();var Cp,wp;(wp=(Cp=Y).__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS)!=null||(Cp.__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS=[]);var v8=new Proxy(Y.__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS,{get(i,s,e){return Reflect.get(i,s,e)}});function A8(i,s){qi.timelineLayersState[s.id]=!1,v8.push({...i,descriptorId:s.id,appRecord:fn(s.app)})}var Dp,xp;(xp=(Dp=Y).__VUE_DEVTOOLS_KIT_INSPECTOR__)!=null||(Dp.__VUE_DEVTOOLS_KIT_INSPECTOR__=[]);var Pr=new Proxy(Y.__VUE_DEVTOOLS_KIT_INSPECTOR__,{get(i,s,e){return Reflect.get(i,s,e)}}),kc=dt(()=>{Ft.hooks.callHook("sendInspectorToClient",cc())});function y8(i,s){var e,t;Pr.push({options:i,descriptor:s,treeFilterPlaceholder:(e=i.treeFilterPlaceholder)!=null?e:"Search tree...",stateFilterPlaceholder:(t=i.stateFilterPlaceholder)!=null?t:"Search state...",treeFilter:"",selectedNodeId:"",appRecord:fn(s.app)}),kc()}function cc(){return Pr.filter(i=>i.descriptor.app===Xi.value.app).filter(i=>i.descriptor.id!=="components").map(i=>{var s;const e=i.descriptor,t=i.options;return{id:t.id,label:t.label,logo:e.logo,icon:`custom-ic-baseline-${(s=t==null?void 0:t.icon)==null?void 0:s.replace(/_/g,"-")}`,packageName:e.packageName,homepage:e.homepage,pluginId:e.id}})}function za(i,s){return Pr.find(e=>e.options.id===i&&(s?e.descriptor.app===s:!0))}function m8(){const i=sc();i.hook("addInspector",({inspector:t,plugin:a})=>{y8(t,a.descriptor)});const s=dt(async({inspectorId:t,plugin:a})=>{var n;if(!t||!((n=a==null?void 0:a.descriptor)!=null&&n.app)||qi.highPerfModeEnabled)return;const l=za(t,a.descriptor.app),r={app:a.descriptor.app,inspectorId:t,filter:(l==null?void 0:l.treeFilter)||"",rootNodes:[]};await new Promise(h=>{i.callHookWith(async d=>{await Promise.all(d.map(o=>o(r))),h()},"getInspectorTree")}),i.callHookWith(async h=>{await Promise.all(h.map(d=>d({inspectorId:t,rootNodes:r.rootNodes})))},"sendInspectorTreeToClient")},120);i.hook("sendInspectorTree",s);const e=dt(async({inspectorId:t,plugin:a})=>{var n;if(!t||!((n=a==null?void 0:a.descriptor)!=null&&n.app)||qi.highPerfModeEnabled)return;const l=za(t,a.descriptor.app),r={app:a.descriptor.app,inspectorId:t,nodeId:(l==null?void 0:l.selectedNodeId)||"",state:null},h={currentTab:`custom-inspector:${t}`};r.nodeId&&await new Promise(d=>{i.callHookWith(async o=>{await Promise.all(o.map(k=>k(r,h))),d()},"getInspectorState")}),i.callHookWith(async d=>{await Promise.all(d.map(o=>o({inspectorId:t,nodeId:r.nodeId,state:r.state})))},"sendInspectorStateToClient")},120);return i.hook("sendInspectorState",e),i.hook("customInspectorSelectNode",({inspectorId:t,nodeId:a,plugin:n})=>{const l=za(t,n.descriptor.app);l&&(l.selectedNodeId=a)}),i.hook("timelineLayerAdded",({options:t,plugin:a})=>{A8(t,a.descriptor)}),i.hook("timelineEventAdded",({options:t,plugin:a})=>{var n;const l=["performance","component-event","keyboard","mouse"];qi.highPerfModeEnabled||!((n=qi.timelineLayersState)!=null&&n[a.descriptor.id])&&!l.includes(t.layerId)||i.callHookWith(async r=>{await Promise.all(r.map(h=>h(t)))},"sendTimelineEventToClient")}),i.hook("getComponentInstances",async({app:t})=>{const a=t.__VUE_DEVTOOLS_NEXT_APP_RECORD__;if(!a)return null;const n=a.id.toString();return[...a.instanceMap].filter(([r])=>r.split(":")[0]===n).map(([,r])=>r)}),i.hook("getComponentBounds",async({instance:t})=>je(t)),i.hook("getComponentName",({instance:t})=>En(t)),i.hook("componentHighlight",({uid:t})=>{const a=Xi.value.instanceMap.get(t);a&&l8(a)}),i.hook("componentUnhighlight",()=>{oc()}),i}var Sp,Tp;(Tp=(Sp=Y).__VUE_DEVTOOLS_KIT_APP_RECORDS__)!=null||(Sp.__VUE_DEVTOOLS_KIT_APP_RECORDS__=[]);var Op,Lp;(Lp=(Op=Y).__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__)!=null||(Op.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__={});var Ip,Pp;(Pp=(Ip=Y).__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__)!=null||(Ip.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__="");var Rp,jp;(jp=(Rp=Y).__VUE_DEVTOOLS_KIT_CUSTOM_TABS__)!=null||(Rp.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__=[]);var Mp,Vp;(Vp=(Mp=Y).__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__)!=null||(Mp.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__=[]);var Te="__VUE_DEVTOOLS_KIT_GLOBAL_STATE__";function B8(){return{connected:!1,clientConnected:!1,vitePluginDetected:!0,appRecords:[],activeAppRecordId:"",tabs:[],commands:[],highPerfModeEnabled:!0,devtoolsClientDetected:{},perfUniqueGroupId:0,timelineLayersState:g8()}}var Np,$p;($p=(Np=Y)[Te])!=null||(Np[Te]=B8());var f8=dt(i=>{Ft.hooks.callHook("devtoolsStateUpdated",{state:i})});dt((i,s)=>{Ft.hooks.callHook("devtoolsConnectedUpdated",{state:i,oldState:s})});var bn=new Proxy(Y.__VUE_DEVTOOLS_KIT_APP_RECORDS__,{get(i,s,e){return s==="value"?Y.__VUE_DEVTOOLS_KIT_APP_RECORDS__:Y.__VUE_DEVTOOLS_KIT_APP_RECORDS__[s]}}),Xi=new Proxy(Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__,{get(i,s,e){return s==="value"?Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__:s==="id"?Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__:Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__[s]}});function uc(){f8({...Y[Te],appRecords:bn.value,activeAppRecordId:Xi.id,tabs:Y.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__,commands:Y.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__})}function E8(i){Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__=i,uc()}function b8(i){Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__=i,uc()}var qi=new Proxy(Y[Te],{get(i,s){return s==="appRecords"?bn:s==="activeAppRecordId"?Xi.id:s==="tabs"?Y.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__:s==="commands"?Y.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__:Y[Te][s]},deleteProperty(i,s){return delete i[s],!0},set(i,s,e){return{...Y[Te]},i[s]=e,Y[Te][s]=e,!0}});function F8(i={}){var s,e,t;const{file:a,host:n,baseUrl:l=window.location.origin,line:r=0,column:h=0}=i;if(a){if(n==="chrome-extension"){const d=a.replace(/\\/g,"\\\\"),o=(e=(s=window.VUE_DEVTOOLS_CONFIG)==null?void 0:s.openInEditorHost)!=null?e:"/";fetch(`${o}__open-in-editor?file=${encodeURI(a)}`).then(k=>{if(!k.ok){const c=`Opening component ${d} failed`;console.log(`%c${c}`,"color:red")}})}else if(qi.vitePluginDetected){const d=(t=Y.__VUE_DEVTOOLS_OPEN_IN_EDITOR_BASE_URL__)!=null?t:l;Y.__VUE_INSPECTOR__.openInEditor(d,a,r,h)}}}S();S();S();S();S();var qp,Hp;(Hp=(qp=Y).__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__)!=null||(qp.__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__=[]);var Rr=new Proxy(Y.__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__,{get(i,s,e){return Reflect.get(i,s,e)}});function Ol(i){const s={};return Object.keys(i).forEach(e=>{s[e]=i[e].defaultValue}),s}function jr(i){return`__VUE_DEVTOOLS_NEXT_PLUGIN_SETTINGS__${i}__`}function _8(i){var s,e,t;const a=(e=(s=Rr.find(n=>{var l;return n[0].id===i&&!!((l=n[0])!=null&&l.settings)}))==null?void 0:s[0])!=null?e:null;return(t=a==null?void 0:a.settings)!=null?t:null}function gc(i,s){var e,t,a;const n=jr(i);if(n){const l=localStorage.getItem(n);if(l)return JSON.parse(l)}if(i){const l=(t=(e=Rr.find(r=>r[0].id===i))==null?void 0:e[0])!=null?t:null;return Ol((a=l==null?void 0:l.settings)!=null?a:{})}return Ol(s)}function C8(i,s){const e=jr(i);localStorage.getItem(e)||localStorage.setItem(e,JSON.stringify(Ol(s)))}function w8(i,s,e){const t=jr(i),a=localStorage.getItem(t),n=JSON.parse(a||"{}"),l={...n,[s]:e};localStorage.setItem(t,JSON.stringify(l)),Ft.hooks.callHookWith(r=>{r.forEach(h=>h({pluginId:i,key:s,oldValue:n[s],newValue:e,settings:l}))},"setPluginSettings")}S();S();S();S();S();S();S();S();S();S();S();var Up,zp,us=(zp=(Up=Y).__VUE_DEVTOOLS_HOOK)!=null?zp:Up.__VUE_DEVTOOLS_HOOK=sc(),D8={vueAppInit(i){us.hook("app:init",i)},vueAppUnmount(i){us.hook("app:unmount",i)},vueAppConnected(i){us.hook("app:connected",i)},componentAdded(i){return us.hook("component:added",i)},componentEmit(i){return us.hook("component:emit",i)},componentUpdated(i){return us.hook("component:updated",i)},componentRemoved(i){return us.hook("component:removed",i)},setupDevtoolsPlugin(i){us.hook("devtools-plugin:setup",i)},perfStart(i){return us.hook("perf:start",i)},perfEnd(i){return us.hook("perf:end",i)}},vc={on:D8,setupDevToolsPlugin(i,s){return us.callHook("devtools-plugin:setup",i,s)}},x8=class{constructor({plugin:i,ctx:s}){this.hooks=s.hooks,this.plugin=i}get on(){return{visitComponentTree:i=>{this.hooks.hook("visitComponentTree",i)},inspectComponent:i=>{this.hooks.hook("inspectComponent",i)},editComponentState:i=>{this.hooks.hook("editComponentState",i)},getInspectorTree:i=>{this.hooks.hook("getInspectorTree",i)},getInspectorState:i=>{this.hooks.hook("getInspectorState",i)},editInspectorState:i=>{this.hooks.hook("editInspectorState",i)},inspectTimelineEvent:i=>{this.hooks.hook("inspectTimelineEvent",i)},timelineCleared:i=>{this.hooks.hook("timelineCleared",i)},setPluginSettings:i=>{this.hooks.hook("setPluginSettings",i)}}}notifyComponentUpdate(i){var s;if(qi.highPerfModeEnabled)return;const e=cc().find(t=>t.packageName===this.plugin.descriptor.packageName);if(e!=null&&e.id){if(i){const t=[i.appContext.app,i.uid,(s=i.parent)==null?void 0:s.uid,i];us.callHook("component:updated",...t)}else us.callHook("component:updated");this.hooks.callHook("sendInspectorState",{inspectorId:e.id,plugin:this.plugin})}}addInspector(i){this.hooks.callHook("addInspector",{inspector:i,plugin:this.plugin}),this.plugin.descriptor.settings&&C8(i.id,this.plugin.descriptor.settings)}sendInspectorTree(i){qi.highPerfModeEnabled||this.hooks.callHook("sendInspectorTree",{inspectorId:i,plugin:this.plugin})}sendInspectorState(i){qi.highPerfModeEnabled||this.hooks.callHook("sendInspectorState",{inspectorId:i,plugin:this.plugin})}selectInspectorNode(i,s){this.hooks.callHook("customInspectorSelectNode",{inspectorId:i,nodeId:s,plugin:this.plugin})}visitComponentTree(i){return this.hooks.callHook("visitComponentTree",i)}now(){return qi.highPerfModeEnabled?0:Date.now()}addTimelineLayer(i){this.hooks.callHook("timelineLayerAdded",{options:i,plugin:this.plugin})}addTimelineEvent(i){qi.highPerfModeEnabled||this.hooks.callHook("timelineEventAdded",{options:i,plugin:this.plugin})}getSettings(i){return gc(i??this.plugin.descriptor.id,this.plugin.descriptor.settings)}getComponentInstances(i){return this.hooks.callHook("getComponentInstances",{app:i})}getComponentBounds(i){return this.hooks.callHook("getComponentBounds",{instance:i})}getComponentName(i){return this.hooks.callHook("getComponentName",{instance:i})}highlightElement(i){const s=i.__VUE_DEVTOOLS_NEXT_UID__;return this.hooks.callHook("componentHighlight",{uid:s})}unhighlightElement(){return this.hooks.callHook("componentUnhighlight")}},S8=x8;S();S();S();S();var T8="__vue_devtool_undefined__",O8="__vue_devtool_infinity__",L8="__vue_devtool_negative_infinity__",I8="__vue_devtool_nan__";S();S();var P8={[T8]:"undefined",[I8]:"NaN",[O8]:"Infinity",[L8]:"-Infinity"};Object.entries(P8).reduce((i,[s,e])=>(i[e]=s,i),{});S();S();S();S();S();var Wp,Gp;(Gp=(Wp=Y).__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__)!=null||(Wp.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__=new Set);function R8(i,s){return vc.setupDevToolsPlugin(i,s)}function j8(i,s){const[e,t]=i;if(e.app!==s)return;const a=new S8({plugin:{setupFn:t,descriptor:e},ctx:Ft});e.packageName==="vuex"&&a.on.editInspectorState(n=>{a.sendInspectorState(n.inspectorId)}),t(a)}function Ac(i){Y.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__.has(i)||qi.highPerfModeEnabled||(Y.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__.add(i),Rr.forEach(s=>{j8(s,i)}))}S();S();var ea="__VUE_DEVTOOLS_ROUTER__",ot="__VUE_DEVTOOLS_ROUTER_INFO__",Kp,Jp;(Jp=(Kp=Y)[ot])!=null||(Kp[ot]={currentRoute:null,routes:[]});var Yp,Xp;(Xp=(Yp=Y)[ea])!=null||(Yp[ea]={});new Proxy(Y[ot],{get(i,s){return Y[ot][s]}});new Proxy(Y[ea],{get(i,s){if(s==="value")return Y[ea]}});function M8(i){const s=new Map;return((i==null?void 0:i.getRoutes())||[]).filter(e=>!s.has(e.path)&&s.set(e.path,1))}function Mr(i){return i.map(s=>{let{path:e,name:t,children:a,meta:n}=s;return a!=null&&a.length&&(a=Mr(a)),{path:e,name:t,children:a,meta:n}})}function V8(i){if(i){const{fullPath:s,hash:e,href:t,path:a,name:n,matched:l,params:r,query:h}=i;return{fullPath:s,hash:e,href:t,path:a,name:n,params:r,query:h,matched:Mr(l)}}return i}function N8(i,s){function e(){var t;const a=(t=i.app)==null?void 0:t.config.globalProperties.$router,n=V8(a==null?void 0:a.currentRoute.value),l=Mr(M8(a)),r=console.warn;console.warn=()=>{},Y[ot]={currentRoute:n?fp(n):{},routes:fp(l)},Y[ea]=a,console.warn=r}e(),vc.on.componentUpdated(dt(()=>{var t;((t=s.value)==null?void 0:t.app)===i.app&&(e(),!qi.highPerfModeEnabled&&Ft.hooks.callHook("routerInfoUpdated",{state:Y[ot]}))},200))}function $8(i){return{async getInspectorTree(s){const e={...s,app:Xi.value.app,rootNodes:[]};return await new Promise(t=>{i.callHookWith(async a=>{await Promise.all(a.map(n=>n(e))),t()},"getInspectorTree")}),e.rootNodes},async getInspectorState(s){const e={...s,app:Xi.value.app,state:null},t={currentTab:`custom-inspector:${s.inspectorId}`};return await new Promise(a=>{i.callHookWith(async n=>{await Promise.all(n.map(l=>l(e,t))),a()},"getInspectorState")}),e.state},editInspectorState(s){const e=new Kv,t={...s,app:Xi.value.app,set:(a,n=s.path,l=s.state.value,r)=>{e.set(a,n,l,r||e.createDefaultSetCallback(s.state))}};i.callHookWith(a=>{a.forEach(n=>n(t))},"editInspectorState")},sendInspectorState(s){const e=za(s);i.callHook("sendInspectorState",{inspectorId:s,plugin:{descriptor:e.descriptor,setupFn:()=>({})}})},inspectComponentInspector(){return p8()},cancelInspectComponentInspector(){return h8()},getComponentRenderCode(s){const e=xl(Xi.value,s);if(e)return(e==null?void 0:e.type)instanceof Function?e.type.toString():e.render.toString()},scrollToComponent(s){return d8({id:s})},openInEditor:F8,getVueInspector:c8,toggleApp(s){const e=bn.value.find(t=>t.id===s);e&&(b8(s),E8(e),N8(e,Xi),kc(),Ac(e.app))},inspectDOM(s){const e=xl(Xi.value,s);if(e){const[t]=Tr(e);t&&(Y.__VUE_DEVTOOLS_INSPECT_DOM_TARGET__=t)}},updatePluginSettings(s,e,t){w8(s,e,t)},getPluginSettings(s){return{options:_8(s),values:gc(s)}}}}S();var Qp,Zp;(Zp=(Qp=Y).__VUE_DEVTOOLS_ENV__)!=null||(Qp.__VUE_DEVTOOLS_ENV__={vitePluginDetected:!1});var id=m8(),sd,ed;(ed=(sd=Y).__VUE_DEVTOOLS_KIT_CONTEXT__)!=null||(sd.__VUE_DEVTOOLS_KIT_CONTEXT__={hooks:id,get state(){return{...qi,activeAppRecordId:Xi.id,activeAppRecord:Xi.value,appRecords:bn.value}},api:$8(id)});var Ft=Y.__VUE_DEVTOOLS_KIT_CONTEXT__;S();$v(Hv());var td,ad;(ad=(td=Y).__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__)!=null||(td.__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__={id:0,appIds:new Set});S();function q8(i){qi.highPerfModeEnabled=i??!qi.highPerfModeEnabled,!i&&Xi.value&&Ac(Xi.value.app)}S();S();S();function H8(i){qi.devtoolsClientDetected={...qi.devtoolsClientDetected,...i};const s=Object.values(qi.devtoolsClientDetected).some(Boolean);q8(!s)}var nd,ld;(ld=(nd=Y).__VUE_DEVTOOLS_UPDATE_CLIENT_DETECTED__)!=null||(nd.__VUE_DEVTOOLS_UPDATE_CLIENT_DETECTED__=H8);S();S();S();S();S();S();S();var U8=class{constructor(){this.keyToValue=new Map,this.valueToKey=new Map}set(i,s){this.keyToValue.set(i,s),this.valueToKey.set(s,i)}getByKey(i){return this.keyToValue.get(i)}getByValue(i){return this.valueToKey.get(i)}clear(){this.keyToValue.clear(),this.valueToKey.clear()}},yc=class{constructor(i){this.generateIdentifier=i,this.kv=new U8}register(i,s){this.kv.getByValue(i)||(s||(s=this.generateIdentifier(i)),this.kv.set(s,i))}clear(){this.kv.clear()}getIdentifier(i){return this.kv.getByValue(i)}getValue(i){return this.kv.getByKey(i)}},z8=class extends yc{constructor(){super(i=>i.name),this.classToAllowedProps=new Map}register(i,s){typeof s=="object"?(s.allowProps&&this.classToAllowedProps.set(i,s.allowProps),super.register(i,s.identifier)):super.register(i,s)}getAllowedProps(i){return this.classToAllowedProps.get(i)}};S();S();function W8(i){if("values"in Object)return Object.values(i);const s=[];for(const e in i)i.hasOwnProperty(e)&&s.push(i[e]);return s}function G8(i,s){const e=W8(i);if("find"in e)return e.find(s);const t=e;for(let a=0;as(t,e))}function Wa(i,s){return i.indexOf(s)!==-1}function rd(i,s){for(let e=0;es.isApplicable(i))}findByName(i){return this.transfomers[i]}};S();S();var J8=i=>Object.prototype.toString.call(i).slice(8,-1),mc=i=>typeof i>"u",Y8=i=>i===null,ta=i=>typeof i!="object"||i===null||i===Object.prototype?!1:Object.getPrototypeOf(i)===null?!0:Object.getPrototypeOf(i)===Object.prototype,Ll=i=>ta(i)&&Object.keys(i).length===0,fe=i=>Array.isArray(i),X8=i=>typeof i=="string",Q8=i=>typeof i=="number"&&!isNaN(i),Z8=i=>typeof i=="boolean",iA=i=>i instanceof RegExp,aa=i=>i instanceof Map,na=i=>i instanceof Set,Bc=i=>J8(i)==="Symbol",sA=i=>i instanceof Date&&!isNaN(i.valueOf()),eA=i=>i instanceof Error,hd=i=>typeof i=="number"&&isNaN(i),tA=i=>Z8(i)||Y8(i)||mc(i)||Q8(i)||X8(i)||Bc(i),aA=i=>typeof i=="bigint",nA=i=>i===1/0||i===-1/0,lA=i=>ArrayBuffer.isView(i)&&!(i instanceof DataView),rA=i=>i instanceof URL;S();var fc=i=>i.replace(/\./g,"\\."),el=i=>i.map(String).map(fc).join("."),Wt=i=>{const s=[];let e="";for(let a=0;anull,()=>{}),Vs(aA,"bigint",i=>i.toString(),i=>typeof BigInt<"u"?BigInt(i):(console.error("Please add a BigInt polyfill."),i)),Vs(sA,"Date",i=>i.toISOString(),i=>new Date(i)),Vs(eA,"Error",(i,s)=>{const e={name:i.name,message:i.message};return s.allowedErrorProps.forEach(t=>{e[t]=i[t]}),e},(i,s)=>{const e=new Error(i.message);return e.name=i.name,e.stack=i.stack,s.allowedErrorProps.forEach(t=>{e[t]=i[t]}),e}),Vs(iA,"regexp",i=>""+i,i=>{const s=i.slice(1,i.lastIndexOf("/")),e=i.slice(i.lastIndexOf("/")+1);return new RegExp(s,e)}),Vs(na,"set",i=>[...i.values()],i=>new Set(i)),Vs(aa,"map",i=>[...i.entries()],i=>new Map(i)),Vs(i=>hd(i)||nA(i),"number",i=>hd(i)?"NaN":i>0?"Infinity":"-Infinity",Number),Vs(i=>i===0&&1/i===-1/0,"number",()=>"-0",Number),Vs(rA,"URL",i=>i.toString(),i=>new URL(i))];function Fn(i,s,e,t){return{isApplicable:i,annotation:s,transform:e,untransform:t}}var bc=Fn((i,s)=>Bc(i)?!!s.symbolRegistry.getIdentifier(i):!1,(i,s)=>["symbol",s.symbolRegistry.getIdentifier(i)],i=>i.description,(i,s,e)=>{const t=e.symbolRegistry.getValue(s[1]);if(!t)throw new Error("Trying to deserialize unknown symbol");return t}),hA=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,Uint8ClampedArray].reduce((i,s)=>(i[s.name]=s,i),{}),Fc=Fn(lA,i=>["typed-array",i.constructor.name],i=>[...i],(i,s)=>{const e=hA[s[1]];if(!e)throw new Error("Trying to deserialize unknown typed array");return new e(i)});function _c(i,s){return i!=null&&i.constructor?!!s.classRegistry.getIdentifier(i.constructor):!1}var Cc=Fn(_c,(i,s)=>["class",s.classRegistry.getIdentifier(i.constructor)],(i,s)=>{const e=s.classRegistry.getAllowedProps(i.constructor);if(!e)return{...i};const t={};return e.forEach(a=>{t[a]=i[a]}),t},(i,s,e)=>{const t=e.classRegistry.getValue(s[1]);if(!t)throw new Error("Trying to deserialize unknown class - check https://github.com/blitz-js/superjson/issues/116#issuecomment-773996564");return Object.assign(Object.create(t.prototype),i)}),wc=Fn((i,s)=>!!s.customTransformerRegistry.findApplicable(i),(i,s)=>["custom",s.customTransformerRegistry.findApplicable(i).name],(i,s)=>s.customTransformerRegistry.findApplicable(i).serialize(i),(i,s,e)=>{const t=e.customTransformerRegistry.findByName(s[1]);if(!t)throw new Error("Trying to deserialize unknown custom value");return t.deserialize(i)}),pA=[Cc,bc,wc,Fc],pd=(i,s)=>{const e=rd(pA,a=>a.isApplicable(i,s));if(e)return{value:e.transform(i,s),type:e.annotation(i,s)};const t=rd(Ec,a=>a.isApplicable(i,s));if(t)return{value:t.transform(i,s),type:t.annotation}},Dc={};Ec.forEach(i=>{Dc[i.annotation]=i});var dA=(i,s,e)=>{if(fe(s))switch(s[0]){case"symbol":return bc.untransform(i,s,e);case"class":return Cc.untransform(i,s,e);case"custom":return wc.untransform(i,s,e);case"typed-array":return Fc.untransform(i,s,e);default:throw new Error("Unknown transformation: "+s)}else{const t=Dc[s];if(!t)throw new Error("Unknown transformation: "+s);return t.untransform(i,e)}};S();var Ke=(i,s)=>{const e=i.keys();for(;s>0;)e.next(),s--;return e.next().value};function xc(i){if(Wa(i,"__proto__"))throw new Error("__proto__ is not allowed as a property");if(Wa(i,"prototype"))throw new Error("prototype is not allowed as a property");if(Wa(i,"constructor"))throw new Error("constructor is not allowed as a property")}var oA=(i,s)=>{xc(s);for(let e=0;e{if(xc(s),s.length===0)return e(i);let t=i;for(let n=0;nPl(n,s,[...e,...Wt(l)]));return}const[t,a]=i;a&&kt(a,(n,l)=>{Pl(n,s,[...e,...Wt(l)])}),s(t,e)}function kA(i,s,e){return Pl(s,(t,a)=>{i=Il(i,a,n=>dA(n,t,e))}),i}function cA(i,s){function e(t,a){const n=oA(i,Wt(a));t.map(Wt).forEach(l=>{i=Il(i,l,()=>n)})}if(fe(s)){const[t,a]=s;t.forEach(n=>{i=Il(i,Wt(n),()=>i)}),a&&kt(a,e)}else kt(s,e);return i}var uA=(i,s)=>ta(i)||fe(i)||aa(i)||na(i)||_c(i,s);function gA(i,s,e){const t=e.get(i);t?t.push(s):e.set(i,[s])}function vA(i,s){const e={};let t;return i.forEach(a=>{if(a.length<=1)return;s||(a=a.map(r=>r.map(String)).sort((r,h)=>r.length-h.length));const[n,...l]=a;n.length===0?t=l.map(el):e[el(n)]=l.map(el)}),t?Ll(e)?[t]:[t,e]:Ll(e)?void 0:e}var Sc=(i,s,e,t,a=[],n=[],l=new Map)=>{var r;const h=tA(i);if(!h){gA(i,a,s);const v=l.get(i);if(v)return t?{transformedValue:null}:v}if(!uA(i,e)){const v=pd(i,e),m=v?{transformedValue:v.value,annotations:[v.type]}:{transformedValue:i};return h||l.set(i,m),m}if(Wa(n,i))return{transformedValue:null};const d=pd(i,e),o=(r=d==null?void 0:d.value)!=null?r:i,k=fe(o)?[]:{},c={};kt(o,(v,m)=>{if(m==="__proto__"||m==="constructor"||m==="prototype")throw new Error(`Detected property ${m}. This is a prototype pollution risk, please remove it from your object.`);const B=Sc(v,s,e,t,[...a,m],[...n,i],l);k[m]=B.transformedValue,fe(B.annotations)?c[m]=B.annotations:ta(B.annotations)&&kt(B.annotations,(f,b)=>{c[fc(m)+"."+b]=f})});const u=Ll(c)?{transformedValue:k,annotations:d?[d.type]:void 0}:{transformedValue:k,annotations:d?[d.type,c]:c};return h||l.set(i,u),u};S();S();function Tc(i){return Object.prototype.toString.call(i).slice(8,-1)}function dd(i){return Tc(i)==="Array"}function AA(i){if(Tc(i)!=="Object")return!1;const s=Object.getPrototypeOf(i);return!!s&&s.constructor===Object&&s===Object.prototype}function yA(i,s,e,t,a){const n={}.propertyIsEnumerable.call(t,s)?"enumerable":"nonenumerable";n==="enumerable"&&(i[s]=e),a&&n==="nonenumerable"&&Object.defineProperty(i,s,{value:e,enumerable:!1,writable:!0,configurable:!0})}function Rl(i,s={}){if(dd(i))return i.map(a=>Rl(a,s));if(!AA(i))return i;const e=Object.getOwnPropertyNames(i),t=Object.getOwnPropertySymbols(i);return[...e,...t].reduce((a,n)=>{if(dd(s.props)&&!s.props.includes(n))return a;const l=i[n],r=Rl(l,s);return yA(a,n,r,i,s.nonenumerable),a},{})}var Ci=class{constructor({dedupe:i=!1}={}){this.classRegistry=new z8,this.symbolRegistry=new yc(s=>{var e;return(e=s.description)!=null?e:""}),this.customTransformerRegistry=new K8,this.allowedErrorProps=[],this.dedupe=i}serialize(i){const s=new Map,e=Sc(i,s,this,this.dedupe),t={json:e.transformedValue};e.annotations&&(t.meta={...t.meta,values:e.annotations});const a=vA(s,this.dedupe);return a&&(t.meta={...t.meta,referentialEqualities:a}),t}deserialize(i){const{json:s,meta:e}=i;let t=Rl(s);return e!=null&&e.values&&(t=kA(t,e.values,this)),e!=null&&e.referentialEqualities&&(t=cA(t,e.referentialEqualities)),t}stringify(i){return JSON.stringify(this.serialize(i))}parse(i){return this.deserialize(JSON.parse(i))}registerClass(i,s){this.classRegistry.register(i,s)}registerSymbol(i,s){this.symbolRegistry.register(i,s)}registerCustom(i,s){this.customTransformerRegistry.register({name:s,...i})}allowErrorProps(...i){this.allowedErrorProps.push(...i)}};Ci.defaultInstance=new Ci;Ci.serialize=Ci.defaultInstance.serialize.bind(Ci.defaultInstance);Ci.deserialize=Ci.defaultInstance.deserialize.bind(Ci.defaultInstance);Ci.stringify=Ci.defaultInstance.stringify.bind(Ci.defaultInstance);Ci.parse=Ci.defaultInstance.parse.bind(Ci.defaultInstance);Ci.registerClass=Ci.defaultInstance.registerClass.bind(Ci.defaultInstance);Ci.registerSymbol=Ci.defaultInstance.registerSymbol.bind(Ci.defaultInstance);Ci.registerCustom=Ci.defaultInstance.registerCustom.bind(Ci.defaultInstance);Ci.allowErrorProps=Ci.defaultInstance.allowErrorProps.bind(Ci.defaultInstance);S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();var od,kd;(kd=(od=Y).__VUE_DEVTOOLS_KIT_MESSAGE_CHANNELS__)!=null||(od.__VUE_DEVTOOLS_KIT_MESSAGE_CHANNELS__=[]);var cd,ud;(ud=(cd=Y).__VUE_DEVTOOLS_KIT_RPC_CLIENT__)!=null||(cd.__VUE_DEVTOOLS_KIT_RPC_CLIENT__=null);var gd,vd;(vd=(gd=Y).__VUE_DEVTOOLS_KIT_RPC_SERVER__)!=null||(gd.__VUE_DEVTOOLS_KIT_RPC_SERVER__=null);var Ad,yd;(yd=(Ad=Y).__VUE_DEVTOOLS_KIT_VITE_RPC_CLIENT__)!=null||(Ad.__VUE_DEVTOOLS_KIT_VITE_RPC_CLIENT__=null);var md,Bd;(Bd=(md=Y).__VUE_DEVTOOLS_KIT_VITE_RPC_SERVER__)!=null||(md.__VUE_DEVTOOLS_KIT_VITE_RPC_SERVER__=null);var fd,Ed;(Ed=(fd=Y).__VUE_DEVTOOLS_KIT_BROADCAST_RPC_SERVER__)!=null||(fd.__VUE_DEVTOOLS_KIT_BROADCAST_RPC_SERVER__=null);S();S();S();S();S();S();S();const mA=JSON.parse('{"encrypt":{"config":{"/guide/encrypt.html":["$2a$10$3Y53c667X/DNxFNaGFebLuDdfjvNsQGXRCT9j.qcjz/.3PXWHUzmm"],"/other/windows/系统安装.html":["$2a$10$.uNqklRZZmfQHQMl6taxcufJhgB5cKyi91kuGYtv9xo2F9nEW9Iry"]}},"fullscreen":true,"author":{"name":"ChenSino","url":"https://ChenSino.github.io"},"logo":"/logo.svg","repo":"ChenSino/ChenSino.github.io","docsDir":"docs","docsBranch":"dev","footer":"鄂ICP备2024079959号-1 鄂公网安备42018502007734号","copyright":"","displayFooter":true,"themeColor":true,"pageInfo":["Author","Original","Date","Category","Tag","ReadingTime"],"blog":{"name":"ChenSino","avatar":"https://ddns.chensina.cn:29000/afatpig/blog/20220802180305.png","description":"洛星星的笔记","intro":"https://chensina.cn/","medias":{"Baidu":"https://example.com","Weibo":"https://example.com","Zhihu":"https://example.com"}},"locales":{"/":{"lang":"zh-CN","navbarLocales":{"langName":"简体中文","selectLangAriaLabel":"选择语言"},"metaLocales":{"author":"作者","date":"写作日期","origin":"原创","views":"访问量","category":"分类","tag":"标签","readingTime":"阅读时间","words":"字数","toc":"此页内容","prev":"上一页","next":"下一页","lastUpdated":"上次编辑于","contributors":"贡献者","editLink":"编辑此页","print":"打印"},"blogLocales":{"article":"文章","articleList":"文章列表","category":"分类","tag":"标签","timeline":"时间轴","timelineTitle":"昨日不在","all":"全部","intro":"个人介绍","star":"星标","empty":"$text 为空"},"paginationLocales":{"prev":"上一页","next":"下一页","navigate":"跳转到","action":"前往","errorText":"请输入 1 到 $page 之前的页码!"},"outlookLocales":{"themeColor":"主题色","darkmode":"外观","fullscreen":"全屏"},"encryptLocales":{"iconLabel":"文章已加密","placeholder":"输入密码","remember":"记住密码","errorHint":"请输入正确的密码"},"routeLocales":{"skipToContent":"跳至主要內容","notFoundTitle":"页面不存在","notFoundMsg":["这里什么也没有","我们是怎么来到这儿的?","这 是 四 零 四 !","看起来你访问了一个失效的链接"],"back":"返回上一页","home":"带我回家"},"navbar":["/home","/",{"text":"Java","icon":"java","link":"/java/"},{"text":"前端","icon":"javascript","link":"/frontweb/"},{"text":"设计模式","icon":"java","link":"/designpattern/"},{"text":"前后分离项目搭建","icon":"app","link":"/other/web/"},{"text":"C++学习","icon":"app","link":"/cpp/study/"},{"text":"家庭服务器","icon":"app","link":"/myserver/"},{"text":"其他","icon":"others","link":"/other/"}],"sidebar":{"/java/":[{"text":"Java 基础","icon":"discover","prefix":"base/","collapsible":true,"children":"structure"},{"text":"Java 进阶","icon":"blog","prefix":"advance/","collapsible":true,"children":"structure"},{"text":"Java 虚拟机","icon":"write","prefix":"jvm/","collapsible":true,"children":"structure"},{"text":"Java框架","icon":"write","prefix":"framework/","collapsible":true,"children":"structure"},{"text":"Java其他","icon":"java","prefix":"other/","collapsible":true,"children":"structure"}],"/frontweb/":[{"text":"Vue","icon":"vue","prefix":"vue/","collapsible":true,"children":"structure"},{"text":"Vite","icon":"vue","prefix":"vite/","collapsible":true,"children":[]},{"text":"ECMAScript 5","icon":"write","prefix":"es5/","collapsible":true,"children":"structure"},{"text":"ECMAScript 6","icon":"blog","prefix":"es6/","collapsible":true,"children":"structure"},{"text":"TypeScript","icon":"typescript","prefix":"typeScript/","collapsible":true,"children":"structure"},{"text":"NodeJS","icon":"nodejs","prefix":"nodejs/","collapsible":true,"children":"structure"}],"/designpattern/":"structure","/cpp/":[{"text":"基础语法学习","icon":"app","prefix":"study/","collapsible":true,"children":"structure"},{"text":"其他","icon":"app","prefix":"other/","collapsible":true,"children":"structure"}],"/myserver/":"structure","/other/":[{"text":"Git","icon":"git","prefix":"git/","collapsible":true,"children":"structure"},{"text":"Linux","icon":"linux","prefix":"linux/","collapsible":true,"children":"structure"},{"text":"数据库","icon":"repo","prefix":"database/","collapsible":true,"children":"structure"},{"text":"MarkDown","icon":"markdown","prefix":"markdown/","collapsible":true,"children":"structure"},{"text":"工具软件","icon":"software","prefix":"tools/","collapsible":true,"children":"structure"},{"text":"小组分享","icon":"share","prefix":"training/","collapsible":true,"children":"structure"},{"text":"随笔分享","icon":"share","prefix":"essay/","collapsible":true,"children":"structure"},{"text":"web","icon":"share","prefix":"web/","collapsible":true,"children":"structure"},{"text":"分布式微服务","icon":"share","prefix":"distributeservice/","collapsible":true,"children":"structure"},{"text":"OAuth2.0","icon":"share","prefix":"oauth2/","collapsible":true,"children":"structure"},{"text":"Docker","icon":"ubuntu","prefix":"docker/","collapsible":true,"children":"structure"},{"text":"PVE","icon":"class","prefix":"pve/","collapsible":true,"children":"structure"}]}}}}'),BA=Z(mA),Oc=()=>BA,Lc=Symbol(""),fA=()=>{const i=Si(Lc);if(!i)throw new Error("useThemeLocaleData() is called without provider.");return i},EA=(i,s)=>{const{locales:e,...t}=i;return{...t,...e==null?void 0:e[s]}},bA=js({enhance({app:i}){const s=Oc(),e=i._context.provides[Br],t=F(()=>EA(s.value,e.routeLocale.value));i.provide(Lc,t),Object.defineProperties(i.config.globalProperties,{$theme:{get(){return s.value}},$themeLocale:{get(){return t.value}}}),R8({app:i,id:"org.vuejs.vuepress.plugin-theme-data",label:"VuePress Theme Data Plugin",packageName:"@vuepress/plugin-theme-data",homepage:"https://v2.vuepress.vuejs.org",logo:"https://v2.vuepress.vuejs.org/images/hero.png",componentStateTypes:["VuePress"]},a=>{a.on.inspectComponent(n=>{n.instanceData.state.push({type:"VuePress",key:"themeData",editable:!1,value:s.value},{type:"VuePress",key:"themeLocaleData",editable:!1,value:t.value})})})}}),FA=Object.freeze(Object.defineProperty({__proto__:null,default:bA},Symbol.toStringTag,{value:"Module"})),_A=/language-(shellscript|shell|bash|sh|zsh)/,CA=({delay:i=500,duration:s=2e3,locales:e,selector:t,showInMobile:a,ignoreSelector:n=[],transform:l})=>{const r=br("(max-width: 419px)"),h=F(()=>!r.value||a),d=fa(e),o=xi(),k=B=>{var b;if(B.hasAttribute("copy-code"))return;const f=document.createElement("button");f.type="button",f.classList.add("vp-copy-code-button"),f.setAttribute("aria-label",d.value.copy),f.setAttribute("data-copied",d.value.copied),(b=B.parentElement)==null||b.insertBefore(f,B),B.setAttribute("copy-code","")};ui(()=>[o.value.path,h.value],async()=>{document.body.classList.toggle("no-copy-code",!h.value),h.value&&(await zs(),await Cr(i),document.querySelectorAll(t.join(",")).forEach(k))},{immediate:!0});const{copy:u}=p4({legacy:!0}),v=new WeakMap,m=async(B,f,b)=>{const A=f.cloneNode(!0);n.length&&A.querySelectorAll(n.join(",")).forEach(M=>{M.remove()}),l&&l(A);let _=A.textContent||"";if(_A.test(B.className)&&(_=_.replace(/^ *(\$|>) /gm,"")),await u(_),s<=0)return;b.classList.add("copied"),clearTimeout(v.get(b));const L=setTimeout(()=>{b.classList.remove("copied"),b.blur(),v.delete(b)},s);v.set(b,L)};Ni("click",B=>{const f=B.target;if(h.value&&f.matches('div[class*="language-"] > button.vp-copy-code-button')){const b=f.parentElement,A=f.nextElementSibling;if(!b||!A)return;m(b,A,f)}})};var wA=[],DA={"/":{copy:"复制代码",copied:"已复制"}},xA=['[vp-content] div[class*="language-"] pre'];const SA=js({setup:()=>{CA({selector:xA,ignoreSelector:wA,locales:DA,duration:2e3,delay:500,showInMobile:!1})}}),TA=Object.freeze(Object.defineProperty({__proto__:null,default:SA},Symbol.toStringTag,{value:"Module"})),OA=js({setup(){Ni("beforeprint",()=>{document.querySelectorAll("details").forEach(i=>{i.open=!0})})}}),LA=Object.freeze(Object.defineProperty({__proto__:null,default:OA},Symbol.toStringTag,{value:"Module"})),IA=Object.freeze(Object.defineProperty({__proto__:null},Symbol.toStringTag,{value:"Module"})),PA='',RA='';var jA={useBabel:!1,jsLib:[],cssLib:[],codepenLayout:"left",codepenEditors:"101",babel:"https://unpkg.com/@babel/standalone/babel.min.js",vue:"https://unpkg.com/vue/dist/vue.global.prod.js",react:"https://unpkg.com/react/umd/react.production.min.js",reactDOM:"https://unpkg.com/react-dom/umd/react-dom.production.min.js"};const tl=jA,bd={html:{types:["html","slim","haml","md","markdown","vue"],map:{html:"none",vue:"none",md:"markdown"}},js:{types:["js","javascript","coffee","coffeescript","ts","typescript","ls","livescript"],map:{js:"none",javascript:"none",coffee:"coffeescript",ls:"livescript",ts:"typescript"}},css:{types:["css","less","sass","scss","stylus","styl"],map:{css:"none",styl:"stylus"}}},MA=(i,s,e)=>{const t=document.createElement(i);return Bt(s)&&Is(s).forEach(a=>{if(a.indexOf("data"))t[a]=s[a];else{const n=a.replace("data","");t.dataset[n]=s[a]}}),t},Vr=i=>({...tl,...i,jsLib:Array.from(new Set([tl.jsLib??[],i.jsLib??[]].flat())),cssLib:Array.from(new Set([tl.cssLib??[],i.cssLib??[]].flat()))}),et=(i,s)=>{if(wr(i[s]))return i[s];const e=new Promise(t=>{var n;const a=document.createElement("script");a.src=s,(n=document.querySelector("body"))==null||n.appendChild(a),a.onload=()=>{t()}});return i[s]=e,e},VA=(i,s)=>{if(s.css&&Array.from(i.childNodes).every(e=>e.nodeName!=="STYLE")){const e=MA("style",{innerHTML:s.css});i.appendChild(e)}},NA=(i,s,e)=>{const t=e.getScript();if(t&&Array.from(s.childNodes).every(a=>a.nodeName!=="SCRIPT")){const a=document.createElement("script");a.appendChild(document.createTextNode(`{const document=window.document.querySelector('#${i} .vp-code-demo-display').shadowRoot; + */const Ap=(i,s)=>{i.classList.add(s)},yp=(i,s)=>{i.classList.remove(s)},hv=i=>{var s;(s=i==null?void 0:i.parentNode)==null||s.removeChild(i)},Zn=(i,s,e)=>ie?e:i,mp=i=>(-1+i)*100,pv=(()=>{const i=[],s=()=>{const e=i.shift();e&&e(s)};return e=>{i.push(e),i.length===1&&s()}})(),dv=i=>i.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(s,e)=>e.toUpperCase()),Va=(()=>{const i=["Webkit","O","Moz","ms"],s={},e=n=>{const{style:l}=document.body;if(n in l)return n;const r=n.charAt(0).toUpperCase()+n.slice(1);let h=i.length;for(;h--;){const d=`${i[h]}${r}`;if(d in l)return d}return n},t=n=>{const l=dv(n);return s[l]??(s[l]=e(l))},a=(n,l,r)=>{n.style[t(l)]=r};return(n,l)=>{for(const r in l){const h=l[r];Object.hasOwn(l,r)&&wr(h)&&a(n,r,h)}}})(),Ys={minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
    '},Ii={percent:null,isRendered:()=>!!document.getElementById("nprogress"),set:i=>{const{speed:s,easing:e}=Ys,t=Ii.isStarted(),a=Zn(i,Ys.minimum,1);Ii.percent=a===1?null:a;const n=Ii.render(!t),l=n.querySelector(Ys.barSelector);return n.offsetWidth,pv(r=>{Va(l,{transform:`translate3d(${mp(a)}%,0,0)`,transition:`all ${s}ms ${e}`}),a===1?(Va(n,{transition:"none",opacity:"1"}),n.offsetWidth,setTimeout(()=>{Va(n,{transition:`all ${s}ms linear`,opacity:"0"}),setTimeout(()=>{Ii.remove(),r()},s)},s)):setTimeout(()=>{r()},s)}),Ii},isStarted:()=>typeof Ii.percent=="number",start:()=>{Ii.percent||Ii.set(0);const i=()=>{setTimeout(()=>{Ii.percent&&(Ii.trickle(),i())},Ys.trickleSpeed)};return i(),Ii},done:i=>!i&&!Ii.percent?Ii:Ii.increase(.3+.5*Math.random()).set(1),increase:i=>{let{percent:s}=Ii;return s?(s=Zn(s+(typeof i=="number"?i:(1-s)*Zn(Math.random()*s,.1,.95)),0,.994),Ii.set(s)):Ii.start()},trickle:()=>Ii.increase(Math.random()*Ys.trickleRate),render:i=>{if(Ii.isRendered())return document.getElementById("nprogress");Ap(document.documentElement,"nprogress-busy");const s=document.createElement("div");s.id="nprogress",s.innerHTML=Ys.template;const e=s.querySelector(Ys.barSelector),t=document.querySelector(Ys.parent),a=i?"-100":mp(Ii.percent??0);return Va(e,{transition:"all 0 linear",transform:`translate3d(${a}%,0,0)`}),t&&(t!==document.body&&Ap(t,"nprogress-custom-parent"),t.appendChild(s)),s},remove:()=>{yp(document.documentElement,"nprogress-busy"),yp(document.querySelector(Ys.parent),"nprogress-custom-parent"),hv(document.getElementById("nprogress"))}},ov=()=>{Di(()=>{const i=ne(),s=new Set;s.add(i.currentRoute.value.path),i.beforeEach(e=>{s.has(e.path)||Ii.start()}),i.afterEach(e=>{s.add(e.path),Ii.done()})})},kv=js({setup(){ov()}}),cv=Object.freeze(Object.defineProperty({__proto__:null,default:kv},Symbol.toStringTag,{value:"Module"}));var uv=Object.create,Qk=Object.defineProperty,gv=Object.getOwnPropertyDescriptor,xr=Object.getOwnPropertyNames,vv=Object.getPrototypeOf,Av=Object.prototype.hasOwnProperty,yv=(i,s)=>function(){return i&&(s=(0,i[xr(i)[0]])(i=0)),s},mv=(i,s)=>function(){return s||(0,i[xr(i)[0]])((s={exports:{}}).exports,s),s.exports},Bv=(i,s,e,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let a of xr(s))!Av.call(i,a)&&a!==e&&Qk(i,a,{get:()=>s[a],enumerable:!(t=gv(s,a))||t.enumerable});return i},fv=(i,s,e)=>(e=i!=null?uv(vv(i)):{},Bv(Qk(e,"default",{value:i,enumerable:!0}),i)),Ea=yv({"../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.43.0_@types+node@22.9.0__@swc+core@1.5.29_jiti@2.0.0_po_lnt5yfvawfblpk67opvcdwbq7u/node_modules/tsup/assets/esm_shims.js"(){}}),Ev=mv({"../../node_modules/.pnpm/rfdc@1.4.1/node_modules/rfdc/index.js"(i,s){Ea(),s.exports=t;function e(n){return n instanceof Buffer?Buffer.from(n):new n.constructor(n.buffer.slice(),n.byteOffset,n.length)}function t(n){if(n=n||{},n.circles)return a(n);const l=new Map;if(l.set(Date,k=>new Date(k)),l.set(Map,(k,c)=>new Map(h(Array.from(k),c))),l.set(Set,(k,c)=>new Set(h(Array.from(k),c))),n.constructorHandlers)for(const k of n.constructorHandlers)l.set(k[0],k[1]);let r=null;return n.proto?o:d;function h(k,c){const u=Object.keys(k),v=new Array(u.length);for(let m=0;mnew Date(u)),h.set(Map,(u,v)=>new Map(o(Array.from(u),v))),h.set(Set,(u,v)=>new Set(o(Array.from(u),v))),n.constructorHandlers)for(const u of n.constructorHandlers)h.set(u[0],u[1]);let d=null;return n.proto?c:k;function o(u,v){const m=Object.keys(u),B=new Array(m.length);for(let f=0;f(l=xv(i,d,o),l.finally(()=>{if(l=null,e.trailing&&r&&!a){const k=h(d,r);return r=null,k}}),l);return function(...d){return l?(e.trailing&&(r=d),l):new Promise(o=>{const k=!a&&e.leading;clearTimeout(a),a=setTimeout(()=>{a=null;const c=e.leading?t:h(this,d);for(const u of n)u(c);n=[]},s),k?(t=h(this,d),o(t)):n.push(o)})}}async function xv(i,s,e){return await i.apply(s,e)}function Dl(i,s={},e){for(const t in i){const a=i[t],n=e?`${e}:${t}`:t;typeof a=="object"&&a!==null?Dl(a,s,n):typeof a=="function"&&(s[n]=a)}return s}const Sv={run:i=>i()},Tv=()=>Sv,ic=typeof console.createTask<"u"?console.createTask:Tv;function Ov(i,s){const e=s.shift(),t=ic(e);return i.reduce((a,n)=>a.then(()=>t.run(()=>n(...s))),Promise.resolve())}function Lv(i,s){const e=s.shift(),t=ic(e);return Promise.all(i.map(a=>t.run(()=>a(...s))))}function il(i,s){for(const e of[...i])e(s)}class Iv{constructor(){this._hooks={},this._before=void 0,this._after=void 0,this._deprecatedMessages=void 0,this._deprecatedHooks={},this.hook=this.hook.bind(this),this.callHook=this.callHook.bind(this),this.callHookWith=this.callHookWith.bind(this)}hook(s,e,t={}){if(!s||typeof e!="function")return()=>{};const a=s;let n;for(;this._deprecatedHooks[s];)n=this._deprecatedHooks[s],s=n.to;if(n&&!t.allowDeprecated){let l=n.message;l||(l=`${a} hook has been deprecated`+(n.to?`, please use ${n.to}`:"")),this._deprecatedMessages||(this._deprecatedMessages=new Set),this._deprecatedMessages.has(l)||(console.warn(l),this._deprecatedMessages.add(l))}if(!e.name)try{Object.defineProperty(e,"name",{get:()=>"_"+s.replace(/\W+/g,"_")+"_hook_cb",configurable:!0})}catch{}return this._hooks[s]=this._hooks[s]||[],this._hooks[s].push(e),()=>{e&&(this.removeHook(s,e),e=void 0)}}hookOnce(s,e){let t,a=(...n)=>(typeof t=="function"&&t(),t=void 0,a=void 0,e(...n));return t=this.hook(s,a),t}removeHook(s,e){if(this._hooks[s]){const t=this._hooks[s].indexOf(e);t!==-1&&this._hooks[s].splice(t,1),this._hooks[s].length===0&&delete this._hooks[s]}}deprecateHook(s,e){this._deprecatedHooks[s]=typeof e=="string"?{to:e}:e;const t=this._hooks[s]||[];delete this._hooks[s];for(const a of t)this.hook(s,a)}deprecateHooks(s){Object.assign(this._deprecatedHooks,s);for(const e in s)this.deprecateHook(e,s[e])}addHooks(s){const e=Dl(s),t=Object.keys(e).map(a=>this.hook(a,e[a]));return()=>{for(const a of t.splice(0,t.length))a()}}removeHooks(s){const e=Dl(s);for(const t in e)this.removeHook(t,e[t])}removeAllHooks(){for(const s in this._hooks)delete this._hooks[s]}callHook(s,...e){return e.unshift(s),this.callHookWith(Ov,s,...e)}callHookParallel(s,...e){return e.unshift(s),this.callHookWith(Lv,s,...e)}callHookWith(s,e,...t){const a=this._before||this._after?{name:e,args:t,context:{}}:void 0;this._before&&il(this._before,a);const n=s(e in this._hooks?[...this._hooks[e]]:[],t);return n instanceof Promise?n.finally(()=>{this._after&&a&&il(this._after,a)}):(this._after&&a&&il(this._after,a),n)}beforeEach(s){return this._before=this._before||[],this._before.push(s),()=>{if(this._before!==void 0){const e=this._before.indexOf(s);e!==-1&&this._before.splice(e,1)}}}afterEach(s){return this._after=this._after||[],this._after.push(s),()=>{if(this._after!==void 0){const e=this._after.indexOf(s);e!==-1&&this._after.splice(e,1)}}}}function sc(){return new Iv}var Pv=Object.create,ec=Object.defineProperty,Rv=Object.getOwnPropertyDescriptor,Sr=Object.getOwnPropertyNames,jv=Object.getPrototypeOf,Mv=Object.prototype.hasOwnProperty,Vv=(i,s)=>function(){return i&&(s=(0,i[Sr(i)[0]])(i=0)),s},tc=(i,s)=>function(){return s||(0,i[Sr(i)[0]])((s={exports:{}}).exports,s),s.exports},Nv=(i,s,e,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let a of Sr(s))!Mv.call(i,a)&&a!==e&&ec(i,a,{get:()=>s[a],enumerable:!(t=Rv(s,a))||t.enumerable});return i},$v=(i,s,e)=>(e=i!=null?Pv(jv(i)):{},Nv(ec(e,"default",{value:i,enumerable:!0}),i)),S=Vv({"../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.43.0_@types+node@22.9.0__@swc+core@1.5.29_jiti@2.0.0_po_lnt5yfvawfblpk67opvcdwbq7u/node_modules/tsup/assets/esm_shims.js"(){}}),qv=tc({"../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/lib/speakingurl.js"(i,s){S(),function(e){var t={À:"A",Á:"A",Â:"A",Ã:"A",Ä:"Ae",Å:"A",Æ:"AE",Ç:"C",È:"E",É:"E",Ê:"E",Ë:"E",Ì:"I",Í:"I",Î:"I",Ï:"I",Ð:"D",Ñ:"N",Ò:"O",Ó:"O",Ô:"O",Õ:"O",Ö:"Oe",Ő:"O",Ø:"O",Ù:"U",Ú:"U",Û:"U",Ü:"Ue",Ű:"U",Ý:"Y",Þ:"TH",ß:"ss",à:"a",á:"a",â:"a",ã:"a",ä:"ae",å:"a",æ:"ae",ç:"c",è:"e",é:"e",ê:"e",ë:"e",ì:"i",í:"i",î:"i",ï:"i",ð:"d",ñ:"n",ò:"o",ó:"o",ô:"o",õ:"o",ö:"oe",ő:"o",ø:"o",ù:"u",ú:"u",û:"u",ü:"ue",ű:"u",ý:"y",þ:"th",ÿ:"y","ẞ":"SS",ا:"a",أ:"a",إ:"i",آ:"aa",ؤ:"u",ئ:"e",ء:"a",ب:"b",ت:"t",ث:"th",ج:"j",ح:"h",خ:"kh",د:"d",ذ:"th",ر:"r",ز:"z",س:"s",ش:"sh",ص:"s",ض:"dh",ط:"t",ظ:"z",ع:"a",غ:"gh",ف:"f",ق:"q",ك:"k",ل:"l",م:"m",ن:"n",ه:"h",و:"w",ي:"y",ى:"a",ة:"h",ﻻ:"la",ﻷ:"laa",ﻹ:"lai",ﻵ:"laa",گ:"g",چ:"ch",پ:"p",ژ:"zh",ک:"k",ی:"y","َ":"a","ً":"an","ِ":"e","ٍ":"en","ُ":"u","ٌ":"on","ْ":"","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9",က:"k",ခ:"kh",ဂ:"g",ဃ:"ga",င:"ng",စ:"s",ဆ:"sa",ဇ:"z","စျ":"za",ည:"ny",ဋ:"t",ဌ:"ta",ဍ:"d",ဎ:"da",ဏ:"na",တ:"t",ထ:"ta",ဒ:"d",ဓ:"da",န:"n",ပ:"p",ဖ:"pa",ဗ:"b",ဘ:"ba",မ:"m",ယ:"y",ရ:"ya",လ:"l",ဝ:"w",သ:"th",ဟ:"h",ဠ:"la",အ:"a","ြ":"y","ျ":"ya","ွ":"w","ြွ":"yw","ျွ":"ywa","ှ":"h",ဧ:"e","၏":"-e",ဣ:"i",ဤ:"-i",ဉ:"u",ဦ:"-u",ဩ:"aw","သြော":"aw",ဪ:"aw","၀":"0","၁":"1","၂":"2","၃":"3","၄":"4","၅":"5","၆":"6","၇":"7","၈":"8","၉":"9","္":"","့":"","း":"",č:"c",ď:"d",ě:"e",ň:"n",ř:"r",š:"s",ť:"t",ů:"u",ž:"z",Č:"C",Ď:"D",Ě:"E",Ň:"N",Ř:"R",Š:"S",Ť:"T",Ů:"U",Ž:"Z",ހ:"h",ށ:"sh",ނ:"n",ރ:"r",ބ:"b",ޅ:"lh",ކ:"k",އ:"a",ވ:"v",މ:"m",ފ:"f",ދ:"dh",ތ:"th",ލ:"l",ގ:"g",ޏ:"gn",ސ:"s",ޑ:"d",ޒ:"z",ޓ:"t",ޔ:"y",ޕ:"p",ޖ:"j",ޗ:"ch",ޘ:"tt",ޙ:"hh",ޚ:"kh",ޛ:"th",ޜ:"z",ޝ:"sh",ޞ:"s",ޟ:"d",ޠ:"t",ޡ:"z",ޢ:"a",ޣ:"gh",ޤ:"q",ޥ:"w","ަ":"a","ާ":"aa","ި":"i","ީ":"ee","ު":"u","ޫ":"oo","ެ":"e","ޭ":"ey","ޮ":"o","ޯ":"oa","ް":"",ა:"a",ბ:"b",გ:"g",დ:"d",ე:"e",ვ:"v",ზ:"z",თ:"t",ი:"i",კ:"k",ლ:"l",მ:"m",ნ:"n",ო:"o",პ:"p",ჟ:"zh",რ:"r",ს:"s",ტ:"t",უ:"u",ფ:"p",ქ:"k",ღ:"gh",ყ:"q",შ:"sh",ჩ:"ch",ც:"ts",ძ:"dz",წ:"ts",ჭ:"ch",ხ:"kh",ჯ:"j",ჰ:"h",α:"a",β:"v",γ:"g",δ:"d",ε:"e",ζ:"z",η:"i",θ:"th",ι:"i",κ:"k",λ:"l",μ:"m",ν:"n",ξ:"ks",ο:"o",π:"p",ρ:"r",σ:"s",τ:"t",υ:"y",φ:"f",χ:"x",ψ:"ps",ω:"o",ά:"a",έ:"e",ί:"i",ό:"o",ύ:"y",ή:"i",ώ:"o",ς:"s",ϊ:"i",ΰ:"y",ϋ:"y",ΐ:"i",Α:"A",Β:"B",Γ:"G",Δ:"D",Ε:"E",Ζ:"Z",Η:"I",Θ:"TH",Ι:"I",Κ:"K",Λ:"L",Μ:"M",Ν:"N",Ξ:"KS",Ο:"O",Π:"P",Ρ:"R",Σ:"S",Τ:"T",Υ:"Y",Φ:"F",Χ:"X",Ψ:"PS",Ω:"O",Ά:"A",Έ:"E",Ί:"I",Ό:"O",Ύ:"Y",Ή:"I",Ώ:"O",Ϊ:"I",Ϋ:"Y",ā:"a",ē:"e",ģ:"g",ī:"i",ķ:"k",ļ:"l",ņ:"n",ū:"u",Ā:"A",Ē:"E",Ģ:"G",Ī:"I",Ķ:"k",Ļ:"L",Ņ:"N",Ū:"U",Ќ:"Kj",ќ:"kj",Љ:"Lj",љ:"lj",Њ:"Nj",њ:"nj",Тс:"Ts",тс:"ts",ą:"a",ć:"c",ę:"e",ł:"l",ń:"n",ś:"s",ź:"z",ż:"z",Ą:"A",Ć:"C",Ę:"E",Ł:"L",Ń:"N",Ś:"S",Ź:"Z",Ż:"Z",Є:"Ye",І:"I",Ї:"Yi",Ґ:"G",є:"ye",і:"i",ї:"yi",ґ:"g",ă:"a",Ă:"A",ș:"s",Ș:"S",ț:"t",Ț:"T",ţ:"t",Ţ:"T",а:"a",б:"b",в:"v",г:"g",д:"d",е:"e",ё:"yo",ж:"zh",з:"z",и:"i",й:"i",к:"k",л:"l",м:"m",н:"n",о:"o",п:"p",р:"r",с:"s",т:"t",у:"u",ф:"f",х:"kh",ц:"c",ч:"ch",ш:"sh",щ:"sh",ъ:"",ы:"y",ь:"",э:"e",ю:"yu",я:"ya",А:"A",Б:"B",В:"V",Г:"G",Д:"D",Е:"E",Ё:"Yo",Ж:"Zh",З:"Z",И:"I",Й:"I",К:"K",Л:"L",М:"M",Н:"N",О:"O",П:"P",Р:"R",С:"S",Т:"T",У:"U",Ф:"F",Х:"Kh",Ц:"C",Ч:"Ch",Ш:"Sh",Щ:"Sh",Ъ:"",Ы:"Y",Ь:"",Э:"E",Ю:"Yu",Я:"Ya",ђ:"dj",ј:"j",ћ:"c",џ:"dz",Ђ:"Dj",Ј:"j",Ћ:"C",Џ:"Dz",ľ:"l",ĺ:"l",ŕ:"r",Ľ:"L",Ĺ:"L",Ŕ:"R",ş:"s",Ş:"S",ı:"i",İ:"I",ğ:"g",Ğ:"G",ả:"a",Ả:"A",ẳ:"a",Ẳ:"A",ẩ:"a",Ẩ:"A",đ:"d",Đ:"D",ẹ:"e",Ẹ:"E",ẽ:"e",Ẽ:"E",ẻ:"e",Ẻ:"E",ế:"e",Ế:"E",ề:"e",Ề:"E",ệ:"e",Ệ:"E",ễ:"e",Ễ:"E",ể:"e",Ể:"E",ỏ:"o",ọ:"o",Ọ:"o",ố:"o",Ố:"O",ồ:"o",Ồ:"O",ổ:"o",Ổ:"O",ộ:"o",Ộ:"O",ỗ:"o",Ỗ:"O",ơ:"o",Ơ:"O",ớ:"o",Ớ:"O",ờ:"o",Ờ:"O",ợ:"o",Ợ:"O",ỡ:"o",Ỡ:"O",Ở:"o",ở:"o",ị:"i",Ị:"I",ĩ:"i",Ĩ:"I",ỉ:"i",Ỉ:"i",ủ:"u",Ủ:"U",ụ:"u",Ụ:"U",ũ:"u",Ũ:"U",ư:"u",Ư:"U",ứ:"u",Ứ:"U",ừ:"u",Ừ:"U",ự:"u",Ự:"U",ữ:"u",Ữ:"U",ử:"u",Ử:"ư",ỷ:"y",Ỷ:"y",ỳ:"y",Ỳ:"Y",ỵ:"y",Ỵ:"Y",ỹ:"y",Ỹ:"Y",ạ:"a",Ạ:"A",ấ:"a",Ấ:"A",ầ:"a",Ầ:"A",ậ:"a",Ậ:"A",ẫ:"a",Ẫ:"A",ắ:"a",Ắ:"A",ằ:"a",Ằ:"A",ặ:"a",Ặ:"A",ẵ:"a",Ẵ:"A","⓪":"0","①":"1","②":"2","③":"3","④":"4","⑤":"5","⑥":"6","⑦":"7","⑧":"8","⑨":"9","⑩":"10","⑪":"11","⑫":"12","⑬":"13","⑭":"14","⑮":"15","⑯":"16","⑰":"17","⑱":"18","⑲":"18","⑳":"18","⓵":"1","⓶":"2","⓷":"3","⓸":"4","⓹":"5","⓺":"6","⓻":"7","⓼":"8","⓽":"9","⓾":"10","⓿":"0","⓫":"11","⓬":"12","⓭":"13","⓮":"14","⓯":"15","⓰":"16","⓱":"17","⓲":"18","⓳":"19","⓴":"20","Ⓐ":"A","Ⓑ":"B","Ⓒ":"C","Ⓓ":"D","Ⓔ":"E","Ⓕ":"F","Ⓖ":"G","Ⓗ":"H","Ⓘ":"I","Ⓙ":"J","Ⓚ":"K","Ⓛ":"L","Ⓜ":"M","Ⓝ":"N","Ⓞ":"O","Ⓟ":"P","Ⓠ":"Q","Ⓡ":"R","Ⓢ":"S","Ⓣ":"T","Ⓤ":"U","Ⓥ":"V","Ⓦ":"W","Ⓧ":"X","Ⓨ":"Y","Ⓩ":"Z","ⓐ":"a","ⓑ":"b","ⓒ":"c","ⓓ":"d","ⓔ":"e","ⓕ":"f","ⓖ":"g","ⓗ":"h","ⓘ":"i","ⓙ":"j","ⓚ":"k","ⓛ":"l","ⓜ":"m","ⓝ":"n","ⓞ":"o","ⓟ":"p","ⓠ":"q","ⓡ":"r","ⓢ":"s","ⓣ":"t","ⓤ":"u","ⓦ":"v","ⓥ":"w","ⓧ":"x","ⓨ":"y","ⓩ":"z","“":'"',"”":'"',"‘":"'","’":"'","∂":"d",ƒ:"f","™":"(TM)","©":"(C)",œ:"oe",Œ:"OE","®":"(R)","†":"+","℠":"(SM)","…":"...","˚":"o",º:"o",ª:"a","•":"*","၊":",","။":".",$:"USD","€":"EUR","₢":"BRN","₣":"FRF","£":"GBP","₤":"ITL","₦":"NGN","₧":"ESP","₩":"KRW","₪":"ILS","₫":"VND","₭":"LAK","₮":"MNT","₯":"GRD","₱":"ARS","₲":"PYG","₳":"ARA","₴":"UAH","₵":"GHS","¢":"cent","¥":"CNY",元:"CNY",円:"YEN","﷼":"IRR","₠":"EWE","฿":"THB","₨":"INR","₹":"INR","₰":"PF","₺":"TRY","؋":"AFN","₼":"AZN",лв:"BGN","៛":"KHR","₡":"CRC","₸":"KZT",ден:"MKD",zł:"PLN","₽":"RUB","₾":"GEL"},a=["်","ް"],n={"ာ":"a","ါ":"a","ေ":"e","ဲ":"e","ိ":"i","ီ":"i","ို":"o","ု":"u","ူ":"u","ေါင်":"aung","ော":"aw","ော်":"aw","ေါ":"aw","ေါ်":"aw","်":"်","က်":"et","ိုက်":"aik","ောက်":"auk","င်":"in","ိုင်":"aing","ောင်":"aung","စ်":"it","ည်":"i","တ်":"at","ိတ်":"eik","ုတ်":"ok","ွတ်":"ut","ေတ်":"it","ဒ်":"d","ိုဒ်":"ok","ုဒ်":"ait","န်":"an","ာန်":"an","ိန်":"ein","ုန်":"on","ွန်":"un","ပ်":"at","ိပ်":"eik","ုပ်":"ok","ွပ်":"ut","န်ုပ်":"nub","မ်":"an","ိမ်":"ein","ုမ်":"on","ွမ်":"un","ယ်":"e","ိုလ်":"ol","ဉ်":"in","ံ":"an","ိံ":"ein","ုံ":"on","ައް":"ah","ަށް":"ah"},l={en:{},az:{ç:"c",ə:"e",ğ:"g",ı:"i",ö:"o",ş:"s",ü:"u",Ç:"C",Ə:"E",Ğ:"G",İ:"I",Ö:"O",Ş:"S",Ü:"U"},cs:{č:"c",ď:"d",ě:"e",ň:"n",ř:"r",š:"s",ť:"t",ů:"u",ž:"z",Č:"C",Ď:"D",Ě:"E",Ň:"N",Ř:"R",Š:"S",Ť:"T",Ů:"U",Ž:"Z"},fi:{ä:"a",Ä:"A",ö:"o",Ö:"O"},hu:{ä:"a",Ä:"A",ö:"o",Ö:"O",ü:"u",Ü:"U",ű:"u",Ű:"U"},lt:{ą:"a",č:"c",ę:"e",ė:"e",į:"i",š:"s",ų:"u",ū:"u",ž:"z",Ą:"A",Č:"C",Ę:"E",Ė:"E",Į:"I",Š:"S",Ų:"U",Ū:"U"},lv:{ā:"a",č:"c",ē:"e",ģ:"g",ī:"i",ķ:"k",ļ:"l",ņ:"n",š:"s",ū:"u",ž:"z",Ā:"A",Č:"C",Ē:"E",Ģ:"G",Ī:"i",Ķ:"k",Ļ:"L",Ņ:"N",Š:"S",Ū:"u",Ž:"Z"},pl:{ą:"a",ć:"c",ę:"e",ł:"l",ń:"n",ó:"o",ś:"s",ź:"z",ż:"z",Ą:"A",Ć:"C",Ę:"e",Ł:"L",Ń:"N",Ó:"O",Ś:"S",Ź:"Z",Ż:"Z"},sv:{ä:"a",Ä:"A",ö:"o",Ö:"O"},sk:{ä:"a",Ä:"A"},sr:{љ:"lj",њ:"nj",Љ:"Lj",Њ:"Nj",đ:"dj",Đ:"Dj"},tr:{Ü:"U",Ö:"O",ü:"u",ö:"o"}},r={ar:{"∆":"delta","∞":"la-nihaya","♥":"hob","&":"wa","|":"aw","<":"aqal-men",">":"akbar-men","∑":"majmou","¤":"omla"},az:{},ca:{"∆":"delta","∞":"infinit","♥":"amor","&":"i","|":"o","<":"menys que",">":"mes que","∑":"suma dels","¤":"moneda"},cs:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"nebo","<":"mensi nez",">":"vetsi nez","∑":"soucet","¤":"mena"},de:{"∆":"delta","∞":"unendlich","♥":"Liebe","&":"und","|":"oder","<":"kleiner als",">":"groesser als","∑":"Summe von","¤":"Waehrung"},dv:{"∆":"delta","∞":"kolunulaa","♥":"loabi","&":"aai","|":"noonee","<":"ah vure kuda",">":"ah vure bodu","∑":"jumula","¤":"faisaa"},en:{"∆":"delta","∞":"infinity","♥":"love","&":"and","|":"or","<":"less than",">":"greater than","∑":"sum","¤":"currency"},es:{"∆":"delta","∞":"infinito","♥":"amor","&":"y","|":"u","<":"menos que",">":"mas que","∑":"suma de los","¤":"moneda"},fa:{"∆":"delta","∞":"bi-nahayat","♥":"eshgh","&":"va","|":"ya","<":"kamtar-az",">":"bishtar-az","∑":"majmooe","¤":"vahed"},fi:{"∆":"delta","∞":"aarettomyys","♥":"rakkaus","&":"ja","|":"tai","<":"pienempi kuin",">":"suurempi kuin","∑":"summa","¤":"valuutta"},fr:{"∆":"delta","∞":"infiniment","♥":"Amour","&":"et","|":"ou","<":"moins que",">":"superieure a","∑":"somme des","¤":"monnaie"},ge:{"∆":"delta","∞":"usasruloba","♥":"siqvaruli","&":"da","|":"an","<":"naklebi",">":"meti","∑":"jami","¤":"valuta"},gr:{},hu:{"∆":"delta","∞":"vegtelen","♥":"szerelem","&":"es","|":"vagy","<":"kisebb mint",">":"nagyobb mint","∑":"szumma","¤":"penznem"},it:{"∆":"delta","∞":"infinito","♥":"amore","&":"e","|":"o","<":"minore di",">":"maggiore di","∑":"somma","¤":"moneta"},lt:{"∆":"delta","∞":"begalybe","♥":"meile","&":"ir","|":"ar","<":"maziau nei",">":"daugiau nei","∑":"suma","¤":"valiuta"},lv:{"∆":"delta","∞":"bezgaliba","♥":"milestiba","&":"un","|":"vai","<":"mazak neka",">":"lielaks neka","∑":"summa","¤":"valuta"},my:{"∆":"kwahkhyaet","∞":"asaonasme","♥":"akhyait","&":"nhin","|":"tho","<":"ngethaw",">":"kyithaw","∑":"paungld","¤":"ngwekye"},mk:{},nl:{"∆":"delta","∞":"oneindig","♥":"liefde","&":"en","|":"of","<":"kleiner dan",">":"groter dan","∑":"som","¤":"valuta"},pl:{"∆":"delta","∞":"nieskonczonosc","♥":"milosc","&":"i","|":"lub","<":"mniejsze niz",">":"wieksze niz","∑":"suma","¤":"waluta"},pt:{"∆":"delta","∞":"infinito","♥":"amor","&":"e","|":"ou","<":"menor que",">":"maior que","∑":"soma","¤":"moeda"},ro:{"∆":"delta","∞":"infinit","♥":"dragoste","&":"si","|":"sau","<":"mai mic ca",">":"mai mare ca","∑":"suma","¤":"valuta"},ru:{"∆":"delta","∞":"beskonechno","♥":"lubov","&":"i","|":"ili","<":"menshe",">":"bolshe","∑":"summa","¤":"valjuta"},sk:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"alebo","<":"menej ako",">":"viac ako","∑":"sucet","¤":"mena"},sr:{},tr:{"∆":"delta","∞":"sonsuzluk","♥":"ask","&":"ve","|":"veya","<":"kucuktur",">":"buyuktur","∑":"toplam","¤":"para birimi"},uk:{"∆":"delta","∞":"bezkinechnist","♥":"lubov","&":"i","|":"abo","<":"menshe",">":"bilshe","∑":"suma","¤":"valjuta"},vn:{"∆":"delta","∞":"vo cuc","♥":"yeu","&":"va","|":"hoac","<":"nho hon",">":"lon hon","∑":"tong","¤":"tien te"}},h=[";","?",":","@","&","=","+","$",",","/"].join(""),d=[";","?",":","@","&","=","+","$",","].join(""),o=[".","!","~","*","'","(",")"].join(""),k=function(B,f){var b="-",A="",_="",L=!0,M={},x,K,j,T,H,P,ii,ri,ki,W,N,X,li,Pi,Ri="";if(typeof B!="string")return"";if(typeof f=="string"&&(b=f),ii=r.en,ri=l.en,typeof f=="object"){x=f.maintainCase||!1,M=f.custom&&typeof f.custom=="object"?f.custom:M,j=+f.truncate>1&&f.truncate||!1,T=f.uric||!1,H=f.uricNoSlash||!1,P=f.mark||!1,L=!(f.symbols===!1||f.lang===!1),b=f.separator||b,T&&(Ri+=h),H&&(Ri+=d),P&&(Ri+=o),ii=f.lang&&r[f.lang]&&L?r[f.lang]:L?r.en:{},ri=f.lang&&l[f.lang]?l[f.lang]:f.lang===!1||f.lang===!0?{}:l.en,f.titleCase&&typeof f.titleCase.length=="number"&&Array.prototype.toString.call(f.titleCase)?(f.titleCase.forEach(function(vi){M[vi+""]=vi+""}),K=!0):K=!!f.titleCase,f.custom&&typeof f.custom.length=="number"&&Array.prototype.toString.call(f.custom)&&f.custom.forEach(function(vi){M[vi+""]=vi+""}),Object.keys(M).forEach(function(vi){var $i;vi.length>1?$i=new RegExp("\\b"+u(vi)+"\\b","gi"):$i=new RegExp(u(vi),"gi"),B=B.replace($i,M[vi])});for(N in M)Ri+=N}for(Ri+=b,Ri=u(Ri),B=B.replace(/(^\s+|\s+$)/g,""),li=!1,Pi=!1,W=0,X=B.length;W=0?(_+=N,N=""):Pi===!0?(N=n[_]+t[N],_=""):N=li&&t[N].match(/[A-Za-z0-9]/)?" "+t[N]:t[N],li=!1,Pi=!1):N in n?(_+=N,N="",W===X-1&&(N=n[_]),Pi=!0):ii[N]&&!(T&&h.indexOf(N)!==-1)&&!(H&&d.indexOf(N)!==-1)?(N=li||A.substr(-1).match(/[A-Za-z0-9]/)?b+ii[N]:ii[N],N+=B[W+1]!==void 0&&B[W+1].match(/[A-Za-z0-9]/)?b:"",li=!0):(Pi===!0?(N=n[_]+N,_="",Pi=!1):li&&(/[A-Za-z0-9]/.test(N)||A.substr(-1).match(/A-Za-z0-9]/))&&(N=" "+N),li=!1),A+=N.replace(new RegExp("[^\\w\\s"+Ri+"_-]","g"),b);return K&&(A=A.replace(/(\w)(\S*)/g,function(vi,$i,ys){var ls=$i.toUpperCase()+(ys!==null?ys:"");return Object.keys(M).indexOf(ls.toLowerCase())<0?ls:ls.toLowerCase()})),A=A.replace(/\s+/g,b).replace(new RegExp("\\"+b+"+","g"),b).replace(new RegExp("(^\\"+b+"+|\\"+b+"+$)","g"),""),j&&A.length>j&&(ki=A.charAt(j)===b,A=A.slice(0,j),ki||(A=A.slice(0,A.lastIndexOf(b)))),!x&&!K&&(A=A.toLowerCase()),A},c=function(B){return function(b){return k(b,B)}},u=function(B){return B.replace(/[-\\^$*+?.()|[\]{}\/]/g,"\\$&")},v=function(m,B){for(var f in B)if(B[f]===m)return!0};if(typeof s<"u"&&s.exports)s.exports=k,s.exports.createSlug=c;else if(typeof define<"u"&&define.amd)define([],function(){return k});else try{if(e.getSlug||e.createSlug)throw"speakingurl: globals exists /(getSlug|createSlug)/";e.getSlug=k,e.createSlug=c}catch{}}(i)}}),Hv=tc({"../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/index.js"(i,s){S(),s.exports=qv()}});S();S();S();S();S();S();S();function Uv(i){return!!(i&&i.__v_isReadonly)}function ac(i){return Uv(i)?ac(i.__v_raw):!!(i&&i.__v_isReactive)}function sl(i){return!!(i&&i.__v_isRef===!0)}function It(i){const s=i&&i.__v_raw;return s?It(s):i}S();function zv(i){var s;const e=i.name||i._componentTag||i.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__||i.__name;return e==="index"&&((s=i.__file)!=null&&s.endsWith("index.vue"))?"":e}function Wv(i){const s=i.__file;if(s)return Cv(wv(s,".vue"))}function Ep(i,s){return i.type.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__=s,s}function fn(i){if(i.__VUE_DEVTOOLS_NEXT_APP_RECORD__)return i.__VUE_DEVTOOLS_NEXT_APP_RECORD__;if(i.root)return i.appContext.app.__VUE_DEVTOOLS_NEXT_APP_RECORD__}async function Gv(i){const{app:s,uid:e,instance:t}=i;try{if(t.__VUE_DEVTOOLS_NEXT_UID__)return t.__VUE_DEVTOOLS_NEXT_UID__;const a=await fn(s);if(!a)return null;const n=a.rootInstance===t;return`${a.id}:${n?"root":e}`}catch{}}function nc(i){var s,e;const t=(s=i.subTree)==null?void 0:s.type,a=fn(i);return a?((e=a==null?void 0:a.types)==null?void 0:e.Fragment)===t:!1}function En(i){var s,e,t;const a=zv((i==null?void 0:i.type)||{});if(a)return a;if((i==null?void 0:i.root)===i)return"Root";for(const l in(e=(s=i.parent)==null?void 0:s.type)==null?void 0:e.components)if(i.parent.type.components[l]===(i==null?void 0:i.type))return Ep(i,l);for(const l in(t=i.appContext)==null?void 0:t.components)if(i.appContext.components[l]===(i==null?void 0:i.type))return Ep(i,l);const n=Wv((i==null?void 0:i.type)||{});return n||"Anonymous Component"}function xl(i,s){return s=s||`${i.id}:root`,i.instanceMap.get(s)||i.instanceMap.get(":root")}var Kv=class{constructor(){this.refEditor=new Jv}set(i,s,e,t){const a=Array.isArray(s)?s:s.split(".");for(;a.length>1;){const r=a.shift();i instanceof Map&&(i=i.get(r)),i instanceof Set?i=Array.from(i.values())[r]:i=i[r],this.refEditor.isRef(i)&&(i=this.refEditor.get(i))}const n=a[0],l=this.refEditor.get(i)[n];t?t(i,n,e):this.refEditor.isRef(l)?this.refEditor.set(l,e):i[n]=e}get(i,s){const e=Array.isArray(s)?s:s.split(".");for(let t=0;t"u")return!1;const t=Array.isArray(s)?s.slice():s.split("."),a=e?2:1;for(;i&&t.length>a;){const n=t.shift();i=i[n],this.refEditor.isRef(i)&&(i=this.refEditor.get(i))}return i!=null&&Object.prototype.hasOwnProperty.call(i,t[0])}createDefaultSetCallback(i){return(s,e,t)=>{if((i.remove||i.newKey)&&(Array.isArray(s)?s.splice(e,1):It(s)instanceof Map?s.delete(e):It(s)instanceof Set?s.delete(Array.from(s.values())[e]):Reflect.deleteProperty(s,e)),!i.remove){const a=s[i.newKey||e];this.refEditor.isRef(a)?this.refEditor.set(a,t):It(s)instanceof Map?s.set(i.newKey||e,t):It(s)instanceof Set?s.add(t):s[i.newKey||e]=t}}}},Jv=class{set(i,s){if(sl(i))i.value=s;else{if(i instanceof Set&&Array.isArray(s)){i.clear(),s.forEach(a=>i.add(a));return}const e=Object.keys(s);if(i instanceof Map){const a=new Set(i.keys());e.forEach(n=>{i.set(n,Reflect.get(s,n)),a.delete(n)}),a.forEach(n=>i.delete(n));return}const t=new Set(Object.keys(i));e.forEach(a=>{Reflect.set(i,a,Reflect.get(s,a)),t.delete(a)}),t.forEach(a=>Reflect.deleteProperty(i,a))}}get(i){return sl(i)?i.value:i}isRef(i){return sl(i)||ac(i)}};S();function Tr(i){return nc(i)?Yv(i.subTree):i.subTree?[i.subTree.el]:[]}function Yv(i){if(!i.children)return[];const s=[];return i.children.forEach(e=>{e.component?s.push(...Tr(e.component)):e!=null&&e.el&&s.push(e.el)}),s}S();S();function Xv(){const i={top:0,bottom:0,left:0,right:0,get width(){return i.right-i.left},get height(){return i.bottom-i.top}};return i}var Na;function Qv(i){return Na||(Na=document.createRange()),Na.selectNode(i),Na.getBoundingClientRect()}function Zv(i){const s=Xv();if(!i.children)return s;for(let e=0,t=i.children.length;ei.bottom)&&(i.bottom=s.bottom),(!i.left||s.lefti.right)&&(i.right=s.right),i}var bp={top:0,left:0,right:0,bottom:0,width:0,height:0};function je(i){const s=i.subTree.el;return typeof window>"u"?bp:nc(i)?Zv(i.subTree):(s==null?void 0:s.nodeType)===1?s==null?void 0:s.getBoundingClientRect():i.subTree.component?je(i.subTree.component):bp}var lc="__vue-devtools-component-inspector__",rc="__vue-devtools-component-inspector__card__",hc="__vue-devtools-component-inspector__name__",pc="__vue-devtools-component-inspector__indicator__",dc={display:"block",zIndex:2147483640,position:"fixed",backgroundColor:"#42b88325",border:"1px solid #42b88350",borderRadius:"5px",transition:"all 0.1s ease-in",pointerEvents:"none"},s8={fontFamily:"Arial, Helvetica, sans-serif",padding:"5px 8px",borderRadius:"4px",textAlign:"left",position:"absolute",left:0,color:"#e9e9e9",fontSize:"14px",fontWeight:600,lineHeight:"24px",backgroundColor:"#42b883",boxShadow:"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)"},e8={display:"inline-block",fontWeight:400,fontStyle:"normal",fontSize:"12px",opacity:.7};function bt(){return document.getElementById(lc)}function t8(){return document.getElementById(rc)}function a8(){return document.getElementById(pc)}function n8(){return document.getElementById(hc)}function Or(i){return{left:`${Math.round(i.left*100)/100}px`,top:`${Math.round(i.top*100)/100}px`,width:`${Math.round(i.width*100)/100}px`,height:`${Math.round(i.height*100)/100}px`}}function Lr(i){var s;const e=document.createElement("div");e.id=(s=i.elementId)!=null?s:lc,Object.assign(e.style,{...dc,...Or(i.bounds),...i.style});const t=document.createElement("span");t.id=rc,Object.assign(t.style,{...s8,top:i.bounds.top<35?0:"-35px"});const a=document.createElement("span");a.id=hc,a.innerHTML=`<${i.name}>  `;const n=document.createElement("i");return n.id=pc,n.innerHTML=`${Math.round(i.bounds.width*100)/100} x ${Math.round(i.bounds.height*100)/100}`,Object.assign(n.style,e8),t.appendChild(a),t.appendChild(n),e.appendChild(t),document.body.appendChild(e),e}function Ir(i){const s=bt(),e=t8(),t=n8(),a=a8();s&&(Object.assign(s.style,{...dc,...Or(i.bounds)}),Object.assign(e.style,{top:i.bounds.top<35?0:"-35px"}),t.innerHTML=`<${i.name}>  `,a.innerHTML=`${Math.round(i.bounds.width*100)/100} x ${Math.round(i.bounds.height*100)/100}`)}function l8(i){const s=je(i);if(!s.width&&!s.height)return;const e=En(i);bt()?Ir({bounds:s,name:e}):Lr({bounds:s,name:e})}function oc(){const i=bt();i&&(i.style.display="none")}var Sl=null;function Tl(i){const s=i.target;if(s){const e=s.__vueParentComponent;if(e&&(Sl=e,e.vnode.el)){const a=je(e),n=En(e);bt()?Ir({bounds:a,name:n}):Lr({bounds:a,name:n})}}}function r8(i,s){var e;if(i.preventDefault(),i.stopPropagation(),Sl){const t=(e=Xi.value)==null?void 0:e.app;Gv({app:t,uid:t.uid,instance:Sl}).then(a=>{s(a)})}}var an=null;function h8(){oc(),window.removeEventListener("mouseover",Tl),window.removeEventListener("click",an,!0),an=null}function p8(){return window.addEventListener("mouseover",Tl),new Promise(i=>{function s(e){e.preventDefault(),e.stopPropagation(),r8(e,t=>{window.removeEventListener("click",s,!0),an=null,window.removeEventListener("mouseover",Tl);const a=bt();a&&(a.style.display="none"),i(JSON.stringify({id:t}))})}an=s,window.addEventListener("click",s,!0)})}function d8(i){const s=xl(Xi.value,i.id);if(s){const[e]=Tr(s);if(typeof e.scrollIntoView=="function")e.scrollIntoView({behavior:"smooth"});else{const t=je(s),a=document.createElement("div"),n={...Or(t),position:"absolute"};Object.assign(a.style,n),document.body.appendChild(a),a.scrollIntoView({behavior:"smooth"}),setTimeout(()=>{document.body.removeChild(a)},2e3)}setTimeout(()=>{const t=je(s);if(t.width||t.height){const a=En(s),n=bt();n?Ir({...i,name:a,bounds:t}):Lr({...i,name:a,bounds:t}),setTimeout(()=>{n&&(n.style.display="none")},1500)}},1200)}}S();var Fp,_p;(_p=(Fp=Y).__VUE_DEVTOOLS_COMPONENT_INSPECTOR_ENABLED__)!=null||(Fp.__VUE_DEVTOOLS_COMPONENT_INSPECTOR_ENABLED__=!0);function o8(i){let s=0;const e=setInterval(()=>{Y.__VUE_INSPECTOR__&&(clearInterval(e),s+=30,i()),s>=5e3&&clearInterval(e)},30)}function k8(){const i=Y.__VUE_INSPECTOR__,s=i.openInEditor;i.openInEditor=async(...e)=>{i.disable(),s(...e)}}function c8(){return new Promise(i=>{function s(){k8(),i(Y.__VUE_INSPECTOR__)}Y.__VUE_INSPECTOR__?s():o8(()=>{s()})})}S();S();S();var u8="__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS_STATE__";function g8(){if(!Zk||typeof localStorage>"u"||localStorage===null)return{recordingState:!1,mouseEventEnabled:!1,keyboardEventEnabled:!1,componentEventEnabled:!1,performanceEventEnabled:!1,selected:""};const i=localStorage.getItem(u8);return i?JSON.parse(i):{recordingState:!1,mouseEventEnabled:!1,keyboardEventEnabled:!1,componentEventEnabled:!1,performanceEventEnabled:!1,selected:""}}S();S();S();var Cp,wp;(wp=(Cp=Y).__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS)!=null||(Cp.__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS=[]);var v8=new Proxy(Y.__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS,{get(i,s,e){return Reflect.get(i,s,e)}});function A8(i,s){qi.timelineLayersState[s.id]=!1,v8.push({...i,descriptorId:s.id,appRecord:fn(s.app)})}var Dp,xp;(xp=(Dp=Y).__VUE_DEVTOOLS_KIT_INSPECTOR__)!=null||(Dp.__VUE_DEVTOOLS_KIT_INSPECTOR__=[]);var Pr=new Proxy(Y.__VUE_DEVTOOLS_KIT_INSPECTOR__,{get(i,s,e){return Reflect.get(i,s,e)}}),kc=dt(()=>{Ft.hooks.callHook("sendInspectorToClient",cc())});function y8(i,s){var e,t;Pr.push({options:i,descriptor:s,treeFilterPlaceholder:(e=i.treeFilterPlaceholder)!=null?e:"Search tree...",stateFilterPlaceholder:(t=i.stateFilterPlaceholder)!=null?t:"Search state...",treeFilter:"",selectedNodeId:"",appRecord:fn(s.app)}),kc()}function cc(){return Pr.filter(i=>i.descriptor.app===Xi.value.app).filter(i=>i.descriptor.id!=="components").map(i=>{var s;const e=i.descriptor,t=i.options;return{id:t.id,label:t.label,logo:e.logo,icon:`custom-ic-baseline-${(s=t==null?void 0:t.icon)==null?void 0:s.replace(/_/g,"-")}`,packageName:e.packageName,homepage:e.homepage,pluginId:e.id}})}function za(i,s){return Pr.find(e=>e.options.id===i&&(s?e.descriptor.app===s:!0))}function m8(){const i=sc();i.hook("addInspector",({inspector:t,plugin:a})=>{y8(t,a.descriptor)});const s=dt(async({inspectorId:t,plugin:a})=>{var n;if(!t||!((n=a==null?void 0:a.descriptor)!=null&&n.app)||qi.highPerfModeEnabled)return;const l=za(t,a.descriptor.app),r={app:a.descriptor.app,inspectorId:t,filter:(l==null?void 0:l.treeFilter)||"",rootNodes:[]};await new Promise(h=>{i.callHookWith(async d=>{await Promise.all(d.map(o=>o(r))),h()},"getInspectorTree")}),i.callHookWith(async h=>{await Promise.all(h.map(d=>d({inspectorId:t,rootNodes:r.rootNodes})))},"sendInspectorTreeToClient")},120);i.hook("sendInspectorTree",s);const e=dt(async({inspectorId:t,plugin:a})=>{var n;if(!t||!((n=a==null?void 0:a.descriptor)!=null&&n.app)||qi.highPerfModeEnabled)return;const l=za(t,a.descriptor.app),r={app:a.descriptor.app,inspectorId:t,nodeId:(l==null?void 0:l.selectedNodeId)||"",state:null},h={currentTab:`custom-inspector:${t}`};r.nodeId&&await new Promise(d=>{i.callHookWith(async o=>{await Promise.all(o.map(k=>k(r,h))),d()},"getInspectorState")}),i.callHookWith(async d=>{await Promise.all(d.map(o=>o({inspectorId:t,nodeId:r.nodeId,state:r.state})))},"sendInspectorStateToClient")},120);return i.hook("sendInspectorState",e),i.hook("customInspectorSelectNode",({inspectorId:t,nodeId:a,plugin:n})=>{const l=za(t,n.descriptor.app);l&&(l.selectedNodeId=a)}),i.hook("timelineLayerAdded",({options:t,plugin:a})=>{A8(t,a.descriptor)}),i.hook("timelineEventAdded",({options:t,plugin:a})=>{var n;const l=["performance","component-event","keyboard","mouse"];qi.highPerfModeEnabled||!((n=qi.timelineLayersState)!=null&&n[a.descriptor.id])&&!l.includes(t.layerId)||i.callHookWith(async r=>{await Promise.all(r.map(h=>h(t)))},"sendTimelineEventToClient")}),i.hook("getComponentInstances",async({app:t})=>{const a=t.__VUE_DEVTOOLS_NEXT_APP_RECORD__;if(!a)return null;const n=a.id.toString();return[...a.instanceMap].filter(([r])=>r.split(":")[0]===n).map(([,r])=>r)}),i.hook("getComponentBounds",async({instance:t})=>je(t)),i.hook("getComponentName",({instance:t})=>En(t)),i.hook("componentHighlight",({uid:t})=>{const a=Xi.value.instanceMap.get(t);a&&l8(a)}),i.hook("componentUnhighlight",()=>{oc()}),i}var Sp,Tp;(Tp=(Sp=Y).__VUE_DEVTOOLS_KIT_APP_RECORDS__)!=null||(Sp.__VUE_DEVTOOLS_KIT_APP_RECORDS__=[]);var Op,Lp;(Lp=(Op=Y).__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__)!=null||(Op.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__={});var Ip,Pp;(Pp=(Ip=Y).__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__)!=null||(Ip.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__="");var Rp,jp;(jp=(Rp=Y).__VUE_DEVTOOLS_KIT_CUSTOM_TABS__)!=null||(Rp.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__=[]);var Mp,Vp;(Vp=(Mp=Y).__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__)!=null||(Mp.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__=[]);var Te="__VUE_DEVTOOLS_KIT_GLOBAL_STATE__";function B8(){return{connected:!1,clientConnected:!1,vitePluginDetected:!0,appRecords:[],activeAppRecordId:"",tabs:[],commands:[],highPerfModeEnabled:!0,devtoolsClientDetected:{},perfUniqueGroupId:0,timelineLayersState:g8()}}var Np,$p;($p=(Np=Y)[Te])!=null||(Np[Te]=B8());var f8=dt(i=>{Ft.hooks.callHook("devtoolsStateUpdated",{state:i})});dt((i,s)=>{Ft.hooks.callHook("devtoolsConnectedUpdated",{state:i,oldState:s})});var bn=new Proxy(Y.__VUE_DEVTOOLS_KIT_APP_RECORDS__,{get(i,s,e){return s==="value"?Y.__VUE_DEVTOOLS_KIT_APP_RECORDS__:Y.__VUE_DEVTOOLS_KIT_APP_RECORDS__[s]}}),Xi=new Proxy(Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__,{get(i,s,e){return s==="value"?Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__:s==="id"?Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__:Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__[s]}});function uc(){f8({...Y[Te],appRecords:bn.value,activeAppRecordId:Xi.id,tabs:Y.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__,commands:Y.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__})}function E8(i){Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__=i,uc()}function b8(i){Y.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__=i,uc()}var qi=new Proxy(Y[Te],{get(i,s){return s==="appRecords"?bn:s==="activeAppRecordId"?Xi.id:s==="tabs"?Y.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__:s==="commands"?Y.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__:Y[Te][s]},deleteProperty(i,s){return delete i[s],!0},set(i,s,e){return{...Y[Te]},i[s]=e,Y[Te][s]=e,!0}});function F8(i={}){var s,e,t;const{file:a,host:n,baseUrl:l=window.location.origin,line:r=0,column:h=0}=i;if(a){if(n==="chrome-extension"){const d=a.replace(/\\/g,"\\\\"),o=(e=(s=window.VUE_DEVTOOLS_CONFIG)==null?void 0:s.openInEditorHost)!=null?e:"/";fetch(`${o}__open-in-editor?file=${encodeURI(a)}`).then(k=>{if(!k.ok){const c=`Opening component ${d} failed`;console.log(`%c${c}`,"color:red")}})}else if(qi.vitePluginDetected){const d=(t=Y.__VUE_DEVTOOLS_OPEN_IN_EDITOR_BASE_URL__)!=null?t:l;Y.__VUE_INSPECTOR__.openInEditor(d,a,r,h)}}}S();S();S();S();S();var qp,Hp;(Hp=(qp=Y).__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__)!=null||(qp.__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__=[]);var Rr=new Proxy(Y.__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__,{get(i,s,e){return Reflect.get(i,s,e)}});function Ol(i){const s={};return Object.keys(i).forEach(e=>{s[e]=i[e].defaultValue}),s}function jr(i){return`__VUE_DEVTOOLS_NEXT_PLUGIN_SETTINGS__${i}__`}function _8(i){var s,e,t;const a=(e=(s=Rr.find(n=>{var l;return n[0].id===i&&!!((l=n[0])!=null&&l.settings)}))==null?void 0:s[0])!=null?e:null;return(t=a==null?void 0:a.settings)!=null?t:null}function gc(i,s){var e,t,a;const n=jr(i);if(n){const l=localStorage.getItem(n);if(l)return JSON.parse(l)}if(i){const l=(t=(e=Rr.find(r=>r[0].id===i))==null?void 0:e[0])!=null?t:null;return Ol((a=l==null?void 0:l.settings)!=null?a:{})}return Ol(s)}function C8(i,s){const e=jr(i);localStorage.getItem(e)||localStorage.setItem(e,JSON.stringify(Ol(s)))}function w8(i,s,e){const t=jr(i),a=localStorage.getItem(t),n=JSON.parse(a||"{}"),l={...n,[s]:e};localStorage.setItem(t,JSON.stringify(l)),Ft.hooks.callHookWith(r=>{r.forEach(h=>h({pluginId:i,key:s,oldValue:n[s],newValue:e,settings:l}))},"setPluginSettings")}S();S();S();S();S();S();S();S();S();S();S();var Up,zp,us=(zp=(Up=Y).__VUE_DEVTOOLS_HOOK)!=null?zp:Up.__VUE_DEVTOOLS_HOOK=sc(),D8={vueAppInit(i){us.hook("app:init",i)},vueAppUnmount(i){us.hook("app:unmount",i)},vueAppConnected(i){us.hook("app:connected",i)},componentAdded(i){return us.hook("component:added",i)},componentEmit(i){return us.hook("component:emit",i)},componentUpdated(i){return us.hook("component:updated",i)},componentRemoved(i){return us.hook("component:removed",i)},setupDevtoolsPlugin(i){us.hook("devtools-plugin:setup",i)},perfStart(i){return us.hook("perf:start",i)},perfEnd(i){return us.hook("perf:end",i)}},vc={on:D8,setupDevToolsPlugin(i,s){return us.callHook("devtools-plugin:setup",i,s)}},x8=class{constructor({plugin:i,ctx:s}){this.hooks=s.hooks,this.plugin=i}get on(){return{visitComponentTree:i=>{this.hooks.hook("visitComponentTree",i)},inspectComponent:i=>{this.hooks.hook("inspectComponent",i)},editComponentState:i=>{this.hooks.hook("editComponentState",i)},getInspectorTree:i=>{this.hooks.hook("getInspectorTree",i)},getInspectorState:i=>{this.hooks.hook("getInspectorState",i)},editInspectorState:i=>{this.hooks.hook("editInspectorState",i)},inspectTimelineEvent:i=>{this.hooks.hook("inspectTimelineEvent",i)},timelineCleared:i=>{this.hooks.hook("timelineCleared",i)},setPluginSettings:i=>{this.hooks.hook("setPluginSettings",i)}}}notifyComponentUpdate(i){var s;if(qi.highPerfModeEnabled)return;const e=cc().find(t=>t.packageName===this.plugin.descriptor.packageName);if(e!=null&&e.id){if(i){const t=[i.appContext.app,i.uid,(s=i.parent)==null?void 0:s.uid,i];us.callHook("component:updated",...t)}else us.callHook("component:updated");this.hooks.callHook("sendInspectorState",{inspectorId:e.id,plugin:this.plugin})}}addInspector(i){this.hooks.callHook("addInspector",{inspector:i,plugin:this.plugin}),this.plugin.descriptor.settings&&C8(i.id,this.plugin.descriptor.settings)}sendInspectorTree(i){qi.highPerfModeEnabled||this.hooks.callHook("sendInspectorTree",{inspectorId:i,plugin:this.plugin})}sendInspectorState(i){qi.highPerfModeEnabled||this.hooks.callHook("sendInspectorState",{inspectorId:i,plugin:this.plugin})}selectInspectorNode(i,s){this.hooks.callHook("customInspectorSelectNode",{inspectorId:i,nodeId:s,plugin:this.plugin})}visitComponentTree(i){return this.hooks.callHook("visitComponentTree",i)}now(){return qi.highPerfModeEnabled?0:Date.now()}addTimelineLayer(i){this.hooks.callHook("timelineLayerAdded",{options:i,plugin:this.plugin})}addTimelineEvent(i){qi.highPerfModeEnabled||this.hooks.callHook("timelineEventAdded",{options:i,plugin:this.plugin})}getSettings(i){return gc(i??this.plugin.descriptor.id,this.plugin.descriptor.settings)}getComponentInstances(i){return this.hooks.callHook("getComponentInstances",{app:i})}getComponentBounds(i){return this.hooks.callHook("getComponentBounds",{instance:i})}getComponentName(i){return this.hooks.callHook("getComponentName",{instance:i})}highlightElement(i){const s=i.__VUE_DEVTOOLS_NEXT_UID__;return this.hooks.callHook("componentHighlight",{uid:s})}unhighlightElement(){return this.hooks.callHook("componentUnhighlight")}},S8=x8;S();S();S();S();var T8="__vue_devtool_undefined__",O8="__vue_devtool_infinity__",L8="__vue_devtool_negative_infinity__",I8="__vue_devtool_nan__";S();S();var P8={[T8]:"undefined",[I8]:"NaN",[O8]:"Infinity",[L8]:"-Infinity"};Object.entries(P8).reduce((i,[s,e])=>(i[e]=s,i),{});S();S();S();S();S();var Wp,Gp;(Gp=(Wp=Y).__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__)!=null||(Wp.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__=new Set);function R8(i,s){return vc.setupDevToolsPlugin(i,s)}function j8(i,s){const[e,t]=i;if(e.app!==s)return;const a=new S8({plugin:{setupFn:t,descriptor:e},ctx:Ft});e.packageName==="vuex"&&a.on.editInspectorState(n=>{a.sendInspectorState(n.inspectorId)}),t(a)}function Ac(i){Y.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__.has(i)||qi.highPerfModeEnabled||(Y.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__.add(i),Rr.forEach(s=>{j8(s,i)}))}S();S();var ea="__VUE_DEVTOOLS_ROUTER__",ot="__VUE_DEVTOOLS_ROUTER_INFO__",Kp,Jp;(Jp=(Kp=Y)[ot])!=null||(Kp[ot]={currentRoute:null,routes:[]});var Yp,Xp;(Xp=(Yp=Y)[ea])!=null||(Yp[ea]={});new Proxy(Y[ot],{get(i,s){return Y[ot][s]}});new Proxy(Y[ea],{get(i,s){if(s==="value")return Y[ea]}});function M8(i){const s=new Map;return((i==null?void 0:i.getRoutes())||[]).filter(e=>!s.has(e.path)&&s.set(e.path,1))}function Mr(i){return i.map(s=>{let{path:e,name:t,children:a,meta:n}=s;return a!=null&&a.length&&(a=Mr(a)),{path:e,name:t,children:a,meta:n}})}function V8(i){if(i){const{fullPath:s,hash:e,href:t,path:a,name:n,matched:l,params:r,query:h}=i;return{fullPath:s,hash:e,href:t,path:a,name:n,params:r,query:h,matched:Mr(l)}}return i}function N8(i,s){function e(){var t;const a=(t=i.app)==null?void 0:t.config.globalProperties.$router,n=V8(a==null?void 0:a.currentRoute.value),l=Mr(M8(a)),r=console.warn;console.warn=()=>{},Y[ot]={currentRoute:n?fp(n):{},routes:fp(l)},Y[ea]=a,console.warn=r}e(),vc.on.componentUpdated(dt(()=>{var t;((t=s.value)==null?void 0:t.app)===i.app&&(e(),!qi.highPerfModeEnabled&&Ft.hooks.callHook("routerInfoUpdated",{state:Y[ot]}))},200))}function $8(i){return{async getInspectorTree(s){const e={...s,app:Xi.value.app,rootNodes:[]};return await new Promise(t=>{i.callHookWith(async a=>{await Promise.all(a.map(n=>n(e))),t()},"getInspectorTree")}),e.rootNodes},async getInspectorState(s){const e={...s,app:Xi.value.app,state:null},t={currentTab:`custom-inspector:${s.inspectorId}`};return await new Promise(a=>{i.callHookWith(async n=>{await Promise.all(n.map(l=>l(e,t))),a()},"getInspectorState")}),e.state},editInspectorState(s){const e=new Kv,t={...s,app:Xi.value.app,set:(a,n=s.path,l=s.state.value,r)=>{e.set(a,n,l,r||e.createDefaultSetCallback(s.state))}};i.callHookWith(a=>{a.forEach(n=>n(t))},"editInspectorState")},sendInspectorState(s){const e=za(s);i.callHook("sendInspectorState",{inspectorId:s,plugin:{descriptor:e.descriptor,setupFn:()=>({})}})},inspectComponentInspector(){return p8()},cancelInspectComponentInspector(){return h8()},getComponentRenderCode(s){const e=xl(Xi.value,s);if(e)return(e==null?void 0:e.type)instanceof Function?e.type.toString():e.render.toString()},scrollToComponent(s){return d8({id:s})},openInEditor:F8,getVueInspector:c8,toggleApp(s){const e=bn.value.find(t=>t.id===s);e&&(b8(s),E8(e),N8(e,Xi),kc(),Ac(e.app))},inspectDOM(s){const e=xl(Xi.value,s);if(e){const[t]=Tr(e);t&&(Y.__VUE_DEVTOOLS_INSPECT_DOM_TARGET__=t)}},updatePluginSettings(s,e,t){w8(s,e,t)},getPluginSettings(s){return{options:_8(s),values:gc(s)}}}}S();var Qp,Zp;(Zp=(Qp=Y).__VUE_DEVTOOLS_ENV__)!=null||(Qp.__VUE_DEVTOOLS_ENV__={vitePluginDetected:!1});var id=m8(),sd,ed;(ed=(sd=Y).__VUE_DEVTOOLS_KIT_CONTEXT__)!=null||(sd.__VUE_DEVTOOLS_KIT_CONTEXT__={hooks:id,get state(){return{...qi,activeAppRecordId:Xi.id,activeAppRecord:Xi.value,appRecords:bn.value}},api:$8(id)});var Ft=Y.__VUE_DEVTOOLS_KIT_CONTEXT__;S();$v(Hv());var td,ad;(ad=(td=Y).__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__)!=null||(td.__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__={id:0,appIds:new Set});S();function q8(i){qi.highPerfModeEnabled=i??!qi.highPerfModeEnabled,!i&&Xi.value&&Ac(Xi.value.app)}S();S();S();function H8(i){qi.devtoolsClientDetected={...qi.devtoolsClientDetected,...i};const s=Object.values(qi.devtoolsClientDetected).some(Boolean);q8(!s)}var nd,ld;(ld=(nd=Y).__VUE_DEVTOOLS_UPDATE_CLIENT_DETECTED__)!=null||(nd.__VUE_DEVTOOLS_UPDATE_CLIENT_DETECTED__=H8);S();S();S();S();S();S();S();var U8=class{constructor(){this.keyToValue=new Map,this.valueToKey=new Map}set(i,s){this.keyToValue.set(i,s),this.valueToKey.set(s,i)}getByKey(i){return this.keyToValue.get(i)}getByValue(i){return this.valueToKey.get(i)}clear(){this.keyToValue.clear(),this.valueToKey.clear()}},yc=class{constructor(i){this.generateIdentifier=i,this.kv=new U8}register(i,s){this.kv.getByValue(i)||(s||(s=this.generateIdentifier(i)),this.kv.set(s,i))}clear(){this.kv.clear()}getIdentifier(i){return this.kv.getByValue(i)}getValue(i){return this.kv.getByKey(i)}},z8=class extends yc{constructor(){super(i=>i.name),this.classToAllowedProps=new Map}register(i,s){typeof s=="object"?(s.allowProps&&this.classToAllowedProps.set(i,s.allowProps),super.register(i,s.identifier)):super.register(i,s)}getAllowedProps(i){return this.classToAllowedProps.get(i)}};S();S();function W8(i){if("values"in Object)return Object.values(i);const s=[];for(const e in i)i.hasOwnProperty(e)&&s.push(i[e]);return s}function G8(i,s){const e=W8(i);if("find"in e)return e.find(s);const t=e;for(let a=0;as(t,e))}function Wa(i,s){return i.indexOf(s)!==-1}function rd(i,s){for(let e=0;es.isApplicable(i))}findByName(i){return this.transfomers[i]}};S();S();var J8=i=>Object.prototype.toString.call(i).slice(8,-1),mc=i=>typeof i>"u",Y8=i=>i===null,ta=i=>typeof i!="object"||i===null||i===Object.prototype?!1:Object.getPrototypeOf(i)===null?!0:Object.getPrototypeOf(i)===Object.prototype,Ll=i=>ta(i)&&Object.keys(i).length===0,fe=i=>Array.isArray(i),X8=i=>typeof i=="string",Q8=i=>typeof i=="number"&&!isNaN(i),Z8=i=>typeof i=="boolean",iA=i=>i instanceof RegExp,aa=i=>i instanceof Map,na=i=>i instanceof Set,Bc=i=>J8(i)==="Symbol",sA=i=>i instanceof Date&&!isNaN(i.valueOf()),eA=i=>i instanceof Error,hd=i=>typeof i=="number"&&isNaN(i),tA=i=>Z8(i)||Y8(i)||mc(i)||Q8(i)||X8(i)||Bc(i),aA=i=>typeof i=="bigint",nA=i=>i===1/0||i===-1/0,lA=i=>ArrayBuffer.isView(i)&&!(i instanceof DataView),rA=i=>i instanceof URL;S();var fc=i=>i.replace(/\./g,"\\."),el=i=>i.map(String).map(fc).join("."),Wt=i=>{const s=[];let e="";for(let a=0;anull,()=>{}),Vs(aA,"bigint",i=>i.toString(),i=>typeof BigInt<"u"?BigInt(i):(console.error("Please add a BigInt polyfill."),i)),Vs(sA,"Date",i=>i.toISOString(),i=>new Date(i)),Vs(eA,"Error",(i,s)=>{const e={name:i.name,message:i.message};return s.allowedErrorProps.forEach(t=>{e[t]=i[t]}),e},(i,s)=>{const e=new Error(i.message);return e.name=i.name,e.stack=i.stack,s.allowedErrorProps.forEach(t=>{e[t]=i[t]}),e}),Vs(iA,"regexp",i=>""+i,i=>{const s=i.slice(1,i.lastIndexOf("/")),e=i.slice(i.lastIndexOf("/")+1);return new RegExp(s,e)}),Vs(na,"set",i=>[...i.values()],i=>new Set(i)),Vs(aa,"map",i=>[...i.entries()],i=>new Map(i)),Vs(i=>hd(i)||nA(i),"number",i=>hd(i)?"NaN":i>0?"Infinity":"-Infinity",Number),Vs(i=>i===0&&1/i===-1/0,"number",()=>"-0",Number),Vs(rA,"URL",i=>i.toString(),i=>new URL(i))];function Fn(i,s,e,t){return{isApplicable:i,annotation:s,transform:e,untransform:t}}var bc=Fn((i,s)=>Bc(i)?!!s.symbolRegistry.getIdentifier(i):!1,(i,s)=>["symbol",s.symbolRegistry.getIdentifier(i)],i=>i.description,(i,s,e)=>{const t=e.symbolRegistry.getValue(s[1]);if(!t)throw new Error("Trying to deserialize unknown symbol");return t}),hA=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,Uint8ClampedArray].reduce((i,s)=>(i[s.name]=s,i),{}),Fc=Fn(lA,i=>["typed-array",i.constructor.name],i=>[...i],(i,s)=>{const e=hA[s[1]];if(!e)throw new Error("Trying to deserialize unknown typed array");return new e(i)});function _c(i,s){return i!=null&&i.constructor?!!s.classRegistry.getIdentifier(i.constructor):!1}var Cc=Fn(_c,(i,s)=>["class",s.classRegistry.getIdentifier(i.constructor)],(i,s)=>{const e=s.classRegistry.getAllowedProps(i.constructor);if(!e)return{...i};const t={};return e.forEach(a=>{t[a]=i[a]}),t},(i,s,e)=>{const t=e.classRegistry.getValue(s[1]);if(!t)throw new Error("Trying to deserialize unknown class - check https://github.com/blitz-js/superjson/issues/116#issuecomment-773996564");return Object.assign(Object.create(t.prototype),i)}),wc=Fn((i,s)=>!!s.customTransformerRegistry.findApplicable(i),(i,s)=>["custom",s.customTransformerRegistry.findApplicable(i).name],(i,s)=>s.customTransformerRegistry.findApplicable(i).serialize(i),(i,s,e)=>{const t=e.customTransformerRegistry.findByName(s[1]);if(!t)throw new Error("Trying to deserialize unknown custom value");return t.deserialize(i)}),pA=[Cc,bc,wc,Fc],pd=(i,s)=>{const e=rd(pA,a=>a.isApplicable(i,s));if(e)return{value:e.transform(i,s),type:e.annotation(i,s)};const t=rd(Ec,a=>a.isApplicable(i,s));if(t)return{value:t.transform(i,s),type:t.annotation}},Dc={};Ec.forEach(i=>{Dc[i.annotation]=i});var dA=(i,s,e)=>{if(fe(s))switch(s[0]){case"symbol":return bc.untransform(i,s,e);case"class":return Cc.untransform(i,s,e);case"custom":return wc.untransform(i,s,e);case"typed-array":return Fc.untransform(i,s,e);default:throw new Error("Unknown transformation: "+s)}else{const t=Dc[s];if(!t)throw new Error("Unknown transformation: "+s);return t.untransform(i,e)}};S();var Ke=(i,s)=>{const e=i.keys();for(;s>0;)e.next(),s--;return e.next().value};function xc(i){if(Wa(i,"__proto__"))throw new Error("__proto__ is not allowed as a property");if(Wa(i,"prototype"))throw new Error("prototype is not allowed as a property");if(Wa(i,"constructor"))throw new Error("constructor is not allowed as a property")}var oA=(i,s)=>{xc(s);for(let e=0;e{if(xc(s),s.length===0)return e(i);let t=i;for(let n=0;nPl(n,s,[...e,...Wt(l)]));return}const[t,a]=i;a&&kt(a,(n,l)=>{Pl(n,s,[...e,...Wt(l)])}),s(t,e)}function kA(i,s,e){return Pl(s,(t,a)=>{i=Il(i,a,n=>dA(n,t,e))}),i}function cA(i,s){function e(t,a){const n=oA(i,Wt(a));t.map(Wt).forEach(l=>{i=Il(i,l,()=>n)})}if(fe(s)){const[t,a]=s;t.forEach(n=>{i=Il(i,Wt(n),()=>i)}),a&&kt(a,e)}else kt(s,e);return i}var uA=(i,s)=>ta(i)||fe(i)||aa(i)||na(i)||_c(i,s);function gA(i,s,e){const t=e.get(i);t?t.push(s):e.set(i,[s])}function vA(i,s){const e={};let t;return i.forEach(a=>{if(a.length<=1)return;s||(a=a.map(r=>r.map(String)).sort((r,h)=>r.length-h.length));const[n,...l]=a;n.length===0?t=l.map(el):e[el(n)]=l.map(el)}),t?Ll(e)?[t]:[t,e]:Ll(e)?void 0:e}var Sc=(i,s,e,t,a=[],n=[],l=new Map)=>{var r;const h=tA(i);if(!h){gA(i,a,s);const v=l.get(i);if(v)return t?{transformedValue:null}:v}if(!uA(i,e)){const v=pd(i,e),m=v?{transformedValue:v.value,annotations:[v.type]}:{transformedValue:i};return h||l.set(i,m),m}if(Wa(n,i))return{transformedValue:null};const d=pd(i,e),o=(r=d==null?void 0:d.value)!=null?r:i,k=fe(o)?[]:{},c={};kt(o,(v,m)=>{if(m==="__proto__"||m==="constructor"||m==="prototype")throw new Error(`Detected property ${m}. This is a prototype pollution risk, please remove it from your object.`);const B=Sc(v,s,e,t,[...a,m],[...n,i],l);k[m]=B.transformedValue,fe(B.annotations)?c[m]=B.annotations:ta(B.annotations)&&kt(B.annotations,(f,b)=>{c[fc(m)+"."+b]=f})});const u=Ll(c)?{transformedValue:k,annotations:d?[d.type]:void 0}:{transformedValue:k,annotations:d?[d.type,c]:c};return h||l.set(i,u),u};S();S();function Tc(i){return Object.prototype.toString.call(i).slice(8,-1)}function dd(i){return Tc(i)==="Array"}function AA(i){if(Tc(i)!=="Object")return!1;const s=Object.getPrototypeOf(i);return!!s&&s.constructor===Object&&s===Object.prototype}function yA(i,s,e,t,a){const n={}.propertyIsEnumerable.call(t,s)?"enumerable":"nonenumerable";n==="enumerable"&&(i[s]=e),a&&n==="nonenumerable"&&Object.defineProperty(i,s,{value:e,enumerable:!1,writable:!0,configurable:!0})}function Rl(i,s={}){if(dd(i))return i.map(a=>Rl(a,s));if(!AA(i))return i;const e=Object.getOwnPropertyNames(i),t=Object.getOwnPropertySymbols(i);return[...e,...t].reduce((a,n)=>{if(dd(s.props)&&!s.props.includes(n))return a;const l=i[n],r=Rl(l,s);return yA(a,n,r,i,s.nonenumerable),a},{})}var Ci=class{constructor({dedupe:i=!1}={}){this.classRegistry=new z8,this.symbolRegistry=new yc(s=>{var e;return(e=s.description)!=null?e:""}),this.customTransformerRegistry=new K8,this.allowedErrorProps=[],this.dedupe=i}serialize(i){const s=new Map,e=Sc(i,s,this,this.dedupe),t={json:e.transformedValue};e.annotations&&(t.meta={...t.meta,values:e.annotations});const a=vA(s,this.dedupe);return a&&(t.meta={...t.meta,referentialEqualities:a}),t}deserialize(i){const{json:s,meta:e}=i;let t=Rl(s);return e!=null&&e.values&&(t=kA(t,e.values,this)),e!=null&&e.referentialEqualities&&(t=cA(t,e.referentialEqualities)),t}stringify(i){return JSON.stringify(this.serialize(i))}parse(i){return this.deserialize(JSON.parse(i))}registerClass(i,s){this.classRegistry.register(i,s)}registerSymbol(i,s){this.symbolRegistry.register(i,s)}registerCustom(i,s){this.customTransformerRegistry.register({name:s,...i})}allowErrorProps(...i){this.allowedErrorProps.push(...i)}};Ci.defaultInstance=new Ci;Ci.serialize=Ci.defaultInstance.serialize.bind(Ci.defaultInstance);Ci.deserialize=Ci.defaultInstance.deserialize.bind(Ci.defaultInstance);Ci.stringify=Ci.defaultInstance.stringify.bind(Ci.defaultInstance);Ci.parse=Ci.defaultInstance.parse.bind(Ci.defaultInstance);Ci.registerClass=Ci.defaultInstance.registerClass.bind(Ci.defaultInstance);Ci.registerSymbol=Ci.defaultInstance.registerSymbol.bind(Ci.defaultInstance);Ci.registerCustom=Ci.defaultInstance.registerCustom.bind(Ci.defaultInstance);Ci.allowErrorProps=Ci.defaultInstance.allowErrorProps.bind(Ci.defaultInstance);S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();S();var od,kd;(kd=(od=Y).__VUE_DEVTOOLS_KIT_MESSAGE_CHANNELS__)!=null||(od.__VUE_DEVTOOLS_KIT_MESSAGE_CHANNELS__=[]);var cd,ud;(ud=(cd=Y).__VUE_DEVTOOLS_KIT_RPC_CLIENT__)!=null||(cd.__VUE_DEVTOOLS_KIT_RPC_CLIENT__=null);var gd,vd;(vd=(gd=Y).__VUE_DEVTOOLS_KIT_RPC_SERVER__)!=null||(gd.__VUE_DEVTOOLS_KIT_RPC_SERVER__=null);var Ad,yd;(yd=(Ad=Y).__VUE_DEVTOOLS_KIT_VITE_RPC_CLIENT__)!=null||(Ad.__VUE_DEVTOOLS_KIT_VITE_RPC_CLIENT__=null);var md,Bd;(Bd=(md=Y).__VUE_DEVTOOLS_KIT_VITE_RPC_SERVER__)!=null||(md.__VUE_DEVTOOLS_KIT_VITE_RPC_SERVER__=null);var fd,Ed;(Ed=(fd=Y).__VUE_DEVTOOLS_KIT_BROADCAST_RPC_SERVER__)!=null||(fd.__VUE_DEVTOOLS_KIT_BROADCAST_RPC_SERVER__=null);S();S();S();S();S();S();S();const mA=JSON.parse('{"encrypt":{"config":{"/guide/encrypt.html":["$2a$10$cvJDoGQ8QAtAKCLY9PZ5ieBuUx26i1f2eDEEG1rEO0DbEUbBR9QMW"],"/other/windows/系统安装.html":["$2a$10$pTOssQOpoUiznPZnjSK/NO6wABwf1k7xCVja7LPOT5SdOTEjrSTSW"]}},"fullscreen":true,"author":{"name":"ChenSino","url":"https://ChenSino.github.io"},"logo":"/logo.svg","repo":"ChenSino/ChenSino.github.io","docsDir":"docs","docsBranch":"dev","footer":"鄂ICP备2024079959号-1 鄂公网安备42018502007734号","copyright":"","displayFooter":true,"themeColor":true,"pageInfo":["Author","Original","Date","Category","Tag","ReadingTime"],"blog":{"name":"ChenSino","avatar":"https://ddns.chensina.cn:29000/afatpig/blog/20220802180305.png","description":"洛星星的笔记","intro":"https://chensina.cn/","medias":{"Baidu":"https://example.com","Weibo":"https://example.com","Zhihu":"https://example.com"}},"locales":{"/":{"lang":"zh-CN","navbarLocales":{"langName":"简体中文","selectLangAriaLabel":"选择语言"},"metaLocales":{"author":"作者","date":"写作日期","origin":"原创","views":"访问量","category":"分类","tag":"标签","readingTime":"阅读时间","words":"字数","toc":"此页内容","prev":"上一页","next":"下一页","lastUpdated":"上次编辑于","contributors":"贡献者","editLink":"编辑此页","print":"打印"},"blogLocales":{"article":"文章","articleList":"文章列表","category":"分类","tag":"标签","timeline":"时间轴","timelineTitle":"昨日不在","all":"全部","intro":"个人介绍","star":"星标","empty":"$text 为空"},"paginationLocales":{"prev":"上一页","next":"下一页","navigate":"跳转到","action":"前往","errorText":"请输入 1 到 $page 之前的页码!"},"outlookLocales":{"themeColor":"主题色","darkmode":"外观","fullscreen":"全屏"},"encryptLocales":{"iconLabel":"文章已加密","placeholder":"输入密码","remember":"记住密码","errorHint":"请输入正确的密码"},"routeLocales":{"skipToContent":"跳至主要內容","notFoundTitle":"页面不存在","notFoundMsg":["这里什么也没有","我们是怎么来到这儿的?","这 是 四 零 四 !","看起来你访问了一个失效的链接"],"back":"返回上一页","home":"带我回家"},"navbar":["/home","/",{"text":"Java","icon":"java","link":"/java/"},{"text":"前端","icon":"javascript","link":"/frontweb/"},{"text":"设计模式","icon":"java","link":"/designpattern/"},{"text":"C++学习","icon":"app","link":"/cpp/study/"},{"text":"家庭服务器","icon":"app","link":"/myserver/"},{"text":"其他","icon":"others","link":"/other/"}],"sidebar":{"/java/":[{"text":"Java 基础","icon":"discover","prefix":"base/","collapsible":true,"children":"structure"},{"text":"Java 进阶","icon":"blog","prefix":"advance/","collapsible":true,"children":"structure"},{"text":"Java 虚拟机","icon":"write","prefix":"jvm/","collapsible":true,"children":"structure"},{"text":"Java框架","icon":"write","prefix":"framework/","collapsible":true,"children":"structure"},{"text":"Java其他","icon":"java","prefix":"other/","collapsible":true,"children":"structure"}],"/frontweb/":[{"text":"Vue","icon":"vue","prefix":"vue/","collapsible":true,"children":"structure"},{"text":"Vite","icon":"vue","prefix":"vite/","collapsible":true,"children":[]},{"text":"ECMAScript 5","icon":"write","prefix":"es5/","collapsible":true,"children":"structure"},{"text":"ECMAScript 6","icon":"blog","prefix":"es6/","collapsible":true,"children":"structure"},{"text":"TypeScript","icon":"typescript","prefix":"typeScript/","collapsible":true,"children":"structure"},{"text":"NodeJS","icon":"nodejs","prefix":"nodejs/","collapsible":true,"children":"structure"}],"/designpattern/":"structure","/cpp/":[{"text":"基础语法学习","icon":"app","prefix":"study/","collapsible":true,"children":"structure"},{"text":"其他","icon":"app","prefix":"other/","collapsible":true,"children":"structure"}],"/myserver/":"structure","/other/":[{"text":"Git","icon":"git","prefix":"git/","collapsible":true,"children":"structure"},{"text":"Linux","icon":"linux","prefix":"linux/","collapsible":true,"children":"structure"},{"text":"数据库","icon":"repo","prefix":"database/","collapsible":true,"children":"structure"},{"text":"MarkDown","icon":"markdown","prefix":"markdown/","collapsible":true,"children":"structure"},{"text":"工具软件","icon":"software","prefix":"tools/","collapsible":true,"children":"structure"},{"text":"小组分享","icon":"share","prefix":"training/","collapsible":true,"children":"structure"},{"text":"随笔分享","icon":"share","prefix":"essay/","collapsible":true,"children":"structure"},{"text":"web","icon":"share","prefix":"web/","collapsible":true,"children":"structure"},{"text":"分布式微服务","icon":"share","prefix":"distributeservice/","collapsible":true,"children":"structure"},{"text":"OAuth2.0","icon":"share","prefix":"oauth2/","collapsible":true,"children":"structure"},{"text":"Docker","icon":"ubuntu","prefix":"docker/","collapsible":true,"children":"structure"},{"text":"PVE","icon":"class","prefix":"pve/","collapsible":true,"children":"structure"}]}}}}'),BA=Z(mA),Oc=()=>BA,Lc=Symbol(""),fA=()=>{const i=Si(Lc);if(!i)throw new Error("useThemeLocaleData() is called without provider.");return i},EA=(i,s)=>{const{locales:e,...t}=i;return{...t,...e==null?void 0:e[s]}},bA=js({enhance({app:i}){const s=Oc(),e=i._context.provides[Br],t=F(()=>EA(s.value,e.routeLocale.value));i.provide(Lc,t),Object.defineProperties(i.config.globalProperties,{$theme:{get(){return s.value}},$themeLocale:{get(){return t.value}}}),R8({app:i,id:"org.vuejs.vuepress.plugin-theme-data",label:"VuePress Theme Data Plugin",packageName:"@vuepress/plugin-theme-data",homepage:"https://v2.vuepress.vuejs.org",logo:"https://v2.vuepress.vuejs.org/images/hero.png",componentStateTypes:["VuePress"]},a=>{a.on.inspectComponent(n=>{n.instanceData.state.push({type:"VuePress",key:"themeData",editable:!1,value:s.value},{type:"VuePress",key:"themeLocaleData",editable:!1,value:t.value})})})}}),FA=Object.freeze(Object.defineProperty({__proto__:null,default:bA},Symbol.toStringTag,{value:"Module"})),_A=/language-(shellscript|shell|bash|sh|zsh)/,CA=({delay:i=500,duration:s=2e3,locales:e,selector:t,showInMobile:a,ignoreSelector:n=[],transform:l})=>{const r=br("(max-width: 419px)"),h=F(()=>!r.value||a),d=fa(e),o=xi(),k=B=>{var b;if(B.hasAttribute("copy-code"))return;const f=document.createElement("button");f.type="button",f.classList.add("vp-copy-code-button"),f.setAttribute("aria-label",d.value.copy),f.setAttribute("data-copied",d.value.copied),(b=B.parentElement)==null||b.insertBefore(f,B),B.setAttribute("copy-code","")};ui(()=>[o.value.path,h.value],async()=>{document.body.classList.toggle("no-copy-code",!h.value),h.value&&(await zs(),await Cr(i),document.querySelectorAll(t.join(",")).forEach(k))},{immediate:!0});const{copy:u}=p4({legacy:!0}),v=new WeakMap,m=async(B,f,b)=>{const A=f.cloneNode(!0);n.length&&A.querySelectorAll(n.join(",")).forEach(M=>{M.remove()}),l&&l(A);let _=A.textContent||"";if(_A.test(B.className)&&(_=_.replace(/^ *(\$|>) /gm,"")),await u(_),s<=0)return;b.classList.add("copied"),clearTimeout(v.get(b));const L=setTimeout(()=>{b.classList.remove("copied"),b.blur(),v.delete(b)},s);v.set(b,L)};Ni("click",B=>{const f=B.target;if(h.value&&f.matches('div[class*="language-"] > button.vp-copy-code-button')){const b=f.parentElement,A=f.nextElementSibling;if(!b||!A)return;m(b,A,f)}})};var wA=[],DA={"/":{copy:"复制代码",copied:"已复制"}},xA=['[vp-content] div[class*="language-"] pre'];const SA=js({setup:()=>{CA({selector:xA,ignoreSelector:wA,locales:DA,duration:2e3,delay:500,showInMobile:!1})}}),TA=Object.freeze(Object.defineProperty({__proto__:null,default:SA},Symbol.toStringTag,{value:"Module"})),OA=js({setup(){Ni("beforeprint",()=>{document.querySelectorAll("details").forEach(i=>{i.open=!0})})}}),LA=Object.freeze(Object.defineProperty({__proto__:null,default:OA},Symbol.toStringTag,{value:"Module"})),IA=Object.freeze(Object.defineProperty({__proto__:null},Symbol.toStringTag,{value:"Module"})),PA='',RA='';var jA={useBabel:!1,jsLib:[],cssLib:[],codepenLayout:"left",codepenEditors:"101",babel:"https://unpkg.com/@babel/standalone/babel.min.js",vue:"https://unpkg.com/vue/dist/vue.global.prod.js",react:"https://unpkg.com/react/umd/react.production.min.js",reactDOM:"https://unpkg.com/react-dom/umd/react-dom.production.min.js"};const tl=jA,bd={html:{types:["html","slim","haml","md","markdown","vue"],map:{html:"none",vue:"none",md:"markdown"}},js:{types:["js","javascript","coffee","coffeescript","ts","typescript","ls","livescript"],map:{js:"none",javascript:"none",coffee:"coffeescript",ls:"livescript",ts:"typescript"}},css:{types:["css","less","sass","scss","stylus","styl"],map:{css:"none",styl:"stylus"}}},MA=(i,s,e)=>{const t=document.createElement(i);return Bt(s)&&Is(s).forEach(a=>{if(a.indexOf("data"))t[a]=s[a];else{const n=a.replace("data","");t.dataset[n]=s[a]}}),t},Vr=i=>({...tl,...i,jsLib:Array.from(new Set([tl.jsLib??[],i.jsLib??[]].flat())),cssLib:Array.from(new Set([tl.cssLib??[],i.cssLib??[]].flat()))}),et=(i,s)=>{if(wr(i[s]))return i[s];const e=new Promise(t=>{var n;const a=document.createElement("script");a.src=s,(n=document.querySelector("body"))==null||n.appendChild(a),a.onload=()=>{t()}});return i[s]=e,e},VA=(i,s)=>{if(s.css&&Array.from(i.childNodes).every(e=>e.nodeName!=="STYLE")){const e=MA("style",{innerHTML:s.css});i.appendChild(e)}},NA=(i,s,e)=>{const t=e.getScript();if(t&&Array.from(s.childNodes).every(a=>a.nodeName!=="SCRIPT")){const a=document.createElement("script");a.appendChild(document.createTextNode(`{const document=window.document.querySelector('#${i} .vp-code-demo-display').shadowRoot; ${t}}`)),s.appendChild(a)}},$A=["html","js","css"],qA=i=>{const s=Is(i),e={html:[],js:[],css:[],isLegal:!1};return $A.forEach(t=>{const a=s.filter(n=>bd[t].types.includes(n));if(a.length){const n=a[0];e[t]=[i[n].replace(/^\n|\n$/g,""),bd[t].map[n]??n]}}),e.isLegal=(!e.html.length||e.html[1]==="none")&&(!e.js.length||e.js[1]==="none")&&(!e.css.length||e.css[1]==="none"),e},Ic=i=>i.replace(/
    /g,"
    ").replace(/<((\S+)[^<]*?)\s+\/>/g,"<$1>"),Pc=i=>`
    ${Ic(i)}
    `,HA=i=>`${i.replace("export default ","const $reactApp = ").replace(/App\.__style__(\s*)=(\s*)`([\s\S]*)?`/,"")}; -ReactDOM.createRoot(document.getElementById("app")).render(React.createElement($reactApp))`,UA=i=>i.replace(/export\s+default\s*\{(\n*[\s\S]*)\n*\}\s*;?$/u,"Vue.createApp({$1}).mount('#app')").replace(/export\s+default\s*define(Async)?Component\s*\(\s*\{(\n*[\s\S]*)\n*\}\s*\)\s*;?$/u,"Vue.createApp({$1}).mount('#app')").trim(),Rc=i=>`(function(exports){var module={};module.exports=exports;${i};return module.exports.__esModule?exports.default:module.exports;})({})`,zA=(i,s)=>{const e=Vr(s),t=i.js[0]??"";return{...e,html:Ic(i.html[0]??""),js:t,css:i.css[0]??"",isLegal:i.isLegal,getScript:()=>{var a;return e.useBabel?((a=window.Babel.transform(t,{presets:["es2015"]}))==null?void 0:a.code)??"":t}}},WA=/