.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.

        [Authorize]
        public IActionResult Tarifler()
        {
            var yemekTarifleriListesi = context.YemekTarifleri.ToList() ?? new List<YemekTarifi>();

            return View(yemekTarifleriListesi);
        }

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.

    public class ApplicationUser : IdentityUser
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
        public string Location { get; set; }
    }

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.

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions options) : base(options)
        {
        }

        public DbSet<YemekTarifi> YemekTarifleri { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            
            modelBuilder.Entity<YemekTarifi>().ToTable("YemekTarifleri", "dbo");
        }
    }

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;

dotnet ef migrations add InheritedIdentityDbContext

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;

dotnet ef database update

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.

    public class AccountController : Controller
    {
        public IActionResult Register()
        {
            return View();
        }
    }

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.

@model UserModelForRegister
@{
    ViewData["Title"] = "Üye Ol";
}
<div class="row justify-content-center">
  <form asp-action="Register" method="post" class="col-8">
    <h1 style="text-align: center">Üye Olun</h1>
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <div class="form-group">
      <div class="row">
        <div class="col-6">
          <input type="email" class="form-control" asp-for="Email" placeholder="Email adresiniz" />
          <span asp-validation-for="Email" class="text-danger"></span>
        </div>
        <div class="col-6">
          <input type="text" class="form-control" asp-for="UserName" placeholder="Kullanıcı adınız." />
          <span asp-validation-for="UserName" class="text-danger"></span>
        </div>
      </div>
    </div>
    <div class="form-group">
      <div class="row">
        <div class="col-6">
          <input type="text" class="form-control" asp-for="FirstName" placeholder="Adınız" />
          <span asp-validation-for="FirstName" class="text-danger"></span>          
        </div>
        <div class="col-6">
          <input type="text" class="form-control" asp-for="LastName" placeholder="Soyadınız" />
          <span asp-validation-for="LastName" class="text-danger"></span>
        </div>
      </div>
    </div>
    <div class="form-group">
      <div class="row">
        <div class="col-6">
          <input type="password" class="form-control" asp-for="Password" placeholder="Şifre" />      
          <span asp-validation-for="Password" class="text-danger"></span>          
        </div>
        <div class="col-6">
          <input type="password" class="form-control" asp-for="ConfirmPassword" placeholder="Şifre (Tekrar)" />      
          <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
        </div>
      </div>
    </div>
    <div class="form-group">
      <div class="row">
        <div class="col-6">
          <input type="number" class="form-control" asp-for="Age" placeholder="Yaşınız" min="0" />      
          <span asp-validation-for="Age" class="text-danger"></span>          
        </div>
        <div class="col-6">
          <input type="text" class="form-control" asp-for="Location" placeholder="Yaşadığınız yer" />      
          <span asp-validation-for="Location" class="text-danger"></span>
        </div>
      </div>
    </div>
    <button type="submit" class="btn btn-primary">Gönder</button>
  </form>
</div>

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.

    public class UserModelForRegister
    {
        [Required(ErrorMessage = "E-posta adresi belirtmelisiniz")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Şifre belirtmelisiniz")]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        [Compare("Password",ErrorMessage = "Şifreler uyuşmuyor")]
        [DataType(DataType.Password)]
        public string ConfirmPassword { get; set; }

        [Required(ErrorMessage = "Kullanıcı adı belirtmelisiniz")]
        public string UserName { get; set; }

        [Required(ErrorMessage = "Ad belirtmelisiniz")]
        public string FirstName { get; set; }

        [Required(ErrorMessage = "Soyad belirtmelisiniz")]
        public string LastName { get; set; }

        [Required(ErrorMessage = "Yaş belirtmelisiniz")]
        [Range(1,int.MaxValue,ErrorMessage="Yaşınız 0'dan büyük olmalı")]
        public int? Age { get; set; }

        [Required(ErrorMessage = "Konum belirtmelisiniz")]
        public string Location { get; set; }
    }

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.

        private readonly UserManager<ApplicationUser> userManager;

        public AccountController(UserManager<ApplicationUser> userManager)
        {
            this.userManager = userManager;
        }

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

        [HttpPost]
        public async Task<IActionResult> Register(UserModelForRegister userModelForRegister)
        {
            if (ModelState.IsValid)
            {
                if (await UserExists(userModelForRegister.UserName))
                {
                    ModelState.AddModelError("", "Kullanıcı adı zaten var.");
                    return View(userModelForRegister);
                }

                var user = new ApplicationUser
                {
                    UserName = userModelForRegister.UserName,
                    Email = userModelForRegister.Email,
                    FirstName = userModelForRegister.FirstName,
                    LastName = userModelForRegister.LastName,
                    Age = (int)userModelForRegister.Age,
                    Location = userModelForRegister.Location
                };

                var result = await userManager.CreateAsync(user, userModelForRegister.Password);

                if (result.Succeeded)
                {
                    return RedirectToAction("Tarifler", "Home");
                }

                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError("", error.Description);
                }

            }

            return View(userModelForRegister);
        }

        public async Task<bool> UserExists(string userName)
        {
            var user = await userManager.FindByNameAsync(userName);

            return user != null;
        }

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.

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("applicationDbContextConnection")));
            
            // Identity ara katmanı projeye dahil edildi.
            services
                .AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>();

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

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.

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseStaticFiles();

            // Identity ara katmanının web projesi tarafından kullanılması sağlandı.  
            app.UseAuthentication();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

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.

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("applicationDbContextConnection")));

            // Identity ara katmanı projeye dahil edildi.
            services
                .AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>();

            services.Configure<IdentityOptions>(options =>
            {
                options.Password.RequireDigit = false;
                options.Password.RequireLowercase = false;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
            });

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

Ş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.

@model UserModelForLogin
@{
    ViewData["Title"] = "Giriş Yap";
}
<div class="row justify-content-center">
    <form asp-action="Login" method="post" class="col-4">
        <h1 style="text-align: center">Giriş Yapın</h1>
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-group">
            <input type="text" class="form-control" asp-for="UserName" placeholder="Kullanıcı adınız." />
            <span asp-validation-for="UserName" class="text-danger"></span>
        </div>
        <div class="form-group">
            <input type="password" class="form-control" asp-for="Password" placeholder="Şifreniz" />
            <span asp-validation-for="Password" class="text-danger"></span>
        </div>
        <div class="form-group">
            <div class="checkbox">
                <label asp-for="RememberMe">
                    <input type="checkbox" asp-for="RememberMe" /> Beni Hatırla
                </label>
            </div>
        </div>

        <button type="submit" class="btn btn-success">Giriş Yap</button>
        <a asp-action="Register" class="btn btn-primary">Üye Ol</a><br>
        <a asp-action="ForgotPassword">Şifremi Unuttum</a>
    </form>
</div>

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

    public class UserModelForLogin
    {
        [Required(ErrorMessage = "Kullanıcı Adı belirtmelisiniz")]
        public string UserName { get; set; }

        [Required(ErrorMessage= "Şifre belirtmelisiniz")]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        public bool RememberMe { get; set; }
    }

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,

        private readonly UserManager<ApplicationUser> userManager;
        private readonly SignInManager<ApplicationUser> signInManager;

        public AccountController(UserManager<ApplicationUser> userManager,
            SignInManager<ApplicationUser> signInManager)
        {
            this.userManager = userManager;
            this.signInManager = signInManager;
        }

ve Login action’ları ;

        public IActionResult Login(string returnUrl = "/")
        {
            if (User.Identity.IsAuthenticated)
            {
                if (Url.IsLocalUrl(returnUrl))
                    return LocalRedirect(returnUrl);

                return RedirectToAction("Index", "Home");
            }

            ViewData["ReturnUrl"] = returnUrl;

            return View();
        }

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.

        [HttpPost]
        public async Task<IActionResult> Login(UserModelForLogin userModelForLogin, string returnUrl = "/")
        {
            if (ModelState.IsValid)
            {
                var user = await userManager.FindByNameAsync(userModelForLogin.UserName);

                if (user != null)
                {
                    var result = await signInManager
                        .CheckPasswordSignInAsync(user, userModelForLogin.Password, true);

                    if (result.Succeeded)
                    {
                        await signInManager.SignInAsync(user, userModelForLogin.RememberMe);

                        return LocalRedirect(returnUrl);
                    }

                    if (result.IsLockedOut)
                    {
                        ModelState.AddModelError("", "Çok fazla şifre denemesi yaptınız. Lütfen 1 dakika bekleyiniz.");

                        return View(userModelForLogin);
                    }
                }

                ModelState.AddModelError("", "Kullanıcı adı veya Şifre bilgileri yanlış");
            }

            return View(userModelForLogin);
        }

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.

            services.Configure<IdentityOptions>(options =>
            {
                options.Password.RequireDigit = false;
                options.Password.RequireLowercase = false;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;

                // Lockout varsayılan ayarları
                options.Lockout.MaxFailedAccessAttempts = 5;
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
                options.Lockout.AllowedForNewUsers = true;
            });
  • 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.

        public async Task<IActionResult> Logout()
        {
            await signInManager.SignOutAsync();

            return Redirect("/");
        }

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

                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-controller="Home" asp-action="Tarifler">Tarifler</a>
                        </li>
                        @if (User.Identity.IsAuthenticated) {
                        <li class="nav-item">                        
                            <a class="nav-link text-success" asp-controller="Account" asp-action="ChangePassword">Şifre Değiştirme</a>
                        </li>                        
                        <li class="nav-item">                        
                            <a class="btn btn-info" asp-controller="Account" asp-action="Logout">Çıkış</a>
                        </li>
                        } else {
                        <li class="nav-item">                        
                            <a class="nav-link text-danger" asp-controller="Account" asp-action="Login">Üye Girişi</a>
                        </li>
                       } 
                    </ul>

_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:

@{
    ViewData["Title"] = "Şifremi Unuttum";
}
<div class="row justify-content-center">
  <form asp-action="ForgotPassword" method="post" class="col-4">
    <h1 style="text-align: center">Şifremi Unuttum</h1>
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <div class="form-group">
      <input type="text" class="form-control" id="UserName" name="UserName" placeholder="Kullanıcı adınız" required />
    </div>
    <button type="submit" class="btn btn-primary">Gönder</button>
  </form>
</div>

