[ASP.NET Core] 加上簡單的Cookie登入驗證

加上簡單的登入驗證 For MVC

環境

  • Rider 產生的.NetCore MVC的範本專案
  • .NetCore 3.1

1.準備了登入用的View、Action、ViewModel

// MemberController
using EasyIdentitySample.ViewModel;
using Microsoft.AspNetCore.Mvc;

namespace EasyIdentitySample.Controllers
{
    public class MemberController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult Index(MemberViewModel request)
        {
            if (request.Account == "Test" && request.Password == "123")
            {
                // TODO SignIn
                return RedirectToAction("Index", "Home");
            }
            return View();
        }

        [HttpPost]
        public IActionResult SignOut()
        {
            // TODO SignOut
            return RedirectToAction("Index");
        }
    }
}
// MemberViewModel
namespace EasyIdentitySample.ViewModel
{
    public class MemberViewModel
    {
        public string Account { get; set; }

        public string Password { get; set; }
    }
}
<!--Index.cshtml-->
@model EasyIdentitySample.ViewModel.MemberViewModel

@{
    ViewBag.Title = "title";
    Layout = "_Layout";
}

<h2>Login</h2>
<form asp-action="Index" asp-controller="Member" method="post">
    <label asp-for="Account">
        <input asp-for="Account">
    </label>
    <br/>
    <label asp-for="Password">
        <input asp-for="Password">
    </label>
    <br>
    <button>Submit</button>
</form>

2.調整Startup,加上Authentication

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            services.AddAuthentication(options =>
                    {
                        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    })
                    .AddCookie();

            services.AddHttpContextAccessor();
        }
// Configure
app.UseAuthentication();

4.在登入的Action,預計會使用 HttpContext.SignInAsync()來做登入,這邊先把會用到的參數都展開出來

  • 會使用 IHttpContextAccessor 來訪問HttpContext(需要在Startup加上 services.AddHttpContextAccessor())
        [HttpPost]
        public async Task<IActionResult> Index(MemberViewModel request)
        {
            if (request.Account == "Test" && request.Password == "123")
            {
                var claims = new List<Claim>();
                var claimsIdentity = new ClaimsIdentity(claims);
                var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                await _accessor.HttpContext.SignInAsync(claimsPrincipal);
                
                return RedirectToAction("Index", "Home");
            }
            return View();
        }

5.接著調整下登入Action,使用 GenericIdentity 作為身份驗證的物件

        [HttpPost]
        public async Task<IActionResult> Index(MemberViewModel request)
        {
            if (request.Account == "Test" && request.Password == "123")
            {
                var claimsIdentity = new GenericIdentity(ClaimTypes.Name,request.Account);
                var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                await _accessor.HttpContext.SignInAsync(claimsPrincipal);
                
                return RedirectToAction("Index", "Home");
            }
            return View();
        }

6.在其他Action加上登入檢查

        public IActionResult Index()
        {
            if (_accessor.HttpContext.User.Identity.IsAuthenticated==false)
            {
                return RedirectToAction("Index", "Member");
            }
            return View();
        }

7.登入後,在 HttpContext.User.Identity.IsAuthenticated 理應就可以拿到 true了

8.接著回到登入Action,這次我們改用 ClaimsIdentity,並且加上一個Role

  • GenericIdentity是繼承於ClaimsIdentity

        [HttpPost]
        public async Task<IActionResult> Index(MemberViewModel request)
        {
            if (request.Account == "Test" && request.Password == "123")
            {
                var claims = new List<Claim>()
                {
                    new Claim(ClaimTypes.Name, request.Account),
                    new Claim(ClaimTypes.Role, "Admin"),
                };
                var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
                var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                await _accessor.HttpContext.SignInAsync(claimsPrincipal);

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

9.再回到其他Action,可以正常地取得登入使用者的Role

        public IActionResult Index()
        {
            if (_accessor.HttpContext.User.Identity.IsAuthenticated == false)
            {
                return RedirectToAction("Index", "Member");
            }
            var isInRole = _accessor.HttpContext.User.IsInRole("Admin");
            return View();
        }

10.接著繼續完成登出的功能,只需要加上一行程式碼

        [HttpPost]
        public async Task<IActionResult> SignOut()
        {
            await _accessor.HttpContext.SignOutAsync();

            return RedirectToAction("Index");
        }

11.這邊為止,登出登入功能也都完成了,接著再調整下程式碼

12.把登入檢查改用 AuthorizeAttribute

  • 未登入時預設會導向/Login
  • 並且加入一個QueryString的參數"ReturnUrl",代表導向登入頁前的網址
        [Authorize]
        public IActionResult Index()
        {
            var isInRole = _accessor.HttpContext.User.IsInRole("Admin");
            return View();
        }

13.登入預設頁面可以在Startup上設定

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            services.AddAuthentication(options =>
                    {
                        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    })
                    .AddCookie(options =>
                    {
                        options.LoginPath = "/Member/Index";
                    });

            services.AddHttpContextAccessor();
        }

14.接著調整下登入Action及View,讓ReturnUrl有作用

  • 多個檢查判斷,確定是Local Url才導向,否則導向固定頁面
// Login
        [HttpPost]
        public async Task<IActionResult> Index(MemberViewModel request,string returnUrl)
        {
            if (request.Account == "Test" && request.Password == "123")
            {
                var claims = new List<Claim>()
                {
                    new Claim(ClaimTypes.Name, request.Account),
                    new Claim(ClaimTypes.Role, "Admin"),
                };
                var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
                var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                await _accessor.HttpContext.SignInAsync(claimsPrincipal);

                if (Url.IsLocalUrl(returnUrl))
                {
                    return Redirect(returnUrl);
                }
                return RedirectToAction("Index", "Home");
            }
            return View();
        }
@model EasyIdentitySample.ViewModel.MemberViewModel

@{
    ViewBag.Title = "title";
    Layout = "_Layout";
    var returnUrl = Context.Request.Query["ReturnUrl"];
}

<h2>Login</h2>
<form asp-action="Index" asp-controller="Member" method="post" asp-route-returnUrl="@returnUrl">
    <label asp-for="Account">
        <input asp-for="Account">
    </label>
    <br/>
    <label asp-for="Password">
        <input asp-for="Password">
    </label>
    <br>
    <button>Submit</button>
</form>

15.這樣若需要檢查登入,僅需要加上Attribute即可,使用者登入後也能直接導向回欲進入的頁面


微軟Docs https://docs.microsoft.com/zh-tw/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1

Sample Code https://github.com/ianChen806/EasyIdentitySample