ASP。NET MVC和混合模式认证

时间:2022-09-01 18:36:45

I have a scenario whereby I require users to be able to authenticate against an ASP.NET MVC web application using either Windows authentication or Forms authentication. If the user is on the internal network they will use Windows authentication and if they are connecting externally they will use Forms authentication. I’ve seen quite a few people asking the question how do I configure an ASP.NET MVC web application for this, but I haven’t found a complete explanation.

我有一个场景,其中我要求用户能够对ASP进行身份验证。NET MVC web应用程序使用Windows身份验证或表单身份验证。如果用户在内部网络上,他们将使用Windows身份验证,如果他们在外部连接,他们将使用表单身份验证。我见过很多人问我如何配置ASP。NET MVC web应用程序为此,但我还没有找到一个完整的解释。

Please can someone provide a detailed explanation, with code examples, on how this would be done?

请有人提供一个详细的解释,包括代码示例,关于如何实现这一点?

Thanks.

谢谢。

Alan T

艾伦·T

4 个解决方案

#1


14  

This is called mixed authentication mode. Basically you cannot achieve this within a single application because in IIS once you set up Windows authentication for a virtual directory it will no longer accept users from different domains. So basically you need to have two applications, the first with Windows Authentication and the second (the main application) using Forms authentication. The first application will consist of a single address which will simply redirect to the main application by issuing an authentication ticket for the domain user.

这被称为混合身份验证模式。基本上,您无法在单个应用程序中实现这一点,因为在IIS中,一旦您为一个虚拟目录设置了Windows身份验证,它将不再接受来自不同域的用户。因此,基本上需要有两个应用程序,第一个是Windows身份验证,第二个是使用表单身份验证的主要应用程序。第一个应用程序将包含一个单独的地址,它将通过为域用户颁发身份验证票据而直接重定向到主应用程序。

#2


14  

This can be done. Reverse the configuration, set the app/root to use Anonymous and Forms Authentication... In this way, you can configure mixed authentication within the same web application, but it is tricky. So first, configure you app for Forms Authentication with loginUrl="~/WinLogin/WinLogin2.aspx". In MVC, routing overrides authentication rules set by IIS, so need to use an aspx page, as IIS can set authentication on the file. Enable Anonymous and Forms Authentication on the root web application. Enable Windows Authentication and disable anonymous authentication in root/WinLogin directory. Add custom 401 and 401.2 error pages to redirect back to the Account/Signin URL.

这是可以做到的。反向配置,设置app/root使用匿名和表单验证……通过这种方式,您可以在同一个web应用程序中配置混合身份验证,但这很棘手。首先,配置你的应用程序,用loginUrl="~/WinLogin/WinLogin2.aspx"进行表单验证。在MVC中,路由覆盖IIS设置的身份验证规则,因此需要使用一个aspx页面,因为IIS可以在文件上设置身份验证。在根web应用程序上启用匿名和表单身份验证。在root/WinLogin目录中启用Windows身份验证并禁用匿名身份验证。添加自定义401和401.2错误页面重定向到帐户/登录URL。

This will allow any browser capable of pass-through to use windows integrated authentication to auto signin. While some devices will get prompted for credentials (like iPhone) and other devices like blackberry redirected to signin page.

这将允许任何能够通过windows集成身份验证的浏览器使用自动登录。而有些设备会被提示输入凭证(如iPhone),而其他设备如黑莓会被重定向到登录页面。

This also creates a cookie explicitly adding the users roles and creates a Generic principle so that role-based authorization can be used.

这还创建了一个明确添加用户角色的cookie,并创建了一个通用原则,以便可以使用基于角色的授权。

