使用特定帳號密碼存取 Windows 網路分享 (C#)
6 |
透過程式存取 Windows 網路分享的檔案也算常見需求,但存取身分是個問題。之前我慣用的技巧是用有權限的 AD 網域帳號執行排程存取網路分享,但這招要搬進網站或遇到不同網路分享用不同帳號便會破功。最近遇上類似議題,直覺要得回頭靠 WinAPI Impersonation 解決,之前曾寫過通用元件,擔心 11 年前 Windows Vista/7 時代的作品有點過時,就爬文找找更好的做法。
之前的變身做法是改變 Thread 的執行身分,然而針對網路分享還有另一個 WinAPI - WNetAddConnection2,可做到對多個網路分享使用不同登入身分。在 stackoverflow 找到有人分享把它包成搭配 using 使用的 Context 物件,符合我慣用的風格,二話不說,按讚並寫文分享。
為方便使用,我再做了一層包裝,寫了一個 NetworkCopier,將查目錄或複製檔案動作簡化成 Copy(srcNetPath, targetPath, domain, userId, passwd)、DirFiles(srcNetPath, domain, userId, passwd),並支援預設帳密可省略輸入 domain, userId, passwd;另外還有 GetConnetionContext(path, domain, userId, passwd) 可取回 NetworkConnection 物件方便用同一連線身分進行多項操作,若來源與目的屬不同網路分享則可套疊多重身分,寫成:
using (var ctxA = NetworkCopier.GetConnetionContext("\\SvrA\Share", MyAD", "userX", "***")
{
using (var ctxB = NetworkCopier.GetConnetionContext("\\SvrB\Share", MyAD", "userY", "***")
{
File.Copy(@"\\SvrA\Share\SubFolder\test.txt", @"\\SvrB\Share\test.txt");
File.Copy(@"\\SvrA\Share\hello.txt", @"\\SvrB\Share\Temp\hello.txt");
}
}
建立 NetworkConnection 時,需傳入分享路徑而非完整路徑,例如要存取 \\SvrA\Share\SubFolder\test.txt
,建立 NetworkConnection 的參數為 \\SvrA\Share\
,為省去人工截取的麻煩,我寫了一段 Regular Expression 可自動從完整路徑取出分享路徑,使用上更順手。
完整程式如下,需要的朋友請自取使用:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Web;
namespace MyApp.Models
{
public class NetworkCopier
{
static string defaultDomain = null;
static string defaultUserId = null;
static string defaultPasswd = null;
static NetworkCopier()
{
try
{
//TODO: 由設定檔、Registry 或 DB 取得帳號設定,密碼記得要加密保存
var p = ReadCredentialInfo().Split('\t');
defaultDomain = p[0];
defaultUserId = p[1];
defaultPasswd = p[2];
}
catch { }
}
static string NotNull(string s)
{
if (string.IsNullOrEmpty(s))
throw new ApplicationException("未設定預設登入身分");
return s;
}
static string DefaultDomain => NotNull(defaultDomain);
static string DefaultUserId => NotNull(defaultUserId);
static string DefaultPassword => NotNull(defaultPasswd);
static string GetSharePath(string path)
{
var m = Regex.Match(path, @"^\\\\[^\\]+\\[^\\]+");
if (m.Success) return m.Value;
return path;
}
public static void Copy(string srcPath, string dstPath, string domain = null, string userId = null, string passwd = null)
{
using (new NetworkConnection(GetSharePath(srcPath),
new NetworkCredential(userId ?? DefaultUserId, passwd ?? DefaultPassword, domain ?? DefaultDomain)))
{
File.Copy(srcPath, dstPath);
}
}
public static string[] DirFiles(string path, string pattern, string domain = null, string userId = null, string passwd = null)
{
using (new NetworkConnection(GetSharePath(path),
new NetworkCredential(userId ?? DefaultUserId, passwd ?? DefaultPassword, domain ?? DefaultDomain)))
{
return Directory.GetFiles(path, pattern);
}
}
public static NetworkConnection GetConnectionContext(string path, string domain = null, string userId = null, string passwd = null)
{
return new NetworkConnection(GetSharePath(path),
new NetworkCredential(userId ?? DefaultUserId, passwd ?? DefaultPassword, domain ?? DefaultDomain));
}
}
//引用來源: https://stackoverflow.com/a/1197430/288936
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;
public string LocalName;
public string RemoteName;
public string Comment;
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
}
}
Example of using WinAPI to connect network share with specified credential in C#.
Comments
# by Sean
請教黑大,參考以上的程式碼寫成ASP.NET Core 2.1 然後網站掛載到IIS上面 using (var ctxA = EHS.Helper.NetworkConnection.GetConnectionContext(_Folder, "", "myuid", "mypwd")) { using (var stream = new FileStream(path, FileMode.Create)) { newMs.WriteTo(stream); newMs.Close(); } } 在第二次執行時,就會無法new NetworkConnection (WNetAddConnection2 Error result:1312 ERROR_NO_SUCH_LOGON_SESSION) 要隔一陣子才有辦法再次執行。 google了一下網路文章,也有其他人遇到此問題(https://social.msdn.microsoft.com/Forums/zh-TW/9e714b18-6632-4de9-96ed-1ce1843aa1ff/wnetaddconnection2-returns-1312-errornosuchlogonsession-in-64-bit?forum=windowsgeneraldevelopmentissues) 但該討論串沒有下文。 也試過nuget 安裝 SimpleImpersonation試用,一樣有此問題 比較特別的是以donet 指令執行Kestrel 模示,卻不會有此問題 請問是否能指導一下該往那方面除錯呢?感謝 不好意思,排版有點亂,請包涵。
# by Jeffrey
to Sean, 這個做法我有實際商轉,倒沒遇過你說的狀況(但我是 ASP.NET MVC .NET Framework 專案)。爬文找到有人提到類似問題,https://stackoverflow.com/q/29572509/288936 ,實測結果是一分鐘內跑第二次執行或超過十分鐘再跑都不會出錯,超過一分鐘不到十分鐘內則會有問題,蠻神奇的。
# by Seab
感謝黑大撥冗回覆 由於只有單一網路磁碟的需求,目前就先以修改 Application Pool的執行身份處理 感謝您
# by 路人甲
在大大的NetworkCopier 中沒看到GetConnetionContext的Function 請問是漏掉嗎?
# by Jeffrey
to 路人甲,程式碼有漏,已修正 - public static NetworkConnection GetConnectionContext
# by Alex
NetworkCopier寫的是GetConnectionContext 範例寫的是GetConnetionContext 少一個c