再谈使用IdentityServer实现ASP.NET Core Web API的认证与授权

By | 2022年7月12日

在《使用Ocelot、IdentityServer4、Spring Cloud Eureka搭建微服务网关:Step by Step(一)》一文中,我曾经介绍了如何使用IdentityServer4对ASP.NET Core Web API的访问进行身份认证与授权。本文将更加深入讨论有关ApiResource与ApiScope相关的内容。

注意:从2022年12月3日开始,社区版的IdentityServer4将不再被支持,所有的后续开发工作都将在Duende Software组织下进行。这也就意味着,IdentityServer将采用商用许可,不过,对于个人的开发、测试需求,以及年利润小于100万美元的个人或公司项目,IdentityServer仍然免费。因此,在本文的介绍中,将不再使用“IdentityServer4”来介绍,而使用“IdentityServer”这个名字。

在讨论IdentityServer中的相关概念时,可以思考一下,一个基于角色的权限管理系统(RBAC)是如何实现的。总的来说,RBAC关心的是:用户或者用户组,对于什么样的资源,具有什么样的访问权限。比如:用户组A对于天气服务具有读取的权限,而对于国家信息查询服务则没有访问权限,而用户组B则恰好相反。

在IdentityServer中,可以类比地将用户组看成Client;将天气服务、国家信息查询服务看成ApiResource,于是,访问权限的标记,则被看成是ApiScope。为什么说是“访问权限的标记”而不是“访问权限”呢?因为在IdentityServer中,访问权限的定义,是通过Client的AllowedScope以及API的Audience来设定的。ApiScope定义了一组标记,ApiResource可以使用这些标记来决定它能够支持哪些类型的权限设定,而Client又可以指定它能够使用哪些ApiScope来访问API。

下面的代码中定义了3个ApiResource,通常情况下,一个ApiResource可以对应一个需要被IdentityServer鉴权的ASP.NET Core Web API应用程序:

public static IEnumerable<ApiResource> ApiResources =>
    new ApiResource[]
    {
        new ApiResource("management", "Meeting Room Management API")
        {
            Scopes = { "management.read", "management.create", "management.update", "management.delete" }
        },
        new ApiResource("reservation", "Meeting Room Reservation API")
        {
            Scopes = { "reservation.read", "reservation.create", "reservation.update", "reservation.delete" }
        },
        new ApiResource("audit", "Audit API")
        {
            Scopes = { "audit.query", "audit.insert" }
        }
    };

在每个ApiResource定义中,它包含了三个信息:ApiResource的名称、描述,以及它所包含的ApiScope。例如:对于audit ApiResource,它的名称是audit,描述是Audit API,然后它所支持的ApiScope包括audit.query和audit.insert。ApiScope的定义如下:

public static IEnumerable<ApiScope> ApiScopes =>
    new ApiScope[]
    {
        // Management API scopes
        new ApiScope("management.read", "Retrieves the meeting room information."),
        new ApiScope("management.create", "Creates the meeting room."),
        new ApiScope("management.update", "Updates the meeting room."),
        new ApiScope("management.delete", "Deletes the meeting room."),

        // Reservation API scopes
        new ApiScope("reservation.read", "Retrieves the meeting room reservation information."),
        new ApiScope("reservation.create", "Reserves a meeting room."),
        new ApiScope("reservation.update", "Updates the reservation."),
        new ApiScope("reservation.delete", "Cancels the reservation."),

        // Audit API scopes
        new ApiScope("audit.query", "Queries the audit information."),
        new ApiScope("audit.insert", "Inserts a audit record.")
    };

可以看到,ApiScope其实就是定义了一个字符串标记,这个标记建立了Client与ApiResource之间的关系,不仅如此,ApiScope还会以Claim的形式传递给受保护的Web API,以便完成API访问授权。

那么对于一个IdentityServer Client,它就可以指定所允许使用的ApiScope有哪些:

public static IEnumerable<Client> Clients =>
    new Client[]
    {
        // m2m client credentials flow client
        new Client
        {
            ClientId = "m2m.client",
            ClientName = "Client Credentials Client",

            AllowedGrantTypes = GrantTypes.ClientCredentials,
            ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },
            AllowedScopes = { "management.read", "management.create" }
        },
    };

在上面的代码中,m2m.client这个Client它指定了AllowedScope只能是management.read和management.create,因此,对于这个Client而言,它只能用来访问management这个ApiResource(其实也就是Audience为management的ASP.NET Core Web API应用程序),并且仅能访问这个ApiResource中被标记为management.read和management.create的API端点。这一部分后续会介绍。不要忘记在ASP.NET Core中使用IdentityServer:

builder.Services
    .AddIdentityServer(options =>
    {
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseInformationEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseSuccessEvents = true;
        options.EmitStaticAudienceClaim = true;
    })
    .AddInMemoryIdentityResources(Config.IdentityResources)
    .AddInMemoryApiResources(Config.ApiResources)
    .AddInMemoryApiScopes(Config.ApiScopes)
    .AddInMemoryClients(Config.Clients)
    .AddAspNetIdentity<ApplicationUser>();

