.Net Core MVC Web Uygulamalarında Yetkilendirme – 2 (Custom Identity)

Merhaba arkadaşlar.

Önceki yazıda; Scaffold Identity yapısından bahsetmiştik. Scaffold Identity yapısı sayesinde, (Identity Middleware) kimlik ara katmanını projemize hızlı bir şekilde dahil edebilmekteyiz. Scaffold Identity hazır bir yapı olduğu için projemize varsayılan ayarlarıyla eklenmektedir. Bu kez; kendi sayfalarımızı oluşturup, Identity ara katmanını projemize göre nasıl düzenleyebiliriz bundan bahsedeceğiz.

Yazıda, gerçek hayata biraz daha yakın olması ve konunun özümsenebilmesi için; örnek bir senaryo üzerinden projemizi şekillendireceğiz.

Örnek senaryomuza göre;
– bir veri tabanımız var,
– veri tabanı içerisindeki YemekTarifleri isimli tabloda daha önceden eklenmiş olan bilgiler mevcut,
– ve biz veri tabanındaki bu tariflere; /home/tarifler sayfasından, sadece üye olanlar erişebilmesini istiyoruz.

Projenin ilk halini buradan indirebilirsiniz.

Projeyi indirip çalıştıralım;

 

Şuan Tarifler sayfası tüm ziyaretçilere açık durumdadır.
[Authorize] etiketini ekleyerek sayfayı; sadece üyelerin görebileceği şekilde erişimi sınırlandıralım.

Ve son durum;

 

Müthiş oldu 🙂
Artık kendi Identity yapımızı oluşturmanın zamanı gelmiş gibi görünüyor..
Fakat yapıyı oluşturmaya başlamadan önce, işin hikaye kısmına girmek biraz faydalı olacaktır.

– Hikaye başlangıcı –

Scaffold Identity yapısını kullanırken; Code Generation Tool sayesinde de yapının bize hazır olarak sunduğu kod ve dosyaları projeye hızlı bir şekilde dahil etmiştik.
Bu kez, yetkilendirme için gerekli olan yapıyı kendimiz oluşturacağız. Bu cümleyi biraz daha açacak olursak;

  • Kullanıcı (ApplicationUser.cs) modelini kendimiz oluşturacağız,
  • DbContext (ApplicationDbContext.cs) sınıfında  gerekli değişiklikleri yapıp veri tabanını tekrar güncelleyeceğiz,
  • Register, Login, Logout, ForgotPassword, ResetPassword ve ChangePassword sayfalarını oluşturup; bu işlemleri gerçekleştirmek için kullanılan sınıfları inceleyeceğiz,
  • Statup.cs içerisinde gerekli ayarlamaları yapacağız

– Hikaye bitişi –

Sıradan başlayalım..

İlk olarak kullanıcı modelimiz yani ApplicationUser.cs sınıfını oluşturuyoruz.

Kullanıcı modelimiz; ad (FirstName), soyad (LastName), yaş (Age) ve konum (Location) bilgilerini barındırmakta olup, IdentityUser sınıfını miras almaktadır.
“Kullanıcı id, kullanıcı adı, e-posta ve şifre alanları nerede?” diye sorabilirsiniz. Bu alanlar; IdentityUser sınıfıyla birlikte gelmektedir. IdentityUser içerisindeki kullanıcı adı (UserName), e-posta (Email), ve şifre (PasswordHash) alanları string veri tipindedir.

Kullanıcı id (Id) alanı ise varsayılan olarak string tipindedir. Fakat biz istersek bu alan için farklı bir veri tipi de kullanabiliyoruz.
Bir sonraki yazıda bu konunun ayrıntısına gireceğiz.

Şimdi DbContext sınıfımızda gerekli düzenlemeyi yapalım.

DbContext sınıfımızı (ApplicationDbContext.cs) IdentityDbContext<TUser> sınıfından miras aldırıyoruz.

Identity yapısının kullanacağı DbSet‘ler ve diğer özellikler IdentityDbContext sınıfının miras alınmasıyla beraber ApplicationDbContext sınıfına da gelmektedir. Dolayısıyla Code First yaklaşımıyla veri tabanımızı güncellediğimizde Identity yapısı için gerekli olan ve kullanıcı bilgilerininde saklanacağı tablolar otomatik olarak oluşturulmuş olacaktır.

