C# CLR 抛出安全异常,除非标记为不安全

C# CLR throws security exception unless marked as UNSAFE

我在 SQL Server 2012 中有一个从事务中调用的 C# CLR (.NET 4.5) 存储过程。 CLR 调用需要 TLS 1.2 的 WebService。

如果我使用 permission_set = UNSAFE 创建程序集,一切正常,但我真的想避免而是使用 EXTERNAL_ACCESS。然而,事实证明这是一个真正的挑战。

使用EXTERNAL_ACCESS时有两个问题:

1) 我从 CLR 写入日志文件(用于维护和调试目的),这不起作用,因为 不允许锁定 :

private static readonly object Locker = new object();
public static string LogMessage(string message)
{
    message = String.Format("{0}: {1}" + Environment.NewLine, DateTime.Now, message);

    lock (Locker)
    {
        var file = new FileStream(GetConfigValue("LogFile"), FileMode.Append, FileAccess.Write);
        var sw = new StreamWriter(file);
        sw.Write(message);
        sw.Flush();
        sw.Close();
    }
    return message;
}

错误信息:

System.Security.HostProtectionException: Attempted to perform an operation that was forbidden by the CLR host.

The protected resources (only available with full trust) were: All the demanded resources were: Synchronization, ExternalThreading

我可以登录到数据库 table,但是由于对 CLR 的调用是从事务内部进行的,错误会导致回滚,这也会回滚日志记录。我可以使用事件日志,但如果可能的话我真的不想这样做。

2) 我调用的 Web 服务需要 TLS 1.2,但不允许设置 ServerCertificateValidationCallback:

ServicePointManager.ServerCertificateValidationCallback = AcceptAllCertifications;

错误信息:

System.Security.SecurityException: Request for the permission of type 'System.Security.Permissions.SecurityPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
System.Security.SecurityException:
at System.Security.CodeAccessSecurityEngine.Check(Object demand, StackCrawlMark& stackMark, Boolean isPermSet) at System.Security.CodeAccessPermission.Demand() at System.Net.ServicePointManager.set_ServerCertificateValidationCallback(RemoteCertificateValidationCallback value) at AcmeClr.StoredProcedures.ProcessPayment(SqlMoney amount, SqlString ticketNo, SqlString& resultCode, SqlString& resultText)

这个问题有没有解决办法 - 有没有办法在不使用 UNSAFE 的情况下完成这项工作?

对于问题 #1(使用锁来管理同时写入同一日志文件的多个线程/会话),除了 UNSAFE 模式外,没有其他方法可以使用锁。然而,好消息是您可能一开始就不需要使用锁。锁真正完成的是确保同时发生的两个 LogMessage() 调用不会发生冲突,其中一个会出错。这应该可以通过使用 FileStream constructor that allows for passing in the FileShare 选项/枚举的另一个重载来解决。您应该能够传入 FileShare.ReadWrite 以防止错误。您已经将 DateTime.Now 连接到 message 中,这样如果较早的消息写在较晚的消息之后,这应该有助于解决序列问题。

但是除了那个特定的争论之外,即使有锁,您仍然会遇到两个会话大致同时执行 SQLCLR WebService 存储过程并交错其消息的问题。你如何区分消息?我建议在存储过程的开头创建一个新的 Guid 并将其连接到每条消息中,以便您可以将特定调用的消息关联到存储过程(我假设您的代码有多个地方它调用 LogMessage()) 并将这些消息与其他调用(无论是否并发)区分开来。

如果出于任何原因上述两部分(FileShare.ReadWrite 并将 Guid 连接到 message)不能防止错误,那么您可以忽略 FileShare 选项(尽管我仍然更喜欢将它设置为 Read,这样我可以在写入文件时轻松检查它)而不是将 Guid 连接到 message,而是将它附加到 [=22= 的末尾] 值,就在文件扩展名之前。然后特定呼叫的消息会自动相互关联并与其他并发呼叫区分开来。这只是意味着你有一堆日志文件。

关于此的其他三个想法:

  1. 通过在 LogMessage() 方法中使用 lock,您实际上增加了阻塞,因为跨不同会话对该方法的并发调用将不得不等待锁所有者释放锁。毕竟,这首先就是锁的意义所在,对吧?除非在处理事务时,您真的不想延长它们超过绝对必要,并且通过 SQLCLR 调用 Web 服务的本质是您已经使事务依赖于网络延迟和该外部系统的响应能力(即使它是内部系统)。

  2. 您提到:

    I could log to a database table instead, but since the call to the CLR is from within a transaction, errors would cause a rollback, which would also rollback the log record.

    不一定。如果你使用进程内"Context Connection",那么是的,这是真的。如果您使用常规/外部连接也是如此,如果您 在连接字符串中指定 Enlist 关键字,或者如果您指定 Enlist = true; .但是,如果您指定 Enlist = false; 那么它应该是单独的/断开连接的事务,如果调用存储过程的事务被回滚,则不会被回滚。

  3. 虽然这在使用 SQLCLR 代码的情况下可能无济于事,但我至少要提到对于纯 T-SQL 代码,当我想解决这个丢失问题时发生回滚时的日志记录,您可以选择将这些记录最初写入 table 变量。 Table 变量不受事务限制,因此回滚不会影响它们。不利的一面是,如果进程以这样一种方式终止,即它在到达回滚后将 Table 变量的记录插入真实的 table 的部分之前完全终止,那么你做丢失那些记录。这就是有时写入日志文件或使用 SQLCLR 使用 Enlist = false;.
  4. 建立常规/外部连接更好的地方

对于问题 #2(设置 ServicePointManager.ServerCertificateValidationCallback),遗憾的是您对此无能为力。当我尝试将 UseSSL 属性 设置为 trueFtpWebRequest 以支持 FTPS 时,我遇到了同样的问题 运行。在执行此操作时,我还没有找到将程序集标记为 PERMISSION_SET = UNSAFE 的方法。


旁注:

  • 我同意将 Assembly 保留为 EXTERNAL_ACCESS 而不是 UNSAFE 的愿望,但鉴于这是您的代码 and 您没有使用 UNSAFE 来引入任何第 3 方库或不受支持的 .NET Framework 库,实际风险级别非常低。

  • 此外,希望您使用的是非对称密钥或基于证书的登录,以允许程序集拥有非SAFE权限并且没有 将数据库设置为 TRUSTWORTHY ON.

  • 除非问题中显示的代码为了仅发布 "necessary" 代码的目的而被简化,以便传达眼前的问题,否则您缺少围绕外部的错误处理资源。如果进程崩溃,如果您未能正确处理 StreamWriterFileStream,日志文件将被锁定,直到 App Domain 被卸载。您可以在这 5 行周围添加 try / finally 结构,也可以让编译器使用 using() 构造/宏为您完成。