Acessando um arquivo compartilhado (UNC) A partir de um remoto, domínio não confiável com credenciais

StackOverflow https://stackoverflow.com/questions/659013

  •  20-08-2019
  •  | 
  •  

Pergunta

Temos correr em uma situação interessante que precisa resolver, e minhas pesquisas se voltaram-se nula. Por isso, apelo à comunidade SO para obter ajuda.

A questão é esta: temos uma necessidade para programaticamente acessar um arquivo compartilhado que não está em nosso domínio, e não dentro de um domínio confiável externo através de compartilhamento de arquivos remoto / UNC. Naturalmente, é preciso fornecer credenciais para a máquina remota.

Normalmente, um resolve este problema em uma de duas maneiras:

  1. Mapear o compartilhamento de arquivo como uma unidade e fornecer as credenciais nesse momento. Isso normalmente é feito usando o comando NET USE ou as funções Win32 que NET USE duplicado.
  2. Acesse o arquivo com um caminho UNC como se o computador remoto estavam no domínio e garantir que a conta em que o programa é executado é duplicada (incluindo senha) na máquina remota como um usuário local. Basicamente aproveitar o fato de que o Windows irá fornecer automaticamente as credenciais do usuário atual quando o usuário tenta acessar um arquivo compartilhado.
  3. Não use o compartilhamento de arquivos remoto. Use FTP (ou outros meios) para transferir o arquivo, o trabalho sobre-lo localmente, em seguida, transferi-lo de volta.

Para vários e diversos motivos, nossos arquitetos de segurança / rede rejeitaram as duas primeiras abordagens. A segunda abordagem é, obviamente, uma brecha de segurança; se o computador remoto está comprometido, o computador local está agora em risco. A primeira abordagem é insatisfatória porque a unidade recém-montado é um recurso compartilhado disponível para outros programas no computador local durante o acesso ao arquivo pelo programa. Mesmo que seja perfeitamente possível fazer isso temporária, ainda é um buraco em sua opinião.

Eles estão abertos para a terceira opção, mas os administradores de rede remota insistem em SFTP ao invés de FTPS, e FtpWebRequest só suporta FTPS. SFTP é a opção mais firewall-friendly e há algumas bibliotecas que eu poderia usar para essa abordagem, mas eu prefiro a reduzir minhas dependências se eu puder.

Eu procurei MSDN tanto para um meio gerenciado ou um Win32 usando o compartilhamento de arquivo remoto, mas não conseguiram chegar a qualquer coisa útil.

E então eu pergunto: Existe outra maneira? Eu perdi uma função win32 super-secreto que faz o que eu quero? Ou devo buscar alguma variante da opção 3?

Foi útil?

Solução

A maneira de resolver o seu problema é usar uma API Win32 chamado WNetUseConnection .
Use esta função para ligar a um caminho UNC com autenticação, não para mapear uma unidade .

Isso permitirá que você se conectar a uma máquina remota, mesmo se ele não está no mesmo domínio, e mesmo se ele tem um nome de usuário e senha diferente.

Depois de ter utilizado WNetUseConnection você será capaz de acessar o arquivo através de um caminho UNC como se estivesse no mesmo domínio. A melhor maneira é provavelmente através da administrativa construída em ações.
Exemplo: \\ computername \ c $ \ Program Files \ Folder \ file.txt

Eis alguns exemplos de C # código que usa WNetUseConnection .

Note, para o NETRESOURCE, você deve passar nulo para o lpLocalName e lpProvider. O dwType deve ser RESOURCETYPE_DISK. O lpRemoteName deve ser \\ computador.

Outras dicas

Para as pessoas que procuram uma solução rápida, você pode usar o NetworkShareAccesser eu escrevi recentemente (baseado em esta resposta (graças tanto)):

Uso:

using (NetworkShareAccesser.Access(REMOTE_COMPUTER_NAME, DOMAIN, USER_NAME, PASSWORD))
{
    File.Copy(@"C:\Some\File\To\copy.txt", @"\\REMOTE-COMPUTER\My\Shared\Target\file.txt");
}

AVISO: Por favor, tenha absoluta certeza, que Dispose do NetworkShareAccesser é chamado (! Mesmo se sua aplicação falha), caso contrário, uma conexão aberta permanecerá no Windows. Você pode ver todas as conexões abertas abrindo o cmd rápida e entrar net use.

O Código:

/// <summary>
/// Provides access to a network share.
/// </summary>
public class NetworkShareAccesser : IDisposable
{
    private string _remoteUncName;
    private string _remoteComputerName;

