【翻译】asp.net core认证和授权解密

asp.net core2.1认证和授权解密

本篇文章翻译自:https://digitalmccullough.com/posts/aspnetcore-auth-system-demystified.html

全文概述了asp.net core2.0的认证和授权系统是如何运作的,给读者一个较为清晰的解释,感觉不错,所以翻译出来供大家参考。

前面是一堆作者的心路历程和碎碎念,就不翻了,开始正文。

要理解这个系统首先需要理解它的组件和行为。这些组件被拆分成identity、verbs(命令或动作)、authentication handlers和middleware。我会就这里面的每一个组件进行逐一的讲解并在后面的实例中证明他们是如何一起运作的。因为asp.net core的大多数authentication handler都是Cookies auth handler,所以这个示例会使用cookie authentication。

Identity

要理解认证(authentication)是如何工作的话首先要理解在asp.net core2.0中identity是一个什么东西。一句话概括就是有三个类来代表了一个用户的identity(身份):Claim,ClaimsIdentity和ClaimsPrincipal。

Claims

一个Claim代表了用户的一个信息点。它可以是用户的姓,用户的名字,用户的家庭住址,用户的年龄或者其他关于用户的相关信息,总之,它是用户的一个信息点。一个Claim只能包含一个信息点。

在asp.net core2.0中有一个Claim类。它最普通的构造函数接收两个字符串,type和value。type参数是Claim的类型或名字,value参数是这个Claim所代表的用户信息的值。

例如:下面的代码创建了两个Claim,其中一个类型为“FullName”,值为”Pangjianxin“,另一个类型为ClaimTypes.Email,值为343516704@qq.com

//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, "dark.helmet@spaceballs.com");
//ClaimTypes里面包含了一些默认的常量,用来方便的构建Claim

ClaimsIdentity

一个身份(Identity)代表了一种身份证明的形式,或者换种说法,它是一种证明你是谁的方式。在现实生活中,一个身份可以是驾照,在asp.net core中,它是一个ClaimsIdentity类。这个类代表了一个数字形式的身份证明。

一个单一的ClaimsIdentity实例可以被认证(authenticated)也可以不被认证,按照Andrew Lock's 在他的这篇文章中所讲的(https://andrewlock.net/introduction-to-authentication-with-asp-net-core/),简单的设置以一下AuthenticationType属性(在ClaimsIdentity中定义的属性)可以自动的确保IsAuthenticated (在ClaimsIdentity里面的另一个属性,用来判断是否认证)属性的值为true。这是因为如果你以任何一种方式认证了这个身份(ClaimsIdentity),那么,它必须是已被认证的(then it must, by definition, be authenticated.)。

一个陌生人朝你走来并莫名其妙的想你介绍他自己,这个陌生人对于你来说就相当于是一个未认证的CLaimsIdentity。洛克(上面提到的那个人,Lock)写道,这可能会对客人的购物车(在现实生活中,可能在登陆前就能添加一些东西到购物车里)或者其他类似的东西很有用。一个驾照包含了许多的Claim:first name,last name,birthdate等等。类似的,一个ClaimsIdentity也可以包含许多关于用户的Claim。

ClaimsPrincipal

一个主体(Principal)代表了一个真实的用户。它可以包含一个或者多个CLaimsIdentity,就像现实生活中一个人可以有驾照,教师资格证,护照等等,此外,他还必须有一个身份证。每一个证件(ClaimsIdentity)用于一个不同的目的并且可能包含了一组唯一的Claim。但是所有的证件都以不同的方式证明了同一个人。

总结来说,一个CLaimsPrincipal代表了一个用户,他包含了一个或者多个ClaimsIdentity,ClaimsIdentity又代表了一种形式的身份证明,ClaimsIdentity又包含了一个或多个Claim,Claim代表一个用户的信息点。ClaimsPrincipal是HttpContext.SiginAsync方法(准确的来说,是扩展方法)的参数,并且在方法内部被传递给了AuthenticationHandler。

Verbs

verb是动词,或者叫做命令、行为。在asp.net core 2.0的认证系统(auth system)中一共有5个verb被调用,并且他们在调用顺序上是没有要求的。他们都是单独的调用,并且他们之间也没有任何交互,但是,当使用在一起时,用户可以登录并访问页面,或者被拒绝。下面是对这5个verb的相关职责的一个简明的描述,在文章的后面还有详细的说明:

注意:这些都是行为,不是方法,但是有一些相同命名的方法实现了这些行为

Authenticate,认证

如果存在的话(比如解码一个用户的Cookie),获取一个用户的信息。

Challenge

要求用户请求认证的过程(比如显示登陆页面)

SignIn

持久化用户的信息并放在某个地方(比如写入Cookie)

SignOut

删除用户已持久化的信息(比如删除Cookie)

Forbid

包含两种情况,一种情况是对于未认证的用户来说,阻止该用户访问相关的资源,另一种情况是对于已认证但是权限不够(这就涉及到授权athorization了)的用户(比如重定向到“权限不足”的页面)。

Authentication Handlers

