介绍 (Introduction)

The title of this post should have been “How to implement wildcard search functionality with Always Encrypted, make deterministic encryption safer, and load initial data using SqlBulkInsert”, but as you understand, that’s just too long for a title.

这篇文章的标题应该是“如何使用Always Encrypted实现通配符搜索功能,使确定性加密更安全,以及使用SqlBulkInsert加载初始数据” ,但正如您所了解的那样,标题太长了。

A number of years ago I built a web application with “Always Encrypted” as VARBINARY columns, before Microsoft offered this feature out of the SQL Server Box. So in case the database server would be compromised by hackers, no client details could be revealed.

几年前,在Microsoft从SQL Server Box中提供此功能之前,我用“始终加密”作为VARBINARY列构建了一个Web应用程序。 因此,如果数据库服务器受到黑客的威胁,则不会透露任何客户端详细信息。

Just like Microsoft my Data Access Layer, built in .NET did the encryption and decryption, transparent for other code that accessed it. And I had the same challenge how to search on encrypted columns.

就像Microsoft一样,我的内置于.NET的数据访问层也进行了加密和解密,对于访问它的其他代码而言是透明的。 对于如何搜索加密列,我也遇到了同样的挑战。

So with (maybe more than average) interest I was looking forward to know how the Always Encrypted feature of SQL Server 2016 works.

因此,以(也许超过平均水平)的兴趣,我很想知道SQL Server 2016的始终加密功能是如何工作的。

And I noticed that Microsoft implements the searching differently from what I did at the time. What I did different (using randomized encryption and search columns) was my inspiration for this article.

而且我注意到Microsoft实施搜索的方式与当时不同。 我所做的不同(使用随机加密和搜索列)是本文的灵感。

But first a little introduction on the feature itself.

但是首先对功能本身进行一些介绍。

Always Encrypted is a client-side encryption technology in which data is automatically encrypted not only when it is written but also when it is read by an approved application. It’s an end-to-end encryption, therefore SQL server only sees (and stores) the encrypted version of the data.

Always Encrypted始终加密)是一种客户端加密技术,其中,不仅在写入数据时,而且在批准的应用程序读取数据时,都会自动对数据进行加密。 这是一种端到端加密 ,因此SQL Server仅查看(并存储)数据的加密版本。

This means that your client application needs to use an Always Encrypted enabled driver to communicate with the database.

这意味着您的客户端应用程序需要使用启用始终加密的驱动程序来与数据库进行通信。

At this time, the available Always Encrypted enabled-drivers are:

目前,可用的始终加密启用驱动程序是:

  • the .NET Framework Data Provider for SQL Server, which requires installation of .NET Framework version 4.6 on the client computer SQL Server的.NET Framework数据提供程序,需要在客户端计算机上安装.NET Framework 4.6版
  • the JDBC 6.0 driver JDBC 6.0驱动程序
  • the Windows ODBC driver Windows ODBC驱动程序

For more information and to download the drivers see Always Encrypted client development (on MSDN).

有关更多信息和下载驱动程序,请参阅Always Encrypted客户端开发(在MSDN上) 。

To be honest I hate to replicate MSDN or other sites or blog posts when I do not have to. So as an introduction I kindly refer to:

老实说,我讨厌在不需要时复制MSDN或其他站点或博客文章。 因此,作为介绍,我谨提及:

  • Always Encrypted (Database Engine) (MSDN) 始终加密(数据库引擎)(MSDN)
  • New Features in SQL Server 2016 – Always encrypted (SQLShack) SQL Server 2016的新功能–始终加密(SQLShack)

如何使用Entity Framework在Always Encrypted列上模拟通配符搜索 (How to mimic a wildcard search on Always Encrypted columns with Entity Framework)

The challenges with searching on a column that is Always Encrypted are twofold:

在始终加密的列上进行搜索面临的挑战是双重的:

  • can only search on an exact value, you cannot do a wildcard search. 只能搜索精确值 ,而不能进行通配符搜索。
  • you have to do a sacrifice in the area of security: you have to use deterministic encryption, which always produces the same encrypted value for a given input value. When the number of distinct values in a column is low (for instance true/false or a domain value with a limited number of values), you can imagine this can be dangerous: you could guess values by comparing the same encrypted value of other rows. Also brute force to encrypt all possible values so you can compare the encrypted values with the ones in the database is a possible threat. So you do not really want that. 您必须在安全性方面做出牺牲 :必须使用确定性加密,该加密对于给定的输入值始终会产生相同的加密值。 当一列中不同值的数量较少(例如,真/假或值的数量有限的域值)时,您可以想象这很危险:您可以通过比较其他行的相同加密值来猜测值。 同样,强行加密所有可能的值,以便将加密后的值与数据库中的值进行比较也是一种潜在的威胁。 因此,您并不是真的想要那个。

Coping with these limitations is not ‘easy’. For instance to implement wildcard search functionality you have to to find out how the business users would like to search.

应对这些局限并非易事。 例如,要实现通配符搜索功能,您必须找出业务用户希望如何搜索。

So if you have encrypted credit card numbers in your database, and the business users want to be able to search on the last 4 digits, you have to “do something” for that.

因此,如果您在数据库中已加密了信用卡号,并且业务用户希望能够搜索最后4位数字 ,则必须为此“做些事情”

If the business users want to be able to search on expiration month and year of a credit card, but a security requirement is to avoid deterministic encryption because of the limited number of distinct values for those two columns (only 12 month numbers, maybe only 5 to 10 years that are still relevant), the same counts: you have to “do something” for that.

如果业务用户希望能够搜索信用卡的到期月份和年份 ,但是安全要求是避免确定性加密,因为这两个列的不同值的数量有限(只有12个月的数字,也许只有5个月的数字)到10年仍然有意义),同样的道理:您必须为此“做某事”

But what is “something”? Okay, to be straight with you, you will need an extra search column to implement each of these search requirements.

但是什么是“东西”? 好的,为了与您保持直接联系, 您将需要一个额外的搜索列来实现所有这些搜索要求。

Follow the demo and I can show you how this works.

跟随演示,我可以向您展示其工作原理。

为演示准备数据库 (Preparing a database for the demo)

First here are my preparation scripts to set up a demo using a CreditCard table.

首先,这里是我的准备脚本,用于使用CreditCard表设置演示。

If you have read the other resources mentioned above you should be able to understand what these scripts do. Also there is some comment in the scripts.

如果您已经阅读了上面提到的其他资源,那么您应该能够理解这些脚本的作用。 脚本中也有一些注释。

010_create_column_master_key.sql:

010_create_column_master_key.sql:


--\
---) Use a separate database [MSSQL_E12_AlwaysEncryptedDemo] for the demo,
---) create this database if it does not exist yet.
--/
IF NOT EXISTS(SELECT 1 FROM sys.databases WHERE name = 'MSSQL_E12_AlwaysEncryptedDemo')EXEC('CREATE DATABASE [MSSQL_E12_AlwaysEncryptedDemo]');
GOUSE [MSSQL_E12_AlwaysEncryptedDemo]
GO--\
---) Create a column master key, if it does not exist yet.
---) Do not use this script for your production environment! This is unsafe, as these keys are now public.
---) Generate your keys yourself, e.g. with SQL Server Management Studio.
--/
IF NOT EXISTS ( SELECT 1 FROM sys.column_master_keysWHERE name = 'CMK_AlwaysEncrypted')
BEGINCREATE COLUMN MASTER KEY [CMK_AlwaysEncrypted]WITH(KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE',KEY_PATH = N'CurrentUser/My/ECF3CBAA957FEA693002BE905768C39E65CDE7D0')
END
GO

020_create_column_encryption_keys.sql:

020_create_column_encryption_keys.sql:


USE [MSSQL_E12_AlwaysEncryptedDemo]
GO--\
---) Create a column encryption keys,
---) for each column that needs to be encrypted a separate key.
---) Do not use this script for your production environment! This is unsafe, as these keys are now public.
---) Generate your keys yourself, e.g. with SQL Server Management Studio.
--/
IF NOT EXISTS ( SELECT 1 FROM sys.column_encryption_keysWHERE name = 'CEK_CreditCard_CardNumber')
BEGINCREATE COLUMN ENCRYPTION KEY [CEK_CreditCard_CardNumber]WITH VALUES(COLUMN_MASTER_KEY = [CMK_AlwaysEncrypted],ALGORITHM = 'RSA_OAEP',ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F006500630066003300630062006100610039003500370066006500610036003900330030003000320062006500390030003500370036003800630033003900650036003500630064006500370064003000BB08A0C879710C99104A22D20A3E6554260C06C0D1449ECB71780B1D98B98CDD1281576E9A274F46765243182EEB343E96AF31D081E64D5687FE99A7C29F606D8C48CAB5CB8974BD4DEB13160F267C18B809888E6951650313208E4FAB2966B281295A6A64E59EEB4C9D00E9EFF842CFC353D0AB63613248F431A9F781F70CCCD6158D1F7D775ECED7F5532ED51B91D9741B0707BF402E13C6092278F85D6DDB35E052EB93C34654E0C67E62176CFEB9DE4C4E9B1B1A781D811DD5FA062327808E3E9E419290B801A5C63AC5BCC6DD45C5DFEEF8696379824362A90321A6063E4074B0F1533A5CEA4C53A730BD1B43C919DBCB4B8767C470E526CD83B0DE2DFF2FA5DD5923D2D177CB86431DE425384F5533425BCF6E1D72562588380E464CE349611187313D426DEACD81C1B24C7844F82DE5CC9A7C70A45CC97871972B2A44EBCD2DC384517CA200395D2DC8E1C4CB2299F29CBA3A22EA418C9853B29174EAECA0FAF2C236A9A9A43EA3B2AB620D61C34B9F98A8207387D5D4D3D460059911FD40174E04DEC571164C69C3E2FC56E5715F62EBF452A8F2961344DEAA2745187DBA710A29D110381CA0988240A00381BD4FB2CC2EA279E0EF2CF7BB833AF3DB577BB73EED528E903D1D3DE48215EAB14E4A8C007555D425DA6F32A603E4600C744488915CA1BA4A4CF85A18B19A88D221E027E6335D97B39C19AE7509845CC9)
END
GOIF NOT EXISTS ( SELECT 1 FROM sys.column_encryption_keysWHERE name = 'CEK_CreditCard_ExpMonth')
BEGINCREATE COLUMN ENCRYPTION KEY [CEK_CreditCard_ExpMonth]WITH VALUES(COLUMN_MASTER_KEY = [CMK_AlwaysEncrypted],ALGORITHM = 'RSA_OAEP',ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F006500630066003300630062006100610039003500370066006500610036003900330030003000320062006500390030003500370036003800630033003900650036003500630064006500370064003000182CFF9D4978426B25AEEC6BBDC352B4F2DC9A969675A6DF55742B8F811D314A04C3C29B3FAFD94AE6A5196AA5AEF03A9B3C05742264D9A2CA87924E5324EC1CFCB5EC7C117D3EFF5E98BBB51CC91F77DC3FAFB6FF4FBC5ABCF963D1AD91531769700941CA3E87B523C275647439A67469B8B2109F1004D8C2C69A1E1C7A8B5CEC101DDA1C3EEEC4781ED27BC706FE97064C776B508B07FB2F725BD52750CC495E002E7BCBB5BE14BEDDF137BD34B45A4BEDDD130584F115D67DFFD63FF3E702DCAAA552B68B942235F8B3F639377EF332BEF6E04468FCB5FC14F41631B7FBDF441980BBE47FFCB42D8E8D10443EB46E64870CF867114BC7268D4A8C65F631529F03F689AE68B4C583D56977E36616598F069D3B74D7BAE03317002905B308C026674691504DA47E28534EC7190EB9E14DDF8805BDE3301BEEBF271558570090F99F2157BB5387DEF887F0A9AF04E470B1711E7E624D00E639CDCD4DCE4EA9D31E5DE721E9BCE61752429545C5121FECB4A2A106F7741C56BFBB6CBE5A87B60041B6E93F84D5D4EA405BFA4E48DB46634F2A58B41B6B5C96D9105FE9FCD593DCCCAE7B8DB24AFBEF3128C023D555A6DE4B0A51FDA3F24E28DC34C52EFC1BFA745CD62D0A7962039566A14A8D4EF942B6ED5EE93FD1C309D799913550207D7BCB12EB649AEE8EC821295BAA03A4A24B56CC05611FA961606A1EAAF17221A6DB7D)
END
GOIF NOT EXISTS ( SELECT 1 FROM sys.column_encryption_keysWHERE name = 'CEK_CreditCard_ExpYear')
BEGINCREATE COLUMN ENCRYPTION KEY [CEK_CreditCard_ExpYear]WITH VALUES(COLUMN_MASTER_KEY = [CMK_AlwaysEncrypted],ALGORITHM = 'RSA_OAEP',ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F0065006300660033006300620061006100390035003700660065006100360039003300300030003200620065003900300035003700360038006300330039006500360035006300640065003700640030003EB794029B7984A9E11935A2E236264663E2357988814506ACF7C3D577BE944C54F0A13FEAD63FF262AD16F40639D73EBC508729C871ED7F6E32D5B3D0A38AFA7D9D7533AD7FF128E9111B0E63BCFD29DBA55D1C4B3D9159E7E7DD38D37678A09003EC3875FBBBC321A9386B370BBB01A54BEBAFD77C2699BFC15E7745706174E40AC17BF9F712572B67A9D2B1463D62DC878F516B8867B876478F057E0569911FF19EDCA04941406C1BF0A4EFD063C5D8D200F3CB2AFD56EE99CCF16DF24DA1B3830A60F3A94BD81A1415228BDEA47CABD3EF60E8AF2826D1AF9797A0C2D356F9314FC9FDCF95C005D79B5C20374879029EF69C9E948B146FF383C4AFFE3690BE652CAAE3747E156F39B7DA7A5C407AFE67D32FF0595AEB2217C290AC555ECA2026B7059EA3029F13232DF1F8D3C04BC6A2A47DBF93738CEB1807B5F2C05E000B99D78403588CB6621FE423D3B4AEBC088E62C765C706A58787E348C4B080C598EC3F9FBCEBBEC3EB8281D8C6176CD1F9F11C8F81E5F124E2E76D0502909A1AC9EDA9EF4D94D4DD024EA9BD218C61AEE590C6CE962CF904697E738DE290B30451B1C51EF1B197BE5FAA49844F455650E948076E0B568CAA11674A2376FED2D0CF711697AA303DD1090A3590DCBC343E877E10B4ADB122F2F3DFF92303BA6F99D6E9D1D687FC962312C06717BF27A83FEEDB5F9AEA6E9793D71C7C345EE24C81)
END
GO
IF NOT EXISTS ( SELECT 1 FROM sys.column_encryption_keysWHERE name = 'CEK_CreditCard_CardSearch1')
BEGINCREATE COLUMN ENCRYPTION KEY [CEK_CreditCard_CardSearch1]WITH VALUES(COLUMN_MASTER_KEY = [CMK_AlwaysEncrypted],ALGORITHM = 'RSA_OAEP',ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F006500630066003300630062006100610039003500370066006500610036003900330030003000320062006500390030003500370036003800630033003900650036003500630064006500370064003000631C1F382B3A203376A512AE63C7F56FD95BF9048AB3B421C263A520F306DFEC2A1EE33E1B6314AE3A025A793D06C684BFFD24FD521D50831ADC1397F77FEFB4FE63E90E54BF3EF212B963DD25B97D802E85FCE2C4AB9C3FE87C9982B4479D4CF9EE47E9F1413A2018F3D644B0CC6E560DB97FFC00C6EB51994C9852D8C594E54539779B7C4D7268E2F3085C4488F2A4A13B8E47DC3D2518D02FB85A87A574C1E92B910C49BFF4736DFCA2A7B57A581701021255B09F7F904D04DB4B6A0440F30C2832257CF88619B9554BBAB5B63A5714A3CEE74A03A049866094D012B153EECAA01E2C3FCF4C2EB6B388B867F9EEE5C5FC840CB7898FE8187E3244BAB4ED285883FC8FB0D5904D517E286938C2B4330DE5DEE62CEFBFF8EDD3F3E7F2239BCFD761BDAAF586F9C864006BB12F5412D0D1FB961D6DB473BF859C43839689516A97F54AC0A20826858A1FE82FE31D80C979184AB3E59C1C1A6592077E77C126AC395FCF389BB00A2230BD9D954D331D395AD20896EC6B5D8E53318DE532F6EE09FB79CCB9D0B5F21A51AC7603829833D915D4C564ADBFF286A0B1AE7310AFD5D2B38B5C6B76FBF6174E72ED9F27BE713F4CAB5AE05A11CE84875A00B94FBDC7D7BB07768AF05D28132AD991BBB960626627CEF51DFB8B1F36480F753DE20A780D0C36D8AB1C08D444FFE1279465A78434FD095B97A8A11C5205999ACC1C30C001)
END
GO
IF NOT EXISTS ( SELECT 1 FROM sys.column_encryption_keysWHERE name = 'CEK_CreditCard_CardSearch2')
BEGINCREATE COLUMN ENCRYPTION KEY [CEK_CreditCard_CardSearch2]WITH VALUES(COLUMN_MASTER_KEY = [CMK_AlwaysEncrypted],ALGORITHM = 'RSA_OAEP',ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F00650063006600330063006200610061003900350037006600650061003600390033003000300032006200650039003000350037003600380063003300390065003600350063006400650037006400300093136E46AE1774DB0C923EB802FCF4FDA4EA71D9218A6FF4F3159E7ECE0E88055C4C519C386D918B1B9298D68C5F36E998359C6D515014B108C0440DDC2B2E605AA5C38971FD162D671D621621D4A5C7CCA9AF263267EFEAAF3B3746C15EEB10610FB6822529BB4A3AF143823402C9EA557F9745C1033B57DCA596237206CFD6697CF75BE340B21C1EB96F31E81EF449BB59A933A27110BC0ED7A9AF9BB23423FC7AC031F23D696CD7AA98423AA249ADFB5F6BE359BDAB781D62143900D2E1DAAFE402F4ACD5157A3AA1A27F3F208EF37EA36240EC9D52DFB77AA441E753B695810B96174FE4CE756037AAA7F0F995CC09A2403DB50AFB7E05715A142164EF5C3626817CE429887EFBF38CB200F960DEAE313D20AAAFC4DA74C738A2FFE490B2D49C0B5E3B9C78A084D4FC1B021F42330DC231D2B88A00067F875FE19CECB92BD4D5EFB0AAEDD5489D9E11BA501EFE03513004D926988729C7DC55AD2F727A92F93478832166DD539D378BD8BAAD9AABAF40346F963BB3712206D346F3FBEB19ADA2140CDF8219EB36ECA287331BAD281D2B6E19B634D7288494AC523C1A176C90F48E3110E0EBD68A102158D09E650128E6F1A2E6CEA616BAFE7A1E50D2E3BBD1594B83BBE09E6352FF942FC13216C3B43E55ACB7BAA8072F6A74ECA37B904BE930FA0318F883218977377130E4CBADA3667E3783915DC48598836303EF3E40)
END
GO

030_create_tables.sql:

030_create_tables.sql:


USE [MSSQL_E12_AlwaysEncryptedDemo]
GO--\
---) First create a temporary table, without columns encrypted with Always Encrypted.
---) This is to simulate a current table in your database, or a staging table
---) that is used to get the initial encryption done fast with SqlBulkInsert.
--/SET ANSI_NULLS ON
GOSET QUOTED_IDENTIFIER ON
GOCREATE TABLE [dbo].[CreditCardTemp]([CreditCardID] [int] IDENTITY(1,1) NOT NULL,[CardType] [nvarchar](50) NOT NULL,[CardNumber] [nvarchar](25) NOT NULL,[ExpMonth] [tinyint] NOT NULL,[ExpYear] [smallint] NOT NULL,[ModifiedDate] [datetime] NOT NULL,CONSTRAINT [PK_CreditCardTemp_CreditCardID] PRIMARY KEY CLUSTERED
([CreditCardID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]GOALTER TABLE [dbo].[CreditCardTemp] ADD CONSTRAINT [DF_CreditCardTemp_ModifiedDate]  DEFAULT (getdate()) FOR [ModifiedDate];
GO--\
---) Now fill the temporary table from the [AdventureWorks].[Sales].[CreditCard] table.
--/
INSERT INTO [dbo].[CreditCardTemp]([CardType],[CardNumber],[ExpMonth],[ExpYear],[ModifiedDate])
SELECT [CardType],[CardNumber],[ExpMonth],[ExpYear],[ModifiedDate]
FROM [AdventureWorks].[Sales].[CreditCard]
GO--\
---) Now create the [CreditCard] table, that has
---) columns that are encrypted with Always Encrypted.
--/
SET ANSI_NULLS ON
GOSET QUOTED_IDENTIFIER ON
GOCREATE TABLE [dbo].[CreditCard]([CreditCardID] [int] IDENTITY(1,1) NOT NULL,[CardType] [nvarchar](50) NOT NULL,[CardNumber] [nvarchar](25) ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, -- Safe but can’t be searched on   ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',   COLUMN_ENCRYPTION_KEY = [CEK_CreditCard_CardNumber]) NULL,  [ExpMonth] [tinyint] ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, -- Safe but can’t be searched on     ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',   COLUMN_ENCRYPTION_KEY = [CEK_CreditCard_ExpMonth]) NULL,  [ExpYear] [smallint] ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, -- Safe but can’t be searched on     ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',   COLUMN_ENCRYPTION_KEY = [CEK_CreditCard_ExpYear]) NULL,  [CardSearch1] [char](4) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, -- Less safe but can be searched on   ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',   COLUMN_ENCRYPTION_KEY = [CEK_CreditCard_CardSearch1]) NULL,[CardSearch2] [char](7) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, -- Less safe but can be searched on     ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',   COLUMN_ENCRYPTION_KEY = [CEK_CreditCard_CardSearch2]) NULL,[ModifiedDate] [datetime] NOT NULL,CONSTRAINT [PK_CreditCard_CreditCardID] PRIMARY KEY CLUSTERED
([CreditCardID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]GOALTER TABLE [dbo].[CreditCard] ADD  CONSTRAINT [DF_CreditCard_ModifiedDate2]  DEFAULT (getdate()) FOR [ModifiedDate]
GO

使用实体框架核心和实体模型创建.NET控制台应用程序。 (Creating a .NET Console Application with Entity Framework Core and an Entity Model.)

Alright, the encryption keys and database tables are created, now we have to move over to .NET to populate the [CreditCard] table with rows of which some columns are encrypted.

好了,创建了加密密钥和数据库表,现在我们必须移到.NET来用行(其中某些列已加密)填充[CreditCard]表。

After that, I’ll show you how you can mimic wildcard searches.

之后,我将向您展示如何模拟通配符搜索。

For the demo I created a Console Application using Visual Studio 2015 (Visual Studio 2017 seemed unstable during building the solution, so I moved back to VS2015 for now).

对于该演示,我使用Visual Studio 2015创建了一个控制台应用程序(Visual Studio 2017在构建解决方案时似乎不稳定,因此我现在回到了VS2015)。

In this little demo program the following things will be demonstrated:

在这个小演示程序中,将演示以下内容:

  1. populate a table that has Always Encrypted columns from an old or staging table, 使用SqlBulkCopy(非常快)从旧表或临时表using SqlBulkCopy (very fast). 填充具有始终加密列的表。
  2. use RANDOMIZED encryption for your columns while still be able to a ‘like’ search using a different ‘search’ column with DETERMINISTIC encryption. 对您的列使用RANDOMIZED加密,同时仍可以使用具有DETERMINISTIC加密的其他“搜索”列进行“喜欢”搜索。
  3. How to make the DETERMINISTIC encryption of the search columns even safer by adding a random part to it.
  4. 如何通过在其中添加随机部分来使搜索列确定性加密更加安全

This is done by the example of a [CreditCard] table.

这是通过[信用卡]表的示例完成的。

When not installed yet, download and install the Microsoft .NET Framework 4.6.2.

如果尚未安装,请下载并安装Microsoft .NET Framework 4.6.2 。

Then open Visual Studio and create a new Console Application. Make sure to set the .NET Framework to 4.6.1.

然后打开Visual Studio并创建一个新的控制台应用程序。 确保将.NET Framework设置为4.6.1。

Now Entity Framework has to be installed. For this start the menu option Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution.

现在,必须安装实体框架。 为此,请启动菜单选项工具-> NuGet软件包管理器->管理解决方案的NuGet软件包。

I have used the Entity Framework Core v1.1.1 which uses the .NET Framework 4.6.

我已经使用了使用.NET Framework 4.6Entity Framework Core v1.1.1

To install Entity Framework Core v1.1.1 in the Browse tab search on entity framework core and select Microsoft.EntityFrameworkCore from the list.

安装Entity Framework Core v1.1.1 在“ 浏览”选项卡上搜索实体框架核心,然后从列表中选择Microsoft.EntityFrameworkCore

Then select the project name on the right and press the Install button.

然后选择右侧的项目名称,然后按Install按钮。

After reviewing the changes and accepting the license Entity Framework Core will be installed. Installation takes some time, the output window will show when it is finished.

查看更改并接受许可后,将安装Entity Framework Core。 安装需要一些时间,完成后将显示输出窗口。

To use Entity Framework, we have to add a model. Right-click on the project node to add a new item, then follow the steps in the pictures below:

要使用实体框架,我们必须添加一个模型。 右键单击项目节点以添加新项目,然后按照以下图片中的步骤操作:

After this, your project should look as follows:

之后,您的项目应如下所示:

This was not too difficult, right?

这不是太困难,对吧?

Now modify the connection string in App.config, so the connection is prepared for Always Encrypted. To do so, add
;Column Encryption Setting=enabled
at the end of the connection string, as indicated in the pictures below.

现在,在App.config中修改连接字符串,以便为Always Encrypted准备连接。 为此,添加
; Column Encryption Setting =已启用
在连接字符串的末尾,如下图所示。

添加代码以与数据库交互 (Adding code to interact with the database)

As you might have noticed the [CreditCard] table contains two search columns CardSearch1 and CardSearch2, of which the values are derived from other columns, namely CardNumber, ExpMonth and ExpYear. So logic needs to be implemented to calculate the value of those columns. So add a “CreditCardSearchColumns.cs” class/file to the project. Replace the initial code in it with the following code:

正如你可能已经注意到了[信用卡式表包含两列搜索和CardSearch1 CardSearch2,它的值是从其他列,即CardNumber,ExpMonthExpYear的。 因此,需要实现逻辑来计算这些列的值。 因此,将“ CreditCardSearchColumns.cs”类/文件添加到项目中。 用以下代码替换其中的初始代码:


namespace EFCoreAlwaysEncryptedDemo
{using System;/// <summary>/// This partial class "CreditCard" extends the same class generated by Entity Framework./// Every time the model regenerates this class, you have to delete public string CardSearch1 and public string CardSearch2 in CreditCard.cs/// To avoid this, edit the "AlwaysEncryptedModel.tt" t4 template that generates this class, so that it excludes those columns./// </summary>public partial class CreditCard{/// <summary>/// CardSearch2ObfuscationLevel is the number of different encrypted values that you want for the same unencrypted value./// A higher number is more secure, but will require more complex logic to get data out (more values to search for)./// </summary>public static int CardSearch2ObfuscationLevel { get { return 3; } }int _randomForSearchColumn;public CreditCard(){Random r = new Random(DateTime.Now.Millisecond);_randomForSearchColumn = r.Next(CardSearch2ObfuscationLevel); // Produces 0, 1 or 2 when CardSearch2ObfuscationLevel = 3// In this case cardSearch2 can contain three possible values for a expiration month and year (e.g. 03/17):// 0032017, 1032017 and 2032017.}/// <summary>/// CardSearch1 contains the last 4 digits of the credit card number./// </summary>public string CardSearch1{get{return (CardNumber != null && CardNumber.Length > 4) ? CardNumber.Substring(CardNumber.Length - 4) : string.Empty;}set { } // Do nothing}/// <summary>/// CardSearch2 is in format rMMyyyy, where r=random number, MM is expiration month and yyyy is expiration year./// </summary>public string CardSearch2{get{// The column CardSearch2 contains the expiration month and year of the credit card // and is also encrypted using DETERMINISTIC encryption, but it has an extra level // of security: by adding a random number at the beginning of the value, the same unencrypted// values for CardSearch2 can lead to different encrypted values, although DETERMINISTIC encryption// is used!// I have kept it simple by having just 3 different random values.// When you select rows for a certain expiration month and year you have to add three encrypted // values to the WHERE clause in an IN statement.// With more random values added (e.g. 25 or 100) you security becomes even better but your // where clause also longer.return string.Format("{0}{1:00}{2:0000}", _randomForSearchColumn, ExpMonth, ExpYear);}set { } // Do nothing}}
}

This leads to a problem immediately, because both properties CardSearch1 and CardSearch2 are already in a class generated by the installed T4 template AlwaysEncryptedModel.tt.

这立即导致问题,因为属性CardSearch1和CardSearch2已经在由安装的T4模板AlwaysEncryptedModel.tt生成的类中。

This can be solved by deleting the two properties from CreditCard.cs:

这可以通过从CreditCard.cs删除两个属性来解决:

A more elegant solution would be to adjust the AlwaysEncryptedModel.tt T4 template so that it excludes those properties when generating this file, but doing that is beyond the scope of this article.

更为优雅的解决方案是调整AlwaysEncryptedModel.tt T4模板,以便在生成此文件时将这些属性排除在外,但这超出了本文的范围。

We also need code to do the initial load of encrypted values. As you might have noticed, there is also a CreditCardTemp table in the database, that was loaded from the AdventureWorks database. This table does not contain any encrypted columns. We have to copy all rows from this table to the CreditCard table. I do not use Entity Framework for this, but the .NET Framework 4.6 Data Provider for SQL Server directly, so I can use SqlBulkInsert, which is really fast.

我们还需要代码来执行加密值的初始加载。 您可能已经注意到,数据库中还有一个CreditCardTemp表,该表是从AdventureWorks数据库中加载的。 该表不包含任何加密列。 我们必须将所有表从该表复制到CreditCard表。 我没有为此使用实体框架,而是直接为SQL Server使用.NET Framework 4.6数据提供程序,因此我可以使用SqlBulkInsert,这确实非常快。

What needs to be done is add a class InitialLoad to the EFCoreAlwaysEncryptedDemo project, the InitialLoad.cs file has the following contents:

需要做的是向EFCoreAlwaysEncryptedDemo项目添加一个InitialLoad类,InitialLoad.cs文件具有以下内容:


using System;
using System.Threading;
using System.Data.SqlClient;
using System.Data;namespace EFCoreAlwaysEncryptedDemo
{class InitialLoad{/// <summary>/// Initial load is not done with entity framework so that SqlBulkInsert can be used, for performance reasons./// </summary>public static void DoInitialEncryption(){// Setting up a connection, make sure this is part of the connection string: Column Encryption Setting=enabledvar connectionString = "Data Source=localhost; Initial Catalog=MSSQL_E12_AlwaysEncryptedDemo;" + " Integrated Security=true; Column Encryption Setting=enabled";using (SqlConnection connection = new SqlConnection(connectionString)){connection.Open();// Open the new target table, which is still empty, and contains columns that are Always Encrypted.string queryStringTarget ="SELECT [CreditCardID], [CardType], [CardNumber], [ExpMonth], [ExpYear], "+ " [CardSearch1], [CardSearch2], [ModifiedDate] "+ " FROM [dbo].[CreditCard]";var daTarget = new SqlDataAdapter(queryStringTarget, connection);var dsTarget = new DataSet();daTarget.Fill(dsTarget, "CreditCard");if (dsTarget.Tables[0].Rows.Count > 0){// Initial encryption already done.return;}// Open the temporary or old table, which does not contain any columns that are Always Encrypted.string queryStringSource ="SELECT [CreditCardID], [CardType], [CardNumber], [ExpMonth], [ExpYear], "+ " [ModifiedDate]"+ " FROM [dbo].[CreditCardTemp]";var daSource = new SqlDataAdapter(queryStringSource, connection);var dsSource = new DataSet();daSource.Fill(dsSource, "CreditCard");int currentRow = 0;var newCreditCard = new CreditCard();foreach (DataRow drSource in dsSource.Tables["CreditCard"].Rows){currentRow++;DataRow drTarget = dsTarget.Tables["CreditCard"].NewRow();// You could assign columns values to a drTarget column // from a drSource column directly.// The reason to use a CreditCard instance is that this// class will calculate CardSearch1 and CardSearch 2 // for me. Otherwise I would have to copy this logic.newCreditCard.CardType = drSource["CardType"].ToString();newCreditCard.CardNumber = drSource["CardNumber"].ToString();newCreditCard.ExpMonth = byte.Parse(drSource["ExpMonth"].ToString());newCreditCard.ExpYear = short.Parse(drSource["ExpYear"].ToString());newCreditCard.ModifiedDate = DateTime.Parse(drSource["ModifiedDate"].ToString());drTarget["CardType"] = newCreditCard.CardType;drTarget["ModifiedDate"] = newCreditCard.ModifiedDate;// For the following columns the .NET Framework Data Provider for // SQL Server (framework 4.6) does the encryption under the hood:drTarget["CardNumber"] = newCreditCard.CardNumber;drTarget["ExpMonth"] = newCreditCard.ExpMonth;drTarget["ExpYear"] = newCreditCard.ExpYear;drTarget["CardSearch1"] = newCreditCard.CardSearch1;drTarget["CardSearch2"] = newCreditCard.CardSearch2;dsTarget.Tables["CreditCard"].Rows.Add(drTarget);if (currentRow % 100 == 0){// To get different random values, wait a millisecond now and thenThread.Sleep(1);Console.WriteLine("{0} rows encrypted.", currentRow);}}// Now use SqlBulkCopy to get the encrypted data into the new table.Console.WriteLine("Starting SqlBulkCopy");SqlBulkCopy bulkCopy = new SqlBulkCopy(connection,SqlBulkCopyOptions.TableLock | SqlBulkCopyOptions.FireTriggers | SqlBulkCopyOptions.UseInternalTransaction, null);bulkCopy.DestinationTableName = dsTarget.Tables[0].TableName;bulkCopy.WriteToServer(dsTarget.Tables[0]);Console.WriteLine("SqlBulkCopy completed");Console.WriteLine("Press any key to continue ..");Console.ReadKey();}}}
}

Let’s see, where are we .. we are getting near the end, just one class to add and some code to Program.cs.
First add a class DemoPlease to the project, add it to the file DemoPlease.cs

让我们看看,我们到哪里都快结束了,只需要添加一个类,并向Program.cs添加一些代码。
首先将一个DemoPlease类添加到项目中,然后将其添加到文件DemoPlease.cs中

Paste this code into the file (replace existing code):

将此代码粘贴到文件中(替换现有代码):


using System;
using System.Data;
using System.Collections.Generic;
using System.Linq;namespace EFCoreAlwaysEncryptedDemo
{class DemoPlease{/// <summary>/// Adds a creditcard./// </summary>/// <param name="cardType">The card type (issuer of the card)</param>/// <param name="cardNumber">The card number</param>/// <param name="expMonth">The expiration month</param>/// <param name="expYear">The expiration year</param>/// <returns>CreditCard instance</returns>public CreditCard AddCreditCard(string cardType, string cardNumber, byte expMonth, short expYear){var newCreditCard = new CreditCard(){CardType = cardType,CardNumber = cardNumber,ExpMonth = expMonth,ExpYear = expYear,ModifiedDate = DateTime.Now};Console.WriteLine("AddCreditCard started");using (var context = new AlwaysEncryptedDemoEntities()){context.CreditCards.Add(newCreditCard);context.SaveChanges();}Console.WriteLine("AddCreditCard completed, CreditCardID = {0}", newCreditCard.CreditCardID);return newCreditCard;}/// <summary>/// Because the credit card number is encrypted in the database with Always Encrypted /// using RANDOMIZED encryption (for security reasons), you cannot directly search on it./// However, the column CardSearch1 contains the last 4 digits of the credit card number/// and is encrypted with Always Encrypted with DETERMINISTIC encryption./// So internally we search in the database all credit card numbers with the last 4 digits/// of the cardNumber we are looking for./// Then in this method, we do the final selection./// </summary>/// <param name="cardNumber"></param>/// <returns></returns>public CreditCard GetCreditCardByNumber(string cardNumber){Console.WriteLine("GetCreditCardByNumber started");var dictCreditCards = GetCreditCardsByLast4Digits(cardNumber);foreach (KeyValuePair<int, CreditCard> entry in dictCreditCards){if (entry.Value.CardNumber == cardNumber){Console.WriteLine("GetCreditCardByNumber completed: number found.");Console.WriteLine("GetCreditCardByNumber completed, CreditCardID = {0}", entry.Value.CreditCardID);return entry.Value;}}Console.WriteLine("GetCreditCardByNumber completed: number not found.");return null;}/// <summary>/// Returns a dictionary with creditcards with a credit card number that ends on the value of last4DigitsOfCardNumber./// </summary>/// <param name="last4DigitsOfCardNumber"></param>/// <returns></returns>private Dictionary<int, CreditCard> GetCreditCardsByLast4Digits(string last4DigitsOfCardNumber){Console.WriteLine("GetCreditCardsByLast4Digits started");if (last4DigitsOfCardNumber.Length > 4){last4DigitsOfCardNumber = last4DigitsOfCardNumber.Substring(last4DigitsOfCardNumber.Length - 4);}var creditCards = new Dictionary<int, CreditCard>();using (var context = new AlwaysEncryptedDemoEntities()){var dbCreditCards = context.CreditCards.Where(s => s.CardSearch1 == last4DigitsOfCardNumber);foreach (var creditCard in dbCreditCards){creditCards.Add(creditCard.CreditCardID, creditCard);}}Console.WriteLine("GetCreditCardByLast4Digits completed");return creditCards;}/// <summary>/// Deletes a credit card./// </summary>/// <param name="creditCard"></param>public void DeleteCreditCard(CreditCard creditCard){Console.WriteLine("DeleteCreditCard started");using (var context = new AlwaysEncryptedDemoEntities()){CreditCard deleteMe = new CreditCard() { CreditCardID = creditCard.CreditCardID };context.CreditCards.Attach(deleteMe);context.CreditCards.Remove(deleteMe);context.SaveChanges();}Console.WriteLine("DeleteCreditCard completed, CreditCardID = {0}", creditCard.CreditCardID);}/// <summary>/// Updates a credit card./// </summary>/// <param name="creditCard"></param>public void UpdateCreditCard(CreditCard creditCard){Console.WriteLine("UpdateCreditCard started");if (creditCard.CreditCardID <= 0){throw new InvalidOperationException("You cannot update a new row.");}using (var context = new AlwaysEncryptedDemoEntities()){creditCard.ModifiedDate = DateTime.Now;context.CreditCards.Attach(creditCard);var entry = context.Entry(creditCard);var excluded = new[] { "CreditCardID" };foreach (var name in entry.CurrentValues.PropertyNames.Except(excluded)){entry.Property(name).IsModified = true;}context.SaveChanges();}Console.WriteLine("UpdateCreditCard completed");}/// <summary>/// For searching on expiration month and year we use CardSearch2 column, which is encrypted with DETERMINISTIC encryption./// To obfuscate the encrypted value (make sure it is not always the same for the same month and year) it has a /// random number in it. So for searching we have to search on each (encrypted) value, so with every random value./// You could make this more secure by using a hardcoded list of random values (longer than one character), /// instead of the integer value 0, 1 or 2/// </summary>/// <param name="expMonth">The expiration month</param>/// <param name="expYear">The expiration year</param>public void GetCreditCardsByExpirationMonthAndYear(byte expMonth, short expYear){Console.WriteLine("GetCreditCardsByExpirationMonthAndYear started");var creditCards = new Dictionary<int, CreditCard>();using (var context = new AlwaysEncryptedDemoEntities()){for (int i = 0; i < CreditCard.CardSearch2ObfuscationLevel; i++){string searchValue = string.Format("{0}{1:00}{2:0000}", i, expMonth, expYear);var dbCreditCards = context.CreditCards.Where(s => s.CardSearch2 == searchValue);foreach (var creditCard in dbCreditCards){creditCards.Add(creditCard.CreditCardID, creditCard);}}Console.WriteLine("The following creditcards expire in month {0:00}/{1:0000}:", expMonth, expYear);int rowCount = 0;foreach (var creditCard in creditCards){Console.WriteLine("{0} [{1}]", creditCard.Value.CardNumber, creditCard.Value.CardType);rowCount++;}Console.WriteLine("Total number of rows: {0}", rowCount);}Console.WriteLine("GetCreditCardsByExpirationMonthAndYear completed");}}
}

Now it is time to glue everything together! To do this, paste this code into Program.cs (replace all existing code):

现在是时候将所有内容粘合在一起了! 为此,请将以下代码粘贴到Program.cs中(替换所有现有代码):


using System;namespace EFCoreAlwaysEncryptedDemo
{class Program{static void Main(string[] args){InitialLoad.DoInitialEncryption();string cardType = "Visa";string cardNumber = "33332664695311";byte expMonth = 11;short expYear = 2019;var demo = new DemoPlease();// First add a Visa card.Console.WriteLine("");Console.WriteLine("***** Adding credit card {0} *****", cardNumber);var creditCardAdded = demo.AddCreditCard(cardType, cardNumber, expMonth, expYear);// Then retrieve it.Console.WriteLine("");Console.WriteLine("***** Get a credit card {0} from the database *****", cardNumber);var creditCard = demo.GetCreditCardByNumber(cardNumber);// Update the issuer to Diners.Console.WriteLine("");Console.WriteLine("***** Update the issuer to Diners Club *****");creditCard.CardType = "Diners Club";demo.UpdateCreditCard(creditCard);// Get the card by expiration month and year, notice that the CardType is indeed updated to Diners.Console.WriteLine("");Console.WriteLine("***** Get the card by expiration month and year, ");Console.WriteLine("      notice that the CardType is indeed updated to Diners Club *****");demo.GetCreditCardsByExpirationMonthAndYear(expMonth, expYear);Console.WriteLine("Press any key to delete creditCard {0}..", creditCard.CardNumber);Console.ReadKey();demo.DeleteCreditCard(creditCard);// Get the card by expiration month and year, from a month that returns multiple rows.Console.WriteLine("");Console.WriteLine("***** Get the card by expiration month and year, from a month that returns multiple rows (01/2006). *****");Console.WriteLine("Press any key to continue ..");Console.ReadKey();demo.GetCreditCardsByExpirationMonthAndYear(1, 2006);Console.ReadKey();}}
}

Now it’s time to press <F5> (or the Start Debugging from the Debug Menu)! Watch how all the methods from DemoPlease work.

现在是时候按<F5>键 (或从“调试”菜单开始调试 )了! 观看DemoDemo中的所有方法如何工作。

结论/总结 (Conclusion / Wrap up)

In this blog post I shared my insights on how to cope with the limitations of searching on columns that were encrypted using SQL Server’s new feature Always Encrypted, by introducing extra Search columns, so that wildcard searches can be simulated and security is not weakened so much by the less safe deterministic encryption. To demonstrate this I used the Entity Framework Core v1.1.1 that uses the .NET Framework 4.6 Data Provider for SQL Server. This driver can be used to interact with Always Encrypted columns.

在这篇博客文章中,我通过引入额外的“ 搜索” 分享了我的见解,以解决如何使用SQL Server的新功能“ 始终加密”对加密的列进行搜索的局限性,从而可以模拟通配符搜索,并且不会削弱安全性。通过不太安全的确定性加密。 为了证明这一点,我使用了Entity Framework Core v1.1.1,该版本使用用于SQL Server的.NET Framework 4.6数据提供程序。 此驱动程序可用于与“始终加密”列进行交互。

About best practices .. please forgive me that I took some shortcuts, which enabled me to focus on the main subject. Things to remember when building production software:

关于最佳做法..请原谅我采取了一些捷径,使我能够专注于主要主题。 构建生产软件时要记住的事情:

  • Follow best practices for Always Encrypted when implementing it. For instance, your application should not run on the same server as the SQL Server Database Engine. 实施时,请遵循“始终加密”的最佳做法。 例如,您的应用程序不应与SQL Server数据库引擎在同一服务器上运行。
  • Do not hardcode a connection string in an executable program. 不要在可执行程序中对连接字符串进行硬编码。

翻译自: https://www.sqlshack.com/how-to-mimic-a-wildcard-search-on-always-encrypted-columns-with-entity-framework/

如何使用Entity Framework在Always Encrypted列上模拟通配符搜索相关推荐

  1. Entity Framework Core 生成跟踪列

    注意:我使用的是 Entity Framework Core 2.0 (2.0.0-preview2-final).正式版发布时,功能可能存在变动. 当您设计数据库时,有时需要添加列以跟踪记录何时更改 ...

  2. Entity Framework Core 2.0 全局查询过滤器

    本博文翻译自: http://gunnarpeipman.com/2017/08/ef-core-global-query-filters/ Entity Framework Core 2.0 全局查 ...

  3. Entity Framework Core 执行SQL语句和存储过程

    无论ORM有多么强大,总会出现一些特殊的情况,它无法满足我们的要求.在这篇文章中,我们介绍几种执行SQL的方法. 表结构 在具体内容开始之前,我们先简单说明一下要使用的表结构. public clas ...

  4. Entity Framework Core 批处理语句

    在Entity Framework Core (EF Core)有许多新的功能,最令人期待的功能之一就是批处理语句.那么批处理语句是什么呢?批处理语句意味着它不会为每个插入/更新/删除语句发送单独的请 ...

  5. Entity Framework Core 命名约定

    注意:我使用的是 Entity Framework Core 2.0 (2.0.0-preview2-final).正式版发布时,功能可能存在变动.Entity Framework 迁移允许从模型生成 ...

  6. Entity Framework Core 软删除与查询过滤器

    注意:我使用的是 Entity Framework Core 2.0 (2.0.0-preview2-final).正式版发布后,功能可能存在变动. 继续探索Entity Framework Core ...

  7. 彻底征服 Entity Framework Core 优化!

    作者 | 喵叔 责编 | 胡巍巍 出品 | CSDN(ID:CSDNnews) 这篇文章我们来讲解一下 Entity Framework Core 的优化方案.Entity Framework Cor ...

  8. Entity Framework CodeFirst数据迁移

    原文:Entity Framework CodeFirst数据迁移 前言 紧接着前面一篇博文Entity Framework CodeFirst尝试. 我们知道无论是"Database Fi ...

  9. entity framework mysql guid,c# – 自动生成不是Entity Framework中主键的GUID列

    我正在从现有的模型和数据库转向Entity Framework.在该数据库中,有几个表具有GUID列,这些表不是主键(或根本不是键!).每个表都有一个ID列. GUID列具有在其上定义的ROWGUID ...

最新文章

  1. 【算法】差分与前缀和 算法详解+例题剖析
  2. LeetCode--258--各位相加*
  3. java中pagex_Java/5_get和post比较.md at master · zaoshangyaochifan/Java · GitHub
  4. CodeCraft-20 (Div. 2) C. Primitive Primes 思维 + 数论
  5. linux下ora 01110,ORA-01003ORA-01110
  6. ASP.NET数据绑定的记忆碎片
  7. tensorflow rnn 最简单实现代码
  8. 使用solrj api操作solr
  9. 打包.Cab时“x.inf contains DirIDs, which are not supported”
  10. 服务器总线协议_第一章----I2C总线协议入门
  11. 人工智能——自然演绎推理
  12. Looking up JNDI object with name [LOGGING_PATTERN_LEVEL]
  13. 华为服务器RH2288 V2 BIOS配置
  14. 你还有“不撞南墙不回头”之心吗?
  15. 计算机学院毕业典礼口号,大学毕业典礼横幅标语大全
  16. 平安人寿“内鬼”泄露近4万条公民信息
  17. VS2008的DDK环境配置DDK下载
  18. 深度学习概念——Epoch, Batch, Iteration
  19. 修改变量名,简单有效地提高代码质量!
  20. L1-039 古风排版-java

热门文章

  1. AI技术人才成长路线十大方向
  2. 【洛谷 P1772】 [ZJOI2006]物流运输(Spfa,dp)
  3. python_day25__02__异常处理__try---exception—else---finally
  4. Linux下切换用户根目录的指令
  5. 数据库读现象和隔离级别
  6. [原创]java WEB学习笔记18:java EE 中的MVC 设计模式(理论)
  7. javascript 数组合并与去重
  8. 条款34:区分接口继承和实现继承(Different between inheritance of interface and inheritance of implemenation)...
  9. 4.二叉搜索树转为有序双向链表(递归算法与非递归算法)
  10. 手工给alv添加合计