跟同事討論到 ASP.NET/IIS 有沒可能同時支援 Windows 驗證跟 Form 驗證?依我的理解,每個 Web Application 只能二者擇一,要嘛走 Windows 驗證、要嘛用 Form 驗證。討論到後來,我對自己發出靈魂拷問:ASP.NET 應用程式真的沒辦法做到使用 Form 驗證又能用 Windows 整合式驗證嗎?

過去我能想到的方法是開兩個 Web Appliation 分別跑 Form 驗證及 Windows 驗證,再自己寫 SSO 機制串接;或是 Form 接入 AD 帳號密碼再走 LDAP 或其他方式驗證帳號密碼(缺點是程式端會拿到 AD 帳密,需面對帳號密碼可能被儲存或外流的質疑,我個人偏好「閃開! 讓專業的瀏覽器跟 IIS 來處理」)。沒法像下面這樣,在原本使用 Form 驗證的 Web Applicaion 加一個鈕讓使用者改用 Windows 驗證登入:

這... 所以,現在大家知道答案了,是的! 這是有可能做到的。

上面的範例是我參考網路文章實做出來的,整理重點如下:

  1. 使用 Form 或 Windows 驗證,由 web.config 的 system.web/authentiation 與 application.config system.webServer/security/authentication 控制 (延伸閱讀:【答客問】IIS 與 web.config 的 Windows 驗證設定),預設我們無法切換特定網址的 Windows 驗證,像是全站用 Form 驗證,只有 LoginAD.aspx 用 Windows 驗證:
    <?xml version="1.0"?>
     <configuration>
       <system.web>
         <compilation debug="true" targetFramework="4.7.2"/>
         <authentication mode="Forms">
           <forms loginUrl="Login.aspx"/>
         </authentication>
         <authorization>
           <deny users="?"/>
         </authorization>
         <pages controlRenderingCompatibilityVersion="4.0"/>
       </system.web>
     	<location path="LoginAD.aspx">
     		<system.webServer>
     			<security>
     				<authentication>
     					<windowsAuthentication enabled="true" />
     				</authentication>
     			</security>
     		</system.webServer>
     	</location>
     </configuration>
    
    將導致 503.19 錯誤:

    但我們調整 IIS 設定:

    或修改 applicationhost.config 可解除此一限制:(衍生風險為無管理者權限者可以修改 web.config 避開 Windows 驗證,但前題要拿到更改網站檔案的權限)

    (用 Visual Studio/IISExpress 測試的話,要改 .vs\ProjName\config\applicationhost.conf)
  2. 由於全站開 Form 驗證,預設連到 LoginAD.aspx 時也會被導向 Login.aspx,要靠 Global.asax 加入 Application_EndRequest() 發現 LoginAD.aspx 被導走時改傳 401:
    <%@ Application Language="C#" %>
    <%@ Import Namespace="System.Security.Principal" %>
    <script RunAt="server">
        void Application_EndRequest(object sender, EventArgs e)
        {
            if (Response.StatusCode == 302 &&
                Response.RedirectLocation.Contains("/Login.aspx") &&
                Response.RedirectLocation.ToLower().Contains("loginad") &&
                Request.Browser.Win32)
            {
                System.Diagnostics.Debug.WriteLine(Response.RedirectLocation);
                Response.ClearContent();
                Response.Write(string.Empty);
                Response.TrySkipIisCustomErrors = true;
                Response.Status = "401 Unauthorized";
                Response.StatusCode = 401;
                //.NET 4.5+
                Response.SuppressFormsAuthenticationRedirect = true;
            }
        }
    </script>
    
  3. LoginAD.aspx 的任務是取得 Windows 登入身分將其轉成 FormAuthentiation Cookie:
    <%@ Page Language="C#" %>
     <%@ Import Namespace="System.Security.Principal" %>
     <script runat="server">
         void Page_Load(object sender, EventArgs e)
         {
             Response.Cache.SetCacheability(HttpCacheability.NoCache);
             Response.Cache.SetNoStore();
             Response.SuppressFormsAuthenticationRedirect = true;
             if (!Request.IsAuthenticated || User.Identity is FormsIdentity)
             {
                 FormsAuthentication.SignOut();
                 Response.StatusCode = 401;
             }
             else
             {
                 var userId = User.Identity.Name.Split('\\').Last();
                 FormsAuthentication.SetAuthCookie(userId, false);
                 Response.Redirect("~/");
             }
         }
     </script>
    

總之透過以上精巧安排,我們就能在 Form 驗證加個按鈕,按下去改用 AD 帳號登入,感覺挺酷的,可適用除了 AD 帳號還有自訂帳號的應用場合。

範例為求簡便是用 WebForm 實作,但搬到 ASP.NET MVC 使用也是 OK 的。老樣子,範例程式已放上 Github,有需要的朋友請自取參考。

【參考資料】

Tips of how to mix Forms and Windows authentication in ASP.NET.


Comments

# by 鳥毅

我以前有做過類似的,最近也會有相同的需求,要 .net core上的實作 https://blog.tenyi.com/2012/12/aspnet-windows-authenticationforms.html

# by Jeffrey

to 鳥毅,感謝分享。.NET Core 我初步想到的是 Application_EndRequest 部分改用 Middleware 實現 (但還沒細想)

Post a comment