声明式策略基础—(菜鸡随笔)

发布时间 2023-04-18 22:24:53作者: 白日梦想家_zery

声明式策略基础—(菜鸡随笔)

声明式策略是ASP.NET Core中用于授权和访问控制的一种高级技术。

使用声明式策略,您可以根据用户身份和角色来限制某些API端点或控制器的访问权限。

在本文中,将简单介绍如何使用声明式策略来实现灵活的授权管理。

友善讨论,谢谢观看

声明式策略基础

在ASP.NET Core中,声明式策略是由Policy对象表示的。每个Policy对象都有一个名称和一个或多个要求(Requirement)。要求是实现了IAuthorizationRequirement接口的类,通常用于验证用户是否满足特定的授权条件。

例如,下面是一个检查用户是否具有“管理员”角色的自定义要求:

public class AdminRequirement : IAuthorizationRequirement
{
}

public class AdminHandler : AuthorizationHandler<AdminRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement)
    {
        // 检查当前用户是否具备"Admin"角色
        if (context.User.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "Admin"))
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }

        return Task.CompletedTask;
    }
}

在AdminRequirement类中,我们没有添加任何属性或方法,因为它只是一个标识性的类,表示需要具备"Admin"角色才能通过授权。

在AdminHandler类中,我们使用了AuthorizationHandler基类,并实现了HandleRequirementAsync方法来处理授权需求。

在这个方法中,我们首先从context.User对象中获取当前用户的角色。然后,检查当前用户是否具备"Admin"角色。如果是,则调用context.Succeed(requirement)方法表示授权成功;否则,调用context.Fail()方法表示授权失败。

与RecordAccessHandler类似,我们将AdminHandler类注册为授权处理程序,并在需要限制只有管理员角色的Controller或Action中使用该类。例如:

// 注册授权处理程序
services.AddSingleton<IAuthorizationHandler, AdminHandler>();

// 在Controller或Action上使用授权策略
[Authorize(Policy = "AdminOnly")]
public class AdminController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    // ...
}

在这个示例中,我们首先在ConfigureServices方法中将AdminHandler注册为授权处理程序。然后,在AdminController的Index方法中,我们将Authorize属性标记为需要"AdminOnly"授权策略才能访问。如果当前用户具备"Admin"角色,则可以访问该Action;否则,将返回401 Unauthorized错误。

// 注册授权处理程序
services.AddSingleton<IAuthorizationHandler, AdminHandler>();

// 在Controller或Action上使用授权策略
[Authorize(Policy = "AdminOnly")]
public class AdminController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    // ...
}

在上面的代码中,我们使用Authorize特性来限制只有拥有“AdminPolicy”策略的用户才能访问该API端点。

自定义要求

如果默认的授权要求无法满足您的需求,您可以创建自定义的授权要求。例如,以下是一个检查请求是否来自具有特定IP地址的客户端的自定义要求:

public class IpAddressRequirement : IAuthorizationRequirement {
    public string IpAddress { get; }

    public IpAddressRequirement(string ipAddress) {
        IpAddress = ipAddress;
    }
}

public class IpAddressRequirementHandler : AuthorizationHandler<IpAddressRequirement> {
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IpAddressRequirement requirement) {
        if (context.Resource is HttpContext httpContext) {
            var remoteIpAddress = httpContext.Connection.RemoteIpAddress.ToString();

            if (remoteIpAddress == requirement.IpAddress) {
                context.Succeed(requirement);
            }
        }

        return Task.CompletedTask;
    }
}

在上面的代码中,我们首先创建了一个名为IpAddressRequirement的自定义要求,并在构造函数中传入IP地址。然后,我们还创建了一个名为IpAddressRequirementHandler的处理程序,用于验证请求是否来自具有指定IP地址的客户端。

最后,我们将IpAddressRequirementHandler添加到服务容器中,并将IpAddressRequirement添加到策略中:

services.AddSingleton<IAuthorizationHandler, IpAddressRequirementHandler>();
services.AddAuthorization(options => {
    options.AddPolicy("LocalhostPolicy", policy => policy.Requirements.Add(new IpAddressRequirement("127.0.0.1")));
});

在上面的代码中,我们使用AddSingleton方法来将IpAddressRequirementHandler添加到服务容器中。然后,使用AddPolicy方法创建一个名为“LocalhostPolicy”的策略,并使用Requiremenets属性将IpAddressRequirement添加到该策略中。这样,当需要对某些API端点或控制器进行授权时,只需指定该策略的名称即可,例如:

[HttpGet("localhost-action")]
[Authorize(Policy = "LocalhostPolicy")]
public async Task<IActionResult> LocalhostAction() {
    // ...
}

在上面的代码中,我们使用Authorize特性来限制只有请求来源IP地址为“127.0.0.1”的客户端才能访问该API端点。

多重要求

有时候,要检查用户是否满足多个授权要求才能访问某些API端点或控制器。例如,以下是一个检查请求是否来自“管理员”角色并且具有特定IP地址的客户端的自定义要求:

public class AdminIpAddressRequirement : IAuthorizationRequirement {
    public string IpAddress { get; }
    
    public AdminIpAddressRequirement(string ipAddress) {
        IpAddress = ipAddress;
    }
}

public class AdminIpAddressRequirementHandler : AuthorizationHandler<AdminIpAddressRequirement> {
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminIpAddressRequirement requirement) {
        if (context.Resource is HttpContext httpContext) {
            var remoteIpAddress = httpContext.Connection.RemoteIpAddress.ToString();

            if (remoteIpAddress == requirement.IpAddress && context.User.IsInRole("admin")) {
                context.Succeed(requirement);
            }
        }

        return Task.CompletedTask;
    }
}

在上面的代码中,我们首先创建了一个名为AdminIpAddressRequirement的自定义要求,并在构造函数中传入IP地址。然后,我们还创建了一个名为AdminIpAddressRequirementHandler的处理程序,用于验证请求是否来自具有指定IP地址的客户端,并且用户拥有“管理员”角色。

最后,我们将AdminIpAddressRequirementHandler添加到服务容器中,并将AdminIpAddressRequirement添加到策略中:

services.AddSingleton<IAuthorizationHandler, AdminIpAddressRequirementHandler>();

services.AddAuthorization(options => {
    options.AddPolicy("AdminLocalhostPolicy", policy => {
        policy.RequireRole("admin");
        policy.Requirements.Add(new AdminIpAddressRequirement("127.0.0.1"));
    });
});

在上面的代码中,我们使用AddSingleton方法来将AdminIpAddressRequirementHandler添加到服务容器中。然后,使用AddPolicy方法创建一个名为“AdminLocalhostPolicy”的策略,并使用RequireRole和Requiremenets属性分别将角色和AdminIpAddressRequirement添加到该策略中。这样,当需要对某些API端点或控制器进行授权时,只需指定该策略的名称即可,例如:

[HttpGet("admin-localhost-action")]
[Authorize(Policy = "AdminLocalhostPolicy")]
public async Task<IActionResult> AdminLocalhostAction() {
    // ...
}

在上面的代码中,我们使用Authorize特性来限制只有请求来源IP地址为“127.0.0.1”且用户拥有“管理员”角色的客户端才能访问该API端点。

策略组合

有时候,要基于多个策略来限制API端点或控制器的访问权限。例如,以下是一个要求同时满足“管理员”角色和具有特定IP地址的客户端的策略:

services.AddAuthorization(options => {
    options.AddPolicy("AdminAndLocalhostPolicy", policy => {
        policy.Requirements.Add(new AdminRequirement());
        policy.Requirements.Add(new IpAddressRequirement("127.0.0.1"));
    });
});

在上面的代码中,我们使用AddPolicy方法创建一个名为“AdminAndLocalhostPolicy”的策略,并使用Requiremenets属性将AdminRequirement和IpAddressRequirement添加到该策略中。

这样,当需要对某些API端点或控制器进行授权时,只需指定该策略的名称即可,例如:

[HttpGet("admin-and-localhost-action")]
[Authorize(Policy = "AdminAndLocalhostPolicy")]
public async Task<IActionResult> AdminAndLocalhostAction() {
    // ...
}

在上面的代码中,我们使用Authorize特性来限制只有同时拥有“管理员”角色和请求来源IP地址为“127.0.0.1”的客户端才能访问该API端点。

自定义策略验证器

有时候,要根据自己的业务逻辑来实现授权管理,而不仅仅是简单地检查用户的身份和角色。例如,您可能希望检查用户是否具有足够的权限执行某个操作,或者根据其他因

素来确定授权决策。在这种情况下,您可以使用自定义的策略验证器来实现更灵活的授权管理。

例如,以下是一个检查用户是否具有足够权限访问某个资源的自定义要求和处理程序:

public class ResourceAccessRequirement : IAuthorizationRequirement {
    public string ResourceName { get; }
    public string Action { get; }

    public ResourceAccessRequirement(string resourceName, string action) {
        ResourceName = resourceName;
        Action = action;
    }
}

public class ResourceAccessRequirementHandler : AuthorizationHandler<ResourceAccessRequirement> {
    private readonly IResourceAccessService _resourceAccessService;

    public ResourceAccessRequirementHandler(IResourceAccessService resourceAccessService) {
        _resourceAccessService = resourceAccessService;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ResourceAccessRequirement requirement) {
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        var hasAccess = await _resourceAccessService.CheckAccessAsync(userId, requirement.ResourceName, requirement.Action);

        if (hasAccess) {
            context.Succeed(requirement);
        }
    }
}

在上面的代码中,我们首先创建了一个名为ResourceAccessRequirement的自定义要求,并在构造函数中传入资源名称和操作类型。

然后,我们还创建了一个名为ResourceAccessRequirementHandler的处理程序,用于调用IResourceAccessService接口检查用户是否具有足够的权限访问该资源。

最后,我们将ResourceAccessRequirementHandler添加到服务容器中,并将ResourceAccessRequirement添加到策略中:

services.AddSingleton<IAuthorizationHandler, ResourceAccessRequirementHandler>();

services.AddAuthorization(options => {
    options.AddPolicy("ResourceAccessPolicy", policy => {
        policy.Requirements.Add(new ResourceAccessRequirement("resource1", "read"));
        policy.Requirements.Add(new ResourceAccessRequirement("resource2", "write"));
    });
});

在上面的代码中,我们使用AddSingleton方法来将ResourceAccessRequirementHandler添加到服务容器中。

然后,使用AddPolicy方法创建一个名为“ResourceAccessPolicy”的策略,并使用Requiremenets属性分别将两个ResourceAccessRequirement添加到该策略中。

这样,当需要对某些API端点或控制器进行授权时,只需指定该策略的名称即可,例如:

[HttpGet("resource1-action")]
[Authorize(Policy = "ResourceAccessPolicy")]
public async Task<IActionResult> Resource1Action() {
    // ...
}

[HttpPost("resource2-action")]
[Authorize(Policy = "ResourceAccessPolicy")]
public async Task<IActionResult> Resource2Action() {
    // ...
}

在上面的代码中,我们使用Authorize特性来限制只有满足ResourceAccessRequirement要求的用户才能访问该API端点。

结论

通过声明式策略,您可以灵活地定义和管理API端点和控制器的访问权限,根据用户角色、身份和其他条件来限制不同用户的访问权限。在本文中,我们介绍了如何创建自定义的授权要求、处理程序和策略,并演示了如何将它们应用于ASP.NET Core应用程序中。希望这篇文章能够帮助您更好地理解和应用声明式策略。

资源授权

除了对API端点和控制器进行授权外,您还可以使用声明式策略进行资源授权。

资源授权是指限制用户对某些特定资源(如文件、数据库记录等)的访问权限,而不仅仅是限制API端点和控制器的访问权限。

通常情况下,资源授权需要使用自定义的授权要求和处理程序来实现。

例如,以下是一个检查用户是否具有读取文件权限的自定义要求和处理程序:

public class FileReadRequirement : IAuthorizationRequirement { }

public class FileReadRequirementHandler : AuthorizationHandler<FileReadRequirement, string> {
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FileReadRequirement requirement, string resource) {
        if (context.User.HasClaim(c => c.Type == "File" && c.Value == resource && c.Properties.ContainsKey("Read") && bool.Parse(c.Properties["Read"]))) {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

在上面的代码中,我们首先创建了一个名为FileReadRequirement的自定义要求。然后,我们还创建了一个名为FileReadRequirementHandler的处理程序,用于验证用户是否具有读取指定文件的权限。在这个例子中,我们使用了一个名为“File”的声明类型,其中包含有关文件和访问权限的信息。处理程序通过检查用户是否具有带有正确属性(如“Read”)和值的声明来确定用户是否具有读取文件的权限。

最后,我们将FileReadRequirementHandler添加到服务容器中,并在需要授权访问某个资源的地方使用Authorize特性指定要求和资源名称:

services.AddSingleton<IAuthorizationHandler, FileReadRequirementHandler>();

[HttpGet("file-action")]
[Authorize(Policy = "FileReadPolicy", Resource = "file.txt")]
public async Task<IActionResult> FileAction() {
    // ...
}

在上面的代码中,我们使用Authorize特性来限制只有拥有“FileReadPolicy”策略和指定的资源名称的用户才能访问该API端点。

多重处理程序

有时候,您可能需要使用多个处理程序来处理同一个授权要求。例如,一个处理程序用于验证用户是否满足授权条件,另一个处理程序用于记录授权事件。在这种情况下,您可以使用AddHandler方法来添加多个处理程序,如下所示:

services.AddAuthorization(options => {
    options.AddPolicy("AdminPolicy", policy => {
        policy.Requirements.Add(new AdminRequirement());
        policy.AddHandler(new AdminAuditHandler());
    });
});

在上面的代码中,我们使用AddPolicy方法创建一个名为“AdminPolicy”的策略,并使用Requiremenets属性将AdminRequirement添加到该策略中。然后,我们使用AddHandler方法添加一个名为AdminAuditHandler的处理程序。

在处理程序中,您可以通过调用context.Succeed、context.Fail或context.Challenge等方法来控制授权决策。如下所示:

public class AdminAuditHandler : AuthorizationHandler<AdminRequirement> {
    private readonly ILogger<AdminAuditHandler> _logger;

    public AdminAuditHandler(ILogger<AdminAuditHandler> logger) {
        _logger = logger;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement) {
        if (context.User.IsInRole("admin")) {
            _logger.LogInformation($"User {context.User.Identity.Name} is authorized as an administrator.");
            context.Succeed(requirement);
        } else {
            _logger.LogWarning($"User {context.User.Identity.Name} is not authorized as an administrator.");
            context.Fail();
        }

        return Task.CompletedTask;
    }
}

在上面的代码中,我们创建了一个名为AdminAuditHandler的处理程序,并使用ILogger来记录授权事件。在HandleRequirementAsync方法

策略缓存

声明式策略在授权过程中会对每个请求进行评估,包括验证用户信息、角色和声明等。这个过程可能会很耗时,因此ASP.NET Core引入了策略缓存功能,可以将授权决策结果缓存起来以提高性能。

默认情况下,策略缓存是启用的,并且最长时间为5分钟。如果您想自定义缓存行为,可以使用AddPolicyCache方法来配置缓存选项。例如,以下代码将缓存有效期设置为10分钟:

services.AddAuthorization(options => {
    options.AddPolicy("AdminPolicy", policy => {
        policy.RequireRole("admin");
    });

    options.CacheAuthorizeData = true;
    options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
    options.AddPolicyCache(policy => policy.ExpirationMinutes = 10);
});

在上面的代码中,我们使用AddPolicyCache方法将缓存有效期设置为10分钟。需要注意的是,缓存只适用于基于角色和声明的策略验证,对于自定义的要求和处理程序不适用。

策略推断

当请求未明确授权策略时,ASP.NET Core会通过策略推断来确定哪些策略应该适用于该请求。推断的规则如下:

  • 如果控制器或API端点有[AllowAnonymous]特性,则不需要授权;
  • 如果控制器或API端点有[Authorize]特性但未指定策略名称,则使用默认策略(即RequireAuthenticatedUser);
  • 如果控制器或API端点有[Authorize(Policy = "policy-name")]特性,则使用指定的策略;
  • 如果控制器或API端点未定义任何[AllowAnonymous]或[Authorize]特性,则使用默认策略。

在推断完成后,ASP.NET Core会根据所选策略进行授权验证。

访问资源

除了限制用户访问API端点和控制器外,声明式策略还可以用于限制用户访问资源。资源包括一些涉及隐私和安全的数据、文件、数据库记录等。例如,以下是一个检查用户是否能够访问指定数据库记录的自定义要求和处理程序:

public class RecordAccessRequirement : IAuthorizationRequirement
{
    public int RecordId { get; }

    public RecordAccessRequirement(int recordId)
    {
        RecordId = recordId;
    }
}

public class RecordAccessHandler : AuthorizationHandler<RecordAccessRequirement>
{
    private readonly IRecordService _recordService;

    public RecordAccessHandler(IRecordService recordService)
    {
        _recordService = recordService;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RecordAccessRequirement requirement)
    {
        // 获取当前用户ID
        int userId = int.Parse(context.User.FindFirstValue(ClaimTypes.NameIdentifier));

        // 检查用户是否有访问该记录的权限
        bool hasAccess = await _recordService.CheckAccessAsync(userId, requirement.RecordId);

        if (hasAccess)
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }
    }
}
public interface IRecordService
{
    Task<bool> CheckAccessAsync(int userId, int recordId);
}

public class RecordService : IRecordService
{
    private readonly IDbConnection _dbConnection;

    public RecordService(IDbConnection dbConnection)
    {
        _dbConnection = dbConnection;
    }

    public async Task<bool> CheckAccessAsync(int userId, int recordId)
    {
        // 查询数据库,检查用户是否有访问该记录的权限
        var record = await _db.Set<Record>()
            .FirstOrDefaultAsync(r => r.Id == recordId && r.UserId == userId);

        return record != null;
    }
}

public static class ClaimTypes
{
    public const string NameIdentifier = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
    public const string Name = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name";
    public const string Role = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role";
}

在RecordAccessRequirement类中,我们添加了一个名为RecordId的属性,表示要访问的记录的ID。在构造函数中,我们将这个属性设置为传入的参数值。这个类没有实现其他的方法或属性,因为它只是一个标识性的类,表示需要具备特定的记录访问权限才能通过授权。

Record表包含以下字段:

  • Id:记录的唯一标识符。
  • UserId:创建该记录的用户的ID。
  • Title:记录的标题。
  • Content:记录的内容。
  • CreatedTime:记录创建的时间。
  • LastModifiedTime:记录上次修改的时间。

其中,Id和UserId是必需的字段,用于唯一标识记录并确定记录的所有者。Title和Content是记录的主要内容,可以包含任何有助于描述记录的信息。CreatedTime和LastModifiedTime是记录的时间戳,用于跟踪记录的创建和修改时间。

RecordAccessHandler类继承自AuthorizationHandler类,并实现了HandleRequirementAsync方法来处理授权需求。在这个方法中,我们首先从context.User对象中获取当前用户的ID。然后,调用_recordService.CheckAccessAsync方法来检查用户是否有访问该记录的权限。

如果用户具备访问权限,则调用context.Succeed(requirement)方法表示授权成功。否则,调用context.Fail()方法表示授权失败。

// 注册授权处理程序
services.AddSingleton<IAuthorizationHandler, RecordAccessHandler>();

// 在Controller或Action上使用授权策略
[Authorize(Policy = "AdminOnly")]
public class RecordController : Controller
{
    private readonly IRecordService _recordService;

    public RecordController(IRecordService recordService)
    {
        _recordService = recordService;
    }

    public async Task<IActionResult> Details(int id)
    {
        // 检查当前用户是否有访问该记录的权限
        var requirement = new RecordAccessRequirement(id);
        var result = await HttpContext.AuthorizeAsync(requirement);

        if (!result.Succeeded)
        {
            return Forbid();
        }

        // 查询数据库,获取记录详情
        // ...
    }

    // ...
}

在这个示例中,我们首先在ConfigureServices方法中将RecordAccessHandler注册为授权处理程序。然后,在RecordController的Details方法中,我们创建了一个RecordAccessRequirement对象,并传入记录的ID作为参数。接着,调用HttpContext.AuthorizeAsync方法来验证当前用户是否具备访问该记录的权限。如果验证失败,则返回Forbid结果;否则,可以查询数据库,获取记录详情。

总结

通过声明式策略,您可以灵活地定义和管理API端点、控制器和资源的访问权限,根据用户角色、身份和其他条件来限制不同用户的访问权限。在本文中,我们介绍了声明式策略的基础知识和使用方法,并演示了如何创建自定义的授权要求、处理程序和策略,以及如何将它们应用于ASP.NET Core应用程序中。

需要注意的是,在使用声明式策略时,您应该遵循以下原则:

  • 最小化权限:只授权用户所需的最低权限,避免过度授权;
  • 满足条件:确保每个请求都具有满足访问条件的必要信息;
  • 安全验证:验证用户是否具有访问特定资源的权限,而不是通过隐藏或混淆资源来保护它们;
  • 策略缓存:使用缓存来提高性能,但要注意缓存时间和缓存破坏的问题;
  • 资源授权:除了限制用户访问API端点和控制器外,还可以使用声明式策略限制用户访问资源。

希望本文能够帮助您更好地理解和应用声明式策略,并在开发ASP.NET Core应用程序时提供一些有用的指导。