    public string RemoteComputerName
    {
        get
        {
            return this._remoteComputerName;
        }
        set
        {
            this._remoteComputerName = value;
            this._remoteUncName = @"\\" + this._remoteComputerName;
        }
    }

    public string UserName
    {
        get;
        set;
    }
    public string Password
    {
        get;
        set;
    }

    #region Consts

    private const int RESOURCE_CONNECTED = 0x00000001;
    private const int RESOURCE_GLOBALNET = 0x00000002;
    private const int RESOURCE_REMEMBERED = 0x00000003;

    private const int RESOURCETYPE_ANY = 0x00000000;
    private const int RESOURCETYPE_DISK = 0x00000001;
    private const int RESOURCETYPE_PRINT = 0x00000002;

    private const int RESOURCEDISPLAYTYPE_GENERIC = 0x00000000;
    private const int RESOURCEDISPLAYTYPE_DOMAIN = 0x00000001;
    private const int RESOURCEDISPLAYTYPE_SERVER = 0x00000002;
    private const int RESOURCEDISPLAYTYPE_SHARE = 0x00000003;
    private const int RESOURCEDISPLAYTYPE_FILE = 0x00000004;
    private const int RESOURCEDISPLAYTYPE_GROUP = 0x00000005;

    private const int RESOURCEUSAGE_CONNECTABLE = 0x00000001;
    private const int RESOURCEUSAGE_CONTAINER = 0x00000002;


    private const int CONNECT_INTERACTIVE = 0x00000008;
    private const int CONNECT_PROMPT = 0x00000010;
    private const int CONNECT_REDIRECT = 0x00000080;
    private const int CONNECT_UPDATE_PROFILE = 0x00000001;
    private const int CONNECT_COMMANDLINE = 0x00000800;
    private const int CONNECT_CMD_SAVECRED = 0x00001000;

    private const int CONNECT_LOCALDRIVE = 0x00000100;

    #endregion

    #region Errors

    private const int NO_ERROR = 0;

    private const int ERROR_ACCESS_DENIED = 5;
    private const int ERROR_ALREADY_ASSIGNED = 85;
    private const int ERROR_BAD_DEVICE = 1200;
    private const int ERROR_BAD_NET_NAME = 67;
    private const int ERROR_BAD_PROVIDER = 1204;
    private const int ERROR_CANCELLED = 1223;
    private const int ERROR_EXTENDED_ERROR = 1208;
    private const int ERROR_INVALID_ADDRESS = 487;
    private const int ERROR_INVALID_PARAMETER = 87;
    private const int ERROR_INVALID_PASSWORD = 1216;
    private const int ERROR_MORE_DATA = 234;
    private const int ERROR_NO_MORE_ITEMS = 259;
    private const int ERROR_NO_NET_OR_BAD_PATH = 1203;
    private const int ERROR_NO_NETWORK = 1222;

    private const int ERROR_BAD_PROFILE = 1206;
    private const int ERROR_CANNOT_OPEN_PROFILE = 1205;
    private const int ERROR_DEVICE_IN_USE = 2404;
    private const int ERROR_NOT_CONNECTED = 2250;
    private const int ERROR_OPEN_FILES = 2401;

    #endregion

    #region PInvoke Signatures

    [DllImport("Mpr.dll")]
    private static extern int WNetUseConnection(
        IntPtr hwndOwner,
        NETRESOURCE lpNetResource,
        string lpPassword,
        string lpUserID,
        int dwFlags,
        string lpAccessName,
        string lpBufferSize,
        string lpResult
        );

    [DllImport("Mpr.dll")]
    private static extern int WNetCancelConnection2(
        string lpName,
        int dwFlags,
        bool fForce
        );

    [StructLayout(LayoutKind.Sequential)]
    private class NETRESOURCE
    {
        public int dwScope = 0;
        public int dwType = 0;
        public int dwDisplayType = 0;
        public int dwUsage = 0;
        public string lpLocalName = "";
        public string lpRemoteName = "";
        public string lpComment = "";
        public string lpProvider = "";
    }

    #endregion

    /// <summary>
    /// Creates a NetworkShareAccesser for the given computer name. The user will be promted to enter credentials
    /// </summary>
    /// <param name="remoteComputerName"></param>
    /// <returns></returns>
    public static NetworkShareAccesser Access(string remoteComputerName)
    {
        return new NetworkShareAccesser(remoteComputerName);
    }

    /// <summary>
    /// Creates a NetworkShareAccesser for the given computer name using the given domain/computer name, username and password
    /// </summary>
    /// <param name="remoteComputerName"></param>
    /// <param name="domainOrComuterName"></param>
    /// <param name="userName"></param>
    /// <param name="password"></param>
    public static NetworkShareAccesser Access(string remoteComputerName, string domainOrComuterName, string userName, string password)
    {
        return new NetworkShareAccesser(remoteComputerName,
                                        domainOrComuterName + @"\" + userName,
                                        password);
    }

    /// <summary>
    /// Creates a NetworkShareAccesser for the given computer name using the given username (format: domainOrComputername\Username) and password
    /// </summary>
    /// <param name="remoteComputerName"></param>
    /// <param name="userName"></param>
    /// <param name="password"></param>
    public static NetworkShareAccesser Access(string remoteComputerName, string userName, string password)
    {
        return new NetworkShareAccesser(remoteComputerName, 
                                        userName,
                                        password);
    }

    private NetworkShareAccesser(string remoteComputerName)
    {
        RemoteComputerName = remoteComputerName;               

        this.ConnectToShare(this._remoteUncName, null, null, true);
    }

    private NetworkShareAccesser(string remoteComputerName, string userName, string password)
    {
        RemoteComputerName = remoteComputerName;
        UserName = userName;
        Password = password;

        this.ConnectToShare(this._remoteUncName, this.UserName, this.Password, false);
    }

    private void ConnectToShare(string remoteUnc, string username, string password, bool promptUser)
    {
        NETRESOURCE nr = new NETRESOURCE
        {
            dwType = RESOURCETYPE_DISK,
            lpRemoteName = remoteUnc
        };

        int result;
        if (promptUser)
        {
            result = WNetUseConnection(IntPtr.Zero, nr, "", "", CONNECT_INTERACTIVE | CONNECT_PROMPT, null, null, null);
        }
        else
        {
            result = WNetUseConnection(IntPtr.Zero, nr, password, username, 0, null, null, null);
        }

        if (result != NO_ERROR)
        {
            throw new Win32Exception(result);
        }
    }

    private void DisconnectFromShare(string remoteUnc)
    {
        int result = WNetCancelConnection2(remoteUnc, CONNECT_UPDATE_PROFILE, false);
        if (result != NO_ERROR)
        {
            throw new Win32Exception(result);
        }
    }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    /// <filterpriority>2</filterpriority>
    public void Dispose()
    {
        this.DisconnectFromShare(this._remoteUncName);
    }
}

AFAIK, você não precisa Mapa o caminho UNC para uma letra de unidade a fim de estabelecer as credenciais para um servidor. I usado regularmente scripts em lotes como:

net use \\myserver /user:username password

:: do something with \\myserver\the\file\i\want.xml

net use /delete \\my.server.com

No entanto, qualquer programa em execução na mesma conta como o programa ainda seria capaz de acessar tudo o que username:password tem acesso. Uma possível solução poderia ser a de isolar o seu programa em sua própria conta de usuário local (o acesso UNC é local para a conta que chamou NET USE).

Nota: Usando SMB accross domínios não é um bom uso da tecnologia, IMO. Se a segurança é importante, o fato de que SMB carece de criptografia é um pouco de um amortecedor por si só.

Ao invés de WNetUseConnection, eu recomendaria NetUseAdd . WNetUseConnection é uma função legado que foi substituído pelo WNetUseConnection2 e WNetUseConnection3, mas todas essas funções criar um dispositivo de rede que é visível no Windows Explorer. NetUseAdd é o equivalente de chamar net use em um prompt do DOS para autenticar em um computador remoto.

Se você chamar NetUseAdd seguida, as tentativas posteriores para acessar o diretório deve ter êxito.

Enquanto eu não me conheço, eu certamente espero que # 2 está incorreta ... Eu gostaria de pensar que o Windows não vai AUTOMATICAMENTE dar minhas informações de login (menos de toda a minha senha!) a qualquer máquina, muito menos uma que não faz parte da minha confiança.

De qualquer maneira, você tem explorado a arquitetura representação? Seu código vai ser semelhante a esta:

using (System.Security.Principal.WindowsImpersonationContext context = System.Security.Principal.WindowsIdentity.Impersonate(token))
{
    // Do network operations here

    context.Undo();
}

Neste caso, a variável token é um IntPtr. A fim de obter um valor para esta variável, você vai ter que chamar a função API LogonUser do Windows não gerenciado. Uma rápida visita à pinvoke.net nos dá a seguinte assinatura:

[System.Runtime.InteropServices.DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(
    string lpszUsername,
    string lpszDomain,
    string lpszPassword,
    int dwLogonType,
    int dwLogonProvider,
    out IntPtr phToken
);

Nome de usuário, domínio e senha deve parecer bastante óbvia. Ter um olhar para os vários valores que podem ser passados ??para dwLogonType e dwLogonProvider para determinar o que melhor se adapte às suas necessidades.

Este código não foi testado, como eu não tenho um segundo domínio aqui onde eu possa verificar, mas isso deve esperamos colocá-lo no caminho certo.

A maioria dos servidores SFTP apoiar SCP bem que pode ser muito mais fácil de encontrar bibliotecas para. Você poderia até mesmo apenas chamar um cliente existente do seu código como pscp incluído com PuTTY .

Se o tipo de arquivo que você está trabalhando com é algo simples como um arquivo de texto ou XML, você pode até mesmo ir tão longe como para escrever sua própria implementação cliente / servidor para manipular o arquivo usando algo parecido com .NET Remoting ou web serviços.

Eu vi a opção 3 implementado com ferramentas JSCAPE de uma forma simples bonito. Você pode experimentá-lo. Não é gratuito, mas faz o seu trabalho.

Aqui uma classe POC mínima w / todo o cruft removido

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

public class UncShareWithCredentials : IDisposable
{
    private string _uncShare;

    public UncShareWithCredentials(string uncShare, string userName, string password)
    {
        var nr = new Native.NETRESOURCE
        {
            dwType = Native.RESOURCETYPE_DISK,
            lpRemoteName = uncShare
        };

        int result = Native.WNetUseConnection(IntPtr.Zero, nr, password, userName, 0, null, null, null);
        if (result != Native.NO_ERROR)
        {
            throw new Win32Exception(result);
        }
        _uncShare = uncShare;
    }

    public void Dispose()
    {
        if (!string.IsNullOrEmpty(_uncShare))
        {
            Native.WNetCancelConnection2(_uncShare, Native.CONNECT_UPDATE_PROFILE, false);
            _uncShare = null;
        }
    }

    private class Native
    {
        public const int RESOURCETYPE_DISK = 0x00000001;
        public const int CONNECT_UPDATE_PROFILE = 0x00000001;
        public const int NO_ERROR = 0;

        [DllImport("mpr.dll")]
        public static extern int WNetUseConnection(IntPtr hwndOwner, NETRESOURCE lpNetResource, string lpPassword, string lpUserID,
            int dwFlags, string lpAccessName, string lpBufferSize, string lpResult);

        [DllImport("mpr.dll")]
        public static extern int WNetCancelConnection2(string lpName, int dwFlags, bool fForce);

        [StructLayout(LayoutKind.Sequential)]
        public class NETRESOURCE
        {
            public int dwScope;
            public int dwType;
            public int dwDisplayType;
            public int dwUsage;
            public string lpLocalName;
            public string lpRemoteName;
            public string lpComment;
            public string lpProvider;
        }
    }
}

Você pode usar diretamente \\server\share\folder w / WNetUseConnection, não há necessidade de tira-lo de parte \\server única de antemão.

im anexar meu código vb.net com base em brian referência

Imports System.ComponentModel

Imports System.Runtime.InteropServices

Public Class PinvokeWindowsNetworking

Const NO_ERROR As Integer = 0



Private Structure ErrorClass

    Public num As Integer

    Public message As String



    Public Sub New(ByVal num As Integer, ByVal message As String)

        Me.num = num

        Me.message = message

    End Sub

End Structure



Private Shared ERROR_LIST As ErrorClass() = New ErrorClass() {

    New ErrorClass(5, "Error: Access Denied"),

    New ErrorClass(85, "Error: Already Assigned"),

    New ErrorClass(1200, "Error: Bad Device"),

    New ErrorClass(67, "Error: Bad Net Name"),

    New ErrorClass(1204, "Error: Bad Provider"),

    New ErrorClass(1223, "Error: Cancelled"),

    New ErrorClass(1208, "Error: Extended Error"),

    New ErrorClass(487, "Error: Invalid Address"),

    New ErrorClass(87, "Error: Invalid Parameter"),

    New ErrorClass(1216, "Error: Invalid Password"),

    New ErrorClass(234, "Error: More Data"),

    New ErrorClass(259, "Error: No More Items"),

    New ErrorClass(1203, "Error: No Net Or Bad Path"),

    New ErrorClass(1222, "Error: No Network"),

    New ErrorClass(1206, "Error: Bad Profile"),

    New ErrorClass(1205, "Error: Cannot Open Profile"),

    New ErrorClass(2404, "Error: Device In Use"),

    New ErrorClass(2250, "Error: Not Connected"),

    New ErrorClass(2401, "Error: Open Files")}



Private Shared Function getErrorForNumber(ByVal errNum As Integer) As String

    For Each er As ErrorClass In ERROR_LIST

        If er.num = errNum Then Return er.message

    Next



    Try

        Throw New Win32Exception(errNum)

    Catch ex As Exception

        Return "Error: Unknown, " & errNum & " " & ex.Message

    End Try



    Return "Error: Unknown, " & errNum

End Function



<DllImport("Mpr.dll")>

Private Shared Function WNetUseConnection(ByVal hwndOwner As IntPtr, ByVal lpNetResource As NETRESOURCE, ByVal lpPassword As String, ByVal lpUserID As String, ByVal dwFlags As Integer, ByVal lpAccessName As String, ByVal lpBufferSize As String, ByVal lpResult As String) As Integer

End Function



<DllImport("Mpr.dll")>

Private Shared Function WNetCancelConnection2(ByVal lpName As String, ByVal dwFlags As Integer, ByVal fForce As Boolean) As Integer

End Function



<StructLayout(LayoutKind.Sequential)>

Private Class NETRESOURCE

    Public dwScope As Integer = 0

    Public dwType As Integer = 0

    Public dwDisplayType As Integer = 0

    Public dwUsage As Integer = 0

    Public lpLocalName As String = ""

    Public lpRemoteName As String = ""

    Public lpComment As String = ""

    Public lpProvider As String = ""

End Class



Public Shared Function connectToRemote(ByVal remoteUNC As String, ByVal username As String, ByVal password As String) As String

    Return connectToRemote(remoteUNC, username, password, False)

End Function



Public Shared Function connectToRemote(ByVal remoteUNC As String, ByVal username As String, ByVal password As String, ByVal promptUser As Boolean) As String

    Dim nr As NETRESOURCE = New NETRESOURCE()

    nr.dwType = ResourceTypes.Disk

    nr.lpRemoteName = remoteUNC

    Dim ret As Integer



    If promptUser Then

        ret = WNetUseConnection(IntPtr.Zero, nr, "", "", Connects.Interactive Or Connects.Prompt, Nothing, Nothing, Nothing)

    Else

        ret = WNetUseConnection(IntPtr.Zero, nr, password, username, 0, Nothing, Nothing, Nothing)

    End If



    If ret = NO_ERROR Then Return Nothing

    Return getErrorForNumber(ret)

End Function



Public Shared Function disconnectRemote(ByVal remoteUNC As String) As String

    Dim ret As Integer = WNetCancelConnection2(remoteUNC, Connects.UpdateProfile, False)

    If ret = NO_ERROR Then Return Nothing

    Return getErrorForNumber(ret)

End Function


Enum Resources As Integer

    Connected = &H1

    GlobalNet = &H2

    Remembered = &H3

End Enum


Enum ResourceTypes As Integer

    Any = &H0

    Disk = &H1

    Print = &H2

End Enum


Enum ResourceDisplayTypes As Integer

    Generic = &H0

    Domain = &H1

    Server = &H2

    Share = &H3

    File = &H4

    Group = &H5

End Enum


Enum ResourceUsages As Integer

    Connectable = &H1

    Container = &H2

End Enum


Enum Connects As Integer

    Interactive = &H8

    Prompt = &H10

    Redirect = &H80

    UpdateProfile = &H1

    CommandLine = &H800

    CmdSaveCred = &H1000

    LocalDrive = &H100

End Enum


End Class

como usá-lo

Dim login = PinvokeWindowsNetworking.connectToRemote("\\ComputerName", "ComputerName\UserName", "Password")

    If IsNothing(login) Then



        'do your thing on the shared folder



       PinvokeWindowsNetworking.disconnectRemote("\\ComputerName")

    End If

Eu olhei para o MS para encontrar as respostas. A primeira solução assume a conta de usuário que executa o processo de aplicação tem acesso à pasta ou unidade (o mesmo domínio) compartilhado. Verifique se o seu DNS é resolvido ou tente usar o endereço IP. Basta fazer o seguinte:

 DirectoryInfo di = new DirectoryInfo(PATH);
 var files = di.EnumerateFiles("*.*", SearchOption.AllDirectories);

Se você quiser em diferentes domínios NET 2.0 com credenciais siga este modelo:

WebRequest req = FileWebRequest.Create(new Uri(@"\\<server Name>\Dir\test.txt"));

        req.Credentials = new NetworkCredential(@"<Domain>\<User>", "<Password>");
        req.PreAuthenticate = true;

        WebResponse d = req.GetResponse();
        FileStream fs = File.Create("test.txt");

        // here you can check that the cast was successful if you want. 
        fs = d.GetResponseStream() as FileStream;
        fs.Close();
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top