如何在ASP.NET Core應用中實現認證、登錄和註銷?一個實例告訴你

在安全領域,認證和授權是兩個重要的主題。認證是安全體系的第一道屏障,是守護整個應用或者服務的第一道大門。當訪問者請求進入的時候,認證體系通過驗證對方的提供憑證確定其真實身份。認證體系只有在證實了訪問者的真實身份的情況下才會允許其進入。ASP.NET Core提供了多種認證方式,它們的實現都基於相同的認證模型。本篇文章提供了一個極簡的實例讓讀者體驗如何在ASP.NET Core應用中實現認證、登錄和註銷。

一、認證票據

認證是一個旨在確定請求訪問者真實身份的過程,與認證相關的還有其他兩個基本操作——登錄與註銷。要真正理解認證、登錄與註銷這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種角色由同一個主體來扮演。

如何在ASP.NET Core應用中實現認證、登錄和註銷?一個實例告訴你

頒發認證票據的過程就是登錄(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”。

<code>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();

}
}/<code>

三、應用主頁

如下面的代碼片段所示,我們調用IApplicationBuilder接口的UseAuthentication擴展方法就是為了註冊用來實現認證的AuthenticationMiddleware中間件。該中間件的依賴服務是通過調用IServiceCollection接口的AddAuthentication擴展方法註冊的。在註冊這些基礎服務時,我們還設置了默認採用的認證方案,靜態類型CookieAuthenticationDefaults的AuthenticationScheme屬性返回的就是Cookie認證方案的默認方案名稱。

<code>public class Program
{
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();
}
}/<code>

ASP.NET Core提供了一個極具擴展性的認證模型,我們可以利用它支持多種認證方案,針對認證方案的註冊是通過AddAuthentication方法返回的一個AuthenticationBuilder對象來實現的。在上面提供的代碼片段中,我們調用AuthenticationBuilder對象的AddCookie擴展方法完成了針對Cookie認證方案的註冊。

演示實例的主頁是通過如下所示的RenderHomePageAsync方法來呈現的。由於我們要求瀏覽主頁必須是經過認證的用戶,所以該方法會利用HttpContext上下文的User屬性返回的ClaimsPrincipal對象判斷當前請求是否經過認證。對於經過認證的請求,我們會響應一個簡單的HTML文檔,並在其中顯示用戶名和一個註銷鏈接。

<code>public class Program
{
...
public static async Task RenderHomePageAsync(HttpContext context)
{
if (context?.User?.Identity?.IsAuthenticated == true)
{
await context.Response.WriteAsync(
@"
<title>Index/<title>
" +
$"

Welcome {context.User.Identity.Name}

" +
@"

");
}
else
{
await context.ChallengeAsync();
}
}
}/<code>

對於匿名請求,我們希望應用能夠自動重定向到登錄路徑。從如上所示的代碼片段可以看出,我們僅僅調用當前HttpContext上下文的ChallengeAsync擴展方法就完成了針對登錄路徑的重定向。前面提及,註冊的登錄和註銷路徑是基於Cookie的認證方案採用的默認路徑,所以調用ChallengeAsync方法時根本不需要指定重定向路徑。下圖所示就是作為應用的主頁在瀏覽器上呈現的效果。

如何在ASP.NET Core應用中實現認證、登錄和註銷?一個實例告訴你

四、登錄

登錄與註銷分別實現在SignInAsync方法和SignOutAsync方法中,我們採用的是針對“用戶名 + 密碼”的登錄方式,所以可以利用靜態字段_accounts來存儲應用註冊的賬號。在靜態構造函數中,我們添加密碼均為“password”的3個賬號(Foo、Bar和Baz)。

<code>public class Program
{
private static Dictionary<string> _accounts;
static Program()
{
_accounts = new Dictionary<string>(StringComparer.OrdinalIgnoreCase);
_accounts.Add("Foo", "password");
_accounts.Add("Bar", "password");
_accounts.Add("Baz", "password");
}
}/<string>/<string>/<code>

如下所示的代碼片段是用於處理登錄請求的SignInAsync方法的定義,而RenderLoginPageAsync方法用來呈現登錄頁面。如下面的代碼片段所示,對於GET請求,SignInAsync方法會直接調用RenderLoginPageAsync方法來呈現登錄界面。對於POST請求,我們會從提交的表單中提取用戶名和密碼,並對其實施驗證。如果提供的用戶名與密碼一致,我們會根據用戶名創建一個代表身份的GenericIdentity對象,並利用它創建一個代表登錄用戶的ClaimsPrincipal對象,RenderHomePageAsync方法正是利用該對象來檢驗當前用戶是否經過認證的。有了ClaimsPrincipal對象,我們只需要將它作為參數調用HttpContext上下文的SignInAsync擴展方法即可完成登錄,該方法最終會自動重定向到初始方法的路徑,也就是我們的主頁。

<code>public class Program
{
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(
@"
<title>Login/<title>

" +
$"

{errorMessage}

" +
@"
");
}
}/<code>

如果用戶提供的用戶名與密碼不匹配,我們還是會調用RenderLoginPageAsync方法來呈現登錄頁面,該頁面會以下圖所示的形式保留用戶的輸入並顯示錯誤消息。圖19-3還反映了一個細節,調用HttpContext上下文的ChallengeAsync方法會將當前路徑(主頁路徑“/”,經過編碼後為“%2F”)存儲在一個名為ReturnUrl的查詢字符串中,SignInAsync方法正是利用它實現對初始路徑的重定向的。


五、註銷

既然登錄可以通過調用當前HttpContext上下文的SignInAsync擴展方法來完成,那麼註銷操作對應的自然就是SignOutAsync擴展方法。如下面的代碼片段所示,我們定義在Program中的SignOutAsync擴展方法正是調用這個方法來註銷當前登錄狀態的。我們在完成註銷之後將應用重定向到主頁。

<code>public class Program
{
...
public static async Task SignOutAsync(HttpContext context)
{
await context.SignOutAsync();
context.Response.Redirect("/");
}
}/<code>


分享到:


相關文章: