はじめに
前回の検証中に、 IIS で Windows 認証を有効にして Python アプリに認証ユーザを引き渡せたら今後色々と楽でいいのに、と思ったので、調査・検証してみました。
対応方法(概要)
HttpPlatformHandler v1.2 では、Windows 認証トークンを HTTP Header で受け取れるとのことで、その値を Python.NET を活用して処理するようにします。
(下記表最終行に、forwardWindowsAuthToken=true にすることで、HTTP Header として X-IIS-WindowsAuthToken が得られる、CloseHandle は受け取った側で責任持って実施すること、との記載あり)
開発環境
Visual Studio 2022 で .NET Framework 向けに開発した方が良かったのですが、事情により、Visual Studio Code + .NET 8 です。
[インストール物]
Visual Studio Code
.NET 8 SDK
VC# Dev Kit (VS Code Extension)
事前調査
Windows 認証機能はオプションなので、未インストールであればサーバーマネージャーからインストールしておく。
対象サイトに対して Windows 認証を有効に、匿名認証を無効にする。
前回開発物のログを強化し、HTTP Header を出力するようにしてみた。
from flask import Flask, request
+ import logging
+ logger = logging.getLogger(__name__)
+ logger.setLevel(logging.DEBUG)
+ fh = logging.FileHandler("C:\\home\\LogFiles\\test.log")
+ fh.setLevel(logging.DEBUG)
+ fh.setFormatter(logging.Formatter("%(asctime)s : %(message)s"))
+ logger.addHandler(fh)
app = Flask(__name__)
@app.route("/")
def hello_world():
+ logger.debug("----------")
+ logger.debug(request.headers)
return "<p>Hello World! with Waitress1</p>"
if __name__ == '__main__':
app.run(debug=True)
Web.config で当機能をONにして動作確認。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<handlers>
<add name="PythonHandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified"/>
</handlers>
<httpPlatform processPath="C:\WebSites\HelloWorldPyW1\venv\Scripts\python.exe"
arguments="waitress_server.py %HTTP_PLATFORM_PORT%"
stdoutLogEnabled="true"
stdoutLogFile="c:\home\LogFiles\pythong.log"
startupTimeLimit="60"
processesPerApplication="16"
+ forwardWindowsAuthToken="true" >
<environmentVariables>
</environmentVariables>
</httpPlatform>
</system.webServer>
</configuration>
連携されている↓
が、これが認証トークンへのハンドルであり、CloseHandle しろということなので、いきなりアンマネージリソースをリークさせてます(笑) よろしくないです。
開発(DLL)
空のフォルダ(C:\PythonPJ\GetUserInfoCS とした)を用意して、HTTP ヘッダの値をパラメータとして受け取り、ユーザー名を返却する C# DLL を .NET 8 で開発する。
cd C:\PythonPJ\GetUserInfoCS
dotnet new classlib
参考ソースは下記、依存モジュールを減らしたかったので適宜調整。
using System;
using System.Security.Claims;
using System.Globalization;
using System.Security.Principal;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace GetUserInfoCS {
public class TokenConverter
{
internal static class NativeMethods
{
[DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
public static extern bool CloseHandle(IntPtr handle);
}
public string GetUser(string xtoken)
{
var ret = "";
var tokenHeader = xtoken;
int hexHandle;
if (!string.IsNullOrEmpty(tokenHeader)
&& int.TryParse(tokenHeader, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out hexHandle))
{
// Always create the identity if the handle exists, we need to dispose it so it does not leak.
var handle = new IntPtr(hexHandle);
using (WindowsIdentity winIdentity = new WindowsIdentity(handle))
{
ret = winIdentity.Name;
NativeMethods.CloseHandle(handle);
}
}
return ret;
}
}
}
ビルドすると、マルチプラットフォーム対応の .NET なのに Windows 固有の実装をしている旨、警告がでるが無視する。
できたファイル(GetUserInfoCS.dll)は python_iis_w.py と同じパスにコピーする。
Python ソースへの組み込み
Python.NET を組み込んで改訂する。
.\env\Scripts\Activate
pip install pythonnet
毎回確実にリソースリークしないよう対応する必要があり、Flask の before_request で処理するのがよさそう。
from flask import Flask, request
+ from pythonnet import load
+ load('coreclr')
+ import clr
+ clr.AddReference('GetUserInfoCS')
+ from GetUserInfoCS import TokenConverter
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler("C:\\home\\LogFiles\\test.log")
fh.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter("%(asctime)s : %(message)s"))
logger.addHandler(fh)
app = Flask(__name__)
+ @app.before_request
+ def before_request():
+ logger.debug("----------")
+ logger.info(request.headers)
+ xtoken = request.headers.get("X-IIS-WindowsAuthToken")
+ if xtoken:
+ tc = TokenConverter()
+ uid = tc.GetUser(xtoken)
+ logger.info(uid)
@app.route("/")
def hello_world():
- logger.debug("----------")
- logger.info(request.headers)
return "<p>Hello World! with Waitress1</p>"
if __name__ == '__main__':
app.run(debug=True)
動作確認
ドメインID + "\" + ユーザID 形式で取得できたので、ログインユーザをもとにアプリ内の権限レベルをチェックする等、好きにできそうです。
最後に
[参考にさせていただいた主情報]
ほぼこちらの記事が答えでした、ありがとうございます。
https://blog.shibayan.jp/entry/20151029/1446126178