新文章 网摘 文章 随笔 日记

介绍 OData Web API 授权库

克莱门特·哈宾舒蒂

 

我想介绍一下用于 Web API 的 OData 授权库。使用 OData 模型构建器,您可以使用权限限制对 EDM 模型进行注释,以告知 API 哪些操作需要哪些权限。这些批注基于 OData 功能词汇表。但是,没有任何内容会在您的 API 上强制执行这些权限。您需要在应用程序中手动定义授权策略才能应用这些权限。使用新的授权库,只需一些设置代码,即可将这些权限自动应用于 OData API 终结点。

在本教程中,我将向您展示如何使用该库向简单的 OData API 添加授权。

该库目前在 NuGet 上以测试版本的形式提供,作为微软.AspNetCore.OData.授权,它目前仅支持基于具有终结点路由的 AspNetCore 3.1 的 OData WebApi 7.x 应用程序。

创建应用程序

  • 使用 API 模板创建一个 ASP.NET 核心 3.1 Web 应用程序。我们调用应用程序 OData授权演示
  • 安装以下 NuGet 包:
    • Microsoft.AspNetCore.OData(7.5.
    • Microsoft.EntityFrameworkCore(我们将使用 EF 核心与数据库进行交互)
    • Microsoft.EntityFrameworkCore.InMemory(对于此演示,我们将使用内存中数据库)
    • Microsoft.OData.ModelBuilder(1.0.3 或更高版本)(我们将使用 创建 OData 模型并指定权限限制)
    • Microsoft.AspNetCore.OData.Authorization(0.1.0-测试版)(WebApi 授权库,请参阅以下说明)

安装授权包时,请像往常一样在安装包时转到“管理 NuGet 包”。请务必选中包括预发行版复选框。您现在应该能够搜索 .Microsoft.AspNetCore.OData.Authorization

创建数据库上下文和模型类

出于演示目的,我们将仅创建一个实体:。Product

创建一个名为 的文件夹。将以下类文件添加到该文件夹并调用它:ModelsProduct.cs

 

using System.ComponentModel.DataAnnotations;

namespace ODataAuthorizationDemo.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }
    }
}

 

接下来,让我们创建 EF 核心数据库上下文。使用以下代码将文件添加到该文件夹:AppDbContext.csModels

using Microsoft.EntityFrameworkCore;

namespace ODataAuthorizationDemo.Models
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        {
        }

        public DbSet<Product> Products { get; set; }
    }
}

 

创建 OData 模型

对于此演示,我们将使用 OData 模型构建器包基于 C# 模型类创建 OData 模型。我们还将使用模型生成器添加权限限制。

让我们使用以下代码在文件夹中添加 一个:AppEdmModel.csModels

using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;

namespace ODataAuthorizationDemo.Models
{
    public static class AppEdmModel
    {
        public static IEdmModel GetModel()
        {
            var builder = new ODataConventionModelBuilder();
            var products = builder.EntitySet<Product>("Products");

            products.HasReadRestrictions()
                .HasPermissions(p =>
                    p.HasSchemeName("Scheme").HasScopes(s => s.HasScope("Product.Read")))
                .HasReadByKeyRestrictions(r => r.HasPermissions(p =>
                    p.HasSchemeName("Scheme").HasScopes(s => s.HasScope("Product.ReadByKey"))));

            products.HasInsertRestrictions()
                .HasPermissions(p => p.HasSchemeName("Scheme").HasScopes(s => s.HasScope("Product.Create")));

            products.HasUpdateRestrictions()
                .HasPermissions(p => p.HasSchemeName("Scheme").HasScopes(s => s.HasScope("Product.Update")));

            products.HasDeleteRestrictions()
                .HasPermissions(p => p.HasSchemeName("Scheme").HasScopes(s => s.HasScope("Product.Delete")));

            return builder.GetEdmModel();
        }
    }
}

这将创建一个模型,其中包含基于实体类型的实体集。它为该实体集上的不同 CRUD 操作添加权限限制,指定执行这些操作所需的作用域。ProductsProduct

  • 阅读产品()要求用户具有范围GET /ProductsProduct.Read
  • 通过密钥 () 读取单个产品也可以使用范围进行访问,以防用户没有范围。GET /Products(1)Product.ReadByKeyProducts.Read
  • 创建新产品 () 需要范围POST /Products(1)Product.Create
  • 更新产品 () 需要PATCH /Products(1)Product.Update
  • 删除产品( 需要DELETE /Products(1)Product.Delete)

