快速理解asp.netcore的认证与授权(代码片段)

dotNET跨平台 dotNET跨平台     2022-11-29     493

关键词:

ASP.NET Core的认证与授权已经不是什么新鲜事了,微软官方的文档对于如何在ASP.NET Core中实现认证与授权有着非常详细深入的介绍。但有时候在开发过程中,我们也往往会感觉无从下手,或者由于一开始没有进行认证授权机制的设计与规划,使得后期出现一些混乱的情况。这里我就尝试结合一个实际的例子,从0到1来介绍ASP.NET Core中如何实现自己的认证与授权机制。

当我们使用Visual Studio自带的ASP.NET Core Web API项目模板新建一个项目的时候,Visual Studio会问我们是否需要启用认证机制,如果你选择了启用,那么Visual Studio会在项目创建的时候,加入一些辅助依赖和一些辅助类,比如加入对Entity Framework以及ASP.NET Identity的依赖,以帮助你实现基于Entity Framework和ASP.NET Identity的身份认证。如果你还没有了解过ASP.NET Core的认证与授权的一些基础内容,那么当你打开这个由Visual Studio自动创建的项目的时候,肯定会一头雾水,不知从何开始,你甚至会怀疑自动创建的项目中,真的是所有的类或者方法都是必须的吗?所以,为了让本文更加简单易懂,我们还是选择不启用身份认证,直接创建一个最简单的ASP.NET Core Web API应用程序,以便后续的介绍。

新建一个ASP.NET Core Web API应用程序,这里我是在Linux下使用JetBrains Rider新建的项目,也可以使用标准的Visual Studio或者VSCode来创建项目。创建完成后,运行程序,然后使用浏览器访问/WeatherForecast端点,就可以获得一组随机生成的天气及温度数据的数组。你也可以使用下面的curl命令来访问这个API:

1

curl -X GET "http://localhost:5000/WeatherForecast" -H  "accept: text/plain"

现在让我们在WeatherForecastController的Get方法上设置一个断点,重新启动程序,仍然发送上述请求以命中断点,此时我们比较关心User对象的状态,打开监视器查看User对象的属性,发现它的IsAuthenticated属性为false:

在很多情况下,我们可能并不需要在Controller的方法中获取认证用户的信息,因此也从来不会关注User对象是否真的处于已被认证的状态。但是当API需要根据用户的某些信息来执行一些特殊逻辑时,我们就需要在这里让User的认证信息处于一种合理的状态:它是已被认证的,并且包含API所需的信息。这就是本文所要讨论的ASP.NET Core的认证与授权。

认证

应用程序对于使用者的身份认定包含两部分:认证授权。认证是指当前用户是否是系统的合法用户,而授权则是指定合法用户对于哪些系统资源具有怎样的访问权限。我们先来看如何实现认证。

在此,我们单说由ASP.NET Core应用程序本身实现的认证,不讨论具有统一Identity Provider完成身份认证的情况(比如单点登录),这样的话就能够更加清晰地了解ASP.NET Core本身的认证机制。接下来,我们尝试在ASP.NET Core应用程序上,实现Basic认证。

Basic认证需要将用户的认证信息附属在HTTP请求的Authorization的头(Header)上,认证信息是一串由用户名和密码通过BASE64编码后所产生的字符串,例如,当你采用Basic认证,并使用daxnet和password作为访问WeatherForecast API的用户名和密码时,你可能需要使用下面的命令行来调用WeatherForecast:

1

curl -X GET "http://localhost:5000/WeatherForecast" -H  "accept: text/plain" -H "Authorization: Basic ZGF4bmV0OnBhc3N3b3Jk"

在ASP.NET Core Web API中,当应用程序接收到上述请求后,就会从Request的Header里读取Authorization的信息,然后BASE64解码得到用户名和密码,然后访问数据库来确认所提供的用户名和密码是否合法,以判断认证是否成功。这部分工作通常可以采用ASP.NET Core Identity框架来实现,不过在这里,为了能够更加清晰地了解认证的整个过程,我们选择自己动手来实现。

