连接网络共享时如何提供用户名和密码

222

当连接到一个网络共享资源时,如果当前用户(在我的情况下是启用网络服务的用户)没有权限,则需要提供用户名和密码。

我知道如何使用Win32函数(mpr.dll中的WNet*家族)来完成此操作,但想要使用.NET(2.0)功能完成此操作。

有哪些选项可用?

也许提供更多信息有所帮助:

  • 使用情况为Windows服务,而不是Asp.Net应用程序。
  • 服务正在运行在无权访问共享资源的账户下。
  • 客户端不知道访问共享资源所需的用户账户。
  • 客户端和服务器不属于同一域。

8
虽然我没有给你一个有用的答案,但我可以提供一个反答案。正如Marc所提出的那样,当服务器和客户端不在同一域中时,模拟和生成进程是行不通的,除非两个域之间存在信任关系。如果有信任关系,我认为它就可以工作。我本来想回复Marc的评论,但我没有足够的声望来发表评论。 :-/ - Moose
相关 - https://dev59.com/fXTYa4cB1Zd3GeqPyL3c - vapcguy
12个回答

361

我非常喜欢Mark Brackett的答案,因此我自己动手实现了一个简单的版本。如果有人需要的话,这是我的实现:

public class NetworkConnection : IDisposable
{
    string _networkName;

    public NetworkConnection(string networkName, 
        NetworkCredential credentials)
    {
        _networkName = networkName;

        var netResource = new NetResource()
        {
            Scope = ResourceScope.GlobalNetwork,
            ResourceType = ResourceType.Disk,
            DisplayType = ResourceDisplaytype.Share,
            RemoteName = networkName
        };

        var userName = string.IsNullOrEmpty(credentials.Domain)
            ? credentials.UserName
            : string.Format(@"{0}\{1}", credentials.Domain, credentials.UserName);

        var result = WNetAddConnection2(
            netResource, 
            credentials.Password,
            userName,
            0);
            
        if (result != 0)
        {
            throw new Win32Exception(result);
        }   
    }

    ~NetworkConnection()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        WNetCancelConnection2(_networkName, 0, true);
    }

    [DllImport("mpr.dll")]
    private static extern int WNetAddConnection2(NetResource netResource, 
        string password, string username, int flags);

    [DllImport("mpr.dll")]
    private static extern int WNetCancelConnection2(string name, int flags,
        bool force);
}

[StructLayout(LayoutKind.Sequential)]
public class NetResource
{
    public ResourceScope Scope;
    public ResourceType ResourceType;
    public ResourceDisplaytype DisplayType;
    public int Usage;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string LocalName;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string RemoteName;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string Comment;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string Provider;
}

public enum ResourceScope : int
{
    Connected = 1,
    GlobalNetwork,
    Remembered,
    Recent,
    Context
};

public enum ResourceType : int
{
    Any = 0,
    Disk = 1,
    Print = 2,
    Reserved = 8,
}

public enum ResourceDisplaytype : int
{
    Generic = 0x0,
    Domain = 0x01,
    Server = 0x02,
    Share = 0x03,
    File = 0x04,
    Group = 0x05,
    Network = 0x06,
    Root = 0x07,
    Shareadmin = 0x08,
    Directory = 0x09,
    Tree = 0x0a,
    Ndscontainer = 0x0b
}

11
应该使用throw new Win32Exception(result);,因为WNetAddConnection2返回win32错误代码(ERROR_XXX)。 - torvin
2
这是一段非常棒的代码。需要登录到UNIX系统以获取目录列表,以便将其打印到MVC5 Web应用程序中,而这段代码正好解决了这个问题。加一! - Tay
3
为了让上述代码编译通过,需要加入以下三个using语句: using System.Net; using System.Runtime.InteropServices; using System.ComponentModel; - Matt Nelson
5
很抱歉要重新唤起那个旧线程,但看起来它在块完成后并没有关闭连接。我有一个程序要上传几张图片,第一张可以顺利上传,但第二张失败了。连接只有在程序关闭时才被释放。有什么建议吗? - arti
4
我们和 @arti 遇到了同样的问题。只需在 NetworkCredential 对象上设置用户名和密码,应用程序便能够连接到网络驱动器。但之后每次尝试都会出现 ERROR_LOGON_FAILURE 错误,直到应用程序重启为止。我们尝试在 NetworkCredential 对象上提供域名,然后神奇地它就起作用了!我不知道为什么这样解决了问题,特别是它能在没有域名的情况下成功连接一次。 - lsmeby
显示剩余21条评论