配置好IdentityServer应用程序后,启动应用程序,然后使用Postman来调用/connect/token这个API端点,以获得Jwt Access Token:

对应的cURL命令如下:

curl -L -X POST 'https://localhost:9001/connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=m2m.client' \
--data-urlencode 'client_secret=511536EF-F270-4058-80CA-1C89C192F69A' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'response_type=id_token' \
--data-urlencode 'scope=management.read management.create'

此时,会请求/connect/token端点,通过传入client_id、client_secret、grant_type、response_type以及scope这些参数,来获得Jwt Access Token。这里的scope必须是之前Client中定义的AllowedScopes的子集,或者与之相同,否则会出现invalid_scope的错误。其它的参数也都是与Client的设置相匹配,因此,通过调用这个API获得的Jwt Access Token就只能访问management API,并且只能访问被标记为“读取”(read)和“创建”(create)的API端点。不过,这还需要配合受保护的Web API应用程序中对于授权(Authorization)的实现。

接下来,配置我们的Web API应用程序,使其受保护于IdentityServer。在Program.cs中,加入下面的代码:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://localhost:9001";
        options.Audience = "management";
        options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
    });
app.UseAuthentication();

注意这里的Authority为IdentityServer的地址,Audience则为IdentityServer中所设置的对应的ApiResource的名字。上面所获得的Access Token所包含的scope信息为management.read和management.create,两者都是management这个ApiResource所定义的ApiScope,因此,这个Access Token是可以访问我们的Web API的,因为它的Audience正是management。

接下来,在Controller上加上Authorize特性即可:

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class MeetingRoomsController : ControllerBase
{
    // ...
}

此时,如果使用Postman来访问MeetingRoomsController中的Get方法,则会返回401 Unauthorized的错误:

现在,在Postman中,将Auth的Type切换为Bearer Token,然后将Token的值设置为上面所获得的Access Token,再次调用上面的API,此时返回200 OK,调用成功:

ASP.NET Core Web API应用程序还可以基于IdentityServer中所设定的ApiScope来进行基于Claim的访问授权。比如:这里的Access Token仅具有management.read和management.create这两个scope,因此,它理应只能访问Web API应用程序中具有读取和创建功能的API,这可以通过配置Web API应用程序来实现。

在Web API应用程序的Program.cs中,加入下面的代码:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("management.read", policy => policy.RequireClaim("scope", "management.read"));
    options.AddPolicy("management.create", policy => policy.RequireClaim("scope", "management.create"));
    options.AddPolicy("management.update", policy => policy.RequireClaim("scope", "management.update"));
    options.AddPolicy("management.delete", policy => policy.RequireClaim("scope", "management.delete"));
});

这段代码在Web API授权体系中加入了4个策略(Policy),这些Policy会要求Claims中有一个名为scope的Claim,并且具有所对应的值。于是,被标记为management.read这一授权策略的API,在被访问前,ASP.NET Core Web API就会检查Claims中是否有一个的scope为management.read,如果没有,则返回403 Forbidden。要使用Authorization Policy对API进行标记,需要在Controller中的不同的Action上使用Authorize特性来标记,它告诉Web API框架,当前这个API需要采用什么样的授权策略。

下面的代码展示了如何使用Authorize特性来针对不同的API采用不同的授权策略:

[ApiController]
[Route("api/[controller]")]
public class MeetingRoomsController : ControllerBase
{
    [HttpPost]
    [Authorize("management.create")]
    public async Task<IActionResult> CreateMeetingRoom([FromBody] MeetingRoom meetingRoom)
    {
        // ...
    }

    [HttpDelete("{id}")]
    [Authorize("management.delete")]
    public async Task<IActionResult> DeleteMeetingRoom(long id)
    {
        // ...
    }

    [HttpGet("{id}")]
    [Authorize("management.read")]
    public IActionResult GetMeetingRoom(long id)
    {
        // ...
    }

    [HttpGet]
    [Authorize("management.read")]
    public IActionResult GetMeetingRooms([FromQuery] int pageSize = 10, int pageNumber = 0)
    {
        // ...
    }

    [HttpPatch("{id}")]
    [Authorize("management.update")]
    public async Task<IActionResult> PatchMeetingRoom(long id, [FromBody] JsonPatchDocument<MeetingRoom> patchDoc)
    {
        // ...
    }
}

现在,如果使用同样的Access Token来访问GetMeetingRoom和GetMeetingRooms这两个API,则都能成功。然而,如果访问DeleteMeetingRoom这个API,则返回403 Forbidden。因为这个Access Token没有包含management.delete这个scope:

到此为止,我们已经在ASP.NET Core Web API下实现了基于IdentityServer的ApiResource和ApiScope的认证与授权。

(总访问量:677;当日访问量:1)

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据