How to store WindowsIdentity for use at a later, i

2019-07-18 13:27发布

问题:

I've a WCF service that queues and executes commands on behalf of the user at a later, indeterminate time. I was hoping to store the WindowsIdentity as a byte array and stuff it into a database, and then later deserialize and use that object.

Some of the time, the service performs as expected: it correctly serializes, stores, deserializes, and executes the command as the user. Other times, I get an error "Exception has been thrown by the target of an invocation" when deserializing the WindowsIdentity, and still other times the deserialization works but part way through the command execution, the identity is no longer valid.

My question is this: is it possible in the .NET 4.0 framework using WCF to execute a command on behalf of a user at a later, indeterminate time with no explicit username and password?

The code that I'm using is below:

Serialization:

''' <summary>
''' Serializes a WindowsIdentity as a binary encoded byte array.
''' </summary>
''' <param name="identity">The identity to serialize.</param>
''' <returns>A byte array containing a binary representation of the identity.</returns>
Private Function SerializeWindowsIdentity(ByVal identity As WindowsIdentity) As Byte()
    If IsNothing(identity) Then Return Nothing
    Try
        Dim bf As New BinaryFormatter
        Using ms As New MemoryStream()
            bf.Serialize(ms, identity)
            Return ms.ToArray()
        End Using
    Catch ex As Exception
        Return Nothing
    End Try
End Function ' SerializeWindowsIdentity

Deserialization:

''' <summary>
''' Deserializes a WindowsIdentity from a binary encoded byte array.
''' </summary>
''' <param name="identity">A byte array containing a binary representation of a WindowsIdentity</param>
''' <returns>The deserialized WindowsIdentity from the byte array.</returns>
Private Function DeserializeWindowsIdentity(ByVal identity As Byte()) As WindowsIdentity
    If IsNothing(identity) OrElse identity.Count = 0 Then Return Nothing
    Try
        Dim bf As New BinaryFormatter()
        Using ms As New MemoryStream(identity)
            Dim obj As Object = bf.Deserialize(ms)
            Return CType(obj, WindowsIdentity)
        End Using
    Catch ex As Exception
        Return Nothing
    End Try
End Function ' DeserializeWindowsIdentity

WindowsIdentity capture:

identity = SerializeWindowsIdentity(ServiceSecurityContext.Current.WindowsIdentity)

Usage:

Dim cxt As WindowsImpersonationContext
Try
    Dim wi As WindowsIdentity = DeserializeWindowsIdentity(identity)
    If Not IsNothing(wi) Then cxt = wi.Impersonate()

    ' Do Stuff
Finally
    If Not IsNothing(cxt) Then cxt.Dispose()
End If

回答1:

Here is some pseudo-code:

    //When command comes in from the user, queue the work
    private void QueueWork()
    {
        //Authorization Check
        if (IsAuthorized(DoWork, ServiceSecurityContext.Current.WindowsIdentity))
            throw new SecurityAccessDeniedException("Unauthorized");

        //Queue the work for later
        Queue.Enqueue(ServiceSecurityContext.Current.WindowsIdentity.Name);
    }

    //check if 
    private bool IsAuthorized(Action doWork, WindowsIdentity windowsIdentity)
    {
        //custom logic
    }

    //Command executed at a later point in time
    private void DoWork()
    {
        var user = Queue.Dequeue() as string;

        Log(user + " is invoking DoWork");

        //Perform command on behalf of user
        //...
    }

It assumes that you have windows authentication enabled to capture the WindowsIdentity.

Also I am not sure how you are queueing the commands, and how you are executing them later.

You can replace the IsAuthorized method with some other way of authorization if you wish.

The service has permissions to perform the operation, and simply logs the user and command being executed.

If this doesn't quite fit your scenario, let me know and provide more information and I can modify the answer. But hope this leads you into the right direction



回答2:

So, the best solution we have so far is to use the token from the ServiceSecurityContext.Current.WindowsIdentity and create a new primary token that we serialize and store for later. The downside is that the token is invalid once the service is restarted, but it's a fine temporary work-around until we can revise our service so that we don't require a persistent authorization from the user. We looked at using S4U2Proxy which would do what we want, but the requirement for configuring the domain account the service runs under is a bit onerous for our users. The working code snippet is below (note: we probably don't need to serialize any more, but it was easier to keep in place since we don't have to update our database schema. Also, we could break out the serialize routine from the token duplication to make the code more manageable):

Deserialize code:

''' <summary>
''' Deserializes a user token from a binary encoded byte array.
''' </summary>
''' <param name="identity">A byte array containing an binary representation of a user token.</param>
''' <returns>The deserialized user token from the byte array.</returns>
Private Function DeserializeWindowsIdentityToken(ByVal identity As Byte()) As IntPtr
    If IsNothing(identity) Then Return Nothing
    Dim stream As New MemoryStream(identity)
    Dim serializer As New BinaryFormatter()
    Try
        Dim obj As Object = serializer.Deserialize(stream)
        Return CType(obj, IntPtr)
    Catch ex As Exception
        Return IntPtr.Zero
    End Try
End Function ' DeserializeWindowsIdentityToken

Serialize code:

''' <summary>
''' Serializes a user token as a binary encoded byte array.
''' </summary>
''' <param name="identity">The token to serialize.</param>
''' <returns>A byte array containing a binary representation of the token.</returns>
Private Function SerializeWindowsIdentityToken(ByVal identity As IntPtr) As Byte()
    Try
        Dim newToken As IntPtr = IntPtr.Zero
        Const securityDelegation As Int16 = 3
        Const tokenPrimary As Integer = 1
        Const maximumAllowed As Integer = &H2000000

        Dim sa As New SecurityAttributes()
        sa.bInheritHandle = True
        sa.Length = Marshal.SizeOf(sa)
        sa.lpSecurityDescriptor = IntPtr.Zero

        If DuplicateTokenEx(identity, maximumAllowed, sa, securityDelegation, tokenPrimary, newToken) = 0 Then Return Nothing

        Dim streamWriter As New MemoryStream()
        Dim serializer As New BinaryFormatter
        serializer.Serialize(streamWriter, newToken)
        Return streamWriter.ToArray()
    Catch ex As Exception
        Return Nothing
    End Try
End Function ' SerializeWindowsIdentityToken

<StructLayout(LayoutKind.Sequential)>
Private Structure SecurityAttributes
    Public Length As Integer
    Public lpSecurityDescriptor As IntPtr
    Public bInheritHandle As Boolean
End Structure ' SecurityAttributes

<DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
Private Shared Function DuplicateTokenEx(ByVal existingTokenHandle As IntPtr,
                                         ByVal desiredAccess As UInteger,
                                         ByRef threadAttributes As SecurityAttributes,
                                         ByVal impersonationLevel As Integer,
                                         ByVal tokenType As Integer,
                                         ByRef duplicateTokenHandle As IntPtr) As Integer
End Function ' DuplicateTokenEx

Token capture:

Dim storedToken As Byte() = SerializeWindowsIdentityToken(ServiceSecurityContext.Current.WindowsIdentity.Token)

Usage:

Dim identity As IntPtr = DeserializeWindowsIdentityToken(storedToken)
Dim cxt As WindowsImpersonationContext = Nothing
If Not IsNothing(identity) AndAlso identity <> IntPtr.Zero Then
    Try
        Dim identity As New WindowsIdentity(identity)
        cxt = identity.Impersonate()
    Catch ex As Exception
        ' Perform error handling
    End Try
End If

' Perform operations

If Not IsNothing(cxt) Then cxt.Dispose()