163

你可以更改线程身份,也可以使用P/Invoke WNetAddConnection2。我更喜欢后者,因为有时需要维护不同位置的多个凭据。我将其封装成一个IDisposable,并调用WNetCancelConnection2在之后删除凭据(避免多个用户名错误):

using (new NetworkConnection(@"\\server\read", readCredentials))
using (new NetworkConnection(@"\\server2\write", writeCredentials)) {
   File.Copy(@"\\server\read\file", @"\\server2\write\file");
}

4
该服务不是目标域的成员 - 无法进行身份模拟,因为您无法在本地创建安全令牌并使用它进行身份模拟。PInvoke是唯一的方法。 - stephbu
@MarkBrackett 我知道这是一个旧答案,但也许你仍然知道... 访问权限只授予程序还是也授予通过资源管理器登录的用户? - Breeze
@Breeze - 我没有测试过,但我期望它会为登录会话进行身份验证;因此,如果您的程序正在以已登录的用户身份运行,则他们也将具有访问权限(至少在操作期间)。 - Mark Brackett
@MarkBrackett 我已经测试过了,你的期望是正确的。 - Breeze
17
readCredentials和writeCredentials的定义可以包含在答案中。 - Anders Lindén
4
如果您遇到“错误53”,请确保路径不以“\”结尾。 - Mustafa Sadedil

63

今天, 七年后我依然面对着同样的问题,我想分享一下我的解决方案。

它已经可以复制并粘贴使用了 :-) 这就是:

步骤1

在你的代码中(每当需要处理权限时)

ImpersonationHelper.Impersonate(domain, userName, userPassword, delegate
                            {
                                //Your code here 
                                //Let's say file copy:
                                if (!File.Exists(to))
                                {
                                    File.Copy(from, to);
                                }
                            });

第二步

这个助手文件会有神奇的作用

using System;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Security.Principal;    
using Microsoft.Win32.SafeHandles;


namespace BlaBla
{
    public sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        private SafeTokenHandle()
            : base(true)
        {
        }

        [DllImport("kernel32.dll")]
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [SuppressUnmanagedCodeSecurity]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseHandle(IntPtr handle);

        protected override bool ReleaseHandle()
        {
            return CloseHandle(handle);
        }
    }

    public class ImpersonationHelper
    {
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword,
        int dwLogonType, int dwLogonProvider, out SafeTokenHandle phToken);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private extern static bool CloseHandle(IntPtr handle);

        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        public static void Impersonate(string domainName, string userName, string userPassword, Action actionToExecute)
        {
            SafeTokenHandle safeTokenHandle;
            try
            {

                const int LOGON32_PROVIDER_DEFAULT = 0;
                //This parameter causes LogonUser to create a primary token.
                const int LOGON32_LOGON_INTERACTIVE = 2;

                // Call LogonUser to obtain a handle to an access token.
                bool returnValue = LogonUser(userName, domainName, userPassword,
                    LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
                    out safeTokenHandle);
                //Facade.Instance.Trace("LogonUser called.");

                if (returnValue == false)
                {
                    int ret = Marshal.GetLastWin32Error();
                    //Facade.Instance.Trace($"LogonUser failed with error code : {ret}");

                    throw new System.ComponentModel.Win32Exception(ret);
                }

                using (safeTokenHandle)
                {
                    //Facade.Instance.Trace($"Value of Windows NT token: {safeTokenHandle}");
                    //Facade.Instance.Trace($"Before impersonation: {WindowsIdentity.GetCurrent().Name}");

                    // Use the token handle returned by LogonUser.
                    using (WindowsIdentity newId = new WindowsIdentity(safeTokenHandle.DangerousGetHandle()))
                    {
                        using (WindowsImpersonationContext impersonatedUser = newId.Impersonate())
                        {
                            //Facade.Instance.Trace($"After impersonation: {WindowsIdentity.GetCurrent().Name}");
                            //Facade.Instance.Trace("Start executing an action");

                            actionToExecute();

                            //Facade.Instance.Trace("Finished executing an action");
                        }
                    }
                    //Facade.Instance.Trace($"After closing the context: {WindowsIdentity.GetCurrent().Name}");
                }

            }
            catch (Exception ex)
            {
                //Facade.Instance.Trace("Oh no! Impersonate method failed.");
                //ex.HandleException();
                //On purpose: we want to notify a caller about the issue /Pavel Kovalev 9/16/2016 2:15:23 PM)/
                throw;
            }
        }
    }
}

2
根据LogonUser的文档,它仅适用于本地计算机上的用户:“LogonUser函数尝试登录到本地计算机。本地计算机是调用LogonUser的计算机。您不能使用LogonUser登录到远程计算机。”你会收到一个错误“Win32Exception:用户名或密码不正确。”所以我想这些机器至少需要在同一个域中。 - Charles Chen
1
@CharlesChen 刚刚证明了这在跨域上可以正常工作,供参考。我正在运行此代码的服务器位于 DMZ 中,并且肯定通过防火墙连接到不同域上的文件服务器。绝妙的代码片段,Pavel,你太棒了,这可能是今天应该被接受的答案。 - Brian MacKay
这是一个非常棒的解决方案!谢谢,Pavel Kovalev。 - STLDev
这个在LDAP上能用吗?它说我没有可用的登录服务器。我正在使用LDAP认证。 - Julius Limson
再过几年,似乎这对我没有用。我在连接两端都使用 Windows 10。目标电脑的 IP 地址是 192.168.10.255,用户是“user”,是此电脑上的本地用户。我尝试了带和不带 \ 的域名,以及带或不带域的用户名,但我无法登录。通过 Windows 登录完美运行。 - Wolfgang Roth
@WolfgangRoth LogonUser函数尝试将用户登录到本地计算机。本地计算机是调用LogonUser的计算机。您不能使用LogonUser登录到远程计算机。因此,您只能在本地设备上使用它来访问一些(共享的)文件夹。 - Pavel Kovalev

28

我搜索了很多方法,最终用自己的方式解决了问题。你需要通过命令提示符NET USE命令打开两台机器之间的连接,在完成工作后使用命令提示符NET USE "myconnection" /delete清除连接。

你必须像这样从代码后台使用命令提示符进程:

var savePath = @"\\servername\foldername\myfilename.jpg";
var filePath = @"C:\\temp\myfileTosave.jpg";

用法很简单:

SaveACopyfileToServer(filePath, savePath);

以下是函数:

using System.IO
using System.Diagnostics;


public static void SaveACopyfileToServer(string filePath, string savePath)
    {
        var directory = Path.GetDirectoryName(savePath).Trim();
        var username = "loginusername";
        var password = "loginpassword";
        var filenameToSave = Path.GetFileName(savePath);

        if (!directory.EndsWith("\\"))
            filenameToSave = "\\" + filenameToSave;

        var command = "NET USE " + directory + " /delete";
        ExecuteCommand(command, 5000);

        command = "NET USE " + directory + " /user:" + username + " " + password;
        ExecuteCommand(command, 5000);

        command = " copy \"" + filePath + "\"  \"" + directory + filenameToSave + "\"";

        ExecuteCommand(command, 5000);


        command = "NET USE " + directory + " /delete";
        ExecuteCommand(command, 5000);
    }

而且ExecuteCommand函数是:

public static int ExecuteCommand(string command, int timeout)
    {
        var processInfo = new ProcessStartInfo("cmd.exe", "/C " + command)
                              {
                                  CreateNoWindow = true, 
                                  UseShellExecute = false, 
                                  WorkingDirectory = "C:\\",
                              };

        var process = Process.Start(processInfo);
        process.WaitForExit(timeout);
        var exitCode = process.ExitCode;
        process.Close();
        return exitCode;
    } 

这个函数在我的使用中非常快速且稳定。


1
如果共享映射失败,返回码将是什么? - surega

14

虽然Luke Quinane方案看起来不错,但在我的ASP.NET MVC应用程序中只能部分工作。由于在同一服务器上有两个具有不同凭据的共享,我只能将模拟使用于第一个。

WNetAddConnection2存在的问题是在不同的Windows版本上表现不同。这就是为什么我寻找替代方案并发现了LogonUser函数的原因。以下是我的代码,在ASP.NET中也可以正常工作:

public sealed class WrappedImpersonationContext
{
    public enum LogonType : int
    {
        Interactive = 2,
        Network = 3,
        Batch = 4,
        Service = 5,
        Unlock = 7,
        NetworkClearText = 8,
        NewCredentials = 9
    }

    public enum LogonProvider : int
    {
        Default = 0,  // LOGON32_PROVIDER_DEFAULT
        WinNT35 = 1,
        WinNT40 = 2,  // Use the NTLM logon provider.
        WinNT50 = 3   // Use the negotiate logon provider.
    }

    [DllImport("advapi32.dll", EntryPoint = "LogonUserW", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern bool LogonUser(String lpszUsername, String lpszDomain,
        String lpszPassword, LogonType dwLogonType, LogonProvider dwLogonProvider, ref IntPtr phToken);

    [DllImport("kernel32.dll")]
    public extern static bool CloseHandle(IntPtr handle);

    private string _domain, _password, _username;
    private IntPtr _token;
    private WindowsImpersonationContext _context;

    private bool IsInContext
    {
        get { return _context != null; }
    }

    public WrappedImpersonationContext(string domain, string username, string password)
    {
        _domain = String.IsNullOrEmpty(domain) ? "." : domain;
        _username = username;
        _password = password;
    }

    // Changes the Windows identity of this thread. Make sure to always call Leave() at the end.
    [PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")]
    public void Enter()
    {
        if (IsInContext)
            return;

        _token = IntPtr.Zero;
        bool logonSuccessfull = LogonUser(_username, _domain, _password, LogonType.NewCredentials, LogonProvider.WinNT50, ref _token);
        if (!logonSuccessfull)
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }
        WindowsIdentity identity = new WindowsIdentity(_token);
        _context = identity.Impersonate();

        Debug.WriteLine(WindowsIdentity.GetCurrent().Name);
    }

    [PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")]
    public void Leave()
    {
        if (!IsInContext)
            return;

        _context.Undo();

        if (_token != IntPtr.Zero)
        {
            CloseHandle(_token);
        }
        _context = null;
    }
}

使用:

var impersonationContext = new WrappedImpersonationContext(Domain, Username, Password);
impersonationContext.Enter();

//do your stuff here

impersonationContext.Leave();

2
这种方法对我来说效果很好,但是在测试中注意到,当使用域用户帐户的错误密码时,该用户会立即被锁定。我们的域策略要求在3次登录尝试失败之后才会发生这种情况,但通过这种方法,只需一次错误尝试就会被锁定。因此,请谨慎使用... - kellyb

6

对于VB爱好者来说,Luke Quinane代码的VB.NET等效版本(感谢Luke!)

Imports System
Imports System.Net
Imports System.Runtime.InteropServices
Imports System.ComponentModel

Public Class NetworkConnection
    Implements IDisposable

    Private _networkName As String

    Public Sub New(networkName As String, credentials As NetworkCredential)
        _networkName = networkName

        Dim netResource = New NetResource() With {
             .Scope = ResourceScope.GlobalNetwork,
             .ResourceType = ResourceType.Disk,
             .DisplayType = ResourceDisplaytype.Share,
             .RemoteName = networkName
        }

        Dim userName = If(String.IsNullOrEmpty(credentials.Domain), credentials.UserName, String.Format("{0}\{1}", credentials.Domain, credentials.UserName))

        Dim result = WNetAddConnection2(NetResource, credentials.Password, userName, 0)

        If result <> 0 Then
            Throw New Win32Exception(result, "Error connecting to remote share")
        End If
    End Sub

    Protected Overrides Sub Finalize()
        Try
            Dispose (False)
        Finally
            MyBase.Finalize()
        End Try
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose (True)
        GC.SuppressFinalize (Me)
    End Sub

    Protected Overridable Sub Dispose(disposing As Boolean)
        WNetCancelConnection2(_networkName, 0, True)
    End Sub

    <DllImport("mpr.dll")> _
    Private Shared Function WNetAddConnection2(netResource As NetResource, password As String, username As String, flags As Integer) As Integer
    End Function

    <DllImport("mpr.dll")> _
    Private Shared Function WNetCancelConnection2(name As String, flags As Integer, force As Boolean) As Integer
    End Function

End Class

<StructLayout(LayoutKind.Sequential)> _
Public Class NetResource
    Public Scope As ResourceScope
    Public ResourceType As ResourceType
    Public DisplayType As ResourceDisplaytype
    Public Usage As Integer
    Public LocalName As String
    Public RemoteName As String
    Public Comment As String
    Public Provider As String
End Class

Public Enum ResourceScope As Integer
    Connected = 1
    GlobalNetwork
    Remembered
    Recent
    Context
End Enum

Public Enum ResourceType As Integer
    Any = 0
    Disk = 1
    Print = 2
    Reserved = 8
End Enum

Public Enum ResourceDisplaytype As Integer
    Generic = &H0
    Domain = &H1
    Server = &H2
    Share = &H3
    File = &H4
    Group = &H5
    Network = &H6
    Root = &H7
    Shareadmin = &H8
    Directory = &H9
    Tree = &HA
    Ndscontainer = &HB
End Enum

谢谢。你能否请提供一个VB.NET的使用示例? - undefined

4

这可能是最愚蠢的方法,但最近对我很有用,而且非常简单。
当然,只适用于Windows系统。

Process.Start("CMDKEY", @"/add:""NetworkName"" /user:""Username"" /pass:""Password""");

在尝试访问共享之前,您可能希望使用WaitForExit()

您可以再次使用CMDKEY命令删除末尾的凭据。


3
一个可能起作用的选择是使用WindowsIdentity.Impersonate(并更改线程主体)以成为所需用户,就像这样。虽然回到p/invoke,但我担心...
另一个可行但同样不理想的选择可能是生成一个进程来完成工作...ProcessStartInfo接受.UserName.Password.Domain
最后 - 或许在具有访问权限的专用帐户中运行服务? (因为您已经澄清这不是一个选项,所以已被移除)。

1
我认为进程的概念并不是一个坏主意。谷歌发布了一些关于Chrome多进程优势的白皮书。 - Dustin Getz
能否将线程主体更改为在本地机器上没有帐户的用户? - gyrolf
说实话,我不知道... 你需要尝试使用不同的域名来调用LogonUser函数才能找出答案。 - Marc Gravell

3

好的...我可以回答。

免责声明:我刚度过了18个小时(又是这样)...我老了,健忘...我拼不出单词...我的注意力很短,所以我最好快速回答... :-)

问题:

是否可以将线程主体更改为没有本地计算机上帐户的用户?

回答:

是的,即使您使用的凭据未在本地定义或在“森林”之外,也可以更改线程主体。

我在尝试从服务连接到具有NTLM身份验证的SQL服务器时遇到了这个问题。此调用使用与进程关联的凭据,这意味着您需要本地帐户或域帐户进行身份验证,然后才能模拟。废话不多...

但是...

使用 ????_NEW_CREDENTIALS 属性调用 LogonUser(..) 将返回一个安全令牌,而无需验证凭据。太酷了。不必在“森林”内定义该帐户。一旦获得令牌,您可能还需要调用 DuplicateToken() 并启用模拟选项,以生成新令牌。现在调用 SetThreadToken(NULL, token);(它可能是&token?)可能需要调用 ImpersonateLoggedonUser(token);,但我不认为是必需的。查一下...

做你需要做的事情...

如果您调用了 ImpersonateLoggedonUser(),则调用 RevertToSelf(),然后 SetThreadToken(NULL, NULL);(我想是这样...查一下),然后关闭创建的句柄的句柄。

没有承诺,但这对我有用...这只是我的头脑(就像我的头发)和我拼不出单词!


2

你应该考虑添加这样一行代码:

<identity impersonate="true" userName="domain\user" password="****" />

将其添加到您的 web.config 文件中。

更多信息。


一些企业安全措施阻止使用模拟身份,因为它们无法跟踪使用它的应用程序,并且必须在相同或受信任的域中。我认为支持模拟身份是有风险的。使用域服务帐户和PInvoke似乎是解决问题的方法。 - Jim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接