實務上大多情境並不採用ASP.NET Identity處理身分認證授權,而是選擇使用Form表單認證輕量且易於快速實現自訂邏輯,網路上也有許多教學如何在ASP.NET中使用FormIdentity。
此篇將提供的是在ASP.NET MVC 5 使用IAuthenticationFilter,IAuthorizationFilter實現Form表單認證的方法
啟用Form表單認證
修改Web.config將authentication mode改為Forms表單認證(預設是None)
<system.web>
<!--<authentication mode="None"/>-->
<authentication mode="Forms">
<forms loginUrl="~/Account/Login">
</forms>
</authentication>
</system.web>
註解remove,啟用FormsAuthentication模組(ASP.NET 5 專案範本預設移除的)
<system.webServer>
<modules>
<!--<remove name="FormsAuthentication" />-->
</modules>
</system.webServer>
使用FormIdentity實作登入邏輯
通過自訂邏輯後,將使用者資訊放入FormsAuthenticationTicket
的userData
參數,經由加密並建立cookie加入Response回傳給Client
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid) {
return View();
}
var user = _db.Users
.FirstOrDefault(x => x.Email == model.Email);
if (user == null) {
ModelState.AddModelError("", "請輸入正確的帳號或密碼!");
return View();
}
if (user.Password.Equals(model.Password)) {
//string roles = string.Join(",", user.Roles.Select(x => x.Name).ToArray());
var now = DateTime.Now;
var ticket = new FormsAuthenticationTicket(
version: 1,
name: user.Email,
issueDate: now,
expiration: now.AddMinutes(30),
isPersistent: model.RememberMe,
userData: user.Id.ToString(),//userData:roles,
cookiePath: FormsAuthentication.FormsCookiePath);
var encryptedTicket = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
Response.Cookies.Add(cookie);
return RedirectToAction("Index", "Home");
}
else {
ModelState.AddModelError("", "請輸入正確的帳號或密碼!");
return View();
}
}
IAuthenticationFilter認證過濾器-提供自定義登入認證邏輯
Authentication filter是ASP.NET MVC 5的新過濾器,可以在Action方法或Controller或全域設置至全部Controller的驗證邏輯。
在ASP.NET MVC 5處理管線中會先執行認證過濾器OnAuthentication
(提供AuthenticationContext
包含認證主體IPrincipal
),進行認證檢核。
另外提供OnAuthenticationChallenge
將未經授權的請求進行額外處裡(如回應自訂ChallengeResult)再回應。
Lifecycle of an ASP.NET MVC 5 Application
public class CustomAuthenticationFilter : IAuthenticationFilter
{
private readonly Database _db = new Database();
public void OnAuthentication(AuthenticationContext filterContext)
{
if (filterContext.ActionDescriptor.IsDefined(typeof (AllowAnonymousAttribute), inherit: true)
|| filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof (AllowAnonymousAttribute), inherit: true)) {
return;
}
if (filterContext.Principal.Identity.IsAuthenticated && filterContext.Principal.Identity is FormsIdentity)
{
var identity = (FormsIdentity)filterContext.Principal.Identity;
var ticket = identity.Ticket;
if (!string.IsNullOrEmpty(ticket.UserData))
{
//var roles = ticket.UserData.Split(',');
//filterContext.Principal = new GenericPrincipal(identity,roles);
var user = _db.Users.FirstOrDefault(u => u.Id.ToString() == ticket.UserData);
if (user != null) {
var roles = user.Roles.Select(r => r.Name).ToArray();
filterContext.Principal = new GenericPrincipal(identity, roles);
}
}
}
else
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
if (filterContext.Result == null || filterContext.Result is HttpUnauthorizedResult)
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary
{
{"controller","Account"},
{"action","Login"},
{"returnUrl",filterContext.HttpContext.Request.RawUrl }
});
}
//or do something , add challenge to response
}
}
IAuthorizationFilter授權過濾器-提供自定義身分(角色)授權邏輯
Authorization filter驗證使用者是否擁有權限或角色身分執行Action,ASP.NET MVC 已內建提供實作AuthorizeAttribute
只需繼承並覆寫 AuthorizeCore()
方法提供自訂邏輯。
直接繼承IAuthorizationFilter介面實作需要考量更多網路安全性的問題,為了避免發生漏洞或是特性不完全,所以Framwork已經提供經由廣泛測試且功能完善的良好實作類別,只需複用即可。白話:避免明明是自己寫不好,卻要怪是微軟的漏洞xDD
Github:AuthorizeAttribute.cs 有興趣或有需要自製可以前往察看原始碼
public class CustomAuthorizationFilter : AuthorizeAttribute
{
private readonly Database _db = new Database();
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext.Request.IsLocal) {
return true;
}
var identity = httpContext.User.Identity as FormsIdentity;
if (identity ?.Ticket != null)
{
//var userRoles = identity.Ticket.UserData.Split(',');
var userRoles = _db.Users.Find(identity.Ticket.UserData) ?.Roles.Select(r => r.Name);
if (userRoles ?.Intersect(Roles.Split(',')).Any() ?? false)//交集->具有某一角色->有權限
{
return true;
}
//or get current action's roles from db and check the authorization
}
return false;
}
}
補充運作流程圖:
轉載自:一张图看懂ASP.NET MVC5认证和授权过滤器的执行顺序
FilterConfig.cs全域註冊過濾器
註冊後即可達到全站套用過濾器,搭配[AllowAnonymous]
屬性控制各別Action功能是否需要檢查授權。
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new CustomAuthenticationFilter());
//filters.Add(new CustomAuthorizationFilter());
}
}
額外補充:
-
登出
[HttpPost] [ValidateAntiForgeryToken] public ActionResult LogOff() { FormsAuthentication.SignOut(); return RedirectToAction("Index", "Home"); }
-
3A(AAA) - Authentication、Authorization、Accounting
參考&延伸閱讀