跨域SSO的实现之一:架构设计
翻译自CodeProject网站ASP.NET9月份最佳文章Single Sign On (SSO) for cross-domain ASP.NET applications。翻译不妥之处还望大家多多指导、相互交流。文章分为两部分架构设计和程序实现此为第一篇即架构设计或者叫设计蓝图Part-I - The design blue print。:)简介周一的早晨当你正在纳闷周末咋就一眨眼过去了并对接下来漫长的一周感到无比蛋疼之时你收到了一份Email。操蛋的是它既不是微软的offer也不是Google的offer而是客户发来的一个新需求。他说你们现在帮我们公司做了很多的ASP.NET的网站和忽悠我们上线的各种系统现在我想要我的客户只要在我们拥有的任何一个网站上登录一次那么在我所有的网站上该用户就都已经登录了同样随便他从哪个网站上注销掉那么他也就从我们所有的网站上注销了......你受不了客户这么罗嗦了心想不就是要一个SSO功能吗使用ASP.NET的form authentication不就可以实现了因为这样可以在同域的不用网站下共享cookie只需要在machineKey设置一样的配置节就可以了。放狗一搜果然有xxxx条结果。放狗找东西可是我们程序员的特长。开工前你又扫了一眼邮件等等你看到了邮件中的一行话微微一蛋疼我们部署了那些网站但不是都在同一个域名下。你的客户狠狠地给你来了个下马威好像他早就放狗搜过因为cookie不能跨域共享也就不能用来实现跨域验证了。这到底是神马一回事情和老外一样扯玩淡下面正经些ASP.NET中的验证原理这个问题可能是老生常谈了但在解决难题之前还是先回归基础来看一看事物的本质到底是如何的。因此我们重温一下ASP.NET表单验证的原理也并不坏。下面是ASP.NET表单验证的流程图验证流程1你访问一个需要用户验证的ASP.NET页面2在此请求中ASP.NET运行时开始查找cookie(由于表单验证的cookie)如果没有查找到那么将跳转到登录页面(登录页地址配置在了web.config文件中)3在登录页面中你提供了相关的验证凭证并点击了登录按钮系统和已存储的数据对比验证成功后将Thread.CurrentPrincipal.Identity.Name的属性值设置成了你提供的用户名并在Response中写入了cookie(同时还写入了用户信息和一些如cookie名失效日期等)并重定向到登录前的页面。4当你再点击其他的页面(或者点击导航到其他的页面)浏览器发送验证的cookie(也可能包含在该网站下写入的一些其他cookie)这一次已经包含了在上一次response中上次验证获取到的cookie。5和以前一样ASP.NET运行时在请求中查找验证的cookie这一次找到了接下来做一些检查(如失效日期、路径等等)如果还没有失效那么读取出它的值恢复出用户的信息将Thread.CurrentPrincipal.Identity.Name的属性值设置成恢复出的用户名检查该用户是否有权限去访问当前请求的页面如果有那么页面执行的结果返回到用户的浏览器。过程很简单对吗ASP.NET中多站点同域下的验证原理如前所述ASP.NET表单验证完全依赖于cookie。那么只要使得不同的站点共享同样的验证cookie那么就可以实现在一个站点登录实现所有站点的登录。HTTP协议指出如果两个站点是同域(或者是子域)的那么可以共享cookie。本地的处理是浏览器根据网站的URL存储cookie在本地(磁盘或者内存中)。当你请求接下来的任意页面时浏览器读取和当前请求的URL匹配的域或子域的cookies并将此cookies包含在当前的请求中。现在我们假设有下面两个网站www.mydomain.com/site1www.mydomain.com/site2这两个站点共享同样的主机地址(同样的域mydomain.com和子域www)且两个站点都被配置成了对用户验证和授权都使用表单验证。假设你已经登录过了站点www.mydomain.com/site1如前所述你的浏览器现在对于站点www.mydomain.com/site1已经有了表单验证的cookie。现在你随意访问以www.mydomain.com/site1开头的URL表单验证的cookie都将被包含在请求被发送。为什么是因为此cookie本来就属于该站点吗对的但不是完全正确。事实上是因为请求的URLwww.mydomain.com/site1和http://www.mydomain.com/拥有同样的域名和子域名。那么在你登录了www.mydomain.com/site1后如果你点击www.mydomain.com/site2下的URL表单验证的cookie也将被包含在请求中发送这同样是因为www.mydomain.com/site2与站点http://www.mydomain.com/拥有同样的域名和子域名尽管它是不一样的应用站点(site2)。显然在拥有一样主机地址不一样的应用站点名之间是可以共享表单验证cookie的这样就实现了一处登录处处都已经登录的功能(也就是单点登录)。然而ASP.NET没有允许你仅仅通过将同主机地址下的站点部署上表单验证后就自动完成了单点登录。为什么这样呢因为每一个不同的ASP.NET web应用程序使用它自己的密钥去加密和加密cookie(还有诸如ViewState之类的)从而确保了安全。除非你给每一个站点指定了同样的加密密钥那么cookies将被发送但是另一个应用站点不能够读取验证cookies的值。指定同样的验证密钥可以解决这个问题。为每一个ASP.NET应用站点使用同样的machinekey配置节即可如下machineKeyvalidationKey21F090935F6E49C2C797F69BBAAD8402ABD2EE0B667A8B44EA7DD4374267A75DdecryptionKeyABAA84D7EC4BB56D75D217CECFFB9628809BDB8BF91CFCD64568A145BE59719FvalidationSHA1decryptionAES/如果同样的machinekey(包括validationKey和decryptionKey)被用在同域下的所有应用站点时就可以实现了跨站点读取cookie。如果是同样的域不同的子域呢假定你有下面两个站点site1.mydomain.comsite2.mydomain.com这两个站点共享同样的域(同样的二级域名mydomain.com)但拥有不一样的三级域名(不一样的子域site1和site2)。默认情况下浏览器仅仅发送主机地址一样(相同的域和子域)的站点的cookie。因此站点site1.mydomain.com不能获取到站点site2.mydomain.com下的cookie(因为他们没有相同的主机地址它们的子域不同)尽管你为这两个站点配置了相同的machineKey一个站点还是不能获取另一个站点下的cookie。除了你为所有的站点配置了一样的machineKey你还需要为验证cookie定义相同的域以使得浏览器在同样的域名下能够发送任何请求。你需要像下面这样配置表单验证cookieformsnamenameloginUrlURLdefaultUrlURLdomainmydomain.com/那么在不用的域下如何去共享验证cookie呢?显然这是不可能的因为HTTP协议基于安全的原因阻止了你在不同的域之间共享cookie。同样假设有下面这两个域名http://www.domain1.com/http://www.domain2.com/如果你使用表单验证登录进了http://www.domain1.com/当你点击http://www.domain2.com/下的URL时浏览器将不能发送domain1.com的cookie到domain2.com。在ASP.NET中没有内置的方法去完成在两个不同的站点间实现单点登录。要在两个站点间通过访问同样的cookie来实现单点登录还真没有什么高级的技巧或即有的架构模型去解决它。跨域单点登录设计雏形假设有下面三个站点http://www.domain1.com/http://www.domain2.com/http://www.domain3.com/为了实现在这些站之间实现SSO当用户在任意一个站登录时我们需要为所有的站点设置验证cookie。如果用户1登录进http://www.domain1.com/那么在给站点1response前会在response中加入验证的cookie但当我们需要同时能够登录进http://www.domain2.com/和http://www.domain3.com/时我们需要同时在同样的客户端浏览器上为站点2和站点3设置验证cookie。因此在response返回到浏览器前站点1不得不定向到站点2和站点3去设置验证cookie。下面的流程图详细描述了思路操作流程请求 http://www.domain1.com/中一个需要验证的页面状态浏览器没有验证cookie浏览器发送一个请求到 http://www.domain1.com/但请求中没有验证cookie(因为还没有属于 http://www.domain1.com/的cookie)。状态浏览器没有验证cookie因为请求中没有验证cookie所以请求 http://www.domain1.com/的登录页面状态浏览器没有验证cookie用户提供登录凭证点击登录按钮浏览器发送一个POST请求到 http://www.domain1.com/http://www.domain1.com/验证用户提供的登录凭证验证通过后标记用户的状态为已登录添加验证的cookie和其他的用户信息一起添加在response中状态浏览器没有验证cookieresponse并没有返回给浏览器而是将请求重定向到 http://www.domain2.com/的一个页面并将ReturnUrl设置成重定向前 http://www.domain1.com/的URL值。在验证cookie被包含在了response中了后cookie被发送给浏览器。状态浏览器没有验证cookie浏览器接收到了包含验证cookie的response和重定向到 http://www.domain2.com/的命令。浏览器存储了 http://www.domain2.com/的验证cookie并向 http://www.domain2.com/发送请求。状态浏览器包含了http://www.domain2.com/的验证cookiehttp://www.domain2.com/立即再重定向到存储在ReturnUrl中的URL地址在此请求中读取cookie值并为 http://www.domain1.com/设置验证cookie。最终在重定向的命令中也包含了这些验证cookie。状态浏览器包含了http://www.domain2.com/的验证cookie浏览器接收到包含了验证cookie的重定向命令跳转到 http://www.domain1.com/。现在浏览器存储了站点1的验证cookie并开始请求站点1当然在请求中包含了验证cookie。状态浏览器包含了http://www.domain1.com/和http://www.domain2.com/的验证cookie站点1检查了请求中包含了验证cookie就不需要再去跳转到验证页面去验证而是返回用户请求的页面状态浏览器包含了http://www.domain1.com/和http://www.domain2.com/的验证cookie如果此时用户请求站点2 因为浏览器已经存储了站点2的验证cookiecookie将被包含在请求中站点2从cookie中获取到用户信息并为此用户返回请求的页面。当浏览器验证了站点2和站点3后那么用户就已经登录了所有的站点这样就完成了一次单点登录。如何单点注销作为单点登录的一部分我们还需要去关注下单点注销就是说当用户在一个站点注销后那么就认为他从所有的站点都注销了。清除所有站点的cookie和上面登录一样也是请求-重定向-返回的过程。只是和设置验证cookie不一样的是这次从response中移除验证cookie。此单点登录模型的缺点这个模型在两个站点上还是能运行的很好的。从一个站点登录或注销此SSO模型下的站点都将遵从请求-重定向-返回的流程。当用户登录任一页面时因为已经存储了所有站点的验证cookie那么就不需要再执行上面的那个循环的流程了。但是当站点超过两个时问题就变得复杂了当登录站点1时程序将重定向到站点2和站点3进行验证cookie的设置最后站点3在跳转到站点1服务器返回用户请求的页面。这使得每个站点的登录和注销的过程变得复杂并花费较高的代价。如何超过3个站点呢如果这样去设计20站点的单点登录呢这个模型将完全不能胜任了。并且此模型需要每个站点都具备用户验证逻辑因为需要来请求此站点并设置其验证cookie。因此此模型丢失了一般意义上的单点登录的概念我们需要一个更好一点的模型去实现单点登录的功能。更好的跨域单点登录架构前面提到的架构中设置移除cookie都需要跳转到N-1个站点去完成。每个站点还需要知道N-1个站点复杂的登录注销逻辑如果我们为所有的站点只去维护一份验证cookie呢使用一个独立的站点去完成验证用户并设置验证cookie的工作呢这个想法好像不错。要使用单点登录那么就需要用户的数据是统一的这样的话就可以通过一个站点提供web或者WCF服务来完成验证和授权的功能。这样就省去了冗余的用户验证逻辑现在最重要的是这个独立的站点如何在SSO架构中起作用。在这个架构模型中浏览器不存储任何其他站点的验证cookie只存那个独立站点的验证cookie我们就给它起名叫http://www.sso.com/。在此架构中对每一个站点的请求都将被直接跳转到http://www.sso.com/由于检查验证cookie是否存在。如果cookie存在如果存在返回请求的页面如果不存在那么就跳转到对应的登录页面。大致流程图如下便于理解我们假定有下面两个网站http://www.domain1.com/http://www.domain2.com/还有一个用于管理验证cookie的站点http://www.sso.com/。验证流程如下用户请求http://www.domain1.com/中一个需要验证的页面重定向到http://www.sso.com/ReturnUrl参数设置成请求站点1时的URL。http://www.sso.com/检查是否有验证cookie存在如果在请求中没有任何用户令牌存在那么请求中带着用户需要登录的指令就跳转到站点1。在query string中仍然保留着之前ReturnUrl参数的值。站点1从参数中得知是从http://www.sso.com/跳转而来且得知没有用户验证cookie最后跳转到站点1的登录页面进行登录而不跳转到http://www.sso.com/。用户提供验证信息点击登录按钮请求没有回置到站点http://www.sso.com/这时站点1通过http://www.sso.com/提供的web/WCF接口进行用户的验证如果验证成功那么为用户颁发一个令牌(可以是一个GUID)。站点1标志用户已经登录成功在session中存储用户对象一个包含了令牌的URL跳转到http://www.sso.com/设置验证cookieReturnUrl参数还是设置成前面请求的URL。http://www.sso.com/站点检查过来的URL发现有用户令牌但还没有用户验证cookie说明已经通过了站点1的认证现在需要设置站点http://www.sso.com/下的验证cookie。照例设置好了cookie后将cookie添加在response中还添加上用户令牌按照ReturnUrl参数中的URL一并返回。浏览器得知要跳转到站点1并且有了站点http://www.sso.com/的验证cookie在本地存储下sso站点的验证cookie并对站点1发起请求。站点1检查了用户令牌因为是通过站点SSO的web/WCF服务验证并通过的所以站点1返回用户请求的页面。现在用户请求站点2浏览器跳转到sso站点依然设置好ReturnUrl的值。浏览器因为要跳转到sso站点发现本地有了sso站点的验证cookie所以将cookie添加在请求中一并发出。sso站点检查cookie发现cookie还没有过期那么在query string中添加上用户令牌按照ReturnUrl返回。站点2发现有用户令牌证明已经走过验证流程那么就返回用户请求的页面。总结刚开始浏览器没有任何http://www.sso.com/站点下的验证cookie。请求站点1和站点2任何需要验证的页面(需要内部的跳转到sso站点检查验证cookie是否存在)。用户登录后sso站点的验证cookie存储在本地重要的是用户令牌仅仅用户用户登录会话时。现在请求站点1或者站点2都跳转到sso站点浏览器发送sso站点的验证cookie并检查用户令牌验证后再跳转到原始请求的URL原始站点检查用户令牌正确后返回用户请求的页面。传输代价场景1访问公共页面从浏览器到站点站点到浏览器1请求1返回场景2访问一个需要验证的页从浏览器到站点重定向到sso站点检查cookie重定向到原站点没有cookie原站点返回登录页面到浏览器1请求2跳转1返回场景3登录浏览器POST到站点调用验证服务进行用户验证浏览器跳转到SSO站点带有令牌重定向到原站点带有验证cookie通用服务验证令牌返回用户请求的需要验证的页面1请求2验证服务调用2跳转1返回场景4登录后请求一个需要验证的页面请求站点向SSO站点跳转验证cookie带有验证cookie跳转到原站点检查验证cookie调用服务验证令牌返回请求页面1请求2跳转1服务请求1返回场景5:注销请求站点进行注销请求SSO站点进行注销请求原站点移除验证cookie返回1请求2跳转1返回孰是孰非比较这两种架构第一中架构更适合两个站点最多三个站点虽然需要部署复杂冗余的验证逻辑但是随后的页面请求中就是普通的页面请求了1请求1返回。第一种架构不易于扩展且会冗余出很多的用户验证逻辑。而第二种架构不管有多少个需要进行单点登录的网站也不需要其他网站参与此过程验证的cookie只有sso站点管理这样的架构逻辑清晰、易扩且部署方便。然而有一些性能的问题不同于第一种架构这种架构当用户请求一个需要验证的页面时需要请求三次请求sso站点和原站点两次请求是内部的跳转且多的两次请求花费的时间很少空跳转请求用于设置和检查cookie在如今这样的网络环境下是可以接受的。第二种架构的程序实现等有空了来翻译完第二部分程序实现等不及的朋友可以先看原文http://www.codeproject.com/KB/aspnet/CrossDomainSSOExample.aspx程序实现源码下载

