透過程式存取 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

Post a comment