首先,我们定义一个User对象,并且预先设计好几个用户,以便模拟存储用户信息的数据库,这个User对象的代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class User

    public string UserName get; set;

    public string Password get; set;

    public IEnumerable<string> Roles get; set;

    public int Age get; set;

    public override string ToString() => UserName;

    public static readonly User[] AllUsers =

        new User

        

            UserName = "daxnet", Password = "password", Age = 16, Roles = new[] "admin", "super_admin"

        ,

        new User

        

            UserName = "admin", Password = "admin", Age = 29, Roles = new[] "admin"

        

    ;

该User对象包括用户名、密码以及它的角色名称,不过暂时我们不需要关心角色信息。User对象还包含一个静态字段,我们将它作为用户信息数据库来使用。

接下来,在应用程序中添加一个AuthenticationHandler,用来获取Request Header中的用户信息,并对用户信息进行验证,代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationSchemeOptions>

    public BasicAuthenticationHandler(

        IOptionsMonitor<BasicAuthenticationSchemeOptions> options,

        ILoggerFactory logger,

        UrlEncoder encoder,

        ISystemClock clock) : base(options, logger, encoder, clock)

    

    

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()

    

        if (!Request.Headers.ContainsKey("Authorization"))

        

            return Task.FromResult(AuthenticateResult.Fail("Authorization header is not specified."));

        

        var authHeader = Request.Headers["Authorization"].ToString();

        if (!authHeader.StartsWith("Basic "))

        

            return Task.FromResult(

                AuthenticateResult.Fail("Authorization header value is not in a correct format"));

        

        var base64EncodedValue = authHeader["Basic ".Length..];

        var userNamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(base64EncodedValue));

        var userName = userNamePassword.Split(':')[0];

        var password = userNamePassword.Split(':')[1];

        var user = User.AllUsers.FirstOrDefault(u => u.UserName == userName && u.Password == password);

        if (user == null)

        

            return Task.FromResult(AuthenticateResult.Fail("Invalid username or password."));

        

        var claims = new[]

        

            new Claim(ClaimTypes.NameIdentifier, user.UserName),

            new Claim(ClaimTypes.Role, string.Join(',', user.Roles)),

            new Claim(ClaimTypes.UserData, user.Age.ToString())

        ;

        var claimsPrincipal =

            new ClaimsPrincipal(new ClaimsIdentity(

                claims,

                "Basic",

                ClaimTypes.NameIdentifier, ClaimTypes.Role));

        var ticket = new AuthenticationTicket(claimsPrincipal, new AuthenticationProperties

        

            IsPersistent = false

        , "Basic");

        return Task.FromResult(AuthenticateResult.Success(ticket));

    

在上面的HandleAuthenticateAsync代码中,首先对Request Header进行合法性校验,比如是否包含Authorization的Header,以及Authorization Header的值是否合法,然后,将Authorization Header的值解析出来,通过Base64解码后得到用户名和密码,与用户信息数据库里的记录进行匹配,找到匹配的用户。接下来,基于找到的用户对象,创建ClaimsPrincipal,并基于ClaimsPrincipal创建AuthenticationTicket然后返回。