Veri tabanında oluşturulacak tabloların arasındaki ilişkilerinde oluşturulabilmesi için OnModelCreating(ModelBuilder modelBuilder) metodu içerisine base.OnModelCreating(modelBuilder); satırını da ekliyoruz. Böylece IdentityDbContext ile birlikte gelen mapping işlemleri de gerçekleştirilmiş oluyor.

Migration’ı ekleyelim;

ve oluşturulacak tablolara bir göz atalım..

Eklediğimiz migration ile AspNetUsers, AspNetRoles, AspNetUserTokens, AspNetUserRoles, AspNetUserLogins, AspNetUserClaims, AspNetRoleClaims tabloları (veri tabanı güncelleme işlemi ile) oluşturulacaktır.

Veri tabanı güncelleştirme işlemini gerçekleştirelim;

Oluşmuş olan bu tablolarda; kullanıcı bilgileri (AspNetUsers), rol bilgileri (AspNetRoles), kullanıcı token’ları (AspNetUserTokens), kullanıcı girişleri (AspNetUserLogins), kullanıcı talep-izin bilgileri (AspNetUserClaims), rol talep-izin bilgileri (AspNetRoleClaims) ve kullanıcı rolleri (AspNetUserRoles) tutulmaktadır.

Tablolar oluştuğuna göre; kullanıcı kayıt (Register) sayfasından devam edebiliriz.  Kullanıcı ile ilgili işlemlerin tümünü AccountController.cs sınıfı üzerinden yöneteceğiz.

AccountController.cs sınıfına Register isminde ilk action metodunu ekledikten sonra ~/Views/Account/ klasörü altına Register.cshtml sayfamızı da ekliyoruz.

Kayıt esnasında kullanıcılardan Kullanıcı adı (UserName), e-posta adresi (Email) , şifre (Password-ConfirmPassword), yaş (Age) ve konum (Location) bilgilerini alacağız. Aldığımız bu bilgileri UserModelForRegister.cs sınıfı ile Register (HttpPost)  metoduna aktaracağız.

Bilgileri post edeceğimiz Register metoduna geçmeden önce Register sayfasının son haline bir bakalım.

Sayfamız hazır olduğuna göre kullanıcı bilgilerini post edeceğimiz Register metoduna geçebiliriz.

Öncelikle; kullanıcı ile ilgili işlemleri yöneteceğimiz UserManager sınıfını dependency injection yöntemiyle AccountController‘a dahil ediyoruz.

Sonrasında; kullanıcı kayıt işlemini gerçekleştirecek olan [HttpPost] Register metodunu AccountController sınıfına ekliyoruz.

Register metodu içerisinde; ilk olarak girilen kullanıcı adının başka biri tarafından kullanılıp kullanılmadığını kontrol ediyoruz. UserManager sınıfındaki FindByNameAsync() metodu; parametre olarak kullanıcı adı (userName) değerini alır. Eğer parametre olarak verdiğimiz kullanıcı adına ait başka bir kayıt varsa o kayıdı bize geri döner. Kayıt yok ise işlem sonucu null olarak geri dönecektir, ve yeni kullanıcı oluşturma işlemine geçilebilir.

Yeni kullanıcıyı oluşturmak için UserManager sınıfındaki CreateAsync() metodunu kullanıyoruz. Metot, kullanıcı modeli (ApplicationUser) ve şifre (password) olmak üzere 2 parametre almaktadır. Bu metot asenkron şekilde çalışmaktadır. Metodu senkron hale getirdiğimizde bize IdentityResult nesnesi döndürecektir.

Kullanıcı oluşturma işlemi başarılı bir şekilde tamamlanırsa; IdentityResult nesnesindeki Succeed değeri true olarak gelecektir.
Eğer işlem başarısız olursa; Succeed değeri false olur ve başarısızlığa sebep olan hataları Errors değerinden alabiliriz.

Register sayfasını hazırladık, fakat Identity ara katmanını projeye dahil etmedik..
Bunun için Startup.cs dosyasına geliyoruz ve ConfigureServices(IServiceCollection services) metodunu yeniden düzenliyoruz.