这些限制将作为功能批注添加到 OData 模型中。授权中间件将读取这些注释,以提取不同请求所需的权限范围。默认情况下,将允许未定义任何限制的操作。

配置启动服务

现在,让我们在文件中配置不同的服务和应用程序构建器。Startup.cs

让我们修改该方法,使其如下所示:ConfgiureServices

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase("ODataAuthDemo"));

    services.AddOData();

    services.AddRouting();
}

方法:Configure

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.Expand().Filter().Count().OrderBy();
        endpoints.MapODataRoute("odata", "odata", AppEdmModel.GetModel());
    });
}
您可能需要添加以下语句:using
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNet.OData.Extensions;
using ODataAuthorizationDemo.Models;

现在,如果我们运行项目并在Postman等工具中访问端点,我们应该看到生成的EDM模型。模型应包含我们指定的不同限制类型的注释:、 和 。这些限制注释中的每一个都应具有我们定义的作用域的属性。GET /odata/$metadataReadRestrictionsInsertRestrictionsUpdateRestrictionsDeleteRestrictionsPermissions

以下是生成的注释的摘录:

<Annotations Target="Default.Container/Products">
    <Annotation Term="Org.OData.Capabilities.V1.ReadRestrictions">
        <Record>
            <PropertyValue Property="Permissions">
                <Collection>
                    <Record>
                        <PropertyValue Property="SchemeName" String="Scheme" />
                        <PropertyValue Property="Scopes">
                            <Collection>
                                <Record>
                                    <PropertyValue Property="Scope" String="Product.Read" />
                                </Record>
                            </Collection>
                        </PropertyValue>
                    </Record>
                </Collection>
            </PropertyValue>
            <PropertyValue Property="ReadByKeyRestrictions">
                <Record>
                    <PropertyValue Property="Permissions">
                        <Collection>
                            <Record>
                                <PropertyValue Property="SchemeName" String="Scheme" />
                                <PropertyValue Property="Scopes">
                                    <Collection>
                                        <Record>
                                            <PropertyValue Property="Scope" String="Product.ReadByKey" />
                                        </Record>
                                    </Collection>
                                </PropertyValue>
                            </Record>
                        </Collection>
                    </PropertyValue>
                </Record>
            </PropertyValue>
        </Record>
    </Annotation>
...
</Annotations>

添加控制器

让我们在文件夹内创建一个来实现我们的 CRUD 操作:ProductsControllerControllers

using System.Threading.Tasks;
using Microsoft.AspNet.OData;
using Microsoft.AspNetCore.Mvc;
using ODataAuthorizationDemo.Models;

namespace ODataAuthorizationDemo.Controllers
{
    public class ProductsController: ODataController
    {
        private AppDbContext _dbContext;

        public ProductsController(AppDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public IActionResult Get()
        {
            return Ok(_dbContext.Products);
        }

        public IActionResult Get(int key)
        {
            return Ok(_dbContext.Products.Find(key));
        }

        public async Task<IActionResult> Post([FromBody] Product product)
        {
            _dbContext.Products.Add(product);
            await _dbContext.SaveChangesAsync();
            return Ok(product);
        }

        public async Task<IActionResult> Update(int key, [FromBody] Delta<Product> delta)
        {
            var product = await _dbContext.Products.FindAsync(key);
            delta.Patch(product);
            _dbContext.Products.Update(product);
            await _dbContext.SaveChangesAsync();
            return Ok(product);
        }

        public async Task<IActionResult> Delete(int key)
        {
            var product = await _dbContext.Products.FindAsync(key);
            _dbContext.Products.Remove(product);
            await _dbContext.SaveChangesAsync();
            return Ok(product);
        }
    }
}

此时,您应该能够在终结点上执行 CRUD 操作。元数据中定义的权限限制不会自动应用,所有请求仍应获得授权。要应用这些权限,我们需要配置 WebApi 授权中间件。但在我们这样做之前,我们需要设置身份验证。odata/Products

设置身份验证

虽然授权系统需要设置身份验证才能正常工作,但 WebApi 授权不依赖于任何特定的身份验证实现或方案。在本演示中,我们将使用一个简单的基于 Cookie 的身份验证流,以便我们轻松测试不同的范围和方案。

在 中的方法中添加以下语句,以向应用添加身份验证:ConfigureServicesStartup.cs

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();

您可能需要以下 using 语句:

using Microsoft.AspNetCore.Authentication.Cookies;

将身份验证中间件添加到 方法中,在 和 之间(顺序很重要):Configureapp.UseRouting()app.UseEndpoints

app.UseRouting();

app.UseAuthentication();
            
app.UseEndpoints(endpoints => /* ... */)

身份验证控制器

让我们创建一个身份验证控制器来处理登录和注销流。

为了使演示变得简单,我们将不需要用户名/密码凭据。登录终结点将允许用户通过 JSON 指定所需的作用域,并将这些作用域作为声明添加到用户主体。这将使我们能够测试授权中间件如何处理不同请求的不同作用域。

我们的 JSON 正文将如下所示:

{
    "RequestedScopes": ["Product.Read", "Product.Insert"]
}

让我们创建一个模型类来表示这样的有效负载。在文件夹中,使用以下代码创建一个类:ModelsLoginData

namespace ODataAuthorizationDemo.Models
{
    public class LoginData
    {
        public string[] RequestedScopes { get; set; }
    }
}

然后,在文件夹中,创建以下类:ControllersAuthController

using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ODataAuthorizationDemo.Models;

namespace ODataAuthorizationDemo.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login([FromBody] LoginData data)
        {
            // create a claim for each requested scope
            var claims = data.RequestedScopes.Select(s => new Claim("Scope", s));

            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);