in WinLogin2.aspx (in WinLogin directory under "root" web application in IIS, and configured to use Windows Authentication, Anonymous disabled, and Forms enabled (as can't turn off...note IIS will complain when you enable windows authentication, just ignore) :

在WinLogin2。aspx(在WinLogin目录下IIS中的“root”web应用程序中,配置为使用Windows身份验证、匿名禁用和启用表单(因为无法关闭……)注意,当您启用windows身份验证时,IIS将会报错,请忽略):

var logonUser = Request.ServerVariables["LOGON_USER"];
        if (!String.IsNullOrWhiteSpace(logonUser))
        {
            if (logonUser.Split('\\').Length > 1)
            {
                var domain = logonUser.Split('\\')[0];
                var username = logonUser.Split('\\')[1];

                var timeout = 30;

                var encTicket = CreateTicketWithSecurityGroups(false, username, domain, timeout);

                var authCookie = new HttpCookie(".MVCAUTH", encTicket) { HttpOnly = true };
                Response.Cookies.Add(authCookie);


            }
            //else
            //{
            // this is a redirect due to returnUrl being WinLogin page, in which logonUser will no longer have domain attached
            //  ignore as forms ticket should already exist
            //}

            string returnUrl = Request.QueryString["ReturnUrl"];

            if (returnUrl.IsEmpty())
            {
                Response.Redirect("~/");
            }
            else
            {
                Response.Redirect(returnUrl);
            }
        }

        public static string CreateTicketWithSecurityGroups(bool rememberMe, string username, string domain, int timeout)
    {
        using (var context = new PrincipalContext(ContextType.Domain, domain))
        {
            using (var principal = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username))
            {
                var securityGroups = String.Join(";", principal.GetAuthorizationGroups());

                var ticket =
                    new FormsAuthenticationTicket(1,
                                                  username,
                                                  DateTime.UtcNow,
                                                  DateTime.UtcNow.AddMinutes(timeout),
                                                  rememberMe,
                                                  securityGroups,
                                                  "/");

                string encTicket = FormsAuthentication.Encrypt(ticket);
                return encTicket;
            }
        }
    }

In IIS 7.5, click Error Pages, set the 401 page to File path of Redirect401.htm file, with this code:

在IIS 7.5中,单击Error Pages,将401页面设置为Redirect401的文件路径。htm文件,带有以下代码:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org /TR/xhtml1/DTD/xhtml1-transitional.dtd">

    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
       <title></title>
        <script>
           window.location.assign('../Account/Signin');
      </script>
   </head>
  <body>

  </body>
   </html>

In AccountController...

在AccountController…

public ActionResult SignIn()
    {
        return View(new SignInModel());
    }

    //
    // POST: /Account/SignIn

    [HttpPost]
    public ActionResult SignIn(SignInModel model, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            if (Membership.ValidateUser(model.UserName, model.Password))
            {
                string encTicket = CreateTicketWithSecurityGroups(model.RememberMe,  model.UserName, model.Domain, FormsAuthentication.Timeout.Minutes);

                Response.Cookies.Add(new HttpCookie(".MVCAUTH", encTicket));

                //var returnUrl = "";
                for (var i = 0; i < Request.Cookies.Count; i++)
                {
                    HttpCookie cookie = Request.Cookies[i];
                    if (cookie.Name == ".MVCRETURNURL")
                    {
                        returnUrl = cookie.Value;
                        break;
                    }
                }

                if (returnUrl.IsEmpty())
                {
                    return Redirect("~/");
                }

                return Redirect(returnUrl);
            }

            ModelState.AddModelError("Log In Failure", "The username/password combination is invalid");
        }

        return View(model);
    }

    //
    // GET: /Account/SignOut

    public ActionResult SignOut()
    {
        FormsAuthentication.SignOut();

        if (Request.Cookies[".MVCRETURNURL"] != null)
        {
            var returnUrlCookie = new HttpCookie(".MVCRETURNURL") { Expires = DateTime.Now.AddDays(-1d) };
            Response.Cookies.Add(returnUrlCookie);
        }

        // Redirect back to sign in page so user can 
        //   sign in with different credentials

        return RedirectToAction("SignIn", "Account");

In global.asax:

在global.asax:

protected void Application_BeginRequest(object sender, EventArgs e)
    {


        try
        {
            bool cookieFound = false;

            HttpCookie authCookie = null;

            for (int i = 0; i < Request.Cookies.Count; i++)
            {
                HttpCookie cookie = Request.Cookies[i];
                if (cookie.Name == ".MVCAUTH")
                {
                    cookieFound = true;
                    authCookie = cookie;
                    break;
                }
            }

            if (cookieFound)
            {
                // Extract the roles from the cookie, and assign to our current principal, which is attached to the HttpContext.
                FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
                HttpContext.Current.User = new GenericPrincipal(new FormsIdentity(ticket), ticket.UserData.Split(';'));
            }



        }
        catch (Exception ex)
        {
            throw;
        }

    }


     protected void Application_AuthenticateRequest()
    {
        var returnUrl = Request.QueryString["ReturnUrl"];
        if (!Request.IsAuthenticated &&
            !String.IsNullOrWhiteSpace(returnUrl))
        {
            var returnUrlCookie = new HttpCookie(".MVCRETURNURL", returnUrl) {HttpOnly = true};
            Response.Cookies.Add(returnUrlCookie);
        }
    }

web.config

. config

<!--<authorization>
  <deny users="?"/>
</authorization>-->
<authentication mode="Forms">
  <forms name=".MVCAUTH" loginUrl="~/WinLogin/WinLogin2.aspx" timeout="30" enableCrossAppRedirects="true"/>
</authentication>
<membership defaultProvider="AspNetActiveDirectoryMembershipProvider">
  <providers>
    <add name="AspNetActiveDirectoryMembershipProvider" type="System.Web.Security.ActiveDirectoryMembershipProvider,           System.Web, Version=4.0.0.0, Culture=neutral,           PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="ADService" connectionProtection="Secure" enablePasswordReset="false" enableSearchMethods="true" requiresQuestionAndAnswer="true" applicationName="/" description="Default AD connection" requiresUniqueEmail="false" clientSearchTimeout="30" serverSearchTimeout="30" attributeMapPasswordQuestion="department" attributeMapPasswordAnswer="division" attributeMapEmail="mail" attributeMapUsername="sAMAccountName" maxInvalidPasswordAttempts="5" passwordAttemptWindow="10" passwordAnswerAttemptLockoutDuration="30" minRequiredPasswordLength="7" minRequiredNonalphanumericCharacters="1"/>
  </providers>
</membership><machineKey decryptionKey="..." validationKey="..." />  </system.web><connectionStrings> <add name="ADService" connectionString="LDAP://SERVER:389"/></connectionStrings>

Credit owed to http://msdn.microsoft.com/en-us/library/ms972958.aspx

信用卡欠http://msdn.microsoft.com/en-us/library/ms972958.aspx

#3


4  

This will probably live at the bottom of this question and never be found but I was able to implement what was described at

这可能会在这个问题的底部存在,而且永远不会被发现,但我能够实现所描述的内容。

http://mvolo.com/iis-70-twolevel-authentication-with-forms-authentication-and-windows-authentication/

http://mvolo.com/iis - 70二级-验证-形式-验证-和- windows - authentication/

It was quite easy and trivial. Didn't require multiple applications or cookie hacks, just extending the FormsAuthModule and making some web.config changes.

这很容易,也很琐碎。不需要多个应用程序或cookie破解,只需扩展FormsAuthModule并创建一些web。配置更改。

#4


0  

I know this is an old post - but everything lives forever on the internet!

我知道这是一个古老的帖子——但是所有的一切都永远生活在互联网上!

Anyway, I had to move an old website from IIS6 to IIS8. This is a WebForms website, but I assume this very simple solution is the same.

不管怎样,我必须把一个旧网站从IIS6移到IIS8。这是一个WebForms网站,但我认为这个简单的解决方案是一样的。

I received the error : Unable to cast object of type 'System.Security.Principal.WindowsIdentity' to type 'System.Web.Security.FormsIdentity'.

我收到了一个错误:无法强制类型为System.Security.Principal的对象。WindowsIdentity System.Web.Security.FormsIdentity”“输入”。

All I did was create a new application pool for the website. When creating this, I set the Managed pipeline mode to 'Classic'. (Read more here - http://www.hanselman.com/blog/MovingOldAppsFromIIS6ToIIS8AndWhyClassicModeExists.aspx) Dont forget to set the website's application pool to the new pool you just created.

我所做的就是为网站创建一个新的应用程序池。创建这个时,我将托管管道模式设置为“Classic”。(详情请参阅http://www.hanselman.com/blog/MovingOldAppsFromIIS6ToIIS8AndWhyClassicModeExists.aspx)不要忘记将网站的应用程序池设置为您刚刚创建的新池。

#1


14  

This is called mixed authentication mode. Basically you cannot achieve this within a single application because in IIS once you set up Windows authentication for a virtual directory it will no longer accept users from different domains. So basically you need to have two applications, the first with Windows Authentication and the second (the main application) using Forms authentication. The first application will consist of a single address which will simply redirect to the main application by issuing an authentication ticket for the domain user.

这被称为混合身份验证模式。基本上,您无法在单个应用程序中实现这一点,因为在IIS中,一旦您为一个虚拟目录设置了Windows身份验证,它将不再接受来自不同域的用户。因此,基本上需要有两个应用程序,第一个是Windows身份验证,第二个是使用表单身份验证的主要应用程序。第一个应用程序将包含一个单独的地址,它将通过为域用户颁发身份验证票据而直接重定向到主应用程序。

#2


14  

This can be done. Reverse the configuration, set the app/root to use Anonymous and Forms Authentication... In this way, you can configure mixed authentication within the same web application, but it is tricky. So first, configure you app for Forms Authentication with loginUrl="~/WinLogin/WinLogin2.aspx". In MVC, routing overrides authentication rules set by IIS, so need to use an aspx page, as IIS can set authentication on the file. Enable Anonymous and Forms Authentication on the root web application. Enable Windows Authentication and disable anonymous authentication in root/WinLogin directory. Add custom 401 and 401.2 error pages to redirect back to the Account/Signin URL.

这是可以做到的。反向配置,设置app/root使用匿名和表单验证……通过这种方式,您可以在同一个web应用程序中配置混合身份验证,但这很棘手。首先,配置你的应用程序,用loginUrl="~/WinLogin/WinLogin2.aspx"进行表单验证。在MVC中,路由覆盖IIS设置的身份验证规则,因此需要使用一个aspx页面,因为IIS可以在文件上设置身份验证。在根web应用程序上启用匿名和表单身份验证。在root/WinLogin目录中启用Windows身份验证并禁用匿名身份验证。添加自定义401和401.2错误页面重定向到帐户/登录URL。

This will allow any browser capable of pass-through to use windows integrated authentication to auto signin. While some devices will get prompted for credentials (like iPhone) and other devices like blackberry redirected to signin page.

这将允许任何能够通过windows集成身份验证的浏览器使用自动登录。而有些设备会被提示输入凭证(如iPhone),而其他设备如黑莓会被重定向到登录页面。

This also creates a cookie explicitly adding the users roles and creates a Generic principle so that role-based authorization can be used.

这还创建了一个明确添加用户角色的cookie,并创建了一个通用原则,以便可以使用基于角色的授权。

in WinLogin2.aspx (in WinLogin directory under "root" web application in IIS, and configured to use Windows Authentication, Anonymous disabled, and Forms enabled (as can't turn off...note IIS will complain when you enable windows authentication, just ignore) :

在WinLogin2。aspx(在WinLogin目录下IIS中的“root”web应用程序中,配置为使用Windows身份验证、匿名禁用和启用表单(因为无法关闭……)注意,当您启用windows身份验证时,IIS将会报错,请忽略):

var logonUser = Request.ServerVariables["LOGON_USER"];
        if (!String.IsNullOrWhiteSpace(logonUser))
        {
            if (logonUser.Split('\\').Length > 1)
            {
                var domain = logonUser.Split('\\')[0];
                var username = logonUser.Split('\\')[1];

                var timeout = 30;

                var encTicket = CreateTicketWithSecurityGroups(false, username, domain, timeout);

                var authCookie = new HttpCookie(".MVCAUTH", encTicket) { HttpOnly = true };
                Response.Cookies.Add(authCookie);


            }
            //else
            //{
            // this is a redirect due to returnUrl being WinLogin page, in which logonUser will no longer have domain attached
            //  ignore as forms ticket should already exist
            //}

            string returnUrl = Request.QueryString["ReturnUrl"];

            if (returnUrl.IsEmpty())
            {
                Response.Redirect("~/");
            }
            else
            {
                Response.Redirect(returnUrl);
            }
        }

        public static string CreateTicketWithSecurityGroups(bool rememberMe, string username, string domain, int timeout)
    {
        using (var context = new PrincipalContext(ContextType.Domain, domain))
        {
            using (var principal = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username))
            {
                var securityGroups = String.Join(";", principal.GetAuthorizationGroups());

                var ticket =
                    new FormsAuthenticationTicket(1,
                                                  username,
                                                  DateTime.UtcNow,
                                                  DateTime.UtcNow.AddMinutes(timeout),
                                                  rememberMe,
                                                  securityGroups,
                                                  "/");

                string encTicket = FormsAuthentication.Encrypt(ticket);
                return encTicket;
            }
        }
    }

In IIS 7.5, click Error Pages, set the 401 page to File path of Redirect401.htm file, with this code:

在IIS 7.5中,单击Error Pages,将401页面设置为Redirect401的文件路径。htm文件,带有以下代码:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org /TR/xhtml1/DTD/xhtml1-transitional.dtd">

    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
       <title></title>
        <script>
           window.location.assign('../Account/Signin');
      </script>
   </head>
  <body>

  </body>
   </html>

In AccountController...

在AccountController…

public ActionResult SignIn()
    {
        return View(new SignInModel());
    }

    //
    // POST: /Account/SignIn

    [HttpPost]
    public ActionResult SignIn(SignInModel model, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            if (Membership.ValidateUser(model.UserName, model.Password))
            {
                string encTicket = CreateTicketWithSecurityGroups(model.RememberMe,  model.UserName, model.Domain, FormsAuthentication.Timeout.Minutes);

                Response.Cookies.Add(new HttpCookie(".MVCAUTH", encTicket));

                //var returnUrl = "";
                for (var i = 0; i < Request.Cookies.Count; i++)
                {
                    HttpCookie cookie = Request.Cookies[i];
                    if (cookie.Name == ".MVCRETURNURL")
                    {
                        returnUrl = cookie.Value;
                        break;
                    }
                }

                if (returnUrl.IsEmpty())
                {
                    return Redirect("~/");
                }

                return Redirect(returnUrl);
            }

            ModelState.AddModelError("Log In Failure", "The username/password combination is invalid");
        }

        return View(model);
    }

    //
    // GET: /Account/SignOut

    public ActionResult SignOut()
    {
        FormsAuthentication.SignOut();

        if (Request.Cookies[".MVCRETURNURL"] != null)
        {
            var returnUrlCookie = new HttpCookie(".MVCRETURNURL") { Expires = DateTime.Now.AddDays(-1d) };
            Response.Cookies.Add(returnUrlCookie);
        }

        // Redirect back to sign in page so user can 
        //   sign in with different credentials

        return RedirectToAction("SignIn", "Account");

In global.asax:

在global.asax:

protected void Application_BeginRequest(object sender, EventArgs e)
    {


        try
        {
            bool cookieFound = false;

            HttpCookie authCookie = null;

            for (int i = 0; i < Request.Cookies.Count; i++)
            {
                HttpCookie cookie = Request.Cookies[i];
                if (cookie.Name == ".MVCAUTH")
                {
                    cookieFound = true;
                    authCookie = cookie;
                    break;
                }
            }

            if (cookieFound)
            {
                // Extract the roles from the cookie, and assign to our current principal, which is attached to the HttpContext.
                FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
                HttpContext.Current.User = new GenericPrincipal(new FormsIdentity(ticket), ticket.UserData.Split(';'));
            }



        }
        catch (Exception ex)
        {
            throw;
        }

    }


     protected void Application_AuthenticateRequest()
    {
        var returnUrl = Request.QueryString["ReturnUrl"];
        if (!Request.IsAuthenticated &&
            !String.IsNullOrWhiteSpace(returnUrl))
        {
            var returnUrlCookie = new HttpCookie(".MVCRETURNURL", returnUrl) {HttpOnly = true};
            Response.Cookies.Add(returnUrlCookie);
        }
    }

web.config

. config

<!--<authorization>
  <deny users="?"/>
</authorization>-->
<authentication mode="Forms">
  <forms name=".MVCAUTH" loginUrl="~/WinLogin/WinLogin2.aspx" timeout="30" enableCrossAppRedirects="true"/>
</authentication>
<membership defaultProvider="AspNetActiveDirectoryMembershipProvider">
  <providers>
    <add name="AspNetActiveDirectoryMembershipProvider" type="System.Web.Security.ActiveDirectoryMembershipProvider,           System.Web, Version=4.0.0.0, Culture=neutral,           PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="ADService" connectionProtection="Secure" enablePasswordReset="false" enableSearchMethods="true" requiresQuestionAndAnswer="true" applicationName="/" description="Default AD connection" requiresUniqueEmail="false" clientSearchTimeout="30" serverSearchTimeout="30" attributeMapPasswordQuestion="department" attributeMapPasswordAnswer="division" attributeMapEmail="mail" attributeMapUsername="sAMAccountName" maxInvalidPasswordAttempts="5" passwordAttemptWindow="10" passwordAnswerAttemptLockoutDuration="30" minRequiredPasswordLength="7" minRequiredNonalphanumericCharacters="1"/>
  </providers>
</membership><machineKey decryptionKey="..." validationKey="..." />  </system.web><connectionStrings> <add name="ADService" connectionString="LDAP://SERVER:389"/></connectionStrings>

Credit owed to http://msdn.microsoft.com/en-us/library/ms972958.aspx

信用卡欠http://msdn.microsoft.com/en-us/library/ms972958.aspx

#3


4  

This will probably live at the bottom of this question and never be found but I was able to implement what was described at

这可能会在这个问题的底部存在,而且永远不会被发现,但我能够实现所描述的内容。

http://mvolo.com/iis-70-twolevel-authentication-with-forms-authentication-and-windows-authentication/

http://mvolo.com/iis - 70二级-验证-形式-验证-和- windows - authentication/

It was quite easy and trivial. Didn't require multiple applications or cookie hacks, just extending the FormsAuthModule and making some web.config changes.

这很容易,也很琐碎。不需要多个应用程序或cookie破解,只需扩展FormsAuthModule并创建一些web。配置更改。

#4


0  

I know this is an old post - but everything lives forever on the internet!

我知道这是一个古老的帖子——但是所有的一切都永远生活在互联网上!

Anyway, I had to move an old website from IIS6 to IIS8. This is a WebForms website, but I assume this very simple solution is the same.

不管怎样,我必须把一个旧网站从IIS6移到IIS8。这是一个WebForms网站,但我认为这个简单的解决方案是一样的。

I received the error : Unable to cast object of type 'System.Security.Principal.WindowsIdentity' to type 'System.Web.Security.FormsIdentity'.

我收到了一个错误:无法强制类型为System.Security.Principal的对象。WindowsIdentity System.Web.Security.FormsIdentity”“输入”。

All I did was create a new application pool for the website. When creating this, I set the Managed pipeline mode to 'Classic'. (Read more here - http://www.hanselman.com/blog/MovingOldAppsFromIIS6ToIIS8AndWhyClassicModeExists.aspx) Dont forget to set the website's application pool to the new pool you just created.

我所做的就是为网站创建一个新的应用程序池。创建这个时,我将托管管道模式设置为“Classic”。(详情请参阅http://www.hanselman.com/blog/MovingOldAppsFromIIS6ToIIS8AndWhyClassicModeExists.aspx)不要忘记将网站的应用程序池设置为您刚刚创建的新池。