23 June 2008 HowTo, Membership, Security Robert Muehsig

In diesem HowTo habe ich das ASP.NET Membership System mal kurz vorstellt. Ich hab in einem Projekt nun das Membership System eingesetzt und auch seine großen Schattenseiten kennengelernt.

Der erste große Kritikpunkt:
Da entwirft man eine 3-Tier Architektur und am Ende hängt einer der wichtigsten Teile (das Usersystem) mit in der WebApp - das ist mehr als unschön.

Mein Wunsch: Das Usersystem soll mit im Backend verschwinden.
Eine grobe Skizze (wobei man den "Service" noch in andere Schichten unterteilen kann):

image
Die "App.Config" sollte den ConnectionString speichern und auch die Membership-Konfiguration übernehmen.
Ich sage hier bewusst "sollte", da ich es leider nicht ganz so perfekt hinbekommen hab.

Allerdings erst mal Schritt für Schritt: Das Membership-System muss in eine DLL rein.

Grundsätzlicher Aufbau:

image

  • "DllMembership.Lib" ist unser Service in dem wir unseren "MembershipService" haben.
  • "DllMembership.Web" ist eine gewöhnliche ASP.NET Website.
  • "DllMembership.Test" ist unser UnitTest-Projekt

Schritt 1: Membership-System in der DLL Konfigurieren

Als erstes müssen wir in der App.Config folgende Konfiguration erstellen:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="ASPNETDBConnectionString" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename='C:\Users\rmu\Documents\Visual Studio 2008\Projects\Blogposts\DllMembership\DllMembership.Lib\DB\ASPNETDB.MDF';Integrated Security=True;User Instance=True"
     providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <membership>
      <providers>
        <remove name="AspNetSqlMembershipProvider"/>
        <add name="AspNetSqlMembershipProvider"
             type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
             connectionStringName="ASPNETDBConnectionString"
             enablePasswordRetrieval="false"
             enablePasswordReset="true"
             requiresQuestionAndAnswer="false"
             applicationName="/"
             requiresUniqueEmail="true"
             passwordFormat="Hashed"
             maxInvalidPasswordAttempts="12"
             minRequiredPasswordLength="1"
             minRequiredNonalphanumericCharacters="0"
             passwordAttemptWindow="10"
             passwordStrengthRegularExpression="" />
      </providers>
    </membership>
  </system.web>
</configuration>

Die Data Source muss natürlich entsprechend des DB Speicherortes ausgewechselt werden.
Wichtiger Hinweise: Ich verwende dieselbe DB wie aus dem anderen Blogpost.
Ganz wichtig: Damit die App.Config auch angenommen wird, müssen die Properties richtig gesetzt sein:

image

Jetzt fügen wir noch die "System.Web" Referenz hinzu:

image

Schritt 2: Service schreiben

Jetzt implementieren wir unseren sehr einfachen Service:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;

namespace DllMembership.Lib
{
    public class MembershipService
    {
        public IList<User> GetUsers()
        {  
            MembershipUserCollection col = Membership.GetAllUsers();
            List<User> returnList = new List<User>();
            foreach (MembershipUser user in col)
            {
                User u = new User();
                u.Name = user.UserName;
                u.Email = user.Email;
                returnList.Add(u);
            }
            return returnList;
        }

        public User Login(string username, string password)
        {
            User returnUser = new User();

            if (Membership.ValidateUser(username, password))
            {
                returnUser.IsLoggedIn = true;
                returnUser.Name = username;
                returnUser.Password = password;
                return returnUser;
            }
            else
            {
                returnUser.IsLoggedIn = false;
                return returnUser;
            }
        }

        public User GetUser(string username)
        {
            MembershipUser user = Membership.GetUser(username);
            User returnUser = new User();
            returnUser.Name = user.UserName;
            returnUser.Email = user.Email;
            return returnUser;
        }
    }
}

Dieser Service gibt ein "User" Objekt zurück (im Prinzip findet ein Mapping zwischen dem MembershipUser und unserem User statt) :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DllMembership.Lib
{
    public class User
    {
        public bool IsLoggedIn { get; set; }
        public string Name { get; set; }
        public string Password { get; set; }
        public string Email { get; set; }
    }
}

Diese "User" Klasse fungiert ebenfalls als "Result" für unsere Service-Aufrufe. Klappt zum Beispiel die Login-Methode nicht, wird einfach die "IsLoggedIn" Property auf false gesetzt.

Schritt 2.5: Unit Tests

Ich habe 3 Unit-Test Methoden geschrieben, welche die grobe Funktionalität testen. Das klappt soweit.

Schritt 3: Das Web-Projekt
Damit unser Nutzer während einer Session auch eingeloggt bleibt, müssen wir noch die Form-Authentication aktivieren.
Hier müssen wir noch den Cookie "manuell" setzen. Dazu habe ich eine AppHelper mir erstellt:

namespace DllMembership.Web
{
    public static class AppUtil
    {
        public static User GetActiveUser()
        {
        if(HttpContext.Current.User.Identity.IsAuthenticated == false)
            {
                return new User() { IsLoggedIn = false };
            }
            else
            {
                MembershipService service = new MembershipService();
                User returnValue = service.GetUser(HttpContext.Current.User.Identity.Name);
                returnValue.IsLoggedIn = true;
                return returnValue;
            }
        }

        public static User Login(string username, string password)
        {
            MembershipService service = new MembershipService();
            User returnValue = service.Login(username, password);
            if (returnValue.IsLoggedIn)
            {
                FormsAuthentication.SetAuthCookie(returnValue.Name, true);
                returnValue.IsLoggedIn = true;
            }
            else
            {
                returnValue.IsLoggedIn = false;
            }

            return returnValue;
        }
    }
}

Hierbei gibt es zwei Methoden "GetActiveUser" und "Login".
Zum "Login":
Diese Methode übergibt die Parameter zum Service und wenn der Login erfolgreich war, wird dieser über ein Cookie über die Session hinweg authentifiziert.
Die "GetActiveUser":
Diese Methode schaut einfach, ob der User im HttpContext authentifiziert ist, wenn nicht, gibt es keinen angemeldeten Nutzer, ansonsten wird der aktuelle Nutzer geladen.

Schritt 4: Ausgabe des Usernamen auf der Website

            DllMembership.Lib.User returnUser = AppUtil.GetActiveUser();
            if (returnUser.IsLoggedIn == false)
            {
                this.UserName.Text = "unangemeldet";
            }
            else
            {
                this.UserName.Text = returnUser.Name;
            }

Dadurch können wir leicht prüfen, ob jemand angemeldet ist, oder nicht.

Das Problem dabei
Leider geht die Lösung so wie ich sie hier gepostet habe, nicht ganz, denn man muss leider in der Web.Config die Membership Konfiguration und den ConnectionString noch extra angeben.
Das "Witzige" an der Geschichte: Die Unit-Tests laufen. Sobald dies aber auf der Webseite genutzt wird, überschreiben wohl die Web.Config Einstellungen die App.Config Einstellung - ihr könnt es gerne selber ausprobieren.
Ich finde das etwas unschön, aber verschmerzbar (bzw. fällt mir nix anderes ein).
Wenn jemand eine Lösung weiß, dann her damit :)

Fazit
Das ist nur ein "Prototyp", ich habe längst nicht alles fertig mir ausgedacht und würde vielleicht noch extra Properties einbauen und den Service umbasteln. Allerdings sollte dies der erste Schritt sein um zu zeigen, wie man das Membership dahin packt, wo es hingehört: In eine andere Schicht.

[ Download Source Code ]
* In den *.config muss der ConnectionString angepasst werden
** Anmeldedaten stehen in der ReadMe.txt in WebApp Ordner


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!