【问题标题】:SQL Server Impersonation and Connection PoolingSQL Server 模拟和连接池
【发布时间】:2014-02-23 05:42:28
【问题描述】:

我的任务是为我们拥有的旧数据库编写 Web 界面,其中所有用户都有数据库帐户并被分配相应的角色(我们在各处都有触发器记录用户执行某些操作时,所有这些都基于user_name())。

为了使用任何远程现代的东西并避免以纯文本形式存储用户的密码,我正在连接一个对每个用户都具有模拟权限的应用级帐户,并且我正在尝试运行 Execute As User=@usernameRevert 在运行任何 SQL 之前和之后设置和重置执行上下文。

不幸的是,连接池的 reset_connection 调用正在与我的连接一起使用,它最终引发了一些关于物理连接无效的令人讨厌的错误......

我可以通过不使用连接池来解决这个错误。但随后我的应用程序用户需要大量特权才能实际执行模拟。此外,杀死连接池是一件很糟糕的事情......

如何在不牺牲安全性或性能的情况下做到这一点?请记住,我无法改变我的用户拥有数据库登录的事实,而且我对以可检索的方式存储用户密码并不感到兴奋。我唯一的选择是绕过连接池以便我可以模拟(并使用 sa 用户,因此我有足够的权限来实际模拟某人)?

【问题讨论】:

  • 请注意,物理连接错误与以下错误一致:连接已被删除,因为打开它的主体随后假定了一个新的安全上下文,然后尝试在其模拟的安全上下文下重置连接。不支持此方案。请参阅联机丛书中的“模拟概述”。
  • Web 应用程序用户是否会通过 Windows 身份验证连接,域 kerberos 是否支持?
  • 如何[ab]使用连接参数的另一部分,例如应用程序名称或工作站 ID (connectionstrings.com/all-sql-server-connection-string-keywords) 与 App_Name()Host_Name() 函数结合使用?不理想,但考虑到限制可能是一个可接受的解决方法......
  • @Filip no,用户名和密码
  • @gvee,我知道如何破坏连接池,这不是问题。

标签: sql-server impersonation


【解决方案1】:

我知道这很旧,但是这篇文章是我找到的唯一有用的资源,我想我会分享我们基于 Filip De Vos 答案的解决方案。

我们还有一个使用 sp_setapprole 的旧版 VB6 应用程序(我很欣赏这与 OP 的原始帖子不太相符)。我们共享同一个数据库(本质上是应用程序框架的一部分)的 .NET 组件在很大程度上基于 Linq to SQL。

考虑到连接打开和关闭的次数,事实证明为数据上下文连接设置批准很麻烦。

我们最终按照上面的建议编写了一个简单的包装器。唯一被覆盖的方法是 Open()Close(),这是设置和取消设置 approle 的地方。

Public Class ManagedConnection
    Inherits Common.DbConnection

    Private mCookie As Byte()
    Private mcnConnection As SqlClient.SqlConnection

    Public Sub New()
        mcnConnection = New SqlClient.SqlConnection()
    End Sub

    Public Sub New(connectionString As String)
        mcnConnection = New SqlClient.SqlConnection(connectionString)
    End Sub

    Public Sub New(connectionString As String, credential As SqlClient.SqlCredential)
        mcnConnection = New SqlClient.SqlConnection(connectionString, credential)
    End Sub

    Public Overrides Property ConnectionString As String
        Get
            Return mcnConnection.ConnectionString
        End Get
        Set(value As String)
            mcnConnection.ConnectionString = value
        End Set
    End Property

    Public Overrides ReadOnly Property Database As String
        Get
            Return mcnConnection.Database
        End Get
    End Property

    Public Overrides ReadOnly Property DataSource As String
        Get
            Return mcnConnection.DataSource
        End Get
    End Property

    Public Overrides ReadOnly Property ServerVersion As String
        Get
            Return mcnConnection.ServerVersion
        End Get
    End Property

    Public Overrides ReadOnly Property State As ConnectionState
        Get
            Return mcnConnection.State
        End Get
    End Property

    Public Overrides Sub ChangeDatabase(databaseName As String)
        mcnConnection.ChangeDatabase(databaseName)
    End Sub

    Public Overrides Sub Close()

        Using cm As New SqlClient.SqlCommand("sp_unsetapprole")
            cm.Connection = mcnConnection
            cm.CommandType = CommandType.StoredProcedure
            cm.Parameters.Add("@cookie", SqlDbType.VarBinary, 8000).Value = mCookie

            cm.ExecuteNonQuery()
        End Using

        mcnConnection.Close()
    End Sub

    Public Overrides Sub Open()
        mcnConnection.Open()

        Using cm As New SqlClient.SqlCommand("sp_setapprole")
            cm.Connection = mcnConnection
            cm.CommandType = CommandType.StoredProcedure
            cm.Parameters.Add("@rolename", SqlDbType.NVarChar, 128).Value = "UID"
            cm.Parameters.Add("@password", SqlDbType.NVarChar, 128)Value = "PWD"
            cm.Parameters.Add("@fCreateCookie", SqlDbType.Bit).Value = True
            cm.Parameters.Add("@cookie", SqlDbType.VarBinary, 8000).Direction = ParameterDirection.InputOutput
            cm.ExecuteNonQuery()

            mCookie = cm.Parameters("@cookie").Value
        End Using
    End Sub

    Protected Overrides Function BeginDbTransaction(isolationLevel As IsolationLevel) As DbTransaction
        Return mcnConnection.BeginTransaction(isolationLevel)
    End Function

    Protected Overrides Function CreateDbCommand() As DbCommand
        Return mcnConnection.CreateCommand()
    End Function
End Class

之前:

Using dc As New SystemOptionDataContext(sConnectionString)
    intOption= dc.GetIntegerValue("SomeDatabaseOption")
End Using

之后:

Using dc As New SystemOptionDataContext(New ManagedConnection(strConnectionString))
    intOption= dc.GetIntegerValue("SomeDatabaseOption")
End Using

希望这对其他人有所帮助。

【讨论】:

    【解决方案2】:

    为了实现一种“假”委托而不对应用程序/数据库代码进行巨大更改,我建议使用context_info() 将当前用户传输到数据库并将对user_name() 的调用替换为对dbo.fn_user_name() 的调用。

    关于如何构建此解决方案的示例

    创建 fn_user_name() 函数

    我将创建一个函数 fn_user_name,它将从连接上的 context_info() 中提取用户名,或者在没有可用的上下文信息时返回 user_name()。请注意,连接上下文是一个 128 字节的二进制文件。你放在那里的任何东西都会用零个字符填充,为了解决这个问题,我用空格填充值。

    create function dbo.fn_user_name()
    returns sysname
    as
    begin
        declare @user sysname = rtrim(convert(nvarchar(64), context_info()))
        if @user is null 
            return user_name()
        return @user
    end
    go
    

    现在您可以在代码中找到所有对 user_name() 的调用,并用这个函数替换它们。

    在 .net 中的 db 调用中嵌入上下文

    这里有 2 个选项。或者您创建自己的 SqlConnection 类,或者创建一个工厂方法,该方法将返回一个打开的 sqlconnection,如下所示。工厂方法的问题是您运行的每个查询都将是 2 个数据库调用。这是最少的代码。

        public SqlConnection CreateConnection(string connectionString, string user)
        {
            var conn = new SqlConnection(connectionString);
            using (var cmd = new SqlCommand(
                @"declare @a varbinary(128) = convert(varbinary(128), @user + replicate(N' ', 64 - len(@user)))
                  set context_info @a", conn))
            {
                cmd.Parameters.Add("@user", SqlDbType.NVarChar, 64).Value = user;
                conn.Open();
                cmd.ExecuteNonQuery();
            }
            return conn;
        }
    

    你可以这样使用:

    using(var conn = CreateConnection(connectionString, user))
    {
       var cmd = new SqlCommand("select 1", conn);
       return conn.ExecuteScalar()
    }
    

    对于 SqlConnection 的替代版本,您需要重载 DbConnection 并实现 SqlConnection 的所有方法。执行方法将在下面的查询前面加上用户名作为额外参数。

    declare @a varbinary(128) = convert(varbinary(128), @user + replicate(N' ', 64 - len(@user)))
    set context_info @a 
    

    然后该类将用作:

    using(var conn = new SqlContextInfoConnection(connectionString, user))
    {
       var cmd = new SqlCommand("select 1", conn);
       conn.open;
       return conn.ExecuteScalar()
    }
    

    我会亲自实施选项 2,因为它更接近普通 SqlConnection 的工作方式。

    【讨论】:

    • 这个解决方案的几个问题: 1. 它与我们无法更改的VB6旧版应用程序不兼容,并且无法使用上述代码。 2. 我不喜欢在每次想要执行某事时执行两个单独的请求...
    • 您的客户端服务器应用程序不需要更改,如果没有设置上下文,fn_user_name() 的输出与 user_name() 相同。
    • 这就是为什么我提出选项 2,其中上下文调用被添加到要执行的 sql 字符串之前。今晚有空的时候,我会试着做饭。
    • 伙计,我当然希望将来有一天我不必维护这个应用程序。
    猜你喜欢
    • 2021-08-13
    • 2021-11-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-27
    • 2010-09-05
    • 2016-03-28
    相关资源
    最近更新 更多