ASP.NET MVC 5 如何客製化驗證與授權並實作帳號登入機制 (How to custom ASP.NET MVC Auth Filters & Login / Logoff systems)
2020-06-21
夏至的這天上午先去爬山,正午時分不一會就滿身大汗了,果然還是秋天的午後才是最適合的爬山時機。而最近寫程式的機會越來越多,不管是主管刻意的安排或者自己興趣的傾向,總之寫程式在工作中是越來越重要且主要的一部份了。自從有寫筆記的習慣之後,對於學習陳果可以被視覺化的呈現,同時也可以量化,這種看得見累積的感覺讓人更加有動力去學習。
最近深有所感,只有經驗過、練習過的才能真正在開發的時候發揮作用,並且沉入心流的開發體驗。驗證與授權一直是寫每個系統的起手式,特此跟隨一篇教學文章,並整理成筆記。
範例是採 CodeFirst 的方式進行,且使用帳號密碼的方式管理驗證。
Step By Step
新建專案
ASP.NET MVC
.NET Framework : 4.52
驗證方式: 無驗證
套件: MVC
安裝套件
EntityFramework
Codefirst - 設計 Models (User, Role)
/Models/Role.cs
namespace Aspnet_MVC_AuthFilter_POC.Models
{
public class Role
{
public int Id { get; set; }
public string Name { get; set; }
}
}
/Models/User.cs
using System.ComponentModel.DataAnnotations;
namespace Aspnet_MVC_AuthFilter_POC.Models
{
public class User
{
public int Id { get; set; }
[Display(Name = "使用者名稱")]
public string UserId { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public int RoleId { get; set; }
}
}
Codefirst - 加入 DBContext 與 ConnectionStrings
/Models/SqlDbContext.cs
using System.Data.Entity;
namespace Aspnet_MVC_AuthFilter_POC.Models
{
public class SqlDbContext : DbContext
{
public SqlDbContext() : base("name=SqlConnection"){}
public DbSet<User> Users { get; set; }
public DbSet<Role> Roles { get; set; }
}
}
/web.config
<connectionStrings>
<add name="SqlConnection"
connectionString="Data Source=(localdb)\MSSQLLocalDB; Initial Catalog=MvcAuthPOC; Integrated Security=SSPI"
providerName="System.Data.SqlClient"
/>
</connectionStrings>
Codefirst - Enable Migrations
使用 Nuget Package Manager (套件管理器主控台)
enable-migrations
執行完
add-migration Initial
執行完
/Migrations/_Initial.cs
namespace Aspnet_MVC_AuthFilter_POC.Migrations
{
using System;
using System.Data.Entity.Migrations;
public partial class Initial : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.Roles",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
})
.PrimaryKey(t => t.Id);
CreateTable(
"dbo.Users",
c => new
{
Id = c.Int(nullable: false, identity: true),
UserId = c.String(),
UserName = c.String(),
Password = c.String(),
RoleId = c.Int(nullable: false),
})
.PrimaryKey(t => t.Id);
Sql("Insert into Roles (Name) Values ('SystemAdmin')");
Sql("Insert into Roles (Name) Values ('Admin')");
Sql("Insert into Roles (Name) Values ('User')");
Sql("Insert into Users (UserId,UserName,Password,RoleId) Values ('acc1','Ryan System Admin','pwd',1)");
Sql("Insert into Users (UserId,UserName,Password,RoleId) Values ('acc2','Josh Admin','pwd',2)");
Sql("Insert into Users (UserId,UserName,Password,RoleId) Values ('acc3','Kevin User','pwd',3)");
}
public override void Down()
{
DropTable("dbo.Users");
DropTable("dbo.Roles");
}
}
}
執行完
update-database
加入 AccountController
/Controller/AccountController.cs
using Aspnet_MVC_AuthFilter_POC.Models;
using System.Linq;
using System.Web.Mvc;
namespace Aspnet_MVC_AuthFilter_POC.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult Login()
{
return View();
}
[HttpPost]
public ActionResult Login(User model)
{
if (ModelState.IsValid)
{
using (var context = new SqlDbContext())
{
User user = context.Users
.Where(u => u.UserId == model.UserId && u.Password == model.Password)
.FirstOrDefault();
if (user != null)
{
Session["UserName"] = user.UserName;
Session["UserId"] = user.UserId;
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", "Invalid User Name or Password");
return View(model);
}
}
}
else
{
return View(model);
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
Session["UserName"] = string.Empty;
Session["UserId"] = string.Empty;
return RedirectToAction("Index", "Home");
}
}
}
客製化 Authentication Filter
這個部分也是整個教學文章的核心所在,如何客製驗證、授權的機制,則系統的開發就更能隨心所欲的設計,而使用 Filters 的方式同時也讓專案易於擴充與維護。
加入 Filters Folder
/Filters/CustomAuthenticationFilter.cs
using System;
using System.Web.Mvc;
using System.Web.Mvc.Filters;
using System.Web.Routing;
namespace Aspnet_MVC_AuthFilter_POC.Filters
{
public class CustomAuthenticationFilter : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
if (string.IsNullOrEmpty(Convert.ToString(filterContext.HttpContext.Session["UserName"])))
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
if (filterContext.Result == null || filterContext.Result is HttpUnauthorizedResult)
{
//Redirecting the user to the Login View of Account Controller
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "controller", "Account" },
{ "action", "Login" }
});
}
}
}
}
客製化 Authorize Attribute
/Filters/CustomAuthorizeAttribute.cs
using Aspnet_MVC_AuthFilter_POC.Models;
using System;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace Aspnet_MVC_AuthFilter_POC.Filters
{
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
private readonly string[] allowedroles;
public CustomAuthorizeAttribute(params string[] roles)
{
this.allowedroles = roles;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
bool authorize = false;
var userId = Convert.ToString(httpContext.Session["UserId"]);
if (!string.IsNullOrEmpty(userId))
using (var context = new SqlDbContext())
{
var userRole = (from u in context.Users
join r in context.Roles on u.RoleId equals r.Id
where u.UserId == userId
select new
{
r.Name
}).FirstOrDefault();
foreach (var role in allowedroles)
{
if (role == userRole.Name) return true;
}
}
return authorize;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "controller", "Home" },
{ "action", "UnAuthorized" }
});
}
}
}
將 Filter 加入 HomeController
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Aspnet_MVC_AuthFilter_POC.Filters;
namespace Aspnet_MVC_AuthFilter_POC.Controllers
{
[CustomAuthenticationFilter]
public class HomeController : Controller
{
[CustomAuthorize("User", "SystemAdmin")]
public ActionResult Index()
{
...
}
[CustomAuthorize("Admin", "SystemAdmin")]
public ActionResult About()
{
...
}
[CustomAuthorize("SystemAdmin")]
public ActionResult Contact()
{
...
}
public ActionResult UnAuthorized()
{
...
}
}
}
實作 _LoginPartial 顯示登入資訊及登出按鈕
/Views/_LoginPartial.cshtml
@if (!string.IsNullOrEmpty(Convert.ToString(Session["UserName"])))
{
using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))
{
@Html.AntiForgeryToken()
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Welcome : @Session["UserName"]</a></li>
<li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>
</ul>
}
}
else
{
<ul class="nav navbar-nav navbar-right">
<li>@Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
</ul>
}
加入到 /Views/Shared/_Layout.cshtml
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>@Html.ActionLink("首頁", "Index", "Home")</li>
<li>@Html.ActionLink("關於", "About", "Home")</li>
<li>@Html.ActionLink("連絡人", "Contact", "Home")</li>
</ul>
@Html.Partial("_LoginPartial")
</div>
小結
未來可以再根據平時開發的需求,改採 Windows 驗證搭配 Filter Attribute,同時改採 SQLite 的方式儲存使用者資訊及權限。
目前應用的範例 Custom Authorization Fitler
/Filters/AuthorizeIdentityAttribute.cs
public class AuthorizeIdentityAttribute : AuthorizeAttribute
{
private List<string> _permissionLists;
private string _authGroupName;
public AuthorizeIdentityAttribute(string authGroup = "default")
{
_authGroupName = authGroup;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
this._permissionLists = EmpAuthService.GetAuthEmpListByAuthGroupName(_authGroupName);
if (!AuthorizeCore(filterContext.HttpContext))
{
base.HandleUnauthorizedRequest(filterContext);
}
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var loginUser = httpContext.User.Identity.Name;
if (this._permissionLists.Any(i => i == loginUser))
{
return true;
}
return false;
}
}
/Controllers/HomeController.cs
[AuthorizeIdentity("default")] /* 所有 Controller 下的 Action 都受影響 */
public class HomeController : Controller
{
public ActionResult Index()
{
...
}
[OverrideAuthorization] /* 不受 AuthorizeIdentity 影響 */
public ActionResult Details()
{
...
}
}