services nesnesine dahil ettiğimiz AddIdentity<ApplicationUser, IdentityRole>() metodu; varsayılan ayarlara sahip, ve kullanıcı model sınıfı ApplicationUser olan Identity ara katmanını projeye dahil etmektedir. Devamında gelen AddEntityFrameworkStores<ApplicationDbContext>() metodu da; dahil ettiğimiz Identity ara katmanındaki kullanıcı bilgilerini yönetirken hangi DbContext sınıfının kullanılması gerektiğini belirtmektedir.

Örnek projemizdeki yemek tarif bilgileri ve kullanıcı bilgileri; aynı veri tabanından beslenmekte ve aynı DbContext sınıfı üzerinden yönetilmektedir.

Peki; kullanıcı bilgilerini yönetirken başka bir DbContext sınıfı kullanmamız mümkün müdür? Elbetteki mümkündür.  Bunun için kullanılacak DbContext sınıfı;

  1. IdentityDbContext<KullanıcıModeli> sınıfını miras almalıdır.
  2. Kullanacağınız DbContext sınıfı AddIdentity() metodundan önce AddDbContext() metodu ile projeye dahil edilmelidir.
  3. KullanıcıModeli olarak kullanacağınız sınıf,  AddIdentity<KullanıcıModeli, IdentityRole>() metodu içerisinde kullandığınız sınıf ile aynı olmalıdır.
  4. AddIdentity() metodunun devamında AddEntityFrameworkStores() ile örnek projemizde olduğu gibi belirtilmelidir.

Biz, örnek projemizde tek DbContext sınıfı ile yolumuza devam edeceğiz.

Startup.cs dosyasında kısa bir düzenleme daha yaparak web uygulamamıza; bir yetkilendirme mekanizması kullanacağını belirtelim.

Configure(IApplicationBuilder app, IHostingEnvironment env) metodu içerisinde kullandığımız UseAuthentication() metodu; web uygulamamızın, eklediğimiz Identity ara katmanını yetkilendirme için kullanmasını sağlamaktadır.

Şimdi projemizi çalıştıralım ve Register sayfasındaki bilgileri doldurup yeni bir kullanıcı oluşturmayı deneyelim.
Ben kendi kullanıcımı oluşturmak için formdaki tüm bilgileri doldurdum, şifremi 123456 olarak belirlemek istedim.

Ve sonuc:

Aldığımız 3 hata bize Register içerisinde kullandığımız CreateAsync() metodu tarafından döndürülmektedir. Bu 3 hata mesajında, belirtilen şifrenin (bizim şifre 123456 idi);

  • En az 1 adet alfanümerik olmayan karakter ( .?*_ gibi..) içermesi gerektiği,
  • En az 1 adet küçük harf içermesi gerektiği,
  • En az 1 adet büyük harf içermesi gerektiği belirtilmektedir.

Şifre ile ilgili karşımıza çıkan bu kısıtlamalar AddIdentity() metodu ile eklediğimiz Identity ara katmanının varsayılan ayarlarından kaynaklanmaktadır. Startup.cs dosyasına dönelim ve şifre ile ilgili ayarları düzenleyelim.

Şifre ile ilgili kısıtlamalar; Identitiy ara katmanı içerisindeki IdentityOptions sınıfının Password özelliği (PasswordOptions sınıfını referans alır) üzerinden yönetilmektedir.

PasswordOptions sınıfı içerisindeki;

  • RequiredDigit özelliği; şifre içerisinde en az 1 adet rakam zorunluluğu olmasını,
  • RequiredLowercase özelliği; şifre içerisinde en az 1 adet küçük harf zorunluluğu olmasını,
  • RequiredNonAlphanumeric özelliği; şifre içerisinde en az 1 adet alfanümerik karakter zorunluluğu olmasını,
  • RequiredUppercase özelliği; şifre içerisinde en az 1 adet büyük harf zorunluluğu olmasını

