Postaram się pokazać jak użyć Nancy.Authentication.Forms
dla projektu, który jest migrowany z ASP.NET MVC do platform NancyFx + PetaPoco. Połączenie NancyFx i PetaPoco wydaje się bardzo lekkim rozwiązaniem, które jest w stanie przejąć na siebie zadania ociężałego tandemu MVC + EntityFramework.
Instalacja
Aby skorzystać z Nancy.Authentication.Forms należy najpierw zainstalować pakiet używając NuGet
|
Install-Package Nancy.Authentication.Forms |
Jeśli pakiet PetaPoco
nie jest jeszcze zainstalowany to należy go dodać właśnie teraz.
Konieczny także będzie pakiet Microsoft.AspNet.WebPages
, z uwagi na klasę System.Web.Helpers.Crypto
pomocną przy operacjach na hasłach.
|
Install-Package Microsoft.AspNet.WebPages |
Zaczynamy programowanie
Spadek po ASP.NET Identity
Migrując istniejący projekt ASP.NET MVC zapewne mamy zestaw tabel w bazie, które zawierają informacje o użytkownikach. Szczęśliwie możemy posłużyć się istniejącą tabelą AspNetUsers
, której pole Id
zdefiniowane jako nvarchar(128)
w rzeczywistości przechowuje GUIDy. Nie spotkałem się przynajmniej z inną sytuacją. Czemu Guid jest ważny? Okaże się za chwilę.
Zdefiniujmy klasy modelu z użyciem anotacji PetaPoco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
[ TableName("dbo.AspNetUsers") ] [ PrimaryKey("Id", autoIncrement=false) ] [ ExplicitColumns ] public partial class AspNetUser { [ Column ] public string Id { get; set; } [ Column ] public string UserName { get; set; } [ Column ] public string PasswordHash { get; set; } [ Column ] public string SecurityStamp { get; set; } [ Column ] public string Discriminator { get; set; } } [ TableName("dbo.AspNetRoles") ] [ PrimaryKey("Id", autoIncrement=false) ] [ ExplicitColumns ] public partial class AspNetRole { [ Column ] public string Id { get; set; } [ Column ] public string Name { get; set; } } |
IUserIdentity
Następnie należy zaimplementować kilka interfejsów. Na początek IUserIdentity
, który przechowuje podstawowe informacje o zalogowanym użytkowniku czyli jego nazwę i zestaw udzielonych mu praw.
|
public class NancyUserIdentity : Nancy.Security.IUserIdentity { public string UserName { get; set; } public IEnumerable<string> Claims { get; set; } } |
IUserMapper
Z użyciem tej klasy zaimplementujemy interfejs Nancy.Authentication.Forms.IUserMapper
. Zadaniem klasy implementującej ten interfejs jest dostarczenie informacji o użytkowniku (w postaci instancji IUserIdentity
) na podstawie identyfikatora, którym jest Guid.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
public class NancyUserMapper : Nancy.Authentication.Forms.IUserMapper { public Nancy.Security.IUserIdentity GetUserFromIdentifier( Guid identifier, Nancy.NancyContext context) { var db = new PetaPoco.Database("DefaultConnection"); string id = identifier.ToString("D"); var aspUser = db.SingleOrDefault<AspNetUser>("WHERE id=@0", id); if (aspUser == null) return null; var sql = @"SELECT y.Id, y.Name FROM AspNetUserRoles AS x INNER JOIN AspNetRoles AS y ON x.RoleId = y.Id WHERE x.UserId = @0"; var roles = db.Query<AspNetRole>(sql, id); return new NancyUserIdentity() { UserName = aspUser.UserName, Claims = roles.Select(i => i.Name).ToList() }; } } |
Podłączenie do NancyFx
Stworzone powyżej mechanizmy należy podłączyć do Nancy. Do tego celu posłużę się klasą Bootstrapera, który jest odpowiedzialny za wiele aspektów pracy frameworka.
IUserMapper
Jeśli w projekcie wcześniej nie został dodany Bootstraper to należy teraz dodać nową klasę dziedziczącą po DefaultNancyBootstrapper
. Poniższy dodatek rejestruje klasę NancyUserMapper
w kontenerze IoC. Dla mniej wtajemniczonych: oznacza to tyle, że kiedy gdzieś potrzebna będzie klasa implementująca interfejs IUserMapper
to kontener IoC stworzy instancję stworzonej przez nas klasy NancyUserMapper
.
Endy Tjahjono pisze, że jest to krok opcjonalny, po szczegóły odsyłam do jego artykułu – link na dole artykułu.
|
public class Bootstrapper : DefaultNancyBootstrapper { ... protected override void ConfigureRequestContainer(Nancy.TinyIoc.TinyIoCContainer container, NancyContext context) { base.ConfigureRequestContainer(container, context); container.Register<Nancy.Authentication.Forms.IUserMapper, NancyUserMapper>(); } ... } |
Aktywacja FormsAuthentication
Druga zmiana bootstrapera aktywuje mechanizm FormsAuthentication
poprzez dołączenie go na początku kolejki przetwarzania żądania HTTP. Jednocześnie podana jest tam ścieżka ~/login
, gdzie klient zostanie przekierowany w przypadku próby nieautoryzowanego dostępu do chronionego adresu url. Jak łatwo się domyślić pod adresem /login/
znajdzie się formularz logowania.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public class Bootstrapper : DefaultNancyBootstrapper { ... protected override void RequestStartup(Nancy.TinyIoc.TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines, NancyContext context) { base.RequestStartup(container, pipelines, context); Nancy.Authentication.Forms.FormsAuthentication.Enable( pipelines, new Nancy.Authentication.Forms.FormsAuthenticationConfiguration() { RedirectUrl = "~/login", UserMapper = container.Resolve<Nancy.Authentication.Forms.IUserMapper>() } ); } ... } |
Formularz logowania
Obsługę formularza logowania zaimplementujemy w module LoginModule
wspomaganym widokiem Login.cshtml
. Jako model posłuży klasa LoginModel
.
Model
Zacznijmy od modelu zawierające pola niezbędne do zalogowania.
|
public class LoginModel { public string username { get; set; } public string password { get; set; } } |
Moduł
Moduł LoginModule
będzie odpowiedzialny za obsługę żądań HTTP związanych z logowaniem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
public class LoginModule : NancyModule { public LoginModule() { Get["/login"] = GetLogin; Post["/login"] = PostLogin; Get["/logout"] = GetLogout; } public dynamic GetLogin(dynamic parameters) { var data = new LoginModel(); return data; } public dynamic PostLogin(dynamic parameters) { var username = (string)this.Request.Form.username; var password = (string)this.Request.Form.password; var db = new PetaPoco.Database("DefaultConnection"); var aspNetUser = db.SingleOrDefault<AspNetUser>("where UserName=@0", username); if (aspNetUser != null) { if (System.Web.Helpers.Crypto.VerifyHashedPassword(aspNetUser.PasswordHash, password)) { var token = Guid.Parse(aspNetUser.Id); return this.LoginAndRedirect(token, null, "~/"); } } ViewBag.ErrorMessage = "Invalid login or password"; return View["Login"]; } public dynamic GetLogout(dynamic parameters) { return this.LogoutAndRedirect("~/"); } } |
Kilka słów komentarza do modułu:
Należy pamiętać o zaimportowaniu przestrzeni nazw Nancy.Authentication.Forms
gdzie zaimplementowane są metody rozszerzające LoginAndRedirect
i LogoutAndRedirect
.
Przy wywołaniu tych metod należy pamiętać o podaniu ścieżki przekierowania po poprawnym zakończeniu akcji logowania lub wylogowania. Zwłaszcza w przypadku LoginAndRedirect
jest tu pewien haczyk. Trzeci parametr posiada ustawioną wartość domyślną „/” co w wielu przypadkach powoduje, że aplikacja działa poprawnie. Zalecam jednak użycie ścieżki poprzedzonej tyldą np „~/” gdyż w przypadku instalacji aplikacji w wirtualnej ścieżce serwera HTTP przekierowanie bez tyldy nie będzie działać poprawnie.
Do sprawdzenia poprawności hasła użyłem metody VerifyHashedPassword
zdefiniowanej w klasie System.Web.Helpers.Crypto
. W przypadku powodzenia użytkownik jest przekierowywany powrotnie do strony, z której przybył do formularza logowania. W przypadku błędnego logowania wyświetlany jest ponownie formularz logowania opatrzony komunikatem przechowywanym z zmiennej ViewBag.ErrorMessage
.
Widok
Do stworzenia widoku posłużyłem się razorem. Do projektu dodałem plik \Views\Login\Login.cshtml
z kodem podanym poniżej. Poza zwykłym formularzem dodane jest tu odwołanie do szablonu Shared/_Layout.cshtml
(pamiętamy o użyciu / zamiast \). Wskazałem też klasę bazową dla widoku Nancy.ViewEngines.Razor.NancyRazorViewBase
, gdzie LoginModel
jest klasą zdefiniowaną powyżej. Ponadto formularz jest wystylizowany pod Twitter Bootstrap.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<LoginModel> @{ Layout = "Shared/_Layout.cshtml"; // Use slash instead of backslash ViewBag.Title = "Login"; } <h2>@ViewBag.Title</h2> @if (!string.IsNullOrEmpty(ViewBag.ErrorMessage)) { <div class="alert alert-info" id="alert_template"> @ViewBag.ErrorMessage </div> } <form method="post"> <div class="form-horizontal"> <hr /> <div class="form-group"> <label class="control-label col-md-2">Username</label> <div class="col-md-10"> <input type="text" name="username" /> </div> </div> <div class="form-group"> <label class="control-label col-md-2">Password</label> <div class="col-md-10"> <input type="password" name="password" /> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" value="Login" class="btn btn-default" /> </div> </div> </div> </form> |
Koniec wieńczy dzieło
W końcu można przetestować całość dodając żądane autoryzacji do modułu:
|
using Nancy.Security; public class SomenameModule : NancyModule { public SomenameModule () { this.RequiresAuthentication(); Get["/Somename/"] = (parameters) => "I am secured"; } } |
Należy pamiętać o dodaniu przestrzeni nazw Nancy.Security
, gdyż tam znajduje się metoda rozszerzająca RequiresAuthentication
.
Źródła