            var user = new ClaimsPrincipal(claimsIdentity);

            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                user);

            return Ok();
        }

        [HttpPost]
        [Route("logout")]
        public async Task<IActionResult> Logout()
        {
            await HttpContext.SignOutAsync(
                CookieAuthenticationDefaults.AuthenticationScheme);

            return Ok();
        }
    }
}
为了测试我们的登录流程,我们使用 Postman 等工具向登录终结点发出以下 POST 请求:
 
POST /auth/login

{
   "RequestedScopes": ["Product.Read", "Product.Delete"]
}

这将创建身份验证饼干。

要注销,我们会在没有正文的情况下发出以下 POST 请求:

POST /auth/logout
现在我们已经设置了身份验证,我们终于可以设置授权了。
 

添加授权

授权中间件将根据模型的限制注释,将经过身份验证的用户拥有的作用域与当前请求所需的作用域进行比较。由于存储作用域的方式有很多种,我们需要告诉中间件如何从当前用户中提取作用域。

修改以匹配以下内容:ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase("ODataAuthDemo"));

    services.AddOData()
        .AddAuthorization(options =>
        {
            options.ScopesFinder = context =>
            {
                var userScopes = context.User.FindAll("Scope").Select(claim => claim.Value);
                return Task.FromResult(userScopes);
            };

            options.ConfigureAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie();
        });

    services.AddRouting();
}

在上面的代码中,我们添加了对 odata 生成器的调用,以便添加 WebApi 授权服务。我们对其进行配置,使其从身份验证系统添加的用户声明中提取作用域。我们通过向属性提供处理程序方法来执行此操作。我们还可以使用 直接配置身份验证。.AddAuthorization()options.ScopesFinderoptions.ConfigureAuthentication

我们还需要在 中添加 之后 才能添加中间件:app.UseODataAuthorization()app.UseAuthentication()Configure()

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseAuthentication();

    app.UseODataAuthorization();
    

    app.UseEndpoints(endpoints =>
    {
        endpoints.Expand().Filter().Count().OrderBy();
        endpoints.MapODataRoute("odata", "odata", AppEdmModel.GetModel());
    });
}

测试

让我们从在没有任何作用域的情况下登录开始:

POST /auth/login

{ "RequestedScopes": [] }

如果我们尝试对 的任何 CRUD 请求,我们应该得到响应,因为我们没有访问这些端点所需的作用域。/odata/Products403 Forbidden

现在让我们注销:

POST /auth/logout

以及请求和范围:Product.ReadProduct.Create

POST /auth/login

{
    "RequestedScopes": ["Product.Read", "Product.Create"]
}

现在我们应该能够访问 和 。GET /odata/ProductsGET /odata/Products({key})POST /odata/Products

如果我们尝试访问或DELETE /odata/Products({key})PATCH/PUT /odata/Products({key})

注意:通常会返回错误响应,但在此示例中,它可能会返回错误。这是因为默认情况下,当授权失败时,Cookie 身份验证处理程序会尝试重定向到登录页。由于此页面在我们的示例应用程序中不存在,因此将返回错误。403 Forbidden404404 Not found

反馈

我们期待在通用版本发布之前获得您对此库的反馈。该库是开源的,您可以在GitHub存储库上报告问题并贡献拉取请求:https://github.com/OData/WebApiAuthorization

您还可以从官方文档中找到有关该库的更多信息。

 
 
posted @ 2022-09-14 14:31  岭南春  阅读(279)  评论(0)    收藏  举报