这段代码中有几点值得关注:

  1. BasicAuthenticationSchemeOptions本身只是一个继承于AuthenticationSchemeOptions的POCO类。AuthenticationSchemeOptions类通常是为了向AuthenticationHandler提供一些输入参数。比如,在某个自定义的用户认证逻辑中,可能需要通过环境变量读入字符串解密的密钥信息,此时就可以在这个自定义的AuthenticationSchemeOptions中增加一个Passphrase的属性,然后在Startup.cs中,通过service.AddScheme调用将从环境变量中读取的Passphrase的值传入

  2. 除了将用户名作为Identity Claim加入到ClaimsPrincipal中之外,我们还将用户的角色(Role)用逗号串联起来,作为Role Claim添加到ClaimsPrincipal中,目前我们暂时不需要涉及角色相关的内容,但是先将这部分代码放在这里以备后用。另外,我们将用户的年龄(Age)放在UserData claim中,在实际中应该是在用户对象上有该用户的出生日期,这样比较合理,然后这个出生日期应该放在DateOfBirth claim中,这里为了简单起见,就先放在UserData中了

  3. ClaimsPrincipal的构造函数中,可以指定哪个Claim类型可被用作用户名称,而哪个Claim类型又可被用作用户的角色。例如上面代码中,我们选择NameIdentifier类型作为用户名,而Role类型作为用户角色,于是,在接下来的Controller代码中,由NameIdentifier这种Claim所指向的字符串值,就会被看成用户名而被绑定到Identity.Name属性上

回过头来看看BasicAuthenticationSchemeOptions类,它的实现非常简单:

1

2

3

4

public class BasicAuthenticationSchemeOptions : AuthenticationSchemeOptions

接下来,在Startup.cs文件里,修改ConfigureServices和Configure方法,加入Authentication的支持:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public void ConfigureServices(IServiceCollection services)

    services.AddControllers();

    services.AddSwaggerGen(c =>

    

        c.SwaggerDoc("v1", new OpenApiInfo Title = "WebAPIAuthSample", Version = "v1" );

    );

    services.AddAuthentication("Basic")

        .AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>(

            "Basic", options => );

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

    if (env.IsDevelopment())

    

        app.UseDeveloperExceptionPage();

        app.UseSwagger();

        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPIAuthSample v1"));

    

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthentication();

    app.UseEndpoints(endpoints => endpoints.MapControllers(); );

现在,运行应用程序,在WeatherForecastController的Get方法上设置断点,然后执行上面的curl命令,当断点被命中时,观察this.User对象可以发现,IsAuthenticated属性变为了true,Name属性也被设置为用户名:

大多数身份认证框架会提供一些辅助方法来帮助开发人员将AuthenticationHandler注册到应用程序中,例如,基于JWT持有者身份认证的框架会提供一个AddJwtBearer的方法,将JWT身份认证机制加入到应用程序中,它本质上也是调用AddScheme方法来完成AuthenticationHandler的注册。在这里,我们也可以自定义一个AddBasicAuthentication的扩展方法:

1

2

3

4

5

6

7

public static class Extensions

    public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder)

        => builder.AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>(

            "Basic",

            options => );

然后修改Starup.cs文件,将ConfigureServices方法改为下面这个样子:

1

2

3

4

5

6

7

8

9

public void ConfigureServices(IServiceCollection services)

    services.AddControllers();

    services.AddSwaggerGen(c =>

    

        c.SwaggerDoc("v1", new OpenApiInfo Title = "WebAPIAuthSample", Version = "v1" );

    );

    services.AddAuthentication("Basic").AddBasicAuthentication();

这样做的好处是,你可以为开发人员提供更多比较有针对性的配置认证机制的编程接口,这对于一个认证模块/框架的开发是一个很好的设计。

在curl命令中,如果我们没有指定Authorization Header,或者Authorization Header的值不正确,那么WeatherForecast API仍然可以被调用,只不过IsAuthenticated属性为false,也无法从this.User对象得到用户信息。其实,阻止未认证用户访问API并不是认证的事情,API被未认证(或者说未登录)用户访问也是合理的事情,因此,要实现对于未认证用户的访问限制,就需要进一步实现ASP.NET Core Web API的另一个安全控制组件:授权

授权

认证相比,授权的逻辑会比较复杂:认证更多是技术层面的事情,而授权则更多地与业务相关。市面上常见的认证机制顶多也就是那么几种或者十几种,而授权的方式则是多样化的,因为不同app不同业务,对于app资源访问的授权需求是不同的。最为常见的一种授权方式就是RBAC(Role Based Access Control,基于角色的访问控制),它定义了什么样的角色对于什么资源具有怎样的访问权限。在RBAC中,不同的用户都被赋予了不同的角色,而为了管理方便,又为具有相同资源访问权限的用户设计了用户组,而将访问控制设置在用户组上,更进一步,组和组之间还可以有父子关系。

请注意上面的黑体字,每一个黑体标注的词语都是授权相关的概念,在ASP.NET Core中,每一个授权需求(Authorization Requirement)对应一个实现IAuthorizationRequirement的类,并由AuthorizationHandler负责处理相应的授权逻辑。简单地理解,授权需求表示什么样的用户才能够满足被授权的要求,或者说什么样的用户才能够通过授权去访问资源。一个授权需求往往仅定义并处理一种特定的授权逻辑,ASP.NET Core允许将多个授权需求组合成授权策略(Authorization Policy)然后应用到被访问的资源上,这样的设计可以保证授权需求的设计与实现都是小粒度的,从而分离不同授权需求的关注点。在授权策略的层面,通过组合不同授权需求从而达到灵活实现授权业务的目的。

比如:假设app中有的API只允许管理员访问,而有的API只允许满18周岁的用户访问,而另外的一些API需要用户既是超级管理员又满18岁。那么就可以定义两种Authorization Requirement:GreaterThan18Requirement和SuperAdminRequirement,然后设计三种Policy:第一种只包含GreaterThan18Requirement,第二种只包含SuperAdminRequirement,第三种则同时包含这两种Requirement,最后将这些不同的Policy应用到不同的API上就可以了。

回到我们的案例代码,首先定义两个Requirement:SuperAdminRequirement和GreaterThan18Requirement:

1

2

3

4

5

6

public class SuperAdminRequirement : IAuthorizationRequirement

public class GreaterThan18Requirement : IAuthorizationRequirement

然后分别实现SuperAdminAuthorizationHandle和GreaterThan18AuthorizationHandler:



实现逻辑也非常清晰:在GreaterThan18AuthorizationHandler中,通过UserData claim获得年龄信息,如果年龄大于18,则授权成功;在SuperAdminAuthorizationHandler中,通过Role claim获得用户所处的角色,如果角色中包含super_admin,则授权成功。接下来就需要将这两个Requirement加到所需的Policy中,然后注册到应用程序里:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

public void ConfigureServices(IServiceCollection services)

    services.AddControllers();

    services.AddSwaggerGen(c =>

    

        c.SwaggerDoc("v1", new OpenApiInfo Title = "WebAPIAuthSample", Version = "v1" );

    );

    services.AddAuthentication("Basic").AddBasicAuthentication();

    services.AddAuthorization(options =>

    

        options.AddPolicy("AgeMustBeGreaterThan18", builder =>

        

            builder.Requirements.Add(new GreaterThan18Requirement());

        );

        options.AddPolicy("UserMustBeSuperAdmin", builder =>

        

            builder.Requirements.Add(new SuperAdminRequirement());

        );

    );

    services.AddSingleton<IAuthorizationHandler, GreaterThan18AuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler, SuperAdminAuthorizationHandler>();

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

    if (env.IsDevelopment())

    

        app.UseDeveloperExceptionPage();

        app.UseSwagger();

        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPIAuthSample v1"));

    

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthentication();

    app.UseAuthorization();

    app.UseEndpoints(endpoints => endpoints.MapControllers(); );

在ConfigureServices方法中,我们定义了两种Policy:AgeMustBeGreaterThan18和UserMustBeSuperAdmin,最后,在API Controller或者Action上,应用AuthorizeAttribute,从而指定所需的Policy即可。比如,如果希望WeatherForecase API只有年龄大于18岁的用户才能访问,那么就可以这样做:

1

2

3

