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系统很有趣,设计精良。它非常易于扩展,可以轻松使用自定义身份验证处理程序。了解此系统如何在幕后工作是使用它超出模板默认值的第一步。通过使用组件本身而不仅仅依赖模板和便捷方法,可以实现各种自定义身份验证过程。现在去写代码。

猜你喜欢

转载自blog.csdn.net/qqqqqqqqqq198968/article/details/83989932