相关新闻

金融/零售/制造三大行业AI分析落地实录(含脱敏代码库+评估指标集),仅开放48小时下载权限

金融/零售/制造三大行业AI分析落地实录(含脱敏代码库+评估指标集),仅开放48小时下载权限

更多请点击: https://intelliparadigm.com 第一章:金融/零售/制造三大行业AI分析落地实录(含脱敏代码库评估指标集),仅开放48小时下载权限 在真实产业场景中,AI分析模型的价值不在于算法复杂度&#xff0c…

2026/10/16 21:07:02 阅读更多 →
决策树算法全解析:从ID3、C4.5到CART,实战调优与可视化指南

决策树算法全解析:从ID3、C4.5到CART,实战调优与可视化指南

1. 项目概述:从“如果-那么”到数据洞察 决策树,这个名字听起来就带着一种清晰的逻辑美感。我第一次接触它,是在处理一个客户分类项目时,面对一堆杂乱无章的客户数据,从年龄、收入到浏览记录、购买历史,感觉…

2026/10/17 6:27:17 阅读更多 →
ASP.NET Web API(二):安全验证之使用HTTP基本认证

ASP.NET Web API(二):安全验证之使用HTTP基本认证

在前一篇文章ASP.NET Web API(一):使用初探,GET和POST数据中,我们初步接触了微软的REST API: Web API。 我们在接触了Web API的后就立马发现了有安全验证的需求,所以这篇文章我们先来讨论下安全验证一个最简…

2026/10/16 21:50:47 阅读更多 →

最新新闻

2026盘点 10 款AI写小说软件:从大纲范例到全篇创作,TaoToken 统一 Key 让多模型切换不再报 401

2026盘点 10 款AI写小说软件:从大纲范例到全篇创作,TaoToken 统一 Key 让多模型切换不再报 401

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:30:24 阅读更多 →
TRAE 2.0 SOLO 实测:从 PRD 到 Vercel 上线,AI 编程全流程自动跑通

TRAE 2.0 SOLO 实测:从 PRD 到 Vercel 上线,AI 编程全流程自动跑通

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:30:24 阅读更多 →
RCNN人脸检测工程实践:候选区域到边界框精修全解析

RCNN人脸检测工程实践:候选区域到边界框精修全解析

人脸检测这块,这几年被各种端到端模型刷屏,但如果你真正上手做过工程化项目,会发现很多所谓“新方案”在落地时并不比经典方法省心。RCNN_人脸检测这个项目,最初就是我在某一次实际业务需求中,想把基于区域的卷积神经网…

2026/10/17 17:30:24 阅读更多 →
Salesforce CRM Manager 岗位全解析:数据质量、权限与集成实战

Salesforce CRM Manager 岗位全解析:数据质量、权限与集成实战

简介:这份PDF面向希望系统了解Salesforce CRM Manager岗位的求职者、招聘方及CRM从业者,围绕该职位的职责边界、汇报关系与核心技能展开,帮助读者判断自身能力匹配度并规划学习路径。内容涵盖数据质量管理与分析、实时报告输出、系统推广与培…

2026/10/17 17:30:24 阅读更多 →
Unity联机游戏开发入门:从架构选型到状态同步Demo实战

Unity联机游戏开发入门:从架构选型到状态同步Demo实战

1. 从单机到联机:为什么第一个Demo总是跑不起来很多人第一次尝试给Unity游戏加联机功能,都会经历一个相似的挫败过程:按照教程装好了网络库,写了几行同步代码,本地测试一切正常,结果一打包发给朋友&#xf…

2026/10/17 17:29:24 阅读更多 →
AI管理体系ISO 42001:从HLS骨架到风险双轨落地

AI管理体系ISO 42001:从HLS骨架到风险双轨落地

简介:ISO/IEC DIS 42001:2022《信息技术 人工智能 管理体系》是国际标准化组织与国际电工委员会联合发布的人工智能管理体系国际标准草案,面向AI治理、合规与标准化从业者,也适合企业AI项目负责人及高校相关专业师生。草案围绕组织责任、风险…

2026/10/17 17:29:24 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →