[C#] 자격 증명을 사용하여 트러스트되지 않은 원격 도메인에서 공유 파일 (UNC)에 액세스

우리는 해결해야 할 흥미로운 상황에 처해 있었고 검색 결과가 조금씩 바뀌 었습니다. 따라서 SO 커뮤니티에 도움을 요청합니다.

문제는 이것입니다. 도메인에없고 원격 파일 공유 / UNC를 통해 신뢰할 수있는 외부 도메인 내에 있지 않은 공유 파일에 프로그래밍 방식으로 액세스해야합니다. 당연히 원격 시스템에 자격 증명을 제공해야합니다.

일반적으로 다음 두 가지 방법 중 하나로이 문제를 해결합니다.

  1. 파일 공유를 드라이브로 매핑하고 그때 자격 증명을 제공하십시오. 이것은 일반적으로 NET USE명령 또는 Win32 기능을 복제 하여 수행됩니다 NET USE.
  2. 원격 컴퓨터가 도메인에있는 것처럼 UNC 경로로 파일에 액세스하고 프로그램이 실행되는 계정이 원격 컴퓨터에서 로컬 사용자로 복제되었는지 (비밀번호 포함) 확인하십시오. 기본적으로 사용자가 공유 파일에 액세스하려고하면 Windows가 현재 사용자의 자격 증명을 자동으로 제공한다는 사실을 이용하십시오.
  3. 원격 파일 공유를 사용하지 마십시오. FTP (또는 다른 수단)를 사용하여 파일을 전송하고 로컬에서 작업 한 다음 다시 전송하십시오.

다양하고 햇볕이 잘 드는 이유 때문에 보안 / 네트워크 설계자는 처음 두 가지 접근 방식을 거부했습니다. 두 번째 접근법은 분명히 보안 허점입니다. 원격 컴퓨터가 손상된 경우 이제 로컬 컴퓨터가 위험에 노출됩니다. 새로 마운트 된 드라이브는 프로그램이 파일을 액세스하는 동안 로컬 컴퓨터의 다른 프로그램에서 사용할 수있는 공유 리소스이기 때문에 첫 번째 방법은 만족스럽지 않습니다. 이것을 일시적으로 만들 수는 있지만 여전히 의견에는 구멍이 있습니다.

세 번째 옵션을 사용할 수 있지만 원격 네트워크 관리자는 FTPS가 아니라 SFTP를 주장하며 FtpWebRequest는 FTPS 만 지원합니다. SFTP 방화벽에 더 친숙한 옵션이며 해당 접근 방식에 사용할 수있는 몇 가지 라이브러리가 있지만 가능한 경우 종속성을 줄이는 것을 선호합니다.

원격 파일 공유를 사용하는 관리 또는 win32 수단에 대해 MSDN을 검색했지만 유용한 것을 찾지 못했습니다.

그래서 묻습니다. 다른 방법이 있습니까? 내가 원하는 것을 수행하는 비밀 비밀 win32 기능을 놓쳤습니까? 아니면 옵션 3의 변형을 추구해야합니까?



답변

문제를 해결하는 방법은 WNetUseConnection 이라는 Win32 API를 사용하는 것 입니다.
드라이브를 맵핑하지 않고 인증으로 UNC 경로에 연결하려면이 기능을 사용하십시오 .

이렇게하면 동일한 도메인에 있지 않고 다른 사용자 이름과 암호를 사용하더라도 원격 시스템에 연결할 수 있습니다.

WNetUseConnection을 사용한 후에는 동일한 도메인에있는 것처럼 UNC 경로를 통해 파일에 액세스 할 수 있습니다. 가장 좋은 방법은 아마도 관리 내장 주식을 이용하는 것입니다.
예 : \\ computername \ c $ \ program files \ Folder \ file.txt

다음은 WNetUseConnection을 사용하는 샘플 C # 코드입니다.
NetResource의 경우 lpLocalName 및 lpProvider에 대해 널을 전달해야합니다. dwType은 RESOURCETYPE_DISK 여야합니다. lpRemoteName은 \\ ComputerName이어야합니다.

using System;
using System.Runtime.InteropServices ;
using System.Threading;

namespace ExtremeMirror
{
    public class PinvokeWindowsNetworking
    {
        #region Consts
        const int RESOURCE_CONNECTED = 0x00000001;
        const int RESOURCE_GLOBALNET = 0x00000002;
        const int RESOURCE_REMEMBERED = 0x00000003;

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

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

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


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

        const int CONNECT_LOCALDRIVE = 0x00000100;
        #endregion

        #region Errors
        const int NO_ERROR = 0;

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

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

        private struct ErrorClass
        {
            public int num;
            public string message;
            public ErrorClass(int num, string message)
            {
                this.num = num;
                this.message = message;
            }
        }


        // Created with excel formula:
        // ="new ErrorClass("&A1&", """&PROPER(SUBSTITUTE(MID(A1,7,LEN(A1)-6), "_", " "))&"""), "
        private static ErrorClass[] ERROR_LIST = new ErrorClass[] {
            new ErrorClass(ERROR_ACCESS_DENIED, "Error: Access Denied"),
            new ErrorClass(ERROR_ALREADY_ASSIGNED, "Error: Already Assigned"),
            new ErrorClass(ERROR_BAD_DEVICE, "Error: Bad Device"),
            new ErrorClass(ERROR_BAD_NET_NAME, "Error: Bad Net Name"),
            new ErrorClass(ERROR_BAD_PROVIDER, "Error: Bad Provider"),
            new ErrorClass(ERROR_CANCELLED, "Error: Cancelled"),
            new ErrorClass(ERROR_EXTENDED_ERROR, "Error: Extended Error"),
            new ErrorClass(ERROR_INVALID_ADDRESS, "Error: Invalid Address"),
            new ErrorClass(ERROR_INVALID_PARAMETER, "Error: Invalid Parameter"),
            new ErrorClass(ERROR_INVALID_PASSWORD, "Error: Invalid Password"),
            new ErrorClass(ERROR_MORE_DATA, "Error: More Data"),
            new ErrorClass(ERROR_NO_MORE_ITEMS, "Error: No More Items"),
            new ErrorClass(ERROR_NO_NET_OR_BAD_PATH, "Error: No Net Or Bad Path"),
            new ErrorClass(ERROR_NO_NETWORK, "Error: No Network"),
            new ErrorClass(ERROR_BAD_PROFILE, "Error: Bad Profile"),
            new ErrorClass(ERROR_CANNOT_OPEN_PROFILE, "Error: Cannot Open Profile"),
            new ErrorClass(ERROR_DEVICE_IN_USE, "Error: Device In Use"),
            new ErrorClass(ERROR_EXTENDED_ERROR, "Error: Extended Error"),
            new ErrorClass(ERROR_NOT_CONNECTED, "Error: Not Connected"),
            new ErrorClass(ERROR_OPEN_FILES, "Error: Open Files"),
        };

        private static string getErrorForNumber(int errNum)
        {
            foreach (ErrorClass er in ERROR_LIST)
            {
                if (er.num == errNum) return er.message;
            }
            return "Error: Unknown, " + errNum;
        }
        #endregion

        [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 = "";
        }


        public static string connectToRemote(string remoteUNC, string username, string password)
        {
            return connectToRemote(remoteUNC, username, password, false);
        }

        public static string connectToRemote(string remoteUNC, string username, string password, bool promptUser)
        {
            NETRESOURCE nr = new NETRESOURCE();
            nr.dwType = RESOURCETYPE_DISK;
            nr.lpRemoteName = remoteUNC;
            //          nr.lpLocalName = "F:";

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

            if (ret == NO_ERROR) return null;
            return getErrorForNumber(ret);
        }

        public static string disconnectRemote(string remoteUNC)
        {
            int ret = WNetCancelConnection2(remoteUNC, CONNECT_UPDATE_PROFILE, false);
            if (ret == NO_ERROR) return null;
            return getErrorForNumber(ret);
        }
    }
}


답변

빠른 해결책을 찾는 사람들에게는 NetworkShareAccesser최근에 쓴 ( 이 답변을 바탕으로 (감사합니다!))을 사용할 수 있습니다.

용법:

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");
}

경고 : 것을 절대적으로 확인하시기 바랍니다 Dispose이의 NetworkShareAccesser(! 당신이 충돌되는 앱하더라도)라고, 그렇지 않으면 열려있는 연결 Windows에서 유지됩니다. cmd프롬프트 를 열고 를 입력 하면 열려있는 모든 연결을 볼 수 있습니다 net use.

코드:

/// <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에서는 서버의 자격 증명을 설정하기 위해 UNC 경로를 드라이브 문자에 매핑 할 필요가 없습니다 . 나는 정기적으로 다음과 같은 배치 스크립트를 사용했다.

net use \\myserver /user:username password

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

net use /delete \\my.server.com

그러나 프로그램과 동일한 계정으로 실행되는 모든 프로그램은 여전히 ​​액세스 권한이있는 모든 항목에 username:password액세스 할 수 있습니다. 가능한 해결책은 자체 로컬 사용자 계정으로 프로그램을 분리하는 것입니다 (UNC 액세스는을 호출 한 계정에 로컬입니다 NET USE).

참고 : SMB 교차 도메인 사용은 IMO 기술을 잘 사용하지 않습니다. 보안이 그토록 중요하다면 SMB에 암호화가 없다는 사실은 그 자체로 약간 댐퍼입니다.


답변

WNetUseConnection 대신 NetUseAdd를 권장 합니다 . WNetUseConnection은 WNetUseConnection2 및 WNetUseConnection3에 의해 대체 된 레거시 기능이지만 이러한 기능은 모두 Windows 탐색기에 표시되는 네트워크 장치를 만듭니다. NetUseAdd는 DOS 프롬프트에서 net use를 호출하여 원격 컴퓨터에서 인증하는 것과 같습니다.

NetUseAdd를 호출하면 이후 디렉토리 액세스 시도가 성공합니다.


답변

나는 나 자신을 모르지만, 확실히 # 2가 잘못되기를 희망합니다 … Windows가 자동으로 내 로그인 정보 (모든 암호를 거의 사용하지 않음)를 모든 컴퓨터에 제공하지 않을 것이라고 생각하고 싶습니다. 내 신뢰의 일부가 아닌 것은 물론입니다.

그럼에도 불구하고 가장 아키텍처를 탐색 했습니까? 코드는 다음과 유사합니다.

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

    context.Undo();
}

이 경우 token변수는 IntPtr입니다. 이 변수의 값을 얻으려면 관리되지 않는 LogonUser Windows API 함수를 호출해야합니다. pinvoke.net으로 의 빠른 여행 은 다음과 같은 서명을 제공합니다.

[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
);

사용자 이름, 도메인 및 비밀번호는 상당히 분명해 보입니다. dwLogonType 및 dwLogonProvider에 전달할 수있는 다양한 값을 살펴보고 필요에 가장 적합한 값을 결정하십시오.

여기에 확인할 수있는 두 번째 도메인이 없으므로이 코드는 테스트되지 않았지만이 과정을 제대로 진행해야합니다.


답변

여기에 모든 부스러기가 제거 된 최소 POC 클래스

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;
        }
    }
}

\\server\share\folderw /를 직접 사용할 수 있으므로 사전 WNetUseConnection\\server파트로 분리 할 필요가 없습니다 .


답변

대부분의 SFTP 서버는 SCP를 지원하므로 라이브러리를 찾기가 훨씬 쉽습니다. PuTTY에 포함 된 pscp와 같은 코드에서 기존 클라이언트를 호출 할 수도 있습니다 .

작업중인 파일 유형이 텍스트 또는 XML 파일과 같은 단순한 유형 인 경우 .NET Remoting 또는 웹 서비스와 같은 파일을 사용하여 파일을 조작하기 위해 자체 클라이언트 / 서버 구현을 작성할 수 있습니다.