ASP.NET Core 使用最簡潔的代碼實現(xiàn)登錄、認證和注銷
前言
認證是一個確定請求訪問者真實身份的過程,與認證相關(guān)的還有其他兩個基本操作——登錄和注銷。ASP.NET Core利用AuthenticationMiddleware中間件完成針對請求的認證,并提供了用于登錄、注銷以及"質(zhì)詢"的API,本篇文章利用它們使用最簡單的代碼實現(xiàn)這些功能。
一、 認證票據(jù)
要真正理解認證、登錄和注銷這三個核心操作的本質(zhì),就需要對ASP.NET采用的基于"票據(jù)"的認證機制有基本的了解。
ASP.NET Core應(yīng)用的認證實現(xiàn)在AuthenticationMiddleware的中間件中,該中間件在處理分發(fā)給它的請求時會按照指定的認證方案(Authentication Scheme)從請求中提取能夠驗證用戶真實身份的信息,我們一般將此信息稱為安全令牌(Security Token)。
ASP.NET Core應(yīng)用下的安全令牌被稱為認證票據(jù)(Authentication Ticket),它采用基于票據(jù)的認證方式。該中間件實現(xiàn)的整個認證流程涉及圖1所示的三種針對認證票據(jù)的操作,即認證票據(jù)的"頒發(fā)"、"檢驗"和"撤銷"。
我們將這三個操作所涉及的三種角色稱為票據(jù)頒發(fā)者(Ticket Issuer)、驗證者(Authenticator)和撤銷者(Ticket Revoker),在大部分場景下這三種角色由同一個主體來扮演。
圖1 基于票據(jù)的認證
頒發(fā)認證票據(jù)的過程就是登錄(Sign In)操作。用戶試圖通過登錄來獲取認證票據(jù)時需要提供可用來證明自身身份的憑證(Credential),最常見的用戶憑證類型是"用戶名 + 密碼"。
認證方在確定對方真實身份之后,會頒發(fā)一個認證票據(jù),該票據(jù)攜帶著與該用戶有關(guān)的身份、權(quán)限及其他相關(guān)的信息。
一旦擁有了由認證方頒發(fā)的認證票據(jù),客戶端就可以按照雙方協(xié)商的方式(比如通過Cookie或者報頭)在請求中攜帶該認證票據(jù),并以此票據(jù)聲明的身份執(zhí)行目標(biāo)操作或者訪問目標(biāo)資源。
認證票據(jù)一般都具有時效性,一旦過期將變得無效。如果希望在過期之前就讓認證票據(jù)無效,這就是注銷(Sign Out)操作。
ASP.NET的認證系統(tǒng)旨在構(gòu)建一個標(biāo)準(zhǔn)的模型,用來完成針對請求的認證以及與之相關(guān)的登錄和注銷操作。按照慣例,在介紹認證模型的架構(gòu)設(shè)計之前,需要通過一個簡單的實例來演示如何在一個ASP.NET應(yīng)用中實現(xiàn)認證、登錄和注銷的功能。
二、基于Cookie的認證
我們會采用ASP.NET提供的基于Cookie的認證方案。該認證方案采用Cookie來攜帶認證票據(jù)。為了使讀者對基于認證的編程模式有深刻的理解,我們演示的這個應(yīng)用將從一個空白的ASP.NET應(yīng)用開始搭建。
這個應(yīng)該會呈現(xiàn)兩個頁面,認證用戶訪問主頁會呈現(xiàn)一個"歡迎"頁面,匿名請求則會重定向到登錄頁面,我們將這兩個頁面的呈現(xiàn)實現(xiàn)在如下這個IPageRenderer服務(wù)中,PageRenderer類型為該接口的默認實現(xiàn)。
public interface IPageRenderer
{
IResult RenderLoginPage(string? userName = null, string? password = null, string? errorMessage = null);
IResult RenderHomePage(string userName);
}
public class PageRenderer : IPageRenderer
{
public IResult RenderHomePage(string userName)
{
var html = @$"
<html>
<head><title>Index</title></head>
<body>
<h3>Welcome {userName}</h3>
<a href='Account/Logout'>Sign Out</a>
</body>
</html>";
return Results.Content(html, "text/html");
}
public IResult RenderLoginPage(string? userName, string? password, string? errorMessage)
{
var html = @$"
<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>";
return Results.Content(html, "text/html");
}
}
我們采用"用戶名+密碼"的認證方式,密鑰驗證實現(xiàn)的如下這個IAccountService接口的Validate方法中。
在實現(xiàn)的AccountService類型中,我們預(yù)創(chuàng)建了三個密碼為"password"的賬號("foo"、"bar"和"baz")。
public interface IAccountService
{
bool Validate(string userName, string password);
}
public class AccountService: IAccountService
{
private readonly Dictionary<string, string> _accounts = new(StringComparer.OrdinalIgnoreCase)
{
{ "Foo", "password"},
{ "Bar", "password"},
{ "Baz", "password"}
};
public bool Validate(string userName, string password) =>_accounts.TryGetValue(userName, out var pwd) && pwd == password;
}
我們即將創(chuàng)建的這個ASP.NET應(yīng)用主要處理四種類型的請求。主頁需要在登錄之后才能訪問,所以針對主頁的匿名請求會被重定向到登錄頁面。
在登錄頁面輸入正確的用戶名和密碼之后,應(yīng)用會自動重定向到主頁,該頁面會顯示當(dāng)前認證用戶名并提供注銷的鏈接。我們按照如下所示的方式注冊了四個對應(yīng)的終結(jié)點,其中登錄和注銷采用的是約定的路徑"Account/Login"與"Account/Logout"。
using App;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using System.Security.Principal;
var builder = WebApplication.CreateBuilder();
builder.Services
.AddSingleton<IPageRenderer, PageRenderer>()
.AddSingleton<IAccountService, AccountService>()
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
var app = builder.Build();
app.UseAuthentication();
app.Map("/", WelcomeAsync);
app.MapGet("Account/Login", Login);
app.MapPost("Account/Login", SignInAsync);
app.Map("Account/Logout", SignOutAsync);
app.Run();
Task WelcomeAsync () => throw new NotImplementedException();
IResult Login(IPageRenderer renderer) => throw new NotImplementedException();
Task SignInAsync()=> throw new NotImplementedException();
Task SignOutAsync() => throw new NotImplementedException();
上面的演示程序調(diào)用UseAuthentication擴展方法注冊了AuthenticationMiddleware中間件,它所依賴服務(wù)是通過調(diào)用AddAuthentication擴展方法進行注冊。
在調(diào)用該方法時,我們還設(shè)置了默認采用的認證方案,靜態(tài)類型CookieAuthenticationDefaults的AuthenticationScheme屬性返回的就是Cookie認證方案的默認方案名稱。
我們在上面定義的兩個服務(wù)也在這里進行了注冊。圖2所示就是作為應(yīng)用的主頁在瀏覽器上呈現(xiàn)的效果。
圖2 應(yīng)用主頁
三、 強制認證
演示實例的主頁是通過如下所示的WelcomeAsync方法來呈現(xiàn)的,該方法注入了當(dāng)前HttpContext上下文、代表當(dāng)前用戶的ClaimsPrincipal對象和IPageRenderer對象。我們利用ClaimsPrincipal對象確定用戶是否經(jīng)過人證,認證用戶請求將呈現(xiàn)正常的歡迎頁面,匿名請求直接調(diào)用HttpContext上下文的ChallengeAsync方法進行處理。
基于Cookie的認證方案會自動將匿名請求重定向到登錄頁面,由于我們指定的登錄和注銷路徑是Cookie的認證方案約定的路徑,所以調(diào)用ChallengeAsync方法時根本不需要指定重定向路徑。
Task WelcomeAsync(HttpContext context, ClaimsPrincipal user, IPageRenderer renderer)
{
if (user?.Identity?.IsAuthenticated ?? false)
{
return renderer.RenderHomePage(user.Identity.Name!).ExecuteAsync(context);
}
return context.ChallengeAsync();
}
四、登錄與注銷
針對登錄頁面所在地址的請求由兩種類型,針對GET請求的Login方法會登錄頁面呈現(xiàn)出來,針對POST請求的SignInAsync方法檢驗輸入的用戶名和密碼,并在驗證成功后實施"登錄"。如下面的代碼片段所示,SignInAsync方法中注入了當(dāng)前HttpContext上下文、代表請求的HttpRequest對象和額外兩個服務(wù)。
從請求表單將用戶和密碼提取出來后,我們利用IAccountService對象進行驗證。
在驗證通過的情況下,我們會根據(jù)用戶名創(chuàng)建代表當(dāng)前用戶的ClaimsPrincipal對象,并將它作為參數(shù)調(diào)用HttpContext上下文的SignInAsync擴展方法實施登錄, 該方法最終會自動重定向到初始方法的路徑,也就是我們的主頁。
IResult Login(IPageRenderer renderer) => renderer.RenderLoginPage();
Task SignInAsync(HttpContext context, HttpRequest request, IPageRenderer renderer,IAccountService accountService)
{
var username = request.Form["username"];
if (string.IsNullOrEmpty(username))
{
return renderer.RenderLoginPage(null, null, "Please enter user name.").ExecuteAsync(context);
}
var password = request.Form["password"];
if (string.IsNullOrEmpty(password))
{
return renderer.RenderLoginPage(username, null, "Please enter user password.").ExecuteAsync(context);
}
if (!accountService.Validate(username, password))
{
return renderer.RenderLoginPage(username, null, "Invalid user name or password.").ExecuteAsync(context);
}
var identity = new GenericIdentity(name: username, type: "PASSWORD");
var user = new ClaimsPrincipal(identity);
return context.SignInAsync(user);
}
如果用戶名或者密碼沒有提供或者不匹配,登錄頁面會以圖3所示的形式再次呈現(xiàn)出來,并保留輸入的用戶名和錯誤消息。ChallengeAsync方法會將當(dāng)前路徑(主頁路徑“/”,經(jīng)過編碼后為“%2F”)存儲在一個名為ReturnUrl的查詢字符串中,SignInAsync方法正是利用它實現(xiàn)對初始路徑的重定向的。
既然登錄可以通過調(diào)用當(dāng)前HttpContext上下文的SignInAsync擴展方法來完成,那么注銷操作對應(yīng)的自然就是SignOutAsync擴展方法。
如下面的代碼片段所示,SignOutAsync擴展方法正是調(diào)用這個方法來注銷當(dāng)前登錄狀態(tài)的。我們在完成注銷之后將應(yīng)用重定向到主頁。
async Task SignOutAsync(HttpContext context)
{
await context.SignOutAsync();
context.Response.Redirect("/");
}
轉(zhuǎn)自:Artech
鏈接:cnblogs.com/artech/p/inside-asp-net-core-6-39.html
