管理者権限でアプリケーションを実行した際に、共有フォルダなどのネットワークパスへのアクセスができないという事象が発生した。
出た例外は↓
System.IO.IOException: ログオン失敗: ユーザー名を認識できないか、またはパスワードが間違っています。
状態としては、エクスプローラー上でネットワークパスにアクセスし、認証を通してある。
よってアプリケーションの実行者が、ログインしているWindowsアカウントなら問題なくアクセスできるはずだ。しかし例外となる。
原因を探ってみることにした。
一般的に.Netで管理者権限を要する場合、app.manifestを作成し、↓のようにrequestedExecutionLevel=requireAdministratorにして対応する。
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC マニフェスト オプション
Windows のユーザー アカウント制御のレベルを変更するには、
requestedExecutionLevel ノードを以下のいずれかで置換します。
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
requestedExecutionLevel 要素を指定すると、ファイルおよびレジストリの仮想化が無効にされます。
アプリケーションが下位互換性を保つためにこの仮想化を要求する場合、この要素を
削除します。
-->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
:
</assembly>
上記設定後、アプリケーションを実行すると、UAC(ユーザーアカウント制御)の画面が表示され、[はい]を選択すると管理者権限で実行される、という流れだ。
どうやらこの UACで管理者権限昇格して実行したプロセスと、通常のプロセスでは異なるユーザーセッションとなり、通常のプロセスで接続しているネットワークのセッションが引き継がれないようだ。
くまった!!
対策方法は一応↓の3つある。
一応と書いたのは、最初の2つの方法は推奨できないからだ。
UACを無効にすることは他人に提供する手段ではないし、レジストリの値を変更することは、他のアプリで本来食い止めたいケースを食い止められなくなる。
本来あるべき姿は、認証されていない場合は、認証ダイアログを表示してユーザーに入力を促す であろう。
それを実現するのが、3つ目の「WindowsAPI(WNetAddConnection)を使用する」方法だ。
.Netでユーザー認証を行う仕組みは、標準では用意されておらず、WindowsAPIを呼んで認証を通すことになる。
エクスプローラーでもお馴染みの↓の認証ウィンドウだ。
実際に、WindowsAPIを使った認証サンプルを書いてみる。
WNetAddConnection2で認証を通し、共有フォルダにあるmemo.txtの中身を表示する というサンプル
Form1.cs
using System;
using System.IO;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
//共有フォルダのパス
const string NETWORK_DIR = @"\\192.168.0.100\share";
try
{
// 接続を試みる
WinAPIHelper.NetAddConnect(NETWORK_DIR);
string readText = string.Empty;
string path = Path.Combine(NETWORK_DIR, "memo.txt");
using (StreamReader sr = new StreamReader(path))
{
readText = sr.ReadToEnd();
}
MessageBox.Show(readText);
}
catch(Exception ex)
{
MessageBox.Show(ex.ToString());
}
finally
{
// 最後に接続を閉じる
WinAPIHelper.NetCancelConnect(NETWORK_DIR);
}
}
}
}
WinAPIHelper.cs
using System;
using System.Runtime.InteropServices;
namespace WindowsFormsApplication1
{
class WinAPIHelper
{
private const int RESOURCETYPE_ANY = 0x0;
private const int CONNECT_INTERACTIVE = 0x8;
[StructLayout(LayoutKind.Sequential)]
public struct NETRESOURCE
{
public int dwScope;
public int dwType;
public int dwDisplayType;
public int dwUsage;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpLocalName;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpRemoteName;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpConmment;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpProvider;
}
[DllImport("mpr.dll", EntryPoint = "WNetCancelConnection2", CharSet = CharSet.Unicode)]
public static extern int WNetCancelConnection2(string lpName,
int dwFlags,
bool tForce);
[DllImport("mpr.dll", EntryPoint = "WNetAddConnection2", CharSet = CharSet.Unicode)]
public static extern int WNetAddConnection2(ref NETRESOURCE lpNetResource,
string lpPassword,
string lpUserName,
int dwFlags);
/// <summary>
/// 切断する
/// </summary>
/// <param name="connect"></param>
/// <returns></returns>
public static void NetCancelConnect(string connect)
{
// 接続が残っていた場合はクリアする
WNetCancelConnection2(connect, RESOURCETYPE_ANY, true);
}
/// <summary>
/// 接続を試みる
/// </summary>
/// <param name="connect"></param>
public static void NetAddConnect(string connect)
{
NETRESOURCE netResource = new NETRESOURCE()
{
dwScope = 0,
dwType = RESOURCETYPE_ANY,
dwDisplayType = 0,
dwUsage = 0,
lpLocalName = "",
lpRemoteName = connect,
lpProvider = ""
};
// 接続が残っていた場合に備えてクリアする
NetCancelConnect(connect);
// パスワード(第二引数)、ユーザー(第三引数)はnullとする
int ret = WNetAddConnection2(ref netResource, null, null, CONNECT_INTERACTIVE);
if (ret != 0)
throw new Exception($"WNetAddConnection2 error. return value={ret}");
}
}
}
実行してみると、↓の認証ダイアログが表示さる。
接続できるユーザー、パスワードを入力する。
例外とならず、無事に共有フォルダのファイルにアクセスできた。
注意点として、NetCancelConnectは実行した後、数十秒~数分接続できる状態が残るようだ。その間は認証ダイアログが表示されなかった。
今日はココマデ!