4

5

6

7

8

9

10

11

12

13

[HttpGet]

[Authorize(Policy = "AgeMustBeGreaterThan18")]

public IEnumerable<WeatherForecast> Get()

    var rng = new Random();

    return Enumerable.Range(1, 5).Select(index => new WeatherForecast

        

            Date = DateTime.Now.AddDays(index),

            TemperatureC = rng.Next(-20, 55),

            Summary = Summaries[rng.Next(Summaries.Length)]

        )

        .ToArray();

运行程序,假设有三个用户:daxnet、admin和foo,它们的BASE64认证信息分别为:

  • daxnet:ZGF4bmV0OnBhc3N3b3Jk

  • admin:YWRtaW46YWRtaW4=

  • foo:Zm9vOmJhcg==

那么,相同的curl命令,指定不同的用户认证信息时,得到的结果是不一样的:

daxnet用户年龄小于18岁,所以访问API不成功,服务端返回403:

admin用户满足年龄大于18岁的条件,所以可以成功访问API:

而foo用户本身没有在系统中注册,所以服务端返回401,表示用户没有认证成功:

小结

本文简要介绍了ASP.NET Core中用户身份认证与授权的基本实现方法,帮助初学者或者需要使用这些功能的开发人员快速理解这部分内容。ASP.NET Core的认证与授权体系非常灵活,能够集成各种不同的认证机制与授权方式,文章也无法进行全面详细的介绍。不过无论何种框架哪种实现,它的实现基础也就是本文所介绍的这些内容,如果打算自己开发一套认证和授权的框架,也可以参考本文。

认证授权方案之授权初识(代码片段)

...获取或执行目标资源操作的权限。本章就来介绍一下ASP.NETCore的授权系统的简单使用。2.说明授权与身份认证是相互独立,但是,授权却需要一种身份验证机制 查看详情

在asp.netcore中什么是认证和授权

...(Authentication)和授权(Authorization)在Asp.Netcore充当了两个不同的职责。有的老伙计在理解的时候还存在误解。本文我们将会通过一些简单的例子来说明这两个概念。认证(Authentication)识别你是谁,授... 查看详情

.net6.0中使用identity框架实现jwt身份认证与授权(代码片段)

...中,我们将了解如何通过实现JWT身份认证来保护 ASP.NETCoreWebAPI 应用程序。我们还将了解如何在 ASP.NETCore 中使用授权来提供对应用程 查看详情

.net6.0中使用identity框架实现jwt身份认证与授权(代码片段)

...中,我们将了解如何通过实现JWT身份认证来保护 ASP.NETCoreWebAPI 应用程序。我们还将了解如何在 ASP.NETCore 中使用授权来提供对应用程 查看详情

理解asp.netcore(代码片段)

...来的,那么你一定对过滤器(Filter)不陌生。当然,ASP.NETCore仍然继承了过滤器机制。过滤器运行在过滤器管道中,这是一张官方的图,很好地解释了过滤器管道在HTTP请求管道中的位置:可以看到,只有当路由选择了MVCAction之后... 查看详情

如何在asp.netcore中实现一个基础的身份认证

...的代码示例下载地址> HowtoachieveabasicauthorizationinASP.NETCore如何在ASP.NETCore中实现一个基础的身份认证ASP.NET终于可以跨平台了,但是不是我们常用的ASP.NET,而是叫一个ASP.NETCore的新平台,他可以跨Windows,Linux,OSX等平台来部署你的... 查看详情

认证授权方案之授权揭秘(上篇)(代码片段)

...对授权系统已经有了初步的认识和使用,可以发现,asp.netcore为我们提供的授权策略是一个非常强大丰富且灵活的认证授权方案,能够满足大部分的授权场景。在ConfigureServices中配置服务:将授权服务添加到容器publicvoidConfigureServ... 查看详情

asp.netcore中identityserver4实战之claim详解(代码片段)