Authentication handlers(注意是复数)是真正实现了上面所述的五种动作的组件。asp.net core提供的默认的auth handler是Cookies authentication handler,这个东西实现了上面所描述的5个动作。需要注意的是,一个auth handler不必实现上述的全部动作(verb),例如,Oauth handler(oauth是一种授权的协议,在.net core中,实现该协议的开源组件是identity server4,该开源组件同时实现了openid connect,后者是oauth基础上建立的认证协议)不实现SignIn动作,而是将该责任传递给另一个auth handler,比如cookie auth handler。

为了使用并且和认证方案(schemes)关联,authentication handler必须被注册到认证系统中(auth system)。一个scheme就是一个字符串,它在一个身份验证处理程序的字典中识别一个惟一的auth handler。对于Cookies auth handler来说,默认的scheme就是“Cookies”,但是他可以被换成任何其他的字符串。多个身份验证处理程序可以并排使用,有时(比如在上面提到的Oauth handler)使用其他身份验证处理程序提供的功能。

Authentication Middleware

中间件可以被插入到启动管道队列中,每一个http请求都从他们经过。这篇文章只关心认证中间件。这些代码(认证中间件)检查每次请求中的用户是否已认证,回想一下,认证动作获取了用户的信息(从cookie中),但是仅限这些信息存在的情况下。当请求发起,认证中间件调用默认方案代表的那个auth handler来执行认证的代码。Auth handler将信息返回给认证中间件,然后再将信息填充到HttpContext.User属性上面。

Authentication and Authorization Flow

为了能够成功的认证并且授权一个用户访问资源,所有的这些组件必须能够在认证系统中一起使用。这个过程从一个未认证的用户发起一个访问被保护(要求授权)资源的请求开始。

下面展示一个Cookie认证的例程:

1、请求到达了服务器。

2、认证中间件调用了默认的handler上的Authenticate方法并且将任何有用的信息装在到了HttpContext.Use对象上面。

3、请求到达了控制器的action上面。

4、如果action没有被[Authorize]装饰,就直接通过请求并显示相应页面,结束。

5、如果action被[Authorize]修饰的话,授权过滤器(auth filter)检查用户是否已经认证过了。

6、如果用户没有认证,授权过滤器(auth filter)会执行Challenge动作,重定向到合适的登陆认证页面。

7、一旦登陆成功后将用户重定向回去,授权过滤器检查用户是否被允许(authorized)访问相关页面。

8、如果用户被允许访问,就展示这个页面,否则会调用Forbid动作。

代码示例

这个示例不打算做一个全功能的web应用。它使用了一个简单的POCO类来存储用户名和密码,总之就是简单的展现本文的内容,别的不管了。这个示例就是为了阐述认证流程的。示例中会删除和次无关的东西。

Startup类

当应用第一次启动它会触发startup类中的ConfigureService和Configure方法。在aspnetcore2.0中,认证handler是在ConfigureService方法中进行配置的。并且在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();

    ...
}

在ConfigureService方法中AddAuthentication方法将认证服务(中间件?)添加到ServicesCollection上面(DI系统)。并且可以用链式调用的方式将Cookie认证handler添加到认证中间件上。

在Configure方法中UseAuthentication的调用将认证中间件添加到管道上,这样就可以在每个请求中都执行认证了。

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;
    }
}

AccountController类

为了用认证中间件和handler中一些有意义的事,我们涉及了一些action。下面这个AccountController中的方法执行了登陆和登出方法。这个类(AccountController)通过HttpContext类的一些扩展方法依次调用SiginAsync和SignoutAsync方法(用指定的或者默认的handler)执行了登陆和登出。

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");
    }
}

首先,这个类包含了一个关于用户的列表,由两个重载的Login方法,一个接收了一个returnUrl的参数并把它存储在TempData中。第二个Login方法将用户登入。这个login方法被一个[HttpPost]修饰,方法首先检查确保登陆用户输入的信息有效,如果有效,他创建一个ClaimsIdentity。在ClaimsIdentity类的构造方法中接收一个设置AuthenticationType属性的参数,这个参数可以是任意字符串,代表了Identity被检查过了。

我只是传入了一个cookie认证的scheme因为我正在使用cookie认证。但是你可以将他设置为任何值。稍后使用该标识时,我可以使用该属性来验证我是否信任用于认证该Identity的认证方法。

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

然后方法调用了HttpContext.SignInAsync(),将一个cookie认证的scheme和一个ClaimPrincipal传入。

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

最后,该方法确定了returnUrl,如果有returnUrl的话,则会将用户重定向到returnUrl,如果没有的话,重定向到主页。

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

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

Login.cshtml

创建一个Login页面,对应Controller中的那个login action。

@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>
}

HomeController类

在HomeController类中创建一个Members方法,加上[Authorize]标签

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

[Authorize]特性会导致授权过滤器的调用。这个过滤器会决定用户是否已经登陆并且如果没有登陆会通过认证handler调用Challenge这个verb(动作),这个动作导致用户被要求登陆。

Members.cshtml

 接下来创建Members对应的视图:

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

<h2>@ViewBag.Title</h2>

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

_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>
}

结论

认证系统很有趣,设计也很好。它是非常可扩展的,并且很容易使用自定义身份验证处理程序。理解这个系统在引擎盖下是如何工作的,是在模板默认之外使用它的第一步。通过使用组件本身而不是仅仅依赖于模板和便利方法,可以使用各种定制身份验证过程。现在就用吧!

posted @ 2018-07-26 16:28  wall-ee  阅读(5282)  评论(5编辑  收藏  举报