實作客製化的AspNet identity
ASP.NET的Identity可以追朔到2005年,但實際真正應用在專案上的人似乎不多,可能是因為覺得彈性不夠的關係吧。到了2015年,微軟也真的進步得非常快,identity現在已經可以客製化出屬於自己需求的驗證功能。再加上現在社群的蓬勃發展,如果可以讓使用者簡單透過社群的帳號註冊自己網站的登入帳號,相信讓用戶願意註冊的機率將會大大提升。但本篇暫時不對如何實做第三方驗證這塊多做說明,而是希望能簡單的實作一個自訂的identity來滿足客製化的需求,在完成主要架構之後,要與第三方的帳號做驗證,其實就是申請第三方帳號的secret id和做一些Config設定而已。
首先當然是要新增一個MVC專案,並勾選individual User Account來進行驗證,然後按完成,進入專案之後點開Startup類別。
using Owin;//定義IAppBuilder介面
using Microsoft.Owin;//OwinStartup的命名空間,擴充IAppBuilder介面
[assembly: OwinStartup(typeof(DempIdentity.Startup))]
namespace DempIdentity
{
//部分類別
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);//實作封裝在Startup.Auth
}
}
}
先來觀察Startup類別,它引入了一個叫做Owin的命名空間,定義了Open Web Interface相關的內容,Configuration方法內的參數類型"IAppBuilder"就是定義在Owin內。
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
using System;
namespace ErpNext
{
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
// 設定資料庫內容和使用者管理員以針對每個要求使用單一執行個體
app.CreatePerOwinContext(CustomUserManager.Create);
app.CreatePerOwinContext<CustomSignInManager>(CustomSignInManager.Create);
// 讓應用程式使用 Cookie 儲存已登入使用者的資訊
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Home/UnAuth"),
Provider = new CookieAuthenticationProvider
{
//當使用者登入後可透過戳記來來驗證其登入合法性
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<CustomUserManager, CustomUser>(
validateInterval: TimeSpan.FromDays(1),
regenerateIdentity: (manager, user) => manager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie))
}
});
}
}
}
接下來再來看Startup.Auth類別,我們可以發現它也是Startup的部分類別(partial),CreatePerOwinContext提供註冊一個Callback的方法,CallBack方法會回傳指定型別的一個實例(透過泛型),以本例子而言,我們註冊了兩個Callback方法,產生了CustomUserManager(包含管理User的相關邏輯)和CustomSignInManager(包含管理登入的相關邏輯)的實例,這兩個實例會存放在OwinContext,並透過context.Get的方式讀取,最後在實作Controller時會有例子,這裡先知道一下就好,後續會再完成CustomUserManager和CustomSignInManager的實作;再來需要定義UseCookieAuthentication(使用者在成功登入後,使用者驗證的資訊儲存的方式),在本例是透過UseCookieAuthentication來達成,有下列r幾個設定,第一個設定AuthenticationType(資料存放的方式),本例是以ApplicationCookie方式儲存;接下來設定LoginPath(登入頁面的路徑),如果使用者在沒有通過驗證的情況下訪問需驗證的網頁,便會被導向登入頁面;再來定義Provider,可設定驗證的時效,以及過期時可以透過regenerateIdentity內的callback方法重新驗證。
到這裡基本的架構已經完成,剩下來的便是實作CustomUserManager和CustomSignInManager的類別,就可以完成整個驗證流程。
public interface IUser<out TKey>
{
//
// Summary:
// Unique key for the user
TKey Id { get; }
//
// Summary:
// Unique username
string UserName { get; set; }
}
但在那之前,我們得先根據我們客製化的需求來定義我們的User類別,原生的介面已提供了Id和UserName兩個屬性,但一般來說並不是很足夠,所以需要再定義一個自訂的User類別並實作IUser。
public class CustomUser : IUser<string>
{
public string Id { get; set; }
public string UserName { get; set; }
public string Email{ get; set; }
}
為了避免不必要的複雜化,只增加一個Email的欄位來展示如何實作CustomUser即可。
public class CustomSignInManager : SignInManager<CustomUser, string>
{
public CustomSignInManager(CustomUserManager userManager, IAuthenticationManager authenticationManager)
: base(userManager, authenticationManager)
{
}
}
再來便是實作CustomSignInManager,一樣繼承SignInManager,在建構子的部分為了不增加複雜度,所以不添加任何擴充功能的程式碼。
public static CustomSignInManager Create(IdentityFactoryOptions<CustomSignInManager> options, IOwinContext context)
{
return new CustomSignInManager(context.GetUserManager<CustomUserManager>(), context.Authentication);
}
定義一個靜態的Create的方法,這方法主要是提供給Startup.Auth定義的CreatePerOwinContext的當作它的callback方法已產生自訂的CustomSignInManager實例。
public override async Task<SignInStatus> PasswordSignInAsync(string empno, string password, bool isPersistent = false, bool shouldLockout = true)
{
var user = await UserManager.FindAsync(empno, password);
if (user != null)
{
return SignInStatus.Success;
}
else
{
return SignInStatus.Failure;
}
}
SignInManager處理與登入相關的邏輯,但真正在處理判斷user密碼和帳號是否存在的事情,建議封裝給UserManager處理,此處只根據UserManager是否回傳User來判斷是否成功登入,並回傳登入狀態,以達到單一職責的原則。
public class CustomSignInManager : SignInManager<CustomUser, string>
{
public CustomSignInManager(CustomUserManager userManager, IAuthenticationManager authenticationManager)
: base(userManager, authenticationManager)
{
}
public static CustomSignInManager Create(IdentityFactoryOptions<CustomSignInManager> options, IOwinContext context)
{
return new CustomSignInManager(context.GetUserManager<CustomUserManager>(), context.Authentication);
}
public override async Task<SignInStatus> PasswordSignInAsync(string empno, string password, bool isPersistent = false, bool shouldLockout = true)
{
var user = await UserManager.FindAsync(empno, password);
if (user != null)
{
return SignInStatus.Success;
}
else
{
return SignInStatus.Failure;
}
}
}
以上為CustomSignInManager 完整的程式碼。
public class CustomUserManager : UserManager<CustomUser>
{
public CustomUserManager(IUserStore<CustomUser> store)
: base(store)
{
}
}
再來便是實作UserManager,一樣宣告一個CustomUserManager繼承基礎類別,以上為建構子,為了不增加複雜度,暫時不擴充任何程式碼。
public static CustomUserManager Create()
{
//實例CustomUserManager 並給予任意實作IUserStore(負責CRUD實作)的類別當作參數
var manager = new CustomUserManager(new CustomUserStore());
return manager;
}
此處同樣定義一個靜態方法,這方法主要是提供給Startup.Auth定義的CreatePerOwinContext的當作它的callback方法已產生自訂的CustomManager實例。
public override Task<ClaimsIdentity> CreateIdentityAsync(CustomUser user, string authenticationType)
{
Task<ClaimsIdentity> baseTask = base.CreateIdentityAsync(user, authenticationType);
Task<ClaimsIdentity> task = Task.Run(() =>
{
ClaimsIdentity identity = baseTask.Result;
List<Claim> claims = new List<Claim>{
new Claim("Email", user.Email),
};
identity.AddClaims(claims);
return identity;
});
return task;
}
這裡是重點,在建立identity時,我們必須覆寫原本的方法,主要目的只有一個,便是我們的Custom有多一個Email的欄位,如果走原本的實作方式,我們便無法在identity裡取得Email的資訊。而實做的方式首先先呼叫原本父類別的方法產生基本的identity,接下來將我們想要額外儲存的資訊儲存在identity的Claims內,Claim是一組dictionary,完成之後便可將結果回傳,此方法是用非同步的方式實做的,所以必須回傳一個Task。
public override Task<CustomUser> FindAsync(string EMPNO, string password)
{
Task<CustomUser> task = Task.Run(() =>
{
return new CustomUser
{
Id = EMPNO,
Email= "xxx.xxx.com.tw",
UserName = ""
};
});
}
這裡就稍微偷懶了,實際上應該要去資料庫或任何資料來源,搜尋這組帳密是否存在,並將相關的資訊封裝到CustomUser內回傳,此處省略了查找資料庫的邏輯,而直接回傳一組實例化的CustomUser。這裡要強調一個地方,UserName和Id是來自IUser介面,就算你不想用,也請給他一個值,假如未給資料,會造成null的錯誤。
public class CustomUserManager : UserManager<CustomUser>
{
public CustomUserManager(IUserStore<CustomUser> store)
: base(store)
{
}
public static CustomUserManager Create()
{
var manager = new CustomUserManager(new CustomUserStore());
return manager;
}
public override Task<ClaimsIdentity> CreateIdentityAsync(CustomUser user, string authenticationType)
{
Task<ClaimsIdentity> baseTask = base.CreateIdentityAsync(user, authenticationType);
Task<ClaimsIdentity> task = Task.Run(() =>
{
ClaimsIdentity identity = baseTask.Result;
List<Claim> claims = new List<Claim>{
new Claim("Email", user.Email),
};
identity.AddClaims(claims);
return identity;
});
return task;
}
public override Task<CustomUser> FindAsync(string EMPNO, string password)
{
Task<CustomUser> task = Task.Run(() =>
{
return new CustomUser
{
Id = EMPNO,
Email= "xxx.xxx.com.tw",
UserName = ""
};
});
}
}
以上為CustomManager 完整的程式碼。
public class CustomUserStore : IUserStore<CustomUser>
{
/// <summary>
/// ERP CRUD不在此處做
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task CreateAsync(CustomUser user)
{
throw new NotImplementedException();
}
/// <summary>
/// ERP CRUD不在此處做
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task DeleteAsync(CustomUser user)
{
throw new NotImplementedException();
}
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public Task<CustomUser> FindByIdAsync(string userId)
{
throw new NotImplementedException();
}
/// <summary>
///
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
public Task<CustomUser> FindByNameAsync(string userName)
{
throw new NotImplementedException();
}
/// <summary>
///
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task UpdateAsync(CustomUser user)
{
throw new NotImplementedException();
}
public void Dispose()
{
}
}
最後便是CustomUser資料該如何儲存,此處就是相關的CRUD實作,這裡就不特別說明了。
走到這裡,來回顧一下我們做了甚麼,我們定義了Startup(Owin的進入點)和Sartup.Auth(設定Owin相關的Config) 、CustomSignManager(處理是否登入成功的相關邏輯)、CustomManager(處理使用者密碼帳密比對等相關問題)、CustomUser(自訂的使用者類別)、CustomUserStore(管理CustomUser資料庫的CRUD實作),以上都完成後,便完成了自訂的Owin Identity實作。接下來便來看看我們的程式如何使用它吧。
public class DemoController : Controller
{
private CustomSignInManager _signInManager;
public CustomSignInManager CustomSignInManager
{
get
{
//透過Owin的框架取得CustomSignInManager(Startup.Auth 的CreatePerOwinContext註冊)
return _signInManager ?? HttpContext.GetOwinContext().Get<CustomSignInManager>();
}
private set { _signInManager = value; }
}
public IAuthenticationManager AuthenticationManager
{
get
{
//取得目前使用者的驗證,可用來做登出的功能
return HttpContext.GetOwinContext().Authentication;
}
}
}
在Controller內,我們便可以透過HttpContext內的GetOwinContext方法,取得註冊的CustomSignInManager來處理登入以及取得目前使用者的Authentication。
[AllowAnonymous]
[HttpPost]
public async Task<ActionResult> Index(string Empno,string password)
{
//判斷是否已驗證,如無驗證則呼叫登入的方法
if (!Request.IsAuthenticated) {
await CustomSignInManager.PasswordSignInAsync(Empno,password);
}
//取得identity的Claims 為了要取得我們自定義的Email欄位
var identities = ((ClaimsIdentity)HttpContext.User.Identity).Claims;
foreach(var item in identities)
{
switch (item.Type)
{
case "Email":
ViewBag.Email= item.Value;
break;
}
}
return View();
}
[HttpPost]
public ActionResult LogOff()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
return View();
}
簡單處理登入功能,以及透過Claims取得額外新增進去的Email欄位,以及登出的功能,大功告成。