...多支持和关注。上几篇文章主要分享了IdentityServer4在Asp.NetCore3.x中的应用,在上面的几篇分享中有一部分博友问了我这么一个问题"他通过IdentityServer4来搭建授权中心网关服务,怎么才能在访问受保护的Api资源中获取到用户的... 查看详情

探索abp基础架构的横切关注点(代码片段)

...要目标之一是使你的应用“不要重复自己”(DRY),ASP.NETCore已经为一些跨领域的问题提供了一个良好的基础设施,但ABP进一步实现了自动化,让使用更加容易。本章探讨了ABP的基础设施:认证授权用户验证异常处理认证和授权... 查看详情

初识认证

...很难进行扩展,更无法与第三方认证集成,因此,在ASP.NETCore中对认证与授权进行了全新的设计,并使用基于声明的认证(claims-basedauthentication),以适应现代化应用的需求。在运行原理解剖[5]:Authentication中介绍了一下H 查看详情

asp.netcore认证原理和实现

...效,那么API端应该拒绝提供服务。在命名空间Microsoft.AspNetCore.Authentication下,定义关于验证的核心接口。对应的程序集是Microsoft.AspNetCore.Authentication.Abstractions.dll。在ASP.NET下,验证中包含3个基本操作:验证操作负责基于当前请求... 查看详情

asp.netcore按用户角色授权(代码片段)

上次老周和大伙伴们分享了有关按用户Level授权的技巧,本文咱们聊聊以用户角色来授权的事。按用户角色授权其实更好弄,毕竟这个功能是内部集成的,多数场景下我们不需要扩展,不用自己写处理代码。从功能语义上说,授... 查看详情

asp.netcore实现随处可见的基本身份认证(代码片段)

原文:ASP.NETCore实现随处可见的基本身份认证 概览    在HTTP中,基本认证(Basicaccessauthentication,简称BA认证)是一种用来允许网页浏览器或其他客户端程序在请求资源时提供用户名和口令形式的身份凭证的一种... 查看详情

asp.netcore框架探索之authentication(代码片段)

今天我们来探索一下ASP.NETCore中关于权限认证,所谓权限认证,就是通过某些方式获取到用户的信息。需要开启权限认证,我们首先需要在容器中注入认证服务,使用services.AddAuthentication。进入该方法的源码,最重要的其实就是Ad... 查看详情

理解asp.netcore:处理管道(代码片段)

理解ASP.NETCore处理管道在ASP.NETCore的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式。这导致代码的逻辑大大简化,但是,对于熟悉面向对象编程,而不是函数式编程思路的开发者来说,是... 查看详情

在 Asp.net core / 5 中使用 Policy Base 进行令牌认证

】在Asp.netcore/5中使用PolicyBase进行令牌认证【英文标题】:TokenAuthenticationUsingPolicyBaseinAsp.netcore/5【发布时间】:2016-06-1013:57:31【问题描述】:我将令牌从客户端发送到服务器“授权:承载eyJhbGciOiJodHR……”我想授权拥有令牌的用... 查看详情

asp.netcore中identityserver4实战之角色授权详解(代码片段)

...yServer4基于角色的授权详解。IdentityServer4历史文章目录Asp.NetCoreIdentityServer4中的基本概念Asp.NetCore中IdentityServer4授权中心之应用实战Asp.NetCore中IdentityServer4授权中心之自定义授权模式Asp.NetCore中IdentityServer4授权原理及刷新Token的应用... 查看详情

Asp.Net Core 中的多个 JWT 授权机构/发行人

】Asp.NetCore中的多个JWT授权机构/发行人【英文标题】:MultipleJWTauthorities/issuersinAsp.NetCore【发布时间】:2019-03-2721:37:48【问题描述】:我正在尝试使用Ocelot在ASP.NetAPI网关中获取JWT不记名身份验证,以与多个授权/颁发者合作。一个... 查看详情