用最簡單的方式在ASP.NET Core應用中實作認證、登入和登出
在安全領域,認證和授權是兩個重要的主題。認證是安全體系的第一道屏障,是守護整個應用或者服務的第一道大門。當通路者請求進入的時候,認證體系通過驗證對方的提供憑證确定其真實身份。認證體系隻有在證明了通路者的真實身份的情況下才會允許其進入。ASP.NET Core提供了多種認證方式,它們的實作都基于相同的認證模型。本篇文章提供了一個極簡的執行個體讓讀者體驗如何在ASP.NET Core應用中實作認證、登入和登出。
本篇文章節選自《ASP.NET Core 3架構揭秘》(下冊),針對本書的限時5折優惠截至到今天24時,有興趣的朋友可以通過加入讀者群進行購買。入群方式:掃描右方二維碼添加“博文小丸子(broadview002)”,并将本書書号“38462”作為驗證資訊。源代碼從這裡下載下傳。
一、認證票據
認證是一個旨在确定請求通路者真實身份的過程,與認證相關的還有其他兩個基本操作——登入與登出。要真正了解認證、登入與登出這3個核心操作的本質,就需要對ASP.NET Core采用的基于“票據”的認證機制有基本的了解。ASP.NET Core應用的認證實作在一個名為AuthenticationMiddleware的中間件中,該中間件在處理分發給它的請求時會按照指定的認證方案(Authentication Scheme)從請求中提取能夠驗證使用者真實身份的資料,我們一般将該資料稱為安全令牌(Security Token)。ASP.NET Core應用下的安全令牌被稱為認證票據(Authentication Ticket),是以ASP.NET Core應用采用基于票據的認證方式。
AuthenticationMiddleware中間件實作的整個認證流程涉及下圖所示的3種針對認證票據的操作,即認證票據的頒發、檢驗和撤銷。我們将這3個操作所涉及的3種角色稱為票據頒發者(Ticket Issuer)、驗證者(Authenticator)和撤銷者(Ticket Revoker),在大部分場景下這3種角色由同一個主體來扮演。
頒發認證票據的過程就是登入(Sign In)操作。一般來說,使用者試圖通過登入應用以擷取認證票據的時候需要提供可用來證明自身身份的使用者憑證(User Credential),最常見的使用者憑證類型是“使用者名 + 密碼”。認證方在确定對方真實身份之後,會頒發一個認證票據,該票據攜帶着與該使用者相關的身份、權限及其他相關的資訊。
一旦擁有了由認證方頒發的認證票據,我們就可以按照雙方協商的方式(如通過Cookie或者報頭)在請求中攜帶該認證票據,并以此票據聲明的身份執行目标操作或者通路目标資源。認證票據一般都具有時效性,一旦過期将變得無效。我們有的時候甚至希望在過期之前就讓認證票據無效,以免别人使用它冒用自己的身份與應用進行互動,這就是登出(Sign Out)操作。
ASP.NET Core應用的認證系統旨在建構一個标準的模型來完成針對請求的認證以及與之相關的登入和登出操作。接下來我們就通過一個簡單的執行個體來示範如何在一個ASP.NET Core應用中實作認證、登入和登出的功能。
二、基于Cookie的認證
我們會采用ASP.NET Core提供的基于Cookie的認證方案。顧名思義,該認證方案采用Cookie來攜帶認證票據。為了使讀者對基于認證的程式設計模式有深刻的了解,我們示範的這個應用将從一個空白的ASP.NET Core應用開始搭建。
我們即将建立的這個ASP.NET Core應用主要處理3種類型的請求。應用的首頁需要登入之後才能通路,是以針對首頁的匿名請求會被重定向到登入頁面。在登入頁面輸入正确的使用者名和密碼之後,應用會自動重定向到應用首頁,該頁面會顯示目前認證使用者名并提供登出的連結。我們按照如下所示的方式利用路由來處理這3種類型的請求,其中登入和登出采用的是預設路徑“Account/Login”與“Account/Logout”。
public class Program
{
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app
.UseRouting()
.UseEndpoints(endpoints =>{
endpoints.Map(pattern: "/", RenderHomePageAsync);
endpoints.Map("Account/Login", SignInAsync);
endpoints.Map("Account/Logout", SignOutAsync);
})))
.Build()
.Run();
}
public static async Task RenderHomePageAsync(HttpContext context)
{
throw new NotImplementedException();
}
public static async Task SignInAsync(HttpContext context)
{
throw new NotImplementedException();
}
public static async Task SignOutAsync(HttpContext context)
{
throw new NotImplementedException();
}
}
三、應用首頁
如下面的代碼片段所示,我們調用IApplicationBuilder接口的UseAuthentication擴充方法就是為了注冊用來實作認證的AuthenticationMiddleware中間件。該中間件的依賴服務是通過調用IServiceCollection接口的AddAuthentication擴充方法注冊的。在注冊這些基礎服務時,我們還設定了預設采用的認證方案,靜态類型CookieAuthenticationDefaults的AuthenticationScheme屬性傳回的就是Cookie認證方案的預設方案名稱。
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs
.AddRouting()
.AddAuthentication(options => options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie())
.Configure(app => app
.UseAuthentication()
.UseRouting()
.UseEndpoints(endpoints =>{
endpoints.Map(pattern: "/", RenderHomePageAsync);
endpoints.Map("Account/Login", SignInAsync);
endpoints.Map("Account/Logout", SignOutAsync);
})))
.Build()
.Run();
}
ASP.NET Core提供了一個極具擴充性的認證模型,我們可以利用它支援多種認證方案,針對認證方案的注冊是通過AddAuthentication方法傳回的一個AuthenticationBuilder對象來實作的。在上面提供的代碼片段中,我們調用AuthenticationBuilder對象的AddCookie擴充方法完成了針對Cookie認證方案的注冊。
示範執行個體的首頁是通過如下所示的RenderHomePageAsync方法來呈現的。由于我們要求浏覽首頁必須是經過認證的使用者,是以該方法會利用HttpContext上下文的User屬性傳回的ClaimsPrincipal對象判斷目前請求是否經過認證。對于經過認證的請求,我們會響應一個簡單的HTML文檔,并在其中顯示使用者名和一個登出連結。
...
public static async Task RenderHomePageAsync(HttpContext context)
{
if (context?.User?.Identity?.IsAuthenticated == true)
{
await context.Response.WriteAsync(
@"<html>
<head><title>Index</title></head>
<body>" +
$"<h3>Welcome {context.User.Identity.Name}</h3>" +
@"<a href='Account/Logout'>Sign Out</a>
</body>
</html>");
}
else
{
await context.ChallengeAsync();
}
}
對于匿名請求,我們希望應用能夠自動重定向到登入路徑。從如上所示的代碼片段可以看出,我們僅僅調用目前HttpContext上下文的ChallengeAsync擴充方法就完成了針對登入路徑的重定向。前面提及,注冊的登入和登出路徑是基于Cookie的認證方案采用的預設路徑,是以調用ChallengeAsync方法時根本不需要指定重定向路徑。下圖所示就是作為應用的首頁在浏覽器上呈現的效果。
四、登入
登入與登出分别實作在SignInAsync方法和SignOutAsync方法中,我們采用的是針對“使用者名 + 密碼”的登入方式,是以可以利用靜态字段_accounts來存儲應用注冊的賬号。在靜态構造函數中,我們添加密碼均為“password”的3個賬号(Foo、Bar和Baz)。
private static Dictionary<string, string> _accounts;
static Program()
{
_accounts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
_accounts.Add("Foo", "password");
_accounts.Add("Bar", "password");
_accounts.Add("Baz", "password");
}
如下所示的代碼片段是用于處理登入請求的SignInAsync方法的定義,而RenderLoginPageAsync方法用來呈現登入頁面。如下面的代碼片段所示,對于GET請求,SignInAsync方法會直接調用RenderLoginPageAsync方法來呈現登入界面。對于POST請求,我們會從送出的表單中提取使用者名和密碼,并對其實施驗證。如果提供的使用者名與密碼一緻,我們會根據使用者名建立一個代表身份的GenericIdentity對象,并利用它建立一個代表登入使用者的ClaimsPrincipal對象,RenderHomePageAsync方法正是利用該對象來檢驗目前使用者是否經過認證的。有了ClaimsPrincipal對象,我們隻需要将它作為參數調用HttpContext上下文的SignInAsync擴充方法即可完成登入,該方法最終會自動重定向到初始方法的路徑,也就是我們的首頁。
public static async Task SignInAsync(HttpContext context)
{
if (string.Compare(context.Request.Method, "GET") == 0)
{
await RenderLoginPageAsync(context, null, null, null);
}
else
{
var userName = context.Request.Form["username"];
var password = context.Request.Form["password"];
if (_accounts.TryGetValue(userName, out var pwd) && pwd == password)
{
var identity = new GenericIdentity(userName, "Passord");
var principal = new ClaimsPrincipal(identity);
await context.SignInAsync(principal);
}
else
{
await RenderLoginPageAsync(context, userName, password, "Invalid user name or password!");
}
}
}
private static Task RenderLoginPageAsync(HttpContext context, string userName, string password, string errorMessage)
{
context.Response.ContentType = "text/html";
return context.Response.WriteAsync(
@"<html>
<head><title>Login</title></head>
<body>
<form method='post'>" +
$"<input type='text' name='username' placeholder='User name' value ='{userName}'/>" +
$"<input type='password' name='password' placeholder='Password' value ='{password}'/> " +
@"<input type='submit' value='Sign In' /></form>" +
$"<p style='color:red'>{errorMessage}</p>" +
@"</body>
</html>");
}
如果使用者提供的使用者名與密碼不比對,我們還是會調用RenderLoginPageAsync方法來呈現登入頁面,該頁面會以下圖所示的形式保留使用者的輸入并顯示錯誤消息。圖19-3還反映了一個細節,調用HttpContext上下文的ChallengeAsync方法會将目前路徑(首頁路徑“/”,經過編碼後為“%2F”)存儲在一個名為ReturnUrl的查詢字元串中,SignInAsync方法正是利用它實作對初始路徑的重定向的。
五、登出
既然登入可以通過調用目前HttpContext上下文的SignInAsync擴充方法來完成,那麼登出操作對應的自然就是SignOutAsync擴充方法。如下面的代碼片段所示,我們定義在Program中的SignOutAsync擴充方法正是調用這個方法來登出目前登入狀态的。我們在完成登出之後将應用重定向到首頁。
...
public static async Task SignOutAsync(HttpContext context)
{
await context.SignOutAsync();
context.Response.Redirect("/");
}
作者:蔣金楠
原文位址
https://www.cnblogs.com/artech/p/authentication-sign-in-out.html