ASP Roles W poprzednim wpisie stworzyłem proste uwierzytelnienie przy użyciu jednej klasy statycznej FormsAuthentication. Klasa w metodzie “FormsAuthentication.RedirectFromLoginPage” stworzyła ciasteczko użytkownika ,a w metodzie “FormsAuthentication.SignOut()” wylogowała wcześniej uwierzytelnionego użytkownika.

W teorii te proste funkcje wystarczają do stworzenia własnego systemu uwierzytelniającego, który polegałby tylko na wbudowanych mechanizmach zapisu stanu uwierzytelnienia ASP.NET.

 
Contex.User.IsInRole_01

Niestety sprawa się komplikuje gdy chcemy do naszej prostej aplikacji dodać role użytkowników. Tak jak poprzednim razem nie omówię tutaj klas, które narzucają pewne mechanizmy sprawdzania roli jak RoleProvider i RoleMenager . Skoncentrujemy się na końcówce tego procesu czyli jak zapisać role do kontekstu aplikacji, gdy już sprawdzaliśmy role użytkownika na swój sposób.

Role użytkowników

Informacje o uwierzytelnionym użytkowniku są w obiekcie Context.User i przykładowo z obiektu tego mogę pobrać nazwę obecnego uwierzytelnionego użytkownika Context.User.Identity.Name.

protected void Page_Load(object sender, EventArgs e)
{
    lblUserInfo.Text = Context.User.Identity.Name;
}

Najbardziej banalne zabezpieczenie wyglądało by tak.

if (Context.User.Identity.Name == "Cezary")
{
    doSomething()
}

Jest to jednak pułapka. Zamiast sprawdzać tożsamości użytkownika najlepiej sprawdzić członkostwo użytkownika do określonej grupy użytkowników czyli ról. Przykładowo, gdy chcemy aby konkretny użytkownik miał dostęp do funkcji administratorskich dajemy mu role administrator.
Potem możemy sprawdzić członkostwo w kodzie w ten sposób:

if (Context.User.IsInRole("Admin")
{
    doSomething()
}

Z takim kodem możesz zarządzać listą użytkowników i ról bez ingerencji w swój sposób przechowywania i pobierania użytkowników(Baza danych X, XML, Zaszyfrowany plik tekstowy itp). Kod powyżej nigdy się nie zmieni, no chyba ,że zmieni się sam ASP.NET.

Jednak jak to zaimplementować?

Patrząc na kod z poprzedniego wpisu nigdzie przecież nie dodajemy informacji o roli użytkownika.

protected void btnLog_Click(object sender, EventArgs e)
{
    if ((txtLogin.Text == "Cezary") && (txtPassword.Text == "12345"))
    {
        FormsAuthentication.RedirectFromLoginPage
            (txtLogin.Text,chkPersist.Checked);
    }
    else {
        Response.Write("Błędne dane. Spróbuj jeszcze raz");
    }
}

Używając metody “FormsAuthentication.RedirectFromLoginPage” podajemy tylko nazwę użytkownika i trwałość ciasteczka. Logicznie myśląc jeśli nie podaliśmy nigdzie roli użytkownika, który został uwierzytelniony aplikacja później w magiczny sposób nie zgadnie jego roli w metodzie “IsInRole()”.
 
Contex.User.IsInRole_02

Prosty sposób który nie działa

Najprostszym rozwiązaniem jest stworzenie własnych obiektów Principal(w niej znajduje się rola) iIdentity (w niej znajduje się nazwa użytkownika). Możesz stworzyć własne klasy dziedziczące po interfejsachIPrincipal iIIdentity ,ale nie ma takiej potrzeby.

W przestrzeni nazw System.Security.Principal można odnaleźć już klasy stworzone do tego celu GenericPrincipal i GenericIdentity.

Uzbrojony w te obiekty kod logujący z rolami wyglądałby tak. Tym razem ze względu na ilość kodu nie jest on w niebieskiej ramce.

using System;
using System.Web.Security;
using System.Security.Principal;

namespace UwierzytelnienieForm
{
    public partial class LogOn : System.Web.UI.Page
    {
        private string[] baseRole = { "Admin", "Użytkownik", "Moderator", "Grafik" };

        protected void btnLog_Click(object sender, EventArgs e)
        {
            if ((txtLogin.Text == "Cezary") && (txtPassword.Text == "12345"))
            {
                string[] role = { baseRole[0], baseRole[3] };
                AuthenticateUserAndRole("Cezary", role);
            }

            if ((txtLogin.Text == "Franko") && (txtPassword.Text == "09876"))
            {
                string[] role = { baseRole[1] };
                AuthenticateUserAndRole("Franko", role);
            }

            if (Context.User.Identity.IsAuthenticated)
            {
                FormsAuthentication.RedirectFromLoginPage
                    (txtLogin.Text, chkPersist.Checked);
            }
            else
            {
                Response.Write("Błędne dane. Spróbuj jeszcze raz");
            }
        }

        private void AuthenticateUserAndRole(string userName, string[] roles)
        {
            GenericIdentity userIdentity = new GenericIdentity(userName);
            GenericPrincipal userPrincipal =
                new GenericPrincipal(userIdentity, roles);
            Context.User = userPrincipal;
        }
    }
}

Logika tego rozwiązania jest prosta. Jeśli nazwa użytkownika i hasło jest akceptowane budujemy tablicę stringów, która zawiera nazwę ról, które posiada użytkownik i razem z GenericIdentity budujemy obiekt GenericPrincipal. Przypisujemy naszego użytkownika do kontekstu ASP.NET (“Contex”) i wszystko powinno być w porządku. W dalszym cyklu aplikacji sprawdzamy rolę użytkownika w ten sposób.

if (Context.User.IsInRole("Admin")
{
    doSomething();
}

Istnieje jednak jeden poważny problem, co do tego prostego rozwiązania. Ono wcale nie działa.
 

Dlaczego to nie działa

Użytkownik będzie mógł się zalogować oraz zostanie uwierzytelniony ,ale jeśli przypiszesz danego użytkownika do roli Administratora w ten sposób, następna strona domyślna nie rozpozna go do tej roli.
Co się dzieje?
Łatwo to wytłumaczyć za pomocą dwóch rzutów ekranów pokazujących życie obiektu Contex.User.

autos_04


Jak widać na stronie logującej role istnieją role. Widać to w polu prywatnym“m_roles”.
 

autos_03


Jednak na następnej stronie, do której jesteśmy przekierowani po logowaniu pole prywatne my_roles nie zawiera żadnych informacji.

Na pierwszy rzut oka wydaje się ,że dzieje się tutaj coś naprawdę dziwnego w końcu przypisałem do tego obiektu dobry obiekt“GenericPrincipal”. Co się z nim stało ?

Po pierwsze jest to aplikacja WEB ,nie jest to aplikacja okienkowa. Co oznacza , że obiekt Contex po prostu sobie czeka i jest zawsze taki sam. Z każdym requestem strony obiekt Contex jest budowany ponownie. Tak po prostu działa bez stanowa aplikacja.

Od momentu kiedy powiedzieliśmy ASP.NET w web.configu , że używamy uwierzytelnienia typu “Form” ASP.NET sprawdza pewne ciasteczko (cookie) z każdym requestem.
Wywołanie metody RedirectFromLoginPage tworzy to ciasteczko i umieszcza w nim tożsamość użytkownika. Kiedy ASP.NET uwierzytelnia użytkownika, tak naprawdę sprawdza jego dane ciasteczko , a potem przebudowuje jego obiekt Contex.User, jeśli je znalazł.

Twórcy ASP.NET w tym schemacie nie uwzględnili ról i przez to, ta prosta aplikacja nie działa.

…i co teraz?

Pomysł na pewno jest dobry problem leży w jego wykonaniu.

Zamiast budować swoje obiekty bezpieczeństwa przed akcją z formularzem uwierzytelniającym lepiej dodać swoje role po jego akcji. Znaleźć odpowiednie ciasteczko i dopisać do niego rolę. W ten sposób Contex.User będzie zawsze miał przypisane swoje role.

Tym razem odrzucę metodę “FormsAuthentication.RedirectFromLoginPage” i zaprezentuje jak stworzyć takie ciasteczko od podstaw.

Oto jak wygląda kod logujący.

using System;
using System.Web.Security;
using System.Security.Principal;

namespace UwierzytelnienieForm
{
    public partial class LogOn : System.Web.UI.Page
    {
        private string[] baseRole = { "Admin", "Uzytkownik", "Moderator", "Grafik" };

        protected void btnLog_Click(object sender, EventArgs e)
        {
            if ((txtLogin.Text == "Cezary") && (txtPassword.Text == "12345"))
            {
                string[] role = { baseRole[0], baseRole[3] };
                AuthenticateUserAndRole("Cezary", role);
            }

            if ((txtLogin.Text == "Franko") && (txtPassword.Text == "09876"))
            {
                string[] role = { baseRole[1] };
                AuthenticateUserAndRole("Franko", role);
            }

            if (Context.User.Identity.IsAuthenticated)
            {
                FormsAuthentication.RedirectFromLoginPage
                    (txtLogin.Text, chkPersist.Checked);
            }
            else
            {
                Response.Write("Błędne dane. Spróbuj jeszcze raz");
            }
        }

        private void AuthenticateUserAndRole(string userName, string[] roles)
        {
            GenericIdentity userIdentity = new GenericIdentity(userName);
            GenericPrincipal userPrincipal =
                new GenericPrincipal(userIdentity, roles);
            Context.User = userPrincipal;
        }
    }
}


Zwróć uwagę na metodę “CreateCookieAndRedirect”.

Tym razem role są zapisane w postaci jednego stringu ,a znak “|” służy za separator pomiędzy rolami. Później tworzy ona obiekt FormsAuthenticationTicket. Ta klasa zawiera w sobie wszystkie informacje, do których zostaną potem zapisane w ciasteczku.

FormsAuthenticationTicket.

Konstruktor tej klasy potrzebuje aż 6 argumentów.

  1. Numer wersji biletu.Nazwa użytkownika, która ma być użyta w obiekcie Identity.
  2. Czas w którym bilet został utworzony.
  3. Moment, w którym bilet ma zostać unieważniony.
  4. Zmienna logiczna mówiąca o tym czy bilet jest trwały. (istnieje po wyłączeniu przeglądarki)
  5. Specjalny string zawierający informacje o użytkowniku w tym wypadku role.

Ten kod stworzył bilet, w którym są zawarte role użytkownika. Dla bezpieczeństwa bilet ten jest szyfrowany później na jego bazie zostaje tworzone ciasteczko ,a potem to ciasteczko jest dodane do strumienia odpowiedzi aplikacji.

Ostatni krok przekierowuje użytkownika do odpowiedniej strony. Nie musimy tutaj wywoływać metody RediretFromLoginPage ponieważ stworzyłem własnoręcznie swoje ciasteczko i nie muszę polegać już na wbudowanym mechanizmie.

Jednak to jeszcze nie koniec wciąż musimy powiedzieć naszej aplikacji jak ma tworzyć obiekt Contex.User.

Global.asax na ratunek

Pozostało nam utworzyć obiekt “GenericPrincipal” i GenericIdentity na bazie ciasteczka.
Ten proces nie może być wykonany w trakcie cyklu życia jednej strony. Tak jak powiedziałem wcześniej obiekt Contex jest odtwarzany z każdym requestem. Do zarządzania takim zdarzeniami aplikacji WEB jest plik global.asax.Jeśli go nie masz dodaj go do projektu.

image

Plik“Global.asax” służy do zarządzania zdarzeniami całejaplikacji. Na jedną aplikacje przypada jeden plik Global.asax.
 
globalASAX

W tym pliku możesz obsługiwać takie zdarzenia jak “Application_Start” czy “Application_Error” (przydatne zdarzenie do obsługi własnej strony z błędami w ASP.NET).

Nas jednak interesuje zdarzenie “Application_AuthenticateRequest”. To zdarzenie wywoływane jest za każdym razem, gdy aplikacja ASP.NET chce sprawdzić czy obecny użytkownik jest uwierzytelniony,
Z uwierzytelnieniem typu FORMS zdarzenie to będzie uruchamiane z każdym requestem.

Oto kod:

protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
    if (Context.Request.IsAuthenticated)
    {
        HttpCookie authCookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName];
        if (authCookie == null)
            return;

        FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
        string[] roles = authTicket.UserData.Split('|');

        GenericIdentity userIdentity = new GenericIdentity(authTicket.Name);
        GenericPrincipal userPrincipal = new GenericPrincipal(userIdentity, roles);

        Context.User = userPrincipal;
    }
}

Najpierw sprawdzam, czy dany użytkownik jest już uwierzytelniony. Jeśli tak jest sprawdzam czy ma on swoje ciasteczko. Jeśli je ma odkodowuję informacje , a potem muszę utworzyć tablicę ról na bazie napisu z separatorem.

Tworzę obiekt GenericIdentity i GenericPrincipal i przypisuję je do Contex.User.

autos_05

Tym razem strona Index.aspx posiada użytkownika z rolami. Co oznacza , że metoda “IsInRole()” będzie działać poprawnie.

image

Teraz możemy też wyświetlać strony tylko dla wybranych ról użytkowników. Wystarczy dodać taki kod do pliku web.config.

    <location path="ADMIN">
        <system.web>
            <authorization>
                <allow roles="ADMIN"/>
                <deny users="*"/>
            </authorization>
        </system.web>
    </location>
    <location path="Publishers">
        <system.web>
            <authorization>
                <allow roles="PUBLISHER,ADMIN"/>
                <deny users="*"/>
            </authorization>
        </system.web>
    </location>


Od tej pory do strony w folderze “Administrator” będzie dostęp tylko użytkownicy z rolą “Admin”. A do folderu “Publishers” tylko użytkownicy z rolą“Publisher” i “Admin”.

Możesz teraz też korzystać z kontrolki “LoginView” i wyświetlać odpowiednią zawartość dla odpowiednich użytkowników.

<asp:LoginView ID="LoginView1" runat="server"> 
      <RoleGroups> 
                 <asp:RoleGroup Roles="Admin"> 
                          <ContentTemplate> 
                              Tutaj umieść pole administratora.
                          </ContentTemplate> 
                </asp:RoleGroup> 
                <asp:RoleGroup Roles="Użytkownicy" > 
                        <ContentTemplate> 
                             Tutaj umieść logo.
                       </ContentTemplate> 
               </asp:RoleGroup> 
      </RoleGroups> 
</asp:LoginView>


Tyle w tym wpisie. Być może następnym razem opiszę jak stworzyć własne kontrolki logujące.

Przykład można pobrać tutaj. http://dl.dropbox.com/u/22340217/DownloadPrograms/UwierzytelnienieForm.rar