Laden...

MVC: Angemeldet bleiben realisieren

Erstellt von Yheeky vor 12 Jahren Letzter Beitrag vor 12 Jahren 5.764 Views
Y
Yheeky Themenstarter:in
200 Beiträge seit 2008
vor 12 Jahren
MVC: Angemeldet bleiben realisieren

Hi,

ich habe in meiner MVC-Anwendung eine Funktion "Angemeldet bleiben" implementiert. Ich möchte, dass der Benutzer sich nicht jedes Mal anmelden muss, wenn er auf meine Seite kommt (und er natürlich den Haken gesetzt hat). Dies funktioniert lokal auch hervorragend, jedoch nicht auf meinem Webserver.
Weiss jemand woran das liegen könnte?

Hier die Art und Weise, wie ich das mache:

Beim Login wird diese Funktion aufgerufen:

var rememberMeBool = rememberMe == "on" ? true : false;
var timeout = rememberMeBool ? 525600 : 10;
var authTicket = new FormsAuthenticationTicket(username, rememberMeBool, timeout);
var encryptedTicket = FormsAuthentication.Encrypt(authTicket);
var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket)
	{
		Expires = DateTime.Now.AddMinutes(timeout),
        HttpOnly = true
    };
Response.Cookies.Add(authCookie);

HttpContext.User = new GenericPrincipal(new FormsIdentity(authTicket), new[] { String.Empty });

Session["LastAction"] = DateTime.Now;
Session.Timeout = 10;

Bei jedem Seitenaufruf überprüfe ich, ob die "LastAction" in der Session älter ist, als der gesetzte Timeout.

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    var sessionDate = new DateTime();

    if (Session["LastAction"] != null)
    DateTime.TryParse((Session["LastAction"]).ToString(), out sessionDate);
	
    if (HttpContext.User.Identity.IsAuthenticated)
    {
        Session["LastAction"] = DateTime.Now;
	}
}

Ich kann in dem Fall nicht die Standardfunktionalität der FormsAuthentication nutzen, da ich viele AJAX-Aufrufe in meiner Webseite habe (auch welche mit Interval), die natürlich die Session automatisch verlängern und somit nie auslaufen lassen.
Die Session benutze ich also, um die wirkliche Inaktivität herauszufinden.

Ich hoffe mir kann jemand bei dem Problem behilflich sein.

Danke schonmal und viele Grüße,
Yheeky

16.806 Beiträge seit 2008
vor 12 Jahren

Hast Du den <machineKey /> in der Web.Config gesetzt? Erst dann funktioniert ein Remember.

Y
Yheeky Themenstarter:in
200 Beiträge seit 2008
vor 12 Jahren

Hast Du den <machineKey /> in der Web.Config gesetzt? Erst dann funktioniert ein Remember.

Nee, den habe ich nicht gesetzt. Wie muss ich den denn setzen und wieso funktioniert es dann lokal?

16.806 Beiträge seit 2008
vor 12 Jahren

ASP (MVC und WebForms) nutzt den AntiForgetToken und dieser braucht den MachineKey.
Kannst ihn selbst generieren oder zB http://aspnetresources.com/tools/machineKey nutzen.

Dass es lokal funktioniert und dann auf einem Server nicht: das ist nicht unnormal und hab ich schon viele Male gelesen - aber ein Grund, wieso das so ist, kann ich Dir gerade keinen nennen.
Hatte selbst das Problem neulich ebenso, als ich einen neuen CustomMemberShipProvider implementiert und ebenfalls den MachineKey vergessen hatte.

Y
Yheeky Themenstarter:in
200 Beiträge seit 2008
vor 12 Jahren

Cool danke! Habs jetzt mal eingebaut (man muss doch außer dem MachineKey in der web.config nichts machen, oder?) und bisher funktioniert es. Habe die Seite nach einer Stunde wieder aufgemacht und war noch eingeloggt 👍

Vielen Dank nochmal!
Gruß Yheeky

Y
Yheeky Themenstarter:in
200 Beiträge seit 2008
vor 12 Jahren

Mhmm wieder ausgeloggt 😦
Scheint doch noch irgendwo zu haken...noch eine Idee?

16.806 Beiträge seit 2008
vor 12 Jahren

Nach einer bestimmten Zeit ausgeloggt zu werden ist ja normal....?

Log Doch mal, ob er einfach den Cookie nicht mehr findet oder ob die Timeout-Zeit überschritten wurde.

Wieso nutzt Du eigentlich nicht einen MembershipProvider, der sich um so etwas alleine kümmert?
So sieht die Config bei mir aus


<configuration>
    <.....>
    <system.web>
        <.....>
        <authentication mode="Forms">
            <!-- Timeout 
                    protection = all
                    Name of Cookie: MyApplicationNameAuthCookie-->
            <forms loginUrl="~/Einloggen"
                   timeout="600"
                   protection="All"
                   name="MyApplicationNameAuthCookie"
                   enableCrossAppRedirects="false"
                   slidingExpiration="true"
                   cookieless="AutoDetect" />
        </authentication>
        <!-- Machine Key for timeout cookie required ! -->
        <machineKey validationKey="key here"
                    decryptionKey="key here"
                    validation="SHA1"
                    decryption="AES" />
        <membership defaultProvider="MyMemberShipProvider">
            <providers>
                <clear />
                <add name="MyMemberShipProvider"
                     type="XYZ.App.Authentication.MyMemberShipProvider" />
            </providers>
        </membership>
        <roleManager enabled="true" defaultProvider="MyRoleProvider">
            <providers>
                <clear />
                <add name="MyRoleProvider"
                     type="XYZ.App.Authentication.MyRoleProvider" />
            </providers>
        </roleManager>
            <.....>
    </system.web>
        <.....>
</configuration>

Dementsprechend primitiv ist auch die Logik in der Anwendung, da dies ASP übernimmt.

Y
Yheeky Themenstarter:in
200 Beiträge seit 2008
vor 12 Jahren

Hi Abt,

ich hatte mich ja für den manuellen Weg entschieden gehabt, weil ich viele AJAX-Calls verwende und da jedes Mal der Timeout weitergeschoben wird, auch wenn der Benutzer kein "Angemeldet bleiben" ausgewählt hat.
Was mich nun aber noch stutzig macht. Es gibt soviele Stellen, wo von einem Timeout gesprochen wird. Wo sind denn da die Unterschiede?
1.web.config: <forms timeout="10"> 1.web.config: <membership defaultProvider="MyMembershipProvider" userIsOnlineTimeWindow="10"> 1.var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket)
{
Expires = DateTime.Now.AddMinutes(timeout),
HttpOnly = true
};

1.Session.Timeout = 10;

Ich bin irgendwie verwirrt, wo man den überall setzen muss, wo man Hilfestellungen bekommt etc...
Habe leider auch keine aufschlussreichen Tutorials gefunden, die mir da groß weiterhelfen.

Wäre super, wenn du dazu noch was schreiben könntest.

Danke und Gruß,
Yheeky

16.806 Beiträge seit 2008
vor 12 Jahren

ich hatte mich ja für den manuellen Weg entschieden gehabt, weil ich viele AJAX-Calls verwende und da jedes Mal der Timeout weitergeschoben wird, auch wenn der Benutzer kein "Angemeldet bleiben" ausgewählt hat.

Das ist kein Problem.
Kann man auch mit nem Attribut an einem Controller / Action steuern, sodass Du sogar entscheiden kannst, welche Action ein Timeout verlaengert und welches nicht.

Y
Yheeky Themenstarter:in
200 Beiträge seit 2008
vor 12 Jahren

Das ist kein Problem.
Kann man auch mit nem Attribut an einem Controller / Action steuern, sodass Du sogar entscheiden kannst, welche Action ein Timeout verlaengert und welches nicht.

Ja, aber dann muss ich das doch manuell weiterschieben 😉

Hast du ein Beispiel von dem was du meinst?

16.806 Beiträge seit 2008
vor 12 Jahren

Du machst Dir einen eigenen MemberShipProvider zusammen mit einem CustomAuthorizeAttribute. Weiß nicht, ob das CustomAuthorizeAttribute alleine ausreicht. Hab ich grad nicht im Kopf.

Anschließend kannst Du OnAuthorization überschreiben und Deine Anfrage nach Ajax/Nicht Ajax auswerten und den Timeout hochsetzen.
In dieser Methode handelt man den Access ab; ob das Ticket noch gültig ist, ob er in der entsprechenden Rechtegruppe ist etc etc... In dieser Methode würdest Du auch das Timeout neu setzen, sofern es keine Ajax Anfrage ist.

Bedenke aber immer, dass MVC auch nur ein Server-Attribut nutzt, um zu erkennen, ob es eine Ajax-Anfrage ist oder nicht. Gibt schon Addons für Firefox und Co die so etwas verschleiern.

Da einen MVC sehr viel machen lässt, muss man eben an der ein oder anderen Stelle seine Finger manuell ins Spiel bringen; aber wenigstens hat man die Möglichkeit, solche Besonderheiten, wie Du sie forderst, zu implementieren.

Y
Yheeky Themenstarter:in
200 Beiträge seit 2008
vor 12 Jahren

Du machst Dir einen eigenen MemberShipProvider zusammen mit einem CustomAuthorizeAttribute. Weiß nicht, ob das CustomAuthorizeAttribute alleine ausreicht. Hab ich grad nicht im Kopf.

Anschließend kannst Du :::

Das habe ich doch gemacht und auch oben dargestellt. Ich habe einen BaseController, der bei jedem Request (in meinem Fall Seitenaufruf) gefeuert wird. Die normalen Controller werden von BaseController abgeleitet. Alle Ajax-Aufrufe gehen in den AjaxController, der eben kein Event feuert. Vom Prinzip her funktioniert also die Unterscheidung von Ajax und Nicht-Ajax Aufruf.
Nur bekomme ich es nicht hin, dass ein Benutzer angemeldet bleibt.

Was ich allerdings nicht verstehe: beim Einloggen wird der Timeout durch mein Ticket (s. oben im ersten Posting) auf 525600 gesetzt von daher sollte das Ticket ja folglich auch nicht ablaufen. Irgendetwas sorgt allerdings dafür, dass das Ticket abläuft, obwohl der Timeout so hoch ist - das verstehe ich nicht.

16.806 Beiträge seit 2008
vor 12 Jahren

Ich weiß nicht, ob das im Controller vielleicht schon zu spät ist. Kanns derzeit leider auch nicht nachvollziehen; aber ich nehme an, dass Du OnActionExecuting überschrieben hast. Daher meine ich auch das ZugriffsAttribute (Authorize) und nicht den Basis-Controller.
Trotzdem "sollte" man alle Zugriff-relevanten Dinge VOR dem Ausführen des Controllers abgeschlossen haben; dazu gehört eben auch das Validieren der Session.

In OnActionExecuting des Controllers kann man zB eine Liste von Usern füllen/aktualisieren, die derzeit/die letzten x Minuten online sind.

Ich vermute jedenfalls, da Du keinen CustomProvider nutzt, dass MVC neben Deiner eigenen Implementierung auch seinen Standard nutzt und Dir damit in die Quere kommt.
Hab aber bei MVC bisher immer nur die vorhandenen Schnittstellen genutzt und weiß daher nicht mit Sicherheit, wie MVC hier reagiert, wenn man "händisch" dazwischen funkt.

Y
Yheeky Themenstarter:in
200 Beiträge seit 2008
vor 12 Jahren

Ich habe den Timeout in der web.config mal aus Spaß auf 1 gesetzt. Wenn ich nun länger als 1 Minute warte und per Breakpoint ins Programm gehe, gibt es irgendwas, was den Timeout verschiebt, obwohl ich das nun so umgestellt habe, dass ich quasi GARNICHTS mache. Normalerweise müsste doch der Timeout verfolgen, das Ticket abgelaufen sein (IsAuthenticated = false) etc...ich verstehe wirklich nicht, was da passiert!
Hier mal meine relavanten Dateien:

web.config:


<configuration>
    <.....>
    <system.web>
        <.....>
        <authentication mode="Forms">
      <forms loginUrl="~/Home/Index"
             timeout="1"
             defaultUrl="~/Home/Index"
             slidingExpiration="true"
             protection="All"
             enableCrossAppRedirects="false"
             cookieless="AutoDetect"/>
    </authentication>
        <!-- Machine Key for timeout cookie required ! -->
        <machineKey validationKey="key here"
                    decryptionKey="key here"
                    validation="SHA1"
                    decryption="AES" />
        <membership defaultProvider="MeinProvider">
            <providers>
                <clear />
                <add name="MeinProvider"
                     type="Solution.Models.MeinProvider" />
            </providers>
        </membership>
            <.....>
    </system.web>
        <.....>
</configuration>

Einen RoleProvider brauche ich nicht. Einen "MeinProvider" habe ich relativ "nackt" erstellt und sieht so aus:

using System;
using System.Web.Mvc;
using System.Web.Security;
using System.Text;

namespace Solution.Models
{
    public class MeinProvider : MembershipProvider
    {
        public override string ApplicationName
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            throw new NotImplementedException();
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new NotImplementedException();
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new NotImplementedException();
        }

        public override bool EnablePasswordReset
        {
            get { throw new NotImplementedException(); }
        }

        public override bool EnablePasswordRetrieval
        {
            get { throw new NotImplementedException(); }
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException();
        }

        public override string GetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        public override string GetUserNameByEmail(string email)
        {
            throw new NotImplementedException();
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { throw new NotImplementedException(); }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { throw new NotImplementedException(); }
        }

        public override int MinRequiredPasswordLength
        {
            get { throw new NotImplementedException(); }
        }

        public override int PasswordAttemptWindow
        {
            get { throw new NotImplementedException(); }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { throw new NotImplementedException(); }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { throw new NotImplementedException(); }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { throw new NotImplementedException(); }
        }

        public override bool RequiresUniqueEmail
        {
            get { throw new NotImplementedException(); }
        }

        public override string ResetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        public override bool UnlockUser(string userName)
        {
            throw new NotImplementedException();
        }

        public override void UpdateUser(MembershipUser user)
        {
            throw new NotImplementedException();
        }

        public override bool ValidateUser(string username, string password)
        {
            if ((from d in new DatabaseDataContext().Benutzer where d.Benutzername == username && d.Passwort == password select d).Any())
            return true;
			
			return false;
        }
    }
}

Und ein AuthorizeAttribute habe ich auch noch erstellt:


using System;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;

namespace Solution.Models
{
    public sealed class MeinAuthorizeAttribute : AuthorizeAttribute
    {
        public new string Roles;

		private string GetRoleForUser(string username)
        {
            return (from u in new DatabaseDataContext().Benutzer
                    where u.Benutzername == username
                    select u.Rolle).FirstOrDefault();
        }
		
        public MeinAuthorizeAttribute()
        {
		
        }

        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            if (httpContext == null)
                return false;

            if (!httpContext.User.Identity.IsAuthenticated)
                return false;

            var personRole = GetRoleForUser(httpContext.User.Identity.Name);

            return Roles.Contains(personRole);
        }

        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (AuthorizeCore(filterContext.HttpContext))
            {
                // ** IMPORTANT **
                // Since we're performing authorization at the action level, the authorization code runs
                // after the output caching module. In the worst case this could allow an authorized user
                // to cause the page to be cached, then an unauthorized user would later be served the
                // cached page. We work around this by telling proxies not to cache the sensitive page,
                // then we hook our custom authorization code into the caching mechanism so that we have
                // the final say on whether a page should be served from the cache.
                var cachePolicy = filterContext.HttpContext.Response.Cache;
                cachePolicy.SetProxyMaxAge(new TimeSpan(0));
                cachePolicy.AddValidationCallback(CacheValidateHandler, null);

                if (!HttpContext.Current.Request.RawUrl.StartsWith("/Ajax/"))
                {
                    //var createdCookie = FormsAuthentication.GetAuthCookie(HttpContext.Current.User.Identity.Name, true);
					//createdCookie.Expires = DateTime.Now.AddDays(1);
					//Response.Cookies.Add(createdCookie);
                }
            }
            else
            {
                filterContext.HttpContext.Response.Redirect("/Home/NotLoggedIn");
                filterContext.Result = new ContentResult();
            }
        }
    }
}

Aufgerufen werden alle Funktionen in den Controllern nach folgendem Schema:


[MeinAuthorizeAttribute(Roles = "Administrator,Benutzer")]
        public ActionResult Index()
        {
            return View();
        }

Ich bin echt ratlos X(

16.806 Beiträge seit 2008
vor 12 Jahren

Ich würde nicht anhand der URL prüfen, ob es ein Ajax Request ist, sondern die Property IsAjaxRequest abfragen.

Du fragst in meinen Augen die Session gar nicht ab, ob sie überhaupt existiert oder noch gültig ist:
Handle asp.net MVC session expiration
Hier wird das auch über den BaseController machen; persönlich würde ichs dennoch in die Authorize-Schnittstelle verlagern.

Mein Attribut sieht aber so aus:


    protected override bool AuthorizeCore( HttpContextBase httpContext )
        {
            isAuthenticated = httpContext.User.Identity.IsAuthenticated;
            return isAuthenticated;
        }

        /// <summary>
        /// Called when a process requests authorization.
        /// </summary>
        /// <param name="filterContext">The filter context, which encapsulates information for using <see cref="T:System.Web.Mvc.AuthorizeAttribute"/>.</param>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="filterContext"/> parameter is null.</exception>
        public override void OnAuthorization( AuthorizationContext filterContext )
        {
            base.OnAuthorization( filterContext );

            if ( !isAuthenticated )
            {
                filterContext.Result = new RedirectToRouteResult( "Einloggen", new RouteValueDictionary( ) );
            }
            else
            {
...
            

Die Rollenprüfung sollte in OnAuthorization stattfinden, sodass eine Weiterleitung stattfinden kann.

Wirklich arg viel weiter weiß ich auch nicht. Hab jetzt auch mal ein bisschen gesucht, wie man das noch lösen könnte und hab folgendes gefunden:

Detecting Session expiry on ASP.NET MVC
Detecting Session Timeouts using a ASP.Net MVC Action Filter

Es führen allem Anschein mehrere Wege nach Rom.

Y
Yheeky Themenstarter:in
200 Beiträge seit 2008
vor 12 Jahren

Ich habe gerade was gemerkt, was ziemlich faul ist. Ich habe beim Login folgenden Code:


            FormsAuthentication.SetAuthCookie(username, rememberMeBool);
           
            if (rememberMeBool)
            {
                var createdCookie = FormsAuthentication.GetAuthCookie(username, true);
                createdCookie.Expires = DateTime.Now.AddYears(10);
                Response.Cookies.Add(createdCookie);
            }

Der Cookie hat ein Expires-Datum von 2022! Beim nächsten Request springt der Debugger in AuthorizationCode und wenn ich in der Identity nach dem Ticket schaue, steht da wieder 2012, mit einem Expires-Datum was eine Minute höher ist, wie die aktuelle Zeit (Timeout von 1 in web.config).
Er merkt sich das also im Cookie irgendwie nicht 🙁

Kann doch nicht so schwer sein, sowas zu implementieren...

16.806 Beiträge seit 2008
vor 12 Jahren

Eigentlich sollte slidingExpiration das Resetten übernehmen.
Und ist das Request.Cookie.Add() richtig? Der Cookie existiert ja bereits; musst Du nicht - wenn slidingExpiration das nicht eh schon tut - einfach nicht die Expiration des vorhandenen Cookies erweitern, statt den ganzen Cookie neu setzen?

slidingExpiration funktioniert aber allem Anschein, da ja ein neuer Cookie mit der aktuellen Zeit + 1Min existiert.

Aber das ist von meiner Seite nur rumraten und Hinweise in Raum werfen.

Zudem muss ja noch ein Request stattfinden, damit der Cookie an den Client kommt. Daher auch mein Hinweis, obs nicht schon zu spät ist.