当连接到一个网络共享资源时,如果当前用户(在我的情况下是启用网络服务的用户)没有权限,则需要提供用户名和密码。
我知道如何使用Win32函数(mpr.dll中的WNet*家族)来完成此操作,但想要使用.NET(2.0)功能完成此操作。
有哪些选项可用?
也许提供更多信息有所帮助:
- 使用情况为Windows服务,而不是Asp.Net应用程序。
- 服务正在运行在无权访问共享资源的账户下。
- 客户端不知道访问共享资源所需的用户账户。
- 客户端和服务器不属于同一域。
当连接到一个网络共享资源时,如果当前用户(在我的情况下是启用网络服务的用户)没有权限,则需要提供用户名和密码。
我知道如何使用Win32函数(mpr.dll中的WNet*家族)来完成此操作,但想要使用.NET(2.0)功能完成此操作。
有哪些选项可用?
也许提供更多信息有所帮助:
我非常喜欢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
}
throw new Win32Exception(result);
,因为WNetAddConnection2返回win32错误代码(ERROR_XXX
)。 - torvinNetworkCredential
对象上设置用户名和密码,应用程序便能够连接到网络驱动器。但之后每次尝试都会出现 ERROR_LOGON_FAILURE 错误,直到应用程序重启为止。我们尝试在 NetworkCredential
对象上提供域名,然后神奇地它就起作用了!我不知道为什么这样解决了问题,特别是它能在没有域名的情况下成功连接一次。 - lsmeby你可以更改线程身份,也可以使用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");
}
今天, 七年后我依然面对着同样的问题,我想分享一下我的解决方案。
它已经可以复制并粘贴使用了 :-) 这就是:
在你的代码中(每当需要处理权限时)
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;
}
}
}
}
我搜索了很多方法,最终用自己的方式解决了问题。你需要通过命令提示符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;
}
这个函数在我的使用中非常快速且稳定。
虽然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();
对于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
这可能是最愚蠢的方法,但最近对我很有用,而且非常简单。
当然,只适用于Windows系统。
Process.Start("CMDKEY", @"/add:""NetworkName"" /user:""Username"" /pass:""Password""");
在尝试访问共享之前,您可能希望使用WaitForExit()。
您可以再次使用CMDKEY命令删除末尾的凭据。
WindowsIdentity.Impersonate
(并更改线程主体)以成为所需用户,就像这样。虽然回到p/invoke,但我担心...ProcessStartInfo
接受.UserName
、.Password
和.Domain
。好的...我可以回答。
免责声明:我刚度过了18个小时(又是这样)...我老了,健忘...我拼不出单词...我的注意力很短,所以我最好快速回答... :-)
问题:
是否可以将线程主体更改为没有本地计算机上帐户的用户?
回答:
是的,即使您使用的凭据未在本地定义或在“森林”之外,也可以更改线程主体。
我在尝试从服务连接到具有NTLM身份验证的SQL服务器时遇到了这个问题。此调用使用与进程关联的凭据,这意味着您需要本地帐户或域帐户进行身份验证,然后才能模拟。废话不多...
但是...
使用 ????_NEW_CREDENTIALS 属性调用 LogonUser(..) 将返回一个安全令牌,而无需验证凭据。太酷了。不必在“森林”内定义该帐户。一旦获得令牌,您可能还需要调用 DuplicateToken() 并启用模拟选项,以生成新令牌。现在调用 SetThreadToken(NULL, token);(它可能是&token?)可能需要调用 ImpersonateLoggedonUser(token);,但我不认为是必需的。查一下...
做你需要做的事情...
如果您调用了 ImpersonateLoggedonUser(),则调用 RevertToSelf(),然后 SetThreadToken(NULL, NULL);(我想是这样...查一下),然后关闭创建的句柄的句柄。
没有承诺,但这对我有用...这只是我的头脑(就像我的头发)和我拼不出单词!
你应该考虑添加这样一行代码:
<identity impersonate="true" userName="domain\user" password="****" />
将其添加到您的 web.config 文件中。