sağlamaktadır. Saydığımız bu özellikler varsayılan olarak true değerinde gelmektedir. services.Configure<IdentityOptions>() metodu içerisinde bunları false olarak değiştiriyoruz. Bunlardan başka 2 adet daha şifre ile ilgili kısıtlama söz konusudur.

  • RequiredLength özelliği; şifrenin sahip olması gereken minimum karakter sayısını belirler. Varsayılan değeri 6’dır.
  • RequiredUniqueChars özelliği; şifrede, farklı olması gereken minimum karakter sayısını belirler. Varsayılan değeri 1’dir. (Eğer, bu özelliğin değeri 2 olmuş olsaydı; şifre içerisinde en az 2 farklı karakter olması zorunlu olurdu. Yani şifre kullanıcılar şifrelerini 111111 yada aaaaaa şeklinde belirtmeleri durumunda sistem bunu kabul etmeyecek, kullanıcılar; 1 veya a karakterinden farklı en az 1 karakteri daha şifrelerinde kullanmak zorunda olacaklardı.)

Bu şekilde PasswordOptions özelliklerini düzenleyerek; kullanıcılarınızın şifre güvenliklerini biraz daha artırabilirsiniz.

Startup.cs dosyasını düzenledikten sonra projeyi yeniden çalıştırıp ilk kullanıcımızı oluşturmayı deneyelim. (Eğer herşey yolunda giderse; /Home/Tarifler sayfasına yönlendirileceğiz.)

Herşey yolunda giderse /Home/Tarifler sayfasına yönlendirileceğimizi söylemiştik, öyle de oldu. Fakat Tarifler sayfasını kısıtlandırmış, sadece kullanıcı girişi yapanların görebileceği hale getirmiştik. Dolayısıyla sistem; giriş yapmamız için bizi /Account/Login sayfasına yönlendirmeye çalıştı, ve biz henüz Login sayfasını olutşturmadık 🙂

Peki nerden biliyor /Account/Login sayfasından kullanıcı girişi yapılacağını? Tabiiki de varsayılan ayarlardan 🙂

Şimdi biz kendi Login sayfamızı oluşturalım ve gerekli ayarları sisteme belirtelim.

Login.cshml sayfamızı ~/Views/Account/ altına oluşturduktan sonra UserModelForLogin.cs sınıfımızı da oluşturalım.

Artık Login işlemi için gerekli action’ları yazabiliriz. Ama öncesinde; AccountController.cs sınıfının yapıcı (constructor) metodunu düzenleyelim,

ve Login action’ları ;

Giriş yapan bir kullanıcı, oturum süresi dolana kadar tekrar Login sayfasına gelirse; zaten oturumu açık olacağıdan returnUrl değerinde belirtilen sayfaya,  yada Anasayfa‘ya yönlendirilmektedir.

SignInManager; kullanıcıların oturum işlemlerinin yönetildiği sınıftır. İlk olarak [HttpPost] Login action’ı içerisinde SignInManager sınıfına ait olan CheckPasswordSignInAsycn() metodunu kullanmaktayız. Metot; kullanıcı nesnesi (ApplicationUser), şifre (string) ve hatalı şifre denemelerini kısıtlamak amacıyla true yada false değerlerini parametre olarak almaktadır.

Kullanıcı kayıt işlemi gerçekleştirildiğinde; kullanıcı şifreleri, veri tabanına hash’lenmiş bir şekilde kaydedilmektedir. CheckPasswordSignInAsync() metodu; FindByNameAsycnc() ile veri tabanından getirdiğimiz user nesnesindeki hash’lenmiş şifreyi, login olurken girilen şifrenin hash’lenmiş haliyle karşılaştırır ve bize sonuç olarak SignInResult nesnesi döndürür.

Girilen şifrenin yanlış olması durumunda SignInResult nesnesindeki Succeed değeri false olacaktır.

Oturum Kilitlenme (Lockout) Durumu :
Eğer CheckPasswordSignInAsync() metodunun 3. paramesi olan lockoutOnFailure değerini true olarak belirtilirse; kullanıcının arka arkaya hatalı şifre girişi (varsayılan hatalı giriş limiti: 5) yapması durumunda, kullanıcının oturum açma işlemini belirli bir süre (varsayılan süre: 5 dk) askıya alacaktır.  Böyle bir durumda; geri dönen SignInResult nesnesindeki Succeed değerinin false olmasının yanı sıra IsLockedOut değeri de true olacaktır.

Lockout varsayılan değerlerini Startup.cs dosyasından düzenleyebiliyoruz.

  • MaxFailedAccessAttempts değeri; kullanıcıların arka arkaya maksimum kaç hatalı deneme yapabileceklerin,
  • DefaultLockoutTimeSpan değeri; oturumun ne kadar süre kitleneceğini,
  • AllowedForNewUsers değeri de Lockout olayının yeni kayıt yapan kullanıcılar için geçerli olup olmayacağını belirler. Eğer AllowedForNewUsers değerini false yaparsanız, yeni kayıt olan kullanıcılar istediklerini kadar hatalı şifre denemesi yapabilirler. Sistemimizin güvenliği için bu değeri true olarak bırakmak en mantıklısı olacaktır. 🙂

Login sayfasını tamamladıktan sonra Logout işlemine geçelim.

Logout action’ını oluşturduktan sonra Çıkış butonunu da üst menüye ekliyoruz.

_Layout.cshtml dosyasında gerekli düzenlemeyi yaptıktan sonra; oturum açıldığında üst menüde Çıkış butonu görünecektir.

Çıkış butonuna tıklandığında; Logout action’ı içerisinde bulunan ve SignInManager sınıfına ait olan SignOutAsync() metodu çalıştırılacak ve kullanıcı oturumu kapatılacaktır. Sonrasında da kullanıcı, anasayfa’ya yönlendirilecektir.

Logout kısmını da tamamladıktan sonra ForgotPassword sayfasına geçelim.

ForgotPassword.cshtml:

ForgotPassword action’ları;

UserManager sınıfına ait olan GeneratePasswordResetTokenAsync() metodu, şifre sıfırlama yapabilmemiz için bize bir token üretilmektedir. Üretilen token ile şifre sıfırlama linki oluşturulur ve genelde bu şifre sıfırlama linki mail olarak kullanıcıya gönderilir.

Ben bu örnek projede, şifre sıfırlama linkini boş bir sayfaya basacağım.

Projemizi çalıştırım son duruma bir göz atalım;

Token oluşturma işlemleri için; Identity katmanı, token provider‘lara ihtiyaç duymaktadır.

Identity ara katmanını eklediğimiz kısmı yukarıdaki gibi düzenliyoruz. DefaultTokenProviders() metodu ile varsayılan token provider‘ların Identity ara katmanına dahil edilmesini sağlıyoruz.

Düzenlemeleri yaptıktan sonra ForgotPassword sayfasından şifre sıfırlama linkini alabiliriz. Oluşturulan sıfırlama linki bizi ResetPassword sayfasına yönlendirecektir. Yeri gelmişken, ResetPassword sayfasını da oluşturalım.

ResetPassword.cshtml:

ResetPassword action’ları;

ForgotPassword sayfasında şifre sıfırlama linkini oluştururken;

  • uid parametresinde user id değeri,
  • token parametresinde de şifre sıfırlama için gerekli token değeri

linke eklenmiştri.

Şifre sıfırlama bağlantısı ile beraber ResetPassword sayfasına user id (uid) ve token bilgileri de iletilmektedir. Sayfa açıldığında; FindByIdAsync() metodu kullanılarak uid parametresinde gelen user id değerine göre kullanıcı olup olmadığı kontrol ediliyor. Eğer kullanıcı yok ise, Register sayfasına yönlendirme yapılıyor.

ResetPassword sayfası post edildiğinde ise post action’ı içerisindeki ResetPasswordAsync() metodu ile; user nesnesi, token değeri ve yeni şifre değeri (userModelForResetPassword.Password) kullanılarak şifre yenileniyor.

Son olarakta; ChangePassword sayfasını oluşturalım ve şifre değiştime işleminin nasıl yapıldığına bakalım.

ChangePassword.cshtml;

ve ChangePassword action’ları;

Oturumu açık olan bir kullanıcının; kullanıcı adına User.Identity.Name değerinden erişebiliyoruz. UserManager sınıfındaki ChangePasswordAsync() metodu ise kullanıcının user nesnesini, mevcut şifresini ve yeni şifresini alarak şifrenin değiştirilmesini sağlıyor.

 

Uzuuuuun bir yazının daha böylece sonuna geliyoruz.. (çok şükür)

Projenin son haline buradan ulaşabilirsiniz.

Başka bir yazıda görüşmek üzere.

You may also like...

Bir Cevap Yazın

This site uses Akismet to reduce spam. Learn how your comment data is processed.