声明式策略基础One—(菜鸡随笔)
声明式策略基础One—(菜鸡随笔)
在Web应用程序中,安全性是至关重要的。为了保护敏感数据和操作,您需要对用户进行身份验证和授权。ASP.NET Core提供了一种称为声明式策略的功能来管理Web应用程序中的授权。
声明式策略是一个基于角色和声明的授权机制,它允许开发人员在应用程序中定义自定义的授权规则,并使用这些规则来限制特定用户对资源的访问。与基于角色的授权相比,声明式策略更加灵活和精细,可以根据实际业务需要进行配置和调整。
本文将介绍如何在ASP.NET Core应用程序中使用声明式策略,以及最佳实践和注意事项。
基础
在开始之前,我们需要了解一些授权和认证的基础知识。
授权和认证的区别和联系:
- 认证是验证用户身份的过程,通常涉及用户名和密码等凭据;
- 授权是决定用户是否有权访问特定资源或执行特定操作的过程。
策略授权与角色授权的区别:
- 角色授权是一种基于角色的授权机制,其中用户被授予某个角色,并且该角色具有特定的权限;
- 策略授权是一种基于声明的授权机制,其中用户被授予一组声明或属性(如用户名、电子邮件地址等),并且这些声明或属性用于限制特定用户对资源的访问。
声明式策略的基本组成部分:
- 授权要求(Authorization Requirement):定义一个规则,描述某种授权策略,例如:用户必须拥有特定的声明才能访问一个资源。
- 授权处理程序(Authorization Handler):验证授权要求,决定是否授予用户访问资源的权限。
- 授权策略(Authorization Policy):将授权要求和处理程序绑定在一起,以便在授权过程中使用。
策略的应用方式:
- 声明式策略可以应用于API端点和控制器上,限制用户访问这些资源;
- 声明式策略也可以用于资源授权,限制用户访问敏感数据、文件等资源。
实现
定义自定义策略和验证规则
要定义自定义的授权策略和验证规则,请遵循以下步骤:
- 创建一个名为RecordPolicy的授权策略,并指定要求和处理程序。在此示例中,我们使用RecordAccessRequirement和RecordAccessRequirementHandler。
services.AddAuthorization(options => {
options.AddPolicy("RecordPolicy", policy =>
policy.Requirements.Add(new RecordAccessRequirement())
);
});
- 在API端点或控制器上使用[Authorize]特性,并指定要使用的策略。这可以通过指定Policy属性和Resource属性来完成。Resource属性可将当前请求的资源ID传递给授权处理程序。
[HttpGet("{id}")]
[Authorize(Policy = "RecordPolicy", Resource = "{id}")]
public ActionResult<Record> GetRecord(int id)
{
// 处理Get请求
}
在上面的代码中,我们使用[Authorize]特性来限制用户访问GET操作。只有拥有名为"RecordPolicy"的策略并具有指定资源ID的用户才能访问该API。
嵌套策略的使用
嵌套策略是指将多个策略组合在一起使用的情况。例如,您可能会创建一个名为"AdminPolicy"的策略,该策略要求用户具有名为"Admin"的角色或拥有特定的声明。在ASP.NET Core中,您可以通过以下方式实现嵌套策略:
services.AddAuthorization(options => {
options.AddPolicy("AdminPolicy", policy => {
policy.RequireRole("Admin");
policy.RequireClaim("IsAdmin", "true");
});
options.AddPolicy("RecordPolicy", policy =>
policy.Requirements.Add(new RecordAccessRequirement())
);
options.AddPolicy("CompositePolicy", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c => c.Type == "Database" && c.Properties.ContainsKey("Write") && bool.Parse(c.Properties["Write"]))) &&
context.User.HasPolicy("AdminPolicy") &&
context.User.HasPolicy("RecordPolicy")
)
);
});
在上面的代码中,我们创建了一个名为"AdminPolicy"的策略,要求用户具有"Admin"角色或具有名为"IsAdmin"声明且值为"true"。然后,我们创建了两个其他的策略:"RecordPolicy"和"CompositePolicy"。
"RecordPolicy"策略使用了前面定义的RecordAccessRequirement授权要求。
"CompositePolicy"策略是一个嵌套策略,它组合了三个不同的策略。它要求用户具有一些特定的声明、"AdminPolicy"策略和"RecordPolicy"策略。这意味着只有拥有这三个策略的用户才能访问API端点或控制器。
策略缓存的配置
在大多数情况下,应将策略缓存在内存中,以提高性能。
ASP.NET Core提供了两种缓存策略:MemoryCache和DistributedCache。MemoryCache存储在应用程序的进程内,而DistributedCache存储在应用程序的进程之间共享的位置。
以下是如何使用MemoryCache配置策略缓存:
services.AddAuthorization(options => {
options.CacheAuthorizeData = true;
options.AddPolicy("RecordPolicy", policy =>
policy.Requirements.Add(new RecordAccessRequirement())
);
});
在上面的代码中,我们启用了缓存策略,并添加了名为"RecordPolicy"的授权策略。
资源授权
除了限制用户访问API端点和控制器外,还可以使用声明式策略限制用户访问资源,例如数据库、文件等。资源授权是一种基于资源的授权验证,它允许您根据资源ID和访问类型(例如读取或写入)来限制用户对特定资源的访问。
要实现资源授权,请遵循以下步骤:
在授权要求中添加一个资源属性。
public class ResourceAccessRequirement : IAuthorizationRequirement {
public string Resource { get; set; }
}
创建一个自定义的授权处理程序,用于验证用户是否具有特定的声明且该声明与请求的资源匹配。
public class ResourceAccessHandler : AuthorizationHandler<ResourceAccessRequirement> {
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ResourceAccessRequirement requirement) {
if (context.User.HasClaim(c => c.Type == "Resource" && c.Value == requirement.Resource && c.Properties.ContainsKey(requirement.AccessType) && bool.Parse(c.Properties[requirement.AccessType]))) {
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
在上面的代码中,我们检查当前请求的用户是否具有名为"Resource"、值等于要访问的资源ID的声明,并且该声明具有指定的访问类型("Read"或"Write")且属性值为true。如果条件成立,则调用context.Succeed方法以指示授权通过;否则返回Task.CompletedTask表示授权失败。
将ResourceAccessHandler添加到服务容器中。
services.AddSingleton<IAuthorizationHandler, ResourceAccessHandler>();
在授权策略中添加ResourceAccessRequirement要求,并指定要使用的处理程序。
services.AddAuthorization(options => {
options.AddPolicy("ResourcePolicy", policy =>
policy.Requirements.Add(new ResourceAccessRequirement { AccessType = "Read" })
.Add(new ResourceAccessRequirement { AccessType = "Write" })
.HandlerType = typeof(ResourceAccessHandler)
);
});
在上面的代码中,我们创建了一个名为"ResourcePolicy"的授权策略,并将两个ResourceAccessRequirement授权要求添加到该策略中。我们还指定了要使用的ResourceAccessHandler处理程序。
在API端点或控制器上使用[Authorize]特性,并指定要使用的策略和资源ID。
[HttpGet("{id}")]
[Authorize(Policy = "ResourcePolicy", Resource = "{id}")]
public ActionResult<Resource> GetResource(int id) {
// 处理Get请求
}
在上面的代码中,我们使用[Authorize]特性来限制用户访问GET操作。只有拥有名为"ResourcePolicy"的策略且具有指定的资源ID、读取或写入访问类型的用户才能访问该API。
示例
以下示例演示如何在ASP.NET Core应用程序中实现声明式策略。
在此示例中,我们将创建一个简单的Web API应用程序,它允许用户查看和更新他们的个人资料。我们将定义两个授权策略:"ViewProfile"和"UpdateProfile",并限制用户访问API端点。
创建授权要求和处理程序
首先,我们将创建两个授权要求:"ViewProfileRequirement"和"UpdateProfileRequirement",并定义它们的处理程序。
public class ViewProfileRequirement : IAuthorizationRequirement
{
}
public class UpdateProfileRequirement : IAuthorizationRequirement
{
}
public class ProfileAccessHandler : AuthorizationHandler<IAuthorizationRequirement, string>
{
private readonly IUserService _userService;
public ProfileAccessHandler(IUserService userService)
{
_userService = userService;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement, string resource)
{
if (requirement is ViewProfileRequirement && context.User.Identity.Name == resource)
{
context.Succeed(requirement);
}
else if (requirement is UpdateProfileRequirement && context.User.Identity.Name == resource && context.User.HasClaim(c => c.Type == "IsAdmin" && bool.Parse(c.Value)))
{
context.Succeed(requirement);
}
else
{
// 如果没有授权成功,则检查是否存在相应的用户记录
var user = await _userService.GetUserByNameAsync(resource);
if (user != null)
{
context.Fail();
}
}
}
}
在ProfileAccessHandler类中,我们继承了AuthorizationHandler<IAuthorizationRequirement, string>类,并实现了HandleRequirementAsync方法来处理授权需求。在这个方法中,我们首先检查传入的requirement对象是否为ViewProfileRequirement,并且当前用户的用户名是否等于resource(即要访问的用户的用户名)。如果满足这两个条件,则调用context.Succeed(requirement)方法表示授权成功。
然后,我们检查传入的requirement对象是否为UpdateProfileRequirement,并且当前用户的用户名是否等于resource,以及当前用户是否具备名为“IsAdmin”的声明,且该声明的值为true。如果满足这三个条件,则同样调用context.Succeed(requirement)方法表示授权成功。
最后,如果以上两种情况都不成立,则说明当前用户没有访问或更新指定用户的资料的权限。在这种情况下,我们调用_userService.GetUserByNameAsync(resource)方法来获取与指定用户名相对应的用户记录。如果存在这样的用户记录,则调用context.Fail()方法表示授权失败,否则什么也不做(即表示未找到此资源)。
需要注意的是,在这个示例中,我们使用了IUserService服务来查询数据库中的用户记录。在实际应用中,您可以使用自己的数据访问层、ORM框架或其他方式来实现相应的功能。
配置策略
然后,我们将配置授权策略,并将其添加到服务容器中。
services.AddAuthentication(...)
.AddJwtBearer(...)
services.AddAuthorization(options =>
{
options.AddPolicy("ViewProfilePolicy", policy => policy.Requirements.Add(new ViewProfileRequirement()));
options.AddPolicy("UpdateProfilePolicy", policy => policy.Requirements.Add(new UpdateProfileRequirement()));
});
services.AddSingleton<IAuthorizationHandler, ProfileAccessHandler>();
并将两个自定义的身份验证需求添加到名为“ViewProfilePolicy”和“UpdateProfilePolicy”的授权策略中。然后,我们将ProfileAccessHandler类注册为实现IAuthorizationHandler接口的服务。
在需要进行身份验证和授权的控制器、操作方法或 Razor 页面中,添加相应的身份验证和授权特性,例如:
[Authorize(Policy = "ViewProfilePolicy")]
[HttpGet("{username}")]
public async Task<IActionResult> GetProfile(string username)
{
// 处理获取用户资料的逻辑
}
[Authorize(Policy = "UpdateProfilePolicy")]
[HttpPut("{username}")]
public async Task<IActionResult> UpdateProfile(string username, [FromBody] UserProfileModel model)
{
// 处理更新用户资料的逻辑
}
在这个示例中,我们使用了Authorize特性来声明需要进行身份验证和授权。在ViewProfilePolicy策略下,只有具备ViewProfileRequirement身份验证需求的用户才能访问获取用户资料的操作方法。在UpdateProfilePolicy策略下,只有具备UpdateProfileRequirement身份验证需求且具备“IsAdmin”声明的值为true的用户才能访问更新用户资料的操作方法。
在API端点中使用授权策略
最后,我们将在API端点中使用授权策略,以限制用户访问。我们还可以使用[Authorize]特性来限制用户访问整个控制器或API端点。
[HttpGet("{username}")]
[Authorize(Policy = "ViewProfile", Resource = "{username}")]
public ActionResult<UserProfile> GetProfile(string username) {
// 处理GET请求
}
[HttpPut("{username}")]
[Authorize(Policy = "UpdateProfile", Resource = "{username}")]
public ActionResult<UserProfile> UpdateProfile(string username, [FromBody] UserProfile profile) {
// 处理PUT请求
}
在上面的代码中,我们使用[Authorize]特性来限制GET和PUT操作。只有满足指定的授权策略且具有指定的资源ID的用户才能访问这些API。
总结
声明式策略是一种灵活、可维护和可测试的授权机制,它允许您定义自定义的授权规则,并使用这些规则来限制特定用户对资源的访问。在ASP.NET Core中,您可以使用授权要求、授权处理程序和授权策略来实现声明式策略。声明式策略可以应用于API端点和控制器上,也可以用于资源授权,以限制用户访问敏感数据、文件等资源。
在实践中,您应该遵循最佳实践,例如将授权要求和处理程序分离、配置策略缓存和使用嵌套策略等。通过实现这些实践,您可以创建更加灵活、清晰和安全的应用程序,保障用户数据的安全和隐私。
好的,以下是一个完整的示例,演示如何在ASP.NET Core中使用声明式策略来限制用户访问敏感资源。
1. 创建应用程序
首先,我们将创建一个新的ASP.NET Core应用程序,并添加所需的NuGet包。
dotnet new webapi -n SecureApi
cd SecureApi
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
2. 定义数据模型
我们将定义一个User数据模型,用于表示应用程序中的用户。
public class User {
public int Id { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool IsAdmin { get; set; }
}
3. 添加身份验证
我们将使用JWT身份验证机制来保护API端点。我们将在Startup.cs文件中配置身份验证服务和中间件。
public void ConfigureServices(IServiceCollection services) {
// 添加身份验证服务
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
// 添加授权服务
services.AddAuthorization();
// 添加数据库上下文
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase(databaseName: "SecureApi")
);
// 添加控制器
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
// 启用身份验证中间件
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
});
}
在上面的代码中,我们首先使用AddAuthentication方法配置JWT身份验证服务。然后,我们启用身份验证中间件,并使用UseAuthorization方法启用授权中间件。
4. 添加授权要求和处理程序
接下来,我们将定义两个授权要求:"ViewResourceRequirement"和"UpdateResourceRequirement",并添加处理程序"ResourceAccessHandler"。
public class ViewResourceRequirement : IAuthorizationRequirement {
public string Resource { get; set; }
}
public class UpdateResourceRequirement : IAuthorizationRequirement {
public string Resource { get; set; }
}
public class ResourceAccessHandler : AuthorizationHandler<IAuthorizationRequirement> {
private readonly AppDbContext _dbContext;
public ResourceAccessHandler(AppDbContext dbContext) {
_dbContext = dbContext;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement) {
if (requirement is ViewResourceRequirement viewRequirement && context.User.Identity.Name == viewRequirement.Resource) {
context.Succeed(requirement);
} else if (requirement is UpdateResourceRequirement updateRequirement && context.User.Identity.Name == updateRequirement.Resource && context.User.HasClaim(c => c.Type == "IsAdmin" && bool.Parse(c.Value))) {
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
在上面的代码中,我们为每个授权要求编写一个单独的处理程序。在HandleRequirementAsync方法中,我们检查当前请求的用户是否满足授权要求,并调用context.Succeed方法以指示授权通过。
5. 配置授权策略
接下来,我们将配置授权策略,并将其添加到服务容器中。
public void ConfigureServices(IServiceCollection services) {
// 添加授权服务
services.AddAuthorization(options => {
options.AddPolicy("ViewResource", policy =>
policy.Requirements.Add(new ViewResourceRequirement())
.HandlerType = typeof(ResourceAccessHandler)
);
options.AddPolicy("UpdateResource", policy =>
policy.Requirements.Add(new UpdateResourceRequirement())
.HandlerType = typeof(ResourceAccessHandler)
);
});
// 添加数据库上下文
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase(databaseName: "SecureApi")
);
// 添加控制器
services.AddControllers();
}
在上面的代码中,我们将授权服务添加到ConfigureServices方法中,并在其中配置授权策略。我们创建了两个授权策略:"ViewResource"和"UpdateResource"。我们还指定了要使用的ResourceAccessHandler处理程序。
6. 添加API端点
最后,我们将添加两个API端点:GetResource和UpdateResource。
[HttpGet("{id}")]
[Authorize(Policy = "ViewResource", Resource = "{id}")]
public ActionResult<Resource> GetResource(int id) {
var resource = _dbContext.Resources.FirstOrDefault(r => r.Id == id);
if (resource == null) {
return NotFound();
}
return Ok(resource);
}
[HttpPut("{id}")]
[Authorize(Policy = "UpdateResource", Resource = "{id}")]
public ActionResult<Resource> UpdateResource(int id, [FromBody] Resource resource) {
var existingResource = _dbContext.Resources.FirstOrDefault(r => r.Id == id);
if (existingResource == null) {
return NotFound();
}
existingResource.Value = resource.Value;
_dbContext.SaveChanges();
return Ok(existingResource);
}
在上面的代码中,我们使用[Authorize]特性来限制GET和PUT操作。只有满足指定的授权策略且具有指定的资源ID的用户才能访问这些API。
完整示例代码
以下是完整的示例代码:
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Microsoft.EntityFrameworkCore;
using System.IdentityModel.Tokens.Jwt;
namespace SecureApi
{
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool IsAdmin { get; set; }
}
public class Resource
{
public int Id { get; set; }
public string Value { get; set; }
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<User> Users { get; set; }
public DbSet<Resource> Resources { get; set; }
}
public class ViewResourceRequirement : IAuthorizationRequirement
{
public string Resource { get; set; }
}
public class UpdateResourceRequirement : IAuthorizationRequirement
{
public string Resource { get; set; }
}
public class ResourceAccessHandler : AuthorizationHandler<IAuthorizationRequirement>
{
private readonly AppDbContext _dbContext;
public ResourceAccessHandler(AppDbContext dbContext)
{
_dbContext = dbContext;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement)
{
if (requirement is ViewResourceRequirement viewRequirement && context.User.Identity.Name == viewRequirement.Resource)
{
context.Succeed(requirement);
}
else if (requirement is UpdateResourceRequirement updateRequirement && context.User.Identity.Name == updateRequirement.Resource && context.User.HasClaim(c => c.Type == "IsAdmin" && bool.Parse(c.Value)))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly AppDbContext _dbContext;
private readonly IConfiguration _configuration;
public UsersController(AppDbContext dbContext, IConfiguration configuration)
{
_dbContext = dbContext;
_configuration = configuration;
}
[HttpPost("login")]
public ActionResult<string> Login([FromBody] User user)
{
var existingUser = _dbContext.Users.FirstOrDefault(u => u.Username == user.Username && u.Password == user.Password);
if (existingUser == null)
{
return Unauthorized();
}
var claims = new List<Claim> {
new Claim(ClaimTypes.Name, existingUser.Username),
new Claim("IsAdmin", existingUser.IsAdmin.ToString())
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
_configuration["Jwt:Issuer"],
_configuration["Jwt:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: credentials
);
return Ok(new JwtSecurityTokenHandler().WriteToken(token));
}
[HttpGet("{id}")]
[Authorize(Policy = "ViewResource", Resource = "{id}")]
public ActionResult<Resource> GetResource(int id)
{
var resource = _dbContext.Resources.FirstOrDefault(r => r.Id == id);
if (resource == null)
{
return NotFound();
}
return Ok(resource);
}
[HttpPut("{id}")]
[Authorize(Policy = "UpdateResource", Resource = "{id}")]
public ActionResult<Resource> UpdateResource(int id, [FromBody] Resource resource)
{
var existingResource = _dbContext.Resources.FirstOrDefault(r => r.Id == id);
if (existingResource == null)
{
return NotFound();
}
existingResource.Value = resource.Value;
_dbContext.SaveChanges();
return Ok(existingResource);
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// 添加身份验证服务
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
// 添加授权服务
services.AddAuthorization(options => {
options.AddPolicy("ViewResource", policy =>
policy.Requirements.Add(new ViewResourceRequirement())
.HandlerType = typeof(ResourceAccessHandler)
);
options.AddPolicy("UpdateResource", policy =>
policy.Requirements.Add(new UpdateResourceRequirement())
.HandlerType = typeof(ResourceAccessHandler)
);
});
// 添加数据库上下文
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase(databaseName: "SecureApi")
);
// 添加控制器
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 启用身份验证中间件
app.UseAuthentication();
app.UseRouting();
// 启用授权中间件
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
});
}
}
}
在上面的代码中,我们创建了一个用户和资源数据模型以及AppDbContext类。我们还定义了ViewResourceRequirement和UpdateResourceRequirement授权要求,以及ResourceAccessHandler处理程序。
我们使用AddJwtBearer方法配置JWT身份验证服务,并使用UseAuthentication方法启用身份验证中间件。我们还将授权服务添加到ConfigureServices方法中,并配置了两个授权策略:"ViewResource"和"UpdateResource"。
最后,我们添加了两个API端点:GetResource和UpdateResource,并使用[Authorize]特性对它们进行了保护。