ForgotPassword action’ları;

        public IActionResult ForgotPassword()
        {
            return View();
        }

        [HttpPost]
        public async Task<IActionResult> ForgotPassword(string userName)
        {
            if (string.IsNullOrEmpty(userName))
            {
                ModelState.AddModelError("", "Kullanıcı adı belirtmelisiniz");
            }
            else
            {
                var user = await userManager.FindByNameAsync(userName);

                if (user != null)
                {
                    var resetPasswordToken = await userManager.GeneratePasswordResetTokenAsync(user);

                    var resetPasswordLink = Url.Action("ResetPassword", "Account",
                        new { uid = user.Id, token = resetPasswordToken },
                        protocol: Request.Scheme);

                    // Burada şifre sıfırlama linki Email adresine gönderilebilir.

                    return Ok(resetPasswordLink);
                }

                ModelState.AddModelError("", "Kullanıcı bulunamadı");
            }

            return View();
        }

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.

        public void ConfigureServices(IServiceCollection services)
        {
            ...

            // Identity ara katmanı projeye dahil edildi.
            services
                .AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            ...

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

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.

    public class UserModelForResetPassword
    {
        public string UserId { get; set; }

        [Required(ErrorMessage = "Şifre belirtmelisiniz")]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        [Compare("Password", ErrorMessage = "Şifreler uyuşmuyor")]
        public string ConfirmPassword { get; set; }

        public string Token { get; set; }
    }

ResetPassword.cshtml:

@model UserModelForResetPassword
@{
    ViewData["Title"] = "Şifreyi Yenile";
}
<div class="row justify-content-center">
  <form asp-action="ResetPassword" method="post" class="col-4">
    <h1 style="text-align: center">Şifre Sıfırla</h1>
    <div class="form-group">
      <input type="password" class="form-control" asp-for="Password" placeholder="Şifre" />
      <span asp-validation-for="Password" class="text-danger"></span>
    </div>
    <div class="form-group">
      <input type="password" class="form-control" asp-for="ConfirmPassword" placeholder="Şifre (Tekrar)" />
      <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
    </div>
    <input hidden asp-for="UserId" /> 
    <input hidden asp-for="Token" />  
    <button type="submit" class="btn btn-primary">Gönder</button>
  </form>
</div>

ResetPassword action’ları;

        public async Task<IActionResult> ResetPassword(string uid, string token)
        {
            if (string.IsNullOrEmpty(uid) || string.IsNullOrEmpty(token))
                return RedirectToAction("Index");

            var user = await userManager.FindByIdAsync(uid);

            if (user == null)
            {
                return RedirectToAction("Register");
            }

            var model = new UserModelForResetPassword
            {
                UserId = uid,
                Token = token
            };

            return View(model);
        }

        [HttpPost]
        public async Task<IActionResult> ResetPassword(UserModelForResetPassword userModelForResetPassword)
        {
            if (ModelState.IsValid)
            {
                var user = await userManager.FindByIdAsync(userModelForResetPassword.UserId);

                if (user == null)
                {
                    return RedirectToAction("Register");
                }

                var result = await userManager.ResetPasswordAsync(user,
                    userModelForResetPassword.Token,
                    userModelForResetPassword.Password);

                if (result.Succeeded)
                {
                    return RedirectToAction("Login");
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError("", error.Description);
                }
            }

            return View(userModelForResetPassword);
        }

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.

    public class UserModelForChangePassword
    {
        [Required(ErrorMessage = "Eski şifrenizi belirtmelisiniz")]
        [DataType(DataType.Password)]
        public string OldPassword { get; set; }

        [Required(ErrorMessage = "Yeni şifrenizi belirtmelisiniz")]
        [DataType(DataType.Password)]
        public string NewPassword { get; set; }

        [Compare("NewPassword", ErrorMessage = "Yeni şifreler uyuşmuyor")]
        public string ConfirmNewPassword { get; set; }

    }

ChangePassword.cshtml;

@model UserModelForChangePassword
@{
    ViewData["Title"] = "Şifreyi Değiştir";

    bool passwordIsChanged = ViewData.ContainsKey("Succeed");
    ViewData.Remove("Succeed");
}
<div class="row justify-content-center">
  <form asp-action="ChangePassword" method="post" class="col-4">
    <h1 style="text-align: center">Şifre Değiştir</h1>
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    @if(passwordIsChanged) {
    <div class="alert alert-success" role="alert">
        Şifreniz değiştirildi.
    </div>  
    }
    <div class="form-group">
      <input type="password" class="form-control" asp-for="OldPassword" placeholder="Eski şifreniz" />
      <span asp-validation-for="OldPassword" class="text-danger"></span>
    </div>    
    <div class="form-group">
      <input type="password" class="form-control" asp-for="NewPassword" placeholder="Yeni şifreniz" />
      <span asp-validation-for="NewPassword" class="text-danger"></span>
    </div>
    <div class="form-group">
      <input type="password" class="form-control" asp-for="ConfirmNewPassword" placeholder="Yeni şifre (Tekrar)" />
      <span asp-validation-for="ConfirmNewPassword" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary">Gönder</button>
  </form>
</div>

ve ChangePassword action’ları;

        [Authorize]
        public IActionResult ChangePassword()
        {
            return View();
        }

        [Authorize]
        [HttpPost]
        public async Task<IActionResult> ChangePassword(UserModelForChangePassword userModelForChangePassword)
        {
            if (ModelState.IsValid)
            {
                var userName = User.Identity.Name;

                var user = await userManager.FindByNameAsync(userName);

                if (user != null)
                {
                    var result = await userManager
                        .ChangePasswordAsync(user,
                            userModelForChangePassword.OldPassword,
                            userModelForChangePassword.NewPassword);

                    if (result.Succeeded)
                    {
                        ViewData["Succeed"] = true;
                    }

                    foreach (var error in result.Errors)
                    {
                        ModelState.AddModelError("", error.Description);
                    }
                }
            }

            return View();
        }

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.

Last updated