Uwierzytelnianie i routing
Transkrypt
Uwierzytelnianie i routing
ASP.NET MVC 6 Uwierzytelnianie i routing 1 Zaawansowane programowanie internetowe Instrukcja nr 6 1. Cel zajęć Celem zajęć jest zapoznanie się z metodami uwierzytelniania użytkownika aplikacji oraz z mechanizmami routingu w ASP.NET MVC. 2. Odrobina teorii na temat routingu Routing jest mechanizmem, który pozwala na zamianę przyjaznych dla użytkownika adresów internetowych (np. http://localhost:3456/home/details/3) na coś co może zrozumieć serwer i aplikacja internetowa. Mechanizm ten jest częścią składową platformy .NET 4.0 dlatego dostępny jest dla wszystkich aplikacji ASP.NET: • w przypadku klasycznego ASP.NET adresy przyjazne użytkownikowi zamieniane są na adres strony internetowej • w przypadku ASP.NET MVC adresy przyjazne użytkownikowi zamieniane są na strukturę określającą jaki kontroler i która jego akcja powinna być wywołana. Reguły routingu są definiowane w pliku Global.asax. Znajduje się tam metoda RegisterRoutes, która domyślnie ma postać: public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); } W metodzie tej wywoływane są kolejno dwie metody: IgnoreRoute i MapRoute. Pierwsza z nich służy do zdefiniowania tras, które mają być ignorowane (tzn. użytkownik nie będzie mógł się dostać do zasobów, na które wskazują). Druga z nich służy do definiowania nowej trasy (i generalnie ta metoda będzie nas najbardziej interesowała). Metoda MapRoute przyjmuje następujące parametry: 1. nazwa trasy (np.: „Default”), 2. wzorzec URL (np.: „{controller}/{action}/{id}”), który definiuje jak trasa ma wyglądać, 3. domyślne parametry trasy, 4. ograniczenia jakie mogą być narzucone na trasę. Wzorzec URL służy do zdefiniowania wyglądu trasy, czyli adresu strony, który musi podać użytkownik np. w pasku adresu przeglądarki. Wzorzec trasy zbudowany jest z segmentów, które rozdzielane są znakami slash. Każdy segment może składać się ze stałej znakowej lub zmiennej (przy czym zmienne muszą być ujęte w nawiasy klamrowe). Przykładowe wzorce przedstawione są poniżej: 2 Zaawansowane programowanie internetowe Instrukcja nr 6 /Produkty/{kategoria_produktow} /Faktury/{Rok}/{Miesiac}/{Dzien} /Artykuly/{Rok}-{Miesiac}-{Dzien} Następnym parametrem są wartości domyślne dla zmiennych trasy. W domyślnej metodzie wyglądają one następująco: new { controller = "Home", action = "Index", id = UrlParameter.Optional } W tym przypadku jeżeli użytkownik nie poda wartości dla zmiennej „controller” lub „action”, które występują we wzorcu podstawione zostaną wartości domyślne, które zdefiniowane są w tym parametrze. Na szczególną uwagę zasługuje zmienna id do której przypisana jest domyślna wartość UrlParameter.Optional, która oznacza, że zmienna ta może, ale nie musi wystąpić w trasie. Jeżeli wystąpi to wartość ta zostanie podstawiona do odpowiedniej zmiennej, jeżeli nie to żadna domyślna wartość nie będzie podstawiona). UWAGA!!! W przypadku ASP.NET MVC we wzorcu URL powinny wystąpić zmienne controller i action, które określają nazwę kontrolera i nazwę akcji, która ma być wywołana, jeżeli nie wystąpią muszą być do nich przypisane wartości domyślne, inaczej ASP.NET MVC nie będzie mógł albo określić kontrolera, albo akcji do której powinien przekazać sterowanie. Ostatnim parametrem, który może przyjmować metoda MapRoute są ograniczenia, które mogą być nałożone na trasę. Ograniczenia mogą być nałożone albo na zmienne, które występują we wzorcu URL, albo na metodę przesłania danych. Definiowane są albo jako wyrażenie regularne, albo jako obiekt, które implementuje interfejs IRouteConstraint. Przykładowe trasy z ograniczeniami: routes.MapRoute( "ListaOsob", "Osoby", new { controller = "Home", action = "Lista", id = UrlParameter.Optional }, new { httpMethod = new HttpMethodConstraint("Get") } ); routes.MapRoute( "EdycjaOsoby", "Edytuj/{id}", new { controller = "Home", action = "Edycja", id = UrlParameter.Optional }, new { id = @"\d+" } ); Pierwsza z tych tras będzie zastosowana tylko w przypadku, gdy żądanie zostanie przesłane metodą GET, z kolei druga, gdy parametr id będzie liczbą całkowitą. 3 Zaawansowane programowanie internetowe Instrukcja nr 6 Jeżeli powyższe trasy zostały by dołączone do metody RegisterRoutes, wyglądała by ona następująco: public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "ListaOsob", "Osoby", new { controller = "Home", action = "Lista", id = UrlParameter.Optional }, new { httpMethod = new HttpMethodConstraint("Get") } ); routes.MapRoute( "EdycjaOsoby", "Edytuj/{id}", new { controller = "Home", action = "Edycja", id = UrlParameter.Optional }, new { id = @"\d+" } ); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); } Obecnie metoda ta zawiera definicję trzech tras więc rodzi się pytanie w jakiej kolejności będą one dopasowywane do żądania. Otóż trasy będą dopasowywane w kolejności wystąpienia w metodzie RegisterRoutes. Wybrana zostanie pierwsza pasująca trasa, co oznacza, że należy umieszczać je w kolejności od najbardziej szczegółowych do najbardziej ogólnych. 3. Zadanie I Proszę przebudować aplikację, która była utworzona na poprzednich zajęciach, aby umożliwiała uwierzytelnianie użytkownika i dostęp do edycji danych tylko dla określonych użytkowników. Przyjmijmy następujące założenia: 1. Kontroler Home jest ogólnodostępny (również dla użytkowników niezalogowanych) 2. Wyświetlanie danych o przechowywanych w bazie kontaktach jest możliwe tylko dla zalogowanych użytkowników 4 Zaawansowane programowanie internetowe Instrukcja nr 6 3. Dodawanie i edycja danych jest możliwe tylko dla użytkowników z grupy „Edytorzy” i „Administratorzy” 4. Usuwanie danych jest możliwe tylko dla użytkowników z grupy „Administratorzy” W tym celu należy wykonać odpowiednie kroki: 1. Otwórzmy projekt z poprzednich zajęć. 2. Proszę otworzyć stronę ASP.NET Configuration. 3. Następnie dodajmy trzech użytkowników o dowolnych loginach oraz dwie grupy „Edytorzy” i „Administratorzy”. Pierwszego użytkownika nie przypisujmy do żadnej z grup, drugiego przypiszmy do grupy Edytorzy, a trzeciego do grupy Administratorzy. 4. Przejdźmy do spełnienia określonych wyżej założeń: • Ponieważ kontroler Home i jego akcji obecnie są ogólnodostępne wymaganie 1. jest od razu spełnione. • W celu spełnienia wymagania drugiego tzn. aby kontroler Osoba był dostępny tylko dla użytkowników zalogowanych oznaczmy ten kontroler atrybutem Authorize: [Authorize] public class OsobaController : Controller { … • W celu spełnienia wymagań trzeciego i czwartego, konieczne jest oznaczenie metod Create oraz Edit atrybutem Authorize, który będzie określał dodatkowo, które grupy użytkowników mają prawo wywołać określoną akcję np.: [Authorize(Roles="Edytorzy,Administratorzy")] public ActionResult Create() { return View(); } 4. Wykorzystanie mechanizmu ASP.NET Routing Przyjmijmy następujące założenia: 1. Aplikacja powinna umożliwiać dostęp do swoich tak jak dotychczas tzn: użytkownik powinien mieć dostęp do poszczególnych akcji systemu gdy w przeglądarce poda adres wg schematu: {domena}/{kontroler}/{akcja}/{id} 2. Dodatkowo, wymagane jest aby użytkownik miał dostęp do odpowiedniej funkcjonalności systemu jeżeli poda, którąś z poniżej wymienionych tras: 5 Zaawansowane programowanie internetowe Instrukcja nr 6 a) {nazwa_serwera}/Kontakty Wyświetlenie wszystkich kontaków (to samo co: {nazwa_serwera}/Osoba/ ) b) {nazwa_serwera}/NowyKontakt Wyświetlenie formularza dodawania nowej {nazwa_serwera}/Osoba/Create ) osoby (to samo co: c) {nazwa_serwera}/Kontakty/{Nazwisko}/{akcja} Jeżeli w bazie występuje tylko jedna osoba o danym nazwisku aplikacja powinna wyświetlić okno odpowiednie dla wybranej akcji. W przeciwnym wypadku powinna się wyświetlić lista osób o tym nazwisku w celu uszczegółowienia wyboru. d) {nazwa_serwera}/Kontakty/{Imie}_{Nazwisko}/{akcja} Jeżeli w bazie występuje tylko jedna osoba o danym imieniu i nazwisku aplikacja powinna wyświetlić okno odpowiednie dla wybranej akcji. W przeciwnym wypadku powinna się wyświetlić lista osób o tym nazwisku w celu uszczegółowienia wyboru. I. Spełnienie wymagań a) i b) 1. 2. Proszę otworzyć okno Solution Explorer i dwukrotnie kliknąć na pliku global.asax Proszę odnaleźć metodę RegisterRoutes i zmodyfikować ją tak aby uzyskać następujący kod: public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "NowyKontakt" , "NowyKontakt" , new { controller="Osoba", action = "create"} ); routes.MapRoute( "Kontakty" , "Kontakty" , new { controller = "Osoba", action = "index"} ); routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } 6 Zaawansowane programowanie internetowe Instrukcja nr 6 Powyższy kod dodaje dwie nowe trasy, dla których przypisano „na sztywno” odpowiednie kontrolery i ich akcje. 3. Proszę przetestować nowe trasy II. Spełnienie wymagania c) W przypadku poprzednich tras nakład pracy był bardzo niewielki, gdyż wykorzystywaliśmy istniejące już akcje kontrolera Osoba. Wymaganie c) określa, osoby w bazie mają być wyszukiwane po nazwisku. Dodatkowo, jeżeli w bazie danych zapisana jest tylko jedna osoba o danym nazwisku to od aplikacja od razu ma przejść do wyświetlania formularza odpowiedniego dla wybranej akcji, natomiast, gdy takich osób będzie więcej najpierw ma być wyświetlona ich lista, z której użytkownik wybierze odpowiedni wpis. Np. wydając polecenie: {nazwa_serwera}/Kontakty/Nowak/edit, to: a) jeżeli w bazie danych znajdują się informacje tylko o jednej osobie o nazwisku Nowak to od razu ma być wyświetlony formularz edycji danych tej osoby. b) jeżeli w bazie danych znajdują się informacje o więcej niż jednej osobie o nazwisku Nowak to najpierw powinna się wyświetlić lista tych osób, z której dopiero użytkownik wybierze odpowiedni wpis do edycji. W celu realizacji tego zadania wymagane będą następujące kroki: 1. Dodanie nowej metody do interfejsu IOsobaUslugi, która będzie zwracać listę osób o podanym nazwisku. Metoda ta może mieć następującą deklarację: List<OsobaModel> ZwrocOsobyWgNazwiska(string nazwisko); 2. Dodanie nowej metody do klasy OsobaUslugi w celu spełnienia wymagań zmodyfikowanego interfejsu. 3. Dodanie do kontrolera dodatkowych akcji: DetailsByName, EditByName, DeleteByName, które będą realizowały wymaganie c) dla odpowiednich akcji. Z punktu widzenia języka C# możliwe jest stworzenie tych metod o takiej samej nazwie jak już istniejące tj. Details, Edit, Delete, różniących się listą parametrów. Mechanizm ten nie może być jednak wykorzystany w tym przypadku, ponieważ domyślny sposób wybierania akcji do wykonania w ASP.NET MVC nie jest w stanie rozróżnić takich metod. Przykładowo akcja DetailsByName może mieć następujący kod: public ActionResult DetailsByName(string nazwisko) { List<OsobaModel> dane = _osoby.ZwrocOsobyWgNazwiska(nazwisko); if (dane.Count == 1) 7 Zaawansowane programowanie internetowe Instrukcja nr 6 return Details(dane[0].ID); else return View("Index", dane); } Pobranie danych z bazy Sprawdzenie liczby zwróconych obiektów Jeżeli liczba ta jest równa jeden to wywołujemy metodę Details podając jako parametr identyfikator wybranej osoby. W przeciwnym przypadku tworzymy widok “Index” i przekazujemy do niego listę odczytanych obiektów. 4. Obecnie, gdy mamy już wszystkie wymagane metody należy wskazać ASP.NET MVC kiedy ma je wywołać. W tym celu dodajmy nową trasę do metody RegisterRoutes: public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "WgNazwiska" , "Kontakty/{nazwisko}/{action}" , new { controller = "Osoba", action = "DetailsByName" } ); routes.MapRoute( "NowyKontakt" , "NowyKontakt" , new { controller = "Osoba", action = "create" } ); … } 5. 6. 7. Proszę uruchomić aplikację. Proszę przetestować powyższą akcję wpisując w przeglądarce np.: http://localhost:50265/Kontakty/Nowak/DetailsByName/ oczywiście port oraz nazwisko osoby należy dostosować do swojej aplikacji. Jeżeli w bazie danych jest więcej niż jedna osoba o podanym nazwisku wszystko powinno działać poprawnie. Natomiast, gdy w bazie jest tylko jedna osoba o podanym nazwisko to powinniśmy otrzymać błąd: „The view 'detailsbyname' or its master was not found.” Jak łatwo zauważyć pomimo wywołania metody Details ASP.NET MVC dalej próbuje znaleźć widok dla oryginalnej akcji (czyli: DetailsByName). Aby wskazać konkretny widok, który nas interesuje należy lekko zmodyfikować metodę Details: public ActionResult Details(int id) { OsobaModel osoba = _osoby.ZwrocOsobyWgID(id); 8 Zaawansowane programowanie internetowe Instrukcja nr 6 List<TelefonModel> numeryTelefonow = _telefony.PobierzTelefonyDlaOsoby(id); return View("Details", new SzczegolyOsoby { Osoba = osoba, NumeryTelefonow = numeryTelefonow } ); } Obecnie metoda ta jawnie wskazuje, który widok ma być wczytany, co rozwiązało nasz problem. 8. Podobnie do metody DetailsByName należy stworzyć również metody EditByName i DeleteByName. 9. Powyższe rozwiązanie tworzy jednak kilka nowych problemów: a. b. c. Użytkownik musi pamiętać o podaniu nazwy akcji z końcówką ByName, Domyślnie metoda BeginForm wykorzystywana w widokach Edit.aspx, Create.aspx, Delete.aspx, generuje formularz, który wysyłany jest do tej samej akcji, która go wygenerowała. Co oznacza, że Framework po zatwierdzeniu formularza będzie szukał metod EditByName, DeleteByName (ale w naszym projekcie metody te generują formularz, więc się zapętlamy). W projektach wykonywanych na poprzednich zajęciach formularz edycji danych wywoływany był za pomocą polecenia np: http://localhost:50265/Osoba/Edit/5 czyli identyfikator rekordu, który chcieliśmy zmienić był zapisany w adresie. Obecnie tego identyfikatora nie ma więc aplikacja nie będzie wiedziała, który rekord chcemy zmodyfikować. Najprostsze jest rozwiązanie ostatniego problemu, gdyż wystarczy trochę zmodyfikować widok Edit.aspx dołączając do niego kod, który będzie generował ukryte pole formularza dla właściwości ID: … <fieldset> <legend>Fields</legend> <%:Html.HiddenFor(model=>model.ID) %> <div class="editor-label"> <%: Html.LabelFor(model => model.Imie) %> </div> … W celu rozwiązania problemów a) i b) należy stworzyć klasę, która będzie modyfikowała nazwę akcji przed jej wywołaniem (uwaga w pliku zawierającym tę klasę należy dołączyć przestrzeń nazw System.Web.Routing): 9 Zaawansowane programowanie internetowe Instrukcja nr 6 public class ChangeActionNameHandler : IRouteHandler { public IHttpHandler GetHttpHandler( RequestContext requestContext) { var routeData = requestContext.RouteData; var request = requestContext.HttpContext.Request; if (request.HttpMethod == "GET”) routeData.Values[“action”] += "ByName" return new MvcHandler(requestContext); } } Zadaniem tej klasy jest zmodyfikowanie nazwy akcji poprzez dodanie do niej przyrostka „ByName”, co pozwala na podawanie użytkownikowi nazw akcji tj. „Edit”, „Details”, „Delete”. Zmiana ta powinna jednak wystąpić tylko, gdy żądanie jest wysłane metodą GET. Kiedy nasza klasa jest już gotowa należy poinformować ASP.NET MVC aby z niej korzystała w tym celu zmodyfikujmy definicję naszej trasy z: routes.MapRoute( "WgNazwiska" , "Kontakty/{nazwisko}/{action}" , new { controller = "Osoba", action = "DetailsByName" } ); Na: routes.Add("Nazwisko", new Route( "Kontakty/{lastName}/{action}" , new RouteValueDictionary(new { controller = "Osoba", action = "Details" }) , new ChangeActionNameHandler() ) ); Po wprowadzeniu powyższych poprawek, nasza aplikacja powinna działać już prawie poprawnie. Prawie, gdyż została do rozwiązania jeszcze jedna kwestia – akcja Edit oznaczona atrybutem [HttpPost] przekierowuje nas do akcji Index, lecz nasza klasa zmieni jej nazwę na IndexByName, której nie mamy w naszym kontrolerze. Najprostszym rozwiązaniem tej kwestii jest zmodyfikowanie klasy ChangeNameActionHandler: 10 Zaawansowane programowanie internetowe Instrukcja nr 6 public class ChangeActionNameHandler : IRouteHandler { public IHttpHandler GetHttpHandler( RequestContext requestContext) { var routeData = requestContext.RouteData; var request = requestContext.HttpContext.Request; if (routeData.Values[“action”].toLower() == "index”) routeData.Values[“action”] = "Details" if (request.HttpMethod == "GET”) routeData.Values[“action”] += "ByName" return new MvcHandler(requestContext); } } 5. Zadania do samodzielnego wykonania 1. 2. 3. Proszę napisać program rozwiązujący wymaganie d) Proszę rozbudować program tak aby była możliwość korzystania z komend w języku polskim np.: {nazwa_serwera}/Kontakty/Nowak/edytuj {nazwa_serwera}/Kontakty/Nowak/usun Proszę napisać ograniczenie, które pozwoli wykorzystać nowe trasy tylko w parzyste dni miesiąca. 11 Zaawansowane programowanie internetowe Instrukcja nr 6