Skip to content

多个FreeSQL实例之间的AOP事件会互相影响,导致实体对象的数据和状态异常 #2159

@shoulisun

Description

@shoulisun

问题描述及重现代码:

背景:

对于身份证号和手机号等敏感信息,要求在数据库中加密存储。

EFCore:之前使用的EFCore,里面有一个值转换器的东西可以自动实现插入时加密,查询时解密。非常好用,业务只关心原始值,而不用关心加密值。
FreeSQL:没有找到类似的值转换器功能,只能通过AOP事件来实现类似功能。
bug描述:多个 FreeSql 实例时,AOP事件会互相影响,导致状态不一致。
bug重现步骤(以自动加解密的场景为例):
第一步:定义两个 FreeSql 实例 fsql1 和 fsql2,fsql1 配置AOP的自动值处理(插入加密)或DataReader拦截(查询解密),fsql2 不配置。
第二步:使用 fsql1 插入一条数据,触发AOP自动加密。
第三步:使用 fsql1(触发自动解密) 和 fsql2 分别查询该数据。
第四步:期望 fsql1 查询结果为解密后的原始值,fsql2 查询结果为加密后的存储值。但实际结果是两个实例查询结果均为解密后的原始值。
	public class FreesqlAutoCryptoTest2
	{
		public record Fsql1;
		public record Fsql2;

		/// <summary>
		/// 背景:对于身份证号和手机号等敏感信息,要求在数据库中加密存储。
		/// EFCore:之前使用的EFCore,里面有一个值转换器的东西可以自动实现插入时加密,查询时解密。非常好用,业务只关心原始值,而不用关心加密值。
		/// FreeSQL:没有找到类似的值转换器功能,只能通过AOP事件来实现类似功能。
		/// bug描述:多个 FreeSql 实例时,AOP事件会互相影响,导致状态不一致。
		/// bug重现步骤(以自动加解密的场景为例):
		/// 第一步:定义两个 FreeSql 实例 fsql1 和 fsql2,fsql1 配置AOP的自动值处理(插入加密)或DataReader拦截(查询解密),fsql2 不配置。
		/// 第二步:使用 fsql1 插入一条数据,触发AOP自动加密。
		/// 第三步:使用 fsql1(触发自动解密) 和 fsql2 分别查询该数据。
		/// 第四步:期望 fsql1 查询结果为解密后的原始值,fsql2 查询结果为加密后的存储值。但实际结果是两个实例查询结果均为解密后的原始值。
		/// </summary>
		[Test]
		public void InsertUpdateAutoEnrcyptTests()
		{
			var fsql1 = ProvideFsql<Fsql1>();

			// 在第一个 FreeSql 实例上配置自动加解密
			AopConfig(fsql1);

			// 第二个 FreeSql 实例不配置自动加解密
			var fsql2 = ProvideFsql<Fsql2>();

			var stu0 = new Student
			{
				StuName = "测试ceshics张三A测试ceshi",
				CertificateType = "1",
				CertificateNo = "516423156476894261",
				MobilePhone = "12546987465",
				MobilePhone2 = "12546987789",
				Password = "123456789456",
				StudentStatus = 1,
				RegisterTime = DateTime.Now
			};
			// 第一个实例插入数据,自动加密后存到数据库。
			// bug1:这里自动加密会修改原始对象stu0的值。按逻辑说,这里应该修改,但是这种场景应用较少。
			// 能不能提供一个选项或方法,不修改原始对象?
			var stuId = fsql1.Insert(stu0).ExecuteIdentity();

			// 第一个实例查询,触发自动解密,查询结果是解密后的值
			var stu1 = fsql1.Select<Student>().Where(s => s.StuName.Equals(stu0.StuName)).ToOne();

			// 第二个实例查询,不触发自动解密,查询结果是加密后的值
			var stu2 = fsql2.Select<Student>().Where(s => s.StuName.Equals(stu0.StuName)).ToOne();
			fsql1.Delete<Student>().Where(s => s.StuName.Equals(stu0.StuName)).ExecuteAffrows();


			using (Assert.EnterMultipleScope())
			{
				Assert.That(stu1, Is.Not.EqualTo(stu2));

				// 验证第一个实例查询结果是解密后的值
				Assert.That(stu1.CertificateNo, Is.EqualTo("516423156476894261"));
				Assert.That(stu1.MobilePhone, Is.EqualTo("12546987465"));
				Assert.That(stu1.MobilePhone2, Is.EqualTo("12546987789"));

				// 验证第二个实例查询结果是加密后的值
				Assert.That(stu2.CertificateNo, Is.EqualTo("D3DF4F22F89BC68C485BD7056A9DABB1D07E061A156FCCB7C4CA10FCD054F2335A5B53"));
				Assert.That(stu2.MobilePhone, Is.EqualTo("83CD25466A9085D9A2BE0C34456E58DC5F8B7B"));
				Assert.That(stu2.MobilePhone2, Is.EqualTo("19BC0F7AC7258992C25005E67E6C7A36BF07E2"));
			}
		}
		private static IFreeSql<T> ProvideFsql<T>()
		{
			var config = new ConfigurationBuilder().AddEnvironmentVariables().Build();
			var dbConn = ConnectionStringHelper.BuildDbPlaceholder(config.GetConnectionString("ZhongkaoDbPgsql"), "guang_an");
			var fsql = new FreeSqlBuilder()
				.UseConnectionString(DataType.PostgreSQL, dbConn)
				.UseNameConvert(NameConvertType.PascalCaseToUnderscoreWithLower)
				.Build<T>();

			return fsql;
		}
		private static IFreeSql AopConfig(IFreeSql fsql)
		{

			fsql.Aop.AuditValue += (s, e) =>
			{
				if (typeof(Student) == e.Property.DeclaringType)
				{
					switch (e.Property.Name)
					{
						case nameof(Student.CertificateNo):
						case nameof(Student.MobilePhone):
						case nameof(Student.MobilePhone2):
							e.Value = DataEncryptConverterUtility.Forward(e.Value as string)!;
							break;
					}
				}
			};

			fsql.Aop.AuditDataReader += (s, e) =>
			{
				Console.WriteLine($"FreeSql自动解密触发,字段: {e.Property.Name}");
				switch (e.Property.Name)
				{
					case nameof(Student.CertificateNo):
					case nameof(Student.MobilePhone):
					case nameof(Student.MobilePhone2):
						try
						{
							e.Value = DataEncryptConverterUtility.Backward(e.Value as string)!;
						}
						catch (Exception ex)
						{
							Console.WriteLine($"FreeSql自动解密失败,字段: {e.Property.Name}, 错误: {ex.Message}");
							throw;
						}
						break;
				}
			};
			return fsql;
		}

	}

执行结果

Image

数据库版本

(openGauss 3.0.3 build 46134f73) compiled at 2023-01-10 22:43:35 commit 0 last mr on x86_64-unknown-linux-gnu, compiled by g++ (GCC) 7.3.0, 64-bit

安装的Nuget包

FreeSql.Provider.PostgreSQL" Version="3.5.212"

.net framework/. net core? 及具体版本

net10.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions