1. 程式人生 > >ASP.NET Core 2.0身份驗證和授權系統揭祕

ASP.NET Core 2.0身份驗證和授權系統揭祕

ASP.NET Core中存在一個元件,它構成了一個魔法遮蔽,可以保護您網站的部分(或全部)免受未經授權的訪問。像許多人一樣,我從旅程開始就使用過這個元件,但從未理解過。它被一個巫師召喚出來,在我的網站和世界之間提供了一個神奇的屏障。當然,這不是它真正起作用的方式,但如果沒有正確的知識,它也可能。

在試圖弄清楚如何修復我的程式碼中的錯誤時,我碰巧在正確的Slack通道上正確地問了正確的問題。David Fowler恰好是aspnetcore的核心維護者之一,他決定讓每個人都瞭解認證系統(auth系統,從現在開始)如何在ASP.NET Core 2.0中執行。本文基於他即興課程中的資訊。

我後來發現安德魯·洛克的一篇文章詳細介紹了索賠部分,在閱讀了他的文章並進行了更多的研究之後,又增加了關於身份的部分。

首先要了解系統需要了解其元件和行為。它們可以分解為身份,動詞,身份驗證處理程式和中介軟體。我將單獨介紹其中的每一個,然後演示它們如何在示例auth請求中一起工作。由於ASP.NET Core最常見的身份驗證處理程式是Cookie身份驗證處理程式,因此這些示例將使用cookie身份驗證。

身分

理解身份驗證如何工作的關鍵是首先了解ASP.NET Core 2.0中的身份。有三個類代表使用者的身份:Claim,ClaimsIdentity和ClaimsPrincipal

宣告

權利要求表示關於該使用者的一個事實。它可以是使用者的名字,姓氏,年齡,僱主,出生日期或其他任何與使用者相關的內容。單個宣告僅包含一條資訊。代表使用者John Smith的宣告可能是他的第一個名字:約翰。第二個主張是他的姓:史密斯。

宣告由ClaimASP.Net Core中的類表示。它最常見的建構函式接受兩個字串:type和value。'type'引數是宣告的名稱,而值是宣告代表使用者的資訊。

此程式碼將建立兩個新的宣告。一個型別為'FullName',值為'Dark Helmet',第二個型別ClaimTypes.Email和值為'[email protected]'。有一個ClaimsType類,它包含許多表示行業標準宣告型別的字串常量。這些都是URI格式,但宣告型別可以是任何字串。

//This claim uses a standard string
new Claim("FullName","Dark Helmet");

//This claim type expands to 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
new Claim(ClaimTypes.Email, "
[email protected]
");

ClaimsIdentity

身份代表一種身份識別形式,換句話說,就是一種證明自己身份的單一方式。在現實生活中,這可能是駕駛執照。在ASP.Net Core中,它是一個ClaimsIdentity。此類代表一種形式的數字識別。

ClaimsIdentity可以對a的單個例項進行身份驗證或不進行身份驗證。根據Andrew Lock的ASP.NET核心驗證簡介,只需設定AuthenticationType即可自動確保IsAuthenticated屬性為true。這是因為如果您以任何方式驗證了身份,那麼根據定義,它必須經過身份驗證。

一個不知名的人走到你面前,對自己和他們的生活做出各種各樣的主張,對於一個未經認證的人來說是無足輕重的ClaimsIdentity。Lock寫道,這可能對於允許訪客購物車(可能在登入之前)和類似用例有用。

駕駛執照包含許多關於其主題的宣告:名字和姓氏,出生日期,頭髮和眼睛顏色,身高等。類似地,a ClaimsIdentity可以包含關於使用者的許多宣告。

ClaimsPrincipal

一個主要代表實際的使用者。它可以包含一個或多個例項ClaimsIdentity,就像生活中一個人可能擁有駕駛執照,隱藏攜帶許可證,社會保險卡和護照一樣。每個身份用於不同的目的,並且可以包含一組唯一的宣告,但它們都以某種形式或其他形式標識相同的使用者。

總而言之,a ClaimsPrincipal表示使用者並且包含一個或多個例項ClaimsIdentity,其又表示單一形式的標識幷包含一個或多個例項Claim,其表示關於使用者的單條資訊。該ClaimsPrincipal有什麼HttpContext.SignInAsync方法接受並傳遞到指定的AuthenticationHandler

動詞

有5個動詞(這些也可以被認為是命令或行為)由auth系統呼叫,並且不一定按順序呼叫。這些都是不相互通訊的獨立操作,但是,當一起使用時,允許使用者登入並訪問否則被拒絕的頁面。以下是每個動詞負責的簡要說明。我們將在文章中進一步深入探討。

注意:這些是行為,而不是方法(儘管存在實現這些行為的相同名稱的方法)。

  • 認證
    • 獲取使用者的資訊(如果存在)(例如解碼使用者的cookie,如果存在)
  • 挑戰
    • 請求使用者進行身份驗證(例如,顯示登入頁面)
  • 登入
    • 在某處保留使用者的資訊(例如,寫一個cookie)
  • 登出
    • 刪除使用者的持久資訊(例如刪除cookie)
  • 禁止
    • 拒絕為未經身份驗證的使用者或經過身份驗證但未經授權的使用者訪問資源(例如,顯示“未授權”頁面)

身份驗證處理程式

身份驗證處理程式是實際實現上述5個動詞行為的元件。ASP.NET Core提供的預設auth處理程式是Cookies身份驗證處理程式,它實現了所有5個動詞。然而,重要的是要注意,不需要auth處理程式來實現所有動詞。例如,Oauth處理程式不實現SignIn動詞,而是將該責任傳遞給另一個auth處理程式,例如Cookies auth處理程式。

身份驗證處理程式必須在auth系統中註冊才能使用並與方案相關聯。方案只是一個字串,用於標識auth處理程式字典中的唯一auth處理程式。Cookie auth處理程式的預設方案是“Cookies”,但它可以更改為任何內容。多個auth處理程式可以並排使用,有時(如早期的Oauth處理程式示例)使用其他auth處理程式提供的功能。

認證中介軟體

中介軟體是一個可以插入啟動序列並在每個請求上執行的模組。本文所關注的是身份驗證中介軟體。此程式碼檢查使用者是否在每個請求上進行了身份驗證(或不進行身份驗證)。回想一下,Authenticate動詞獲取使用者資訊,但前提是它存在。執行請求時,身份驗證中介軟體會要求預設方案auth處理程式執行其身份驗證程式碼。auth處理程式將資訊返回給身份驗證中介軟體,然後使用返回的資訊填充HttpContext.User物件。

身份驗證和授權流程

所有這些元件必須在auth系統中一起使用,以便成功驗證和授權使用者訪問資源。該過程從未經身份驗證的使用者傳送對需要授權訪問的資源的請求開始。

以下是Cookie身份驗證的示例流程:

  1. 請求到達伺服器。
  2. 身份驗證中介軟體呼叫預設處理程式的Authenticate方法,並使用任何可用資訊填充HttpContext.User物件。
  3. 請求到達控制器操作。
  4. 如果操作未使用[Authorize]屬性修飾,請顯示頁面並在此處停止。
  5. 如果動作飾以[Authorize]中,auth過濾器檢查使用者是否被認證。
  6. 如果使用者不是,則身份驗證過濾器呼叫Challenge,重定向到相應的登入授權。
  7. 一旦登入機構將使用者引導迴應用程式,auth過濾器就會檢查使用者是否有權檢視該頁面。
  8. 如果使用者已獲得授權,則會顯示該頁面,否則會呼叫“禁止”,這會顯示“未授權”頁面。

程式碼示例

注意:您可以訪問https://gitlab.com/free-time-programmer/tutorials/demystify-aspnetcore-auth/tree/master訪問和下載此示例應用程式的原始碼。

此示例不是一個功能齊全的Web應用程式。它使用簡單的POCO來儲存使用者名稱和密碼,不是編寫Web應用程式的安全或功能方式,並且不保證在簡單登入和退出的情況下正確執行。目的是通過程式碼示例說明身份驗證流程。在此示例中,我刪除了與主題無關的所有程式碼。

class Startup

當應用程式首次啟動時,它會觸發Startup類中的ConfigureServices()和Configure()方法。在aspnetcore 2.0中,身份驗證處理程式完全在ConfigureServices方法中註冊和配置,並且只需一次呼叫即可在Configure方法中全部啟用它們。

public void ConfigureServices(IServiceCollection services) {
    //Adds cookie middleware to the services collection and configures it
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options => options.LoginPath = new PathString("/account/login"));

    ...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
    ...

    //Adds the authentication middleware to the pipeline
    app.UseAuthentication();

    ...
}

ConfigureServicesAddAuthentication方法中呼叫將身份驗證中介軟體新增到服務集合中。有一個鏈式方法呼叫,AddCookie它添加了一個Cookies身份驗證處理程式,其中包含一個配置為身份驗證middlware的選項。

在該Configure方法中,UseAuthentication呼叫該方法以將認證中介軟體新增到執行管道。這使得身份驗證中介軟體可以在每個請求上實際執行。

class ApplicationUser

該應用程式需要使用者的表示。這個簡單的類儲存使用者的使用者名稱和密碼。

public class ApplicationUser {
    public string UserName { get; set; }
    public string Password { get; set; }

    public ApplicationUser() { }
    public ApplicationUser(string username, string password) {
        this.UserName = username;
        this.Password = password;
    }
}

class AccountController

為了對身份驗證中介軟體和處理程式執行任何有意義的操作,需要執行一些操作。下面是一個MVC控制器AccountController,其中包含執行登入和退出工作的方法。此類通過HttpContext的便捷方法處理動詞SignInSignOut,後者又呼叫指定的或預設的auth處理程式上的SignInAsyncSignOutAsync方法。

public class AccountController : Controller {
    //A very simplistic user store. This would normally be a database or similar.
    public List<ApplicationUser> Users => new List<ApplicationUser>() {
        new ApplicationUser { UserName = "darkhelmet", Password = "vespa" },
        new ApplicationUser{ UserName = "prezscroob", Password = "12345" }
    };

    public IActionResult Login(string returnUrl = null) {
        TempData["returnUrl"] = returnUrl;
        return View();
    }

    [HttpPost]
    public async Task<IActionResult> Login(ApplicationUser user, string returnUrl = null) {
        const string badUserNameOrPasswordMessage = "Username or password is incorrect.";
        if (user == null) {
            return BadRequest(badUserNameOrPasswordMessage);
        }
        var lookupUser = Users.FirstOrDefault(u => u.UserName == user.UserName);

        if (lookupUser?.Password != user.Password) {
            return BadRequest(badUserNameOrPasswordMessage);
        }

        var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
        identity.AddClaim(new Claim(ClaimTypes.Name, lookupUser.UserName));

        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));

        if(returnUrl == null) {
            returnUrl = TempData["returnUrl"]?.ToString();
        }

        if(returnUrl != null) {
            return Redirect(returnUrl);
        }
        
        return RedirectToAction(nameof(HomeController.Index), "Home");
    }

    public async Task<IActionResult> Logout() {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        return RedirectToAction(nameof(HomeController.Index), "Home");
    }
}

首先,該類包含一個List<string> Users取代使用者儲存並儲存兩個使用者的類。這不是一種在生產中使用的好方法,但可以輕鬆演示身份驗證程式碼而不會增加複雜性。

public List<ApplicationUser> Users => new List<ApplicationUser>() {
    new ApplicationUser { UserName = "darkhelmet", Password = "vespa" },
    new ApplicationUser{ UserName = "prezscroob", Password = "12345" }
};

還有兩種過載Login方法。第一個接受一個字串returnUrl並將其儲存在控制器的TempData儲存庫中。然後它返回操作的預設檢視。

public IActionResult Login(string returnUrl = null) {
    TempData["returnUrl"] = returnUrl;
    return View();
}

第二種Login方法執行登入使用者並獲取ApplicationUser物件和字串的工作returnUrl。此方法也使用[HttpPost]屬性進行修飾,這意味著在沒有http POST操作的情況下無法呼叫它。

該方法首先檢查以確保提交了使用者物件,並且使用者名稱和密碼與使用者儲存中的一個使用者匹配。

const string badUserNameOrPasswordMessage = "Username or password is incorrect.";
if (user == null) {
    return BadRequest(badUserNameOrPasswordMessage);
}
var lookupUser = Users.FirstOrDefault(u => u.UserName == user.UserName);

if (lookupUser?.Password != user.Password) {
    return BadRequest(badUserNameOrPasswordMessage);
}

如果這兩個條件都為真,則會建立一個新的ClaimsIdentity。在這種情況下,建構函式設定ClaimsIdentity的AuthenticationType屬性。根據Andrew Lock的說法,AuthenticationType屬性可以是任何字串,表示身份的驗證方式。

在這種情況下,我將AuthenticationType設定為cookie身份驗證方案,因為我正在使用cookie身份驗證。但是,在此步驟中不需要將其設定為該值。我可以輕鬆地將其設定為“密碼”或“多通”或其他任何東西。稍後使用身份時,我可以使用此屬性來驗證我是否信任用於驗證此身份的身份驗證方法。

var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.Name, lookupUser.UserName));

然後該方法呼叫HttpContext.SignInAsync(),傳遞cookie身份驗證方案和ClaimsPrincipal從上面幾行建立的身份建立的新身份。

await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));

最後,該方法確定returnUrl(如果有的話),並將使用者重定向到returnUrl或主頁(如果沒有)。

if(returnUrl == null) {
    returnUrl = TempData["returnUrl"]?.ToString();
}
if(returnUrl != null) {
    return Redirect(returnUrl);
}

return RedirectToAction(nameof(HomeController.Index), "Home");

Login.cshtml

AccountController存在,但是使用者需要一種方法來看到登入表單,併發送其憑據到Login控制器的動作。這種觀點將解決這兩個問題。

@model ApplicationUser
@{
    <form asp-antiforgery="true" asp-controller="Account" asp-action="Login">
        User name: <input name="username" type="text" />
        Password: <input name="password" type="password" />
        <input name="submit" value="Login" type="submit" />
        <input type="hidden" name="returnUrl" value="@TempData["returnUrl"]" />
    </form>
}

class HomeController

現在有一個AccountController來記錄和退出使用者,必須有一些東西使用它。這是HomeController會員的方法。請注意,該[Authorize]屬性已新增。

[Authorize]
public IActionResult Members() {
    return View();
}

[Authorize]裝飾此操作方法的屬性將導致授權過濾器執行。該過濾器將確定使用者是否已經過身份驗證,如果沒有,則通過身份驗證處理程式發出Challenge動詞,這將提示使用者登入。

Members.cshtml

幾乎任何新的控制器操作都需要一個檢視來向用戶顯示某些內容。這是會員觀看幕後的看法。

@{
    ViewBag.Title = "Members Only";
}

<h2>@ViewBag.Title</h2>

<p>You must be a member. Congratulations, @User.Identity.Name, on your membership!</p>

_Layout.cshtml

最後,讓使用者點選登入或退出的連結或按鈕將非常有用。aspnetcore模板傾向於使用登出表單,其中包含使用javascript通過http POST方法提交表單的連結。這可能比下面的程式碼中的示例更安全,但是出於本示例的目的,簡單的連結和http GET就足夠了。

以下程式碼已新增到_Layout.cshtml檔案中的引導選單中,並將使用該佈局作為模板顯示在每個頁面上。它提供登入和登出選項。

@if(User.Identity.IsAuthenticated) {
    <li><a asp-area="" asp-controller="Account" asp-action="Logout">Logout</a></li>
} else {
    <li><a asp-area="" asp-controller="Account" asp-action="Login">Login</a></li>
}

結論

auth系統很有趣,設計精良。它非常易於擴充套件,可以輕鬆使用自定義身份驗證處理程式。瞭解此係統如何在幕後工作是使用它超出模板預設值的第一步。通過使用元件本身而不僅僅依賴模板和便捷方法,可以實現各種自定義身份驗證過程。現在去寫程式碼。