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

Be the first to post a comment

Post